Merge "Bumping compose-material3 to 1.2.0-beta01." into androidx-main
diff --git a/appcompat/appcompat-resources/api/restricted_current.txt b/appcompat/appcompat-resources/api/restricted_current.txt
index 6a66d3a..1278c5d 100644
--- a/appcompat/appcompat-resources/api/restricted_current.txt
+++ b/appcompat/appcompat-resources/api/restricted_current.txt
@@ -58,7 +58,7 @@
package androidx.appcompat.widget {
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class DrawableUtils {
- method public static boolean canSafelyMutateDrawable(android.graphics.drawable.Drawable);
+ method @Deprecated public static boolean canSafelyMutateDrawable(android.graphics.drawable.Drawable);
method public static android.graphics.Rect getOpticalBounds(android.graphics.drawable.Drawable);
method public static android.graphics.PorterDuff.Mode! parseTintMode(int, android.graphics.PorterDuff.Mode!);
field public static final android.graphics.Rect! INSETS_NONE;
diff --git a/appcompat/appcompat-resources/src/main/java/androidx/appcompat/widget/DrawableUtils.java b/appcompat/appcompat-resources/src/main/java/androidx/appcompat/widget/DrawableUtils.java
index 9349881..04e9541 100644
--- a/appcompat/appcompat-resources/src/main/java/androidx/appcompat/widget/DrawableUtils.java
+++ b/appcompat/appcompat-resources/src/main/java/androidx/appcompat/widget/DrawableUtils.java
@@ -24,20 +24,13 @@
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
-import android.graphics.drawable.DrawableContainer;
-import android.graphics.drawable.GradientDrawable;
-import android.graphics.drawable.InsetDrawable;
-import android.graphics.drawable.LayerDrawable;
-import android.graphics.drawable.ScaleDrawable;
import android.os.Build;
import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
-import androidx.appcompat.graphics.drawable.DrawableWrapperCompat;
import androidx.core.graphics.drawable.DrawableCompat;
-import androidx.core.graphics.drawable.WrappedDrawable;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
@@ -69,13 +62,9 @@
insets.right,
insets.bottom
);
- } else if (Build.VERSION.SDK_INT >= 18) {
+ } else {
return Api18Impl.getOpticalInsets(DrawableCompat.unwrap(drawable));
}
-
- // If we reach here, either we're running on a device pre-v18, the Drawable didn't have
- // any optical insets, or a reflection issue, so we'll just return an empty rect.
- return INSETS_NONE;
}
/**
@@ -101,43 +90,11 @@
/**
* Some drawable implementations have problems with mutation. This method returns false if
* there is a known issue in the given drawable's implementation.
+ *
+ * @deprecated it is always true.
*/
+ @Deprecated
public static boolean canSafelyMutateDrawable(@NonNull Drawable drawable) {
- if (Build.VERSION.SDK_INT >= 17) {
- // We'll never return false on API level >= 17, stop early.
- return true;
- }
-
- if (Build.VERSION.SDK_INT < 15 && drawable instanceof InsetDrawable) {
- return false;
- } else if (Build.VERSION.SDK_INT < 15 && drawable instanceof GradientDrawable) {
- // GradientDrawable has a bug pre-ICS which results in mutate() resulting
- // in loss of color
- return false;
- } else if (Build.VERSION.SDK_INT < 17 && drawable instanceof LayerDrawable) {
- return false;
- }
-
- if (drawable instanceof DrawableContainer) {
- // If we have a DrawableContainer, let's traverse its child array
- final Drawable.ConstantState state = drawable.getConstantState();
- if (state instanceof DrawableContainer.DrawableContainerState) {
- final DrawableContainer.DrawableContainerState containerState =
- (DrawableContainer.DrawableContainerState) state;
- for (final Drawable child : containerState.getChildren()) {
- if (!canSafelyMutateDrawable(child)) {
- return false;
- }
- }
- }
- } else if (drawable instanceof WrappedDrawable) {
- return canSafelyMutateDrawable(((WrappedDrawable) drawable).getWrappedDrawable());
- } else if (drawable instanceof DrawableWrapperCompat) {
- return canSafelyMutateDrawable(((DrawableWrapperCompat) drawable).getDrawable());
- } else if (drawable instanceof ScaleDrawable) {
- return canSafelyMutateDrawable(((ScaleDrawable) drawable).getDrawable());
- }
-
return true;
}
@@ -179,8 +136,7 @@
}
}
- // Only accessible on SDK_INT >= 18 and < 29.
- @RequiresApi(18)
+ // Only accessible on SDK_INT < 29.
static class Api18Impl {
private static final boolean sReflectionSuccessful;
private static final Method sGetOpticalInsets;
diff --git a/appcompat/appcompat-resources/src/main/java/androidx/appcompat/widget/ResourceManagerInternal.java b/appcompat/appcompat-resources/src/main/java/androidx/appcompat/widget/ResourceManagerInternal.java
index 916e4ac..d64a144e 100644
--- a/appcompat/appcompat-resources/src/main/java/androidx/appcompat/widget/ResourceManagerInternal.java
+++ b/appcompat/appcompat-resources/src/main/java/androidx/appcompat/widget/ResourceManagerInternal.java
@@ -203,9 +203,7 @@
final ColorStateList tintList = getTintList(context, resId);
if (tintList != null) {
// First mutate the Drawable, then wrap it and set the tint list
- if (DrawableUtils.canSafelyMutateDrawable(drawable)) {
- drawable = drawable.mutate();
- }
+ drawable = drawable.mutate();
drawable = DrawableCompat.wrap(drawable);
DrawableCompat.setTintList(drawable, tintList);
@@ -438,13 +436,10 @@
static void tintDrawable(Drawable drawable, TintInfo tint, int[] state) {
int[] drawableState = drawable.getState();
- boolean mutated = false;
- if (DrawableUtils.canSafelyMutateDrawable(drawable)) {
- mutated = drawable.mutate() == drawable;
- if (!mutated) {
- Log.d(TAG, "Mutated drawable is not the same instance as the input.");
- return;
- }
+ boolean mutated = drawable.mutate() == drawable;
+ if (!mutated) {
+ Log.d(TAG, "Mutated drawable is not the same instance as the input.");
+ return;
}
// Workaround for b/232275112 where LayerDrawable loses its state on mutate().
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesLateOnCreateActivity.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesLateOnCreateActivity.java
index 35ee8d3..a25e6a5 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesLateOnCreateActivity.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesLateOnCreateActivity.java
@@ -68,10 +68,8 @@
Configuration conf = context.getResources().getConfiguration();
if (Build.VERSION.SDK_INT >= 24) {
conf.setLocales(LocaleList.forLanguageTags(locales.toLanguageTags()));
- } else if (Build.VERSION.SDK_INT >= 17) {
- conf.setLocale(locales.get(0));
} else {
- conf.locale = locales.get(0);
+ conf.setLocale(locales.get(0));
}
// updateConfiguration is required to make the configuration change stick.
// updateConfiguration must be called before any use of the actual Resources.
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeCustomApplicationConfigurationTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeCustomApplicationConfigurationTestCase.kt
index bab064a..80561e1 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeCustomApplicationConfigurationTestCase.kt
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeCustomApplicationConfigurationTestCase.kt
@@ -18,7 +18,6 @@
import android.content.res.Configuration
import android.content.res.Resources
-import android.os.Build
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES
import androidx.appcompat.app.NightModeCustomAttachBaseContextActivity.CUSTOM_FONT_SCALE
import androidx.appcompat.app.NightModeCustomAttachBaseContextActivity.CUSTOM_LOCALE
@@ -115,10 +114,6 @@
companion object {
@JvmStatic
@Parameterized.Parameters
- fun data() = if (Build.VERSION.SDK_INT >= 17) {
- listOf(NightSetMode.DEFAULT, NightSetMode.LOCAL)
- } else {
- listOf(NightSetMode.DEFAULT)
- }
+ fun data() = listOf(NightSetMode.DEFAULT, NightSetMode.LOCAL)
}
}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeCustomApplyOverrideConfigurationActivity.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeCustomApplyOverrideConfigurationActivity.java
index 5c9e24e..9e0e311 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeCustomApplyOverrideConfigurationActivity.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeCustomApplyOverrideConfigurationActivity.java
@@ -59,11 +59,7 @@
if (locale != null) {
// Configuration.setLocale is added after 17 and Configuration.locale is deprecated
// after 24
- if (Build.VERSION.SDK_INT >= 17) {
- config.setLocale(locale);
- } else {
- config.locale = locale;
- }
+ config.setLocale(locale);
}
return config;
}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeCustomApplyOverrideConfigurationTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeCustomApplyOverrideConfigurationTestCase.kt
index fea48d4..e502df0 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeCustomApplyOverrideConfigurationTestCase.kt
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeCustomApplyOverrideConfigurationTestCase.kt
@@ -16,7 +16,6 @@
package androidx.appcompat.app
-import android.os.Build
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES
import androidx.appcompat.app.NightModeCustomApplyOverrideConfigurationActivity.CUSTOM_FONT_SCALE
import androidx.appcompat.app.NightModeCustomApplyOverrideConfigurationActivity.CUSTOM_LOCALE
@@ -83,10 +82,6 @@
companion object {
@JvmStatic
@Parameterized.Parameters
- fun data() = if (Build.VERSION.SDK_INT >= 17) {
- listOf(NightSetMode.DEFAULT, NightSetMode.LOCAL)
- } else {
- listOf(NightSetMode.DEFAULT)
- }
+ fun data() = listOf(NightSetMode.DEFAULT, NightSetMode.LOCAL)
}
}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeCustomAttachBaseContextTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeCustomAttachBaseContextTestCase.kt
index da4f6d1..62d0906 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeCustomAttachBaseContextTestCase.kt
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeCustomAttachBaseContextTestCase.kt
@@ -16,7 +16,6 @@
package androidx.appcompat.app
-import android.os.Build
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES
import androidx.appcompat.app.NightModeCustomAttachBaseContextActivity.CUSTOM_FONT_SCALE
import androidx.appcompat.app.NightModeCustomAttachBaseContextActivity.CUSTOM_LOCALE
@@ -78,10 +77,6 @@
companion object {
@JvmStatic
@Parameterized.Parameters
- fun data() = if (Build.VERSION.SDK_INT >= 17) {
- listOf(NightSetMode.DEFAULT, NightSetMode.LOCAL)
- } else {
- listOf(NightSetMode.DEFAULT)
- }
+ fun data() = listOf(NightSetMode.DEFAULT, NightSetMode.LOCAL)
}
}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModePreventOverrideConfigTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModePreventOverrideConfigTestCase.kt
index 29d06b1..075ff08 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModePreventOverrideConfigTestCase.kt
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModePreventOverrideConfigTestCase.kt
@@ -17,7 +17,6 @@
package androidx.appcompat.app
import android.content.res.Configuration
-import android.os.Build
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES
import androidx.appcompat.testutils.NightModeActivityTestRule
import androidx.appcompat.testutils.NightModeUtils.NightSetMode
@@ -67,10 +66,6 @@
companion object {
@JvmStatic
@Parameterized.Parameters
- fun data() = if (Build.VERSION.SDK_INT >= 17) {
- listOf(NightSetMode.DEFAULT, NightSetMode.LOCAL)
- } else {
- listOf(NightSetMode.DEFAULT)
- }
+ fun data() = listOf(NightSetMode.DEFAULT, NightSetMode.LOCAL)
}
}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeRotateDoesNotRecreateActivityTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeRotateDoesNotRecreateActivityTestCase.kt
index 523d88d..c698bba 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeRotateDoesNotRecreateActivityTestCase.kt
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeRotateDoesNotRecreateActivityTestCase.kt
@@ -17,7 +17,6 @@
package androidx.appcompat.app
import android.content.res.Configuration
-import android.os.Build
import androidx.appcompat.Orientation
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES
@@ -109,10 +108,6 @@
public companion object {
@JvmStatic
@Parameterized.Parameters
- public fun data(): List<NightSetMode> = if (Build.VERSION.SDK_INT >= 17) {
- listOf(NightSetMode.DEFAULT, NightSetMode.LOCAL)
- } else {
- listOf(NightSetMode.DEFAULT)
- }
+ public fun data(): List<NightSetMode> = listOf(NightSetMode.DEFAULT, NightSetMode.LOCAL)
}
}
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 f5f1e53..962dfd5 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeRotateRecreatesActivityWithConfigTestCase.kt
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeRotateRecreatesActivityWithConfigTestCase.kt
@@ -19,7 +19,6 @@
import android.app.Activity
import android.app.Instrumentation
import android.content.res.Configuration
-import android.os.Build
import androidx.appcompat.Orientation
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES
@@ -132,10 +131,6 @@
public companion object {
@JvmStatic
@Parameterized.Parameters
- public fun data(): List<NightSetMode> = if (Build.VERSION.SDK_INT >= 17) {
- listOf(NightSetMode.DEFAULT, NightSetMode.LOCAL)
- } else {
- listOf(NightSetMode.DEFAULT)
- }
+ public fun data(): List<NightSetMode> = listOf(NightSetMode.DEFAULT, NightSetMode.LOCAL)
}
}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeTestCase.kt
index 1e3451f..51fc6d3a 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeTestCase.kt
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeTestCase.kt
@@ -19,7 +19,6 @@
import android.content.Context
import android.content.res.Configuration
import android.location.LocationManager
-import android.os.Build
import android.webkit.WebView
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO
@@ -282,10 +281,6 @@
@Parameterized.Parameters
@JvmStatic
- fun data() = if (Build.VERSION.SDK_INT >= 17) {
- listOf(NightSetMode.DEFAULT, NightSetMode.LOCAL)
- } else {
- listOf(NightSetMode.DEFAULT)
- }
+ fun data() = listOf(NightSetMode.DEFAULT, NightSetMode.LOCAL)
}
}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeUiModeConfigChangesTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeUiModeConfigChangesTestCase.kt
index fe35b47..f0ebe47 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeUiModeConfigChangesTestCase.kt
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeUiModeConfigChangesTestCase.kt
@@ -17,7 +17,6 @@
package androidx.appcompat.app
import android.content.res.Configuration
-import android.os.Build
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES
import androidx.appcompat.testutils.NightModeUtils.NightSetMode
@@ -161,10 +160,6 @@
companion object {
@JvmStatic
@Parameterized.Parameters
- fun data() = if (Build.VERSION.SDK_INT >= 17) {
- listOf(NightSetMode.DEFAULT, NightSetMode.LOCAL)
- } else {
- listOf(NightSetMode.DEFAULT)
- }
+ fun data() = listOf(NightSetMode.DEFAULT, NightSetMode.LOCAL)
}
}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/testutils/NightModeUtils.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/testutils/NightModeUtils.kt
index 1fb9286..a666cda 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/testutils/NightModeUtils.kt
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/testutils/NightModeUtils.kt
@@ -20,7 +20,6 @@
import android.content.Context
import android.content.pm.ActivityInfo
import android.content.res.Configuration
-import android.os.Build
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
@@ -189,12 +188,7 @@
setMode: NightSetMode
) = when (setMode) {
NightSetMode.DEFAULT -> AppCompatDelegate.setDefaultNightMode(nightMode)
- NightSetMode.LOCAL ->
- if (Build.VERSION.SDK_INT >= 17) {
- activity!!.delegate.localNightMode = nightMode
- } else {
- throw Exception("Local night mode is not supported on SDK_INT < 17")
- }
+ NightSetMode.LOCAL -> activity!!.delegate.localNightMode = nightMode
}
@NightMode
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatBaseAutoSizeTest.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatBaseAutoSizeTest.java
index 525e43a..8502458 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatBaseAutoSizeTest.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatBaseAutoSizeTest.java
@@ -431,10 +431,8 @@
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
- if (Build.VERSION.SDK_INT >= 17) {
- autoSizeView.setCompoundDrawablesRelative(
- drawable, drawable, drawable, drawable);
- }
+ autoSizeView.setCompoundDrawablesRelative(
+ drawable, drawable, drawable, drawable);
}
});
mInstrumentation.waitForIdleSync();
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 4976f99..28d90ef 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java
@@ -475,7 +475,7 @@
// Workaround for incorrect default fontScale on earlier SDKs.
overrideConfig.fontScale = 0f;
Configuration referenceConfig =
- Api17Impl.createConfigurationContext(baseContext, overrideConfig)
+ baseContext.createConfigurationContext(overrideConfig)
.getResources().getConfiguration();
// Revert the uiMode change so that the diff doesn't include uiMode.
Configuration baseConfig = baseContext.getResources().getConfiguration();
@@ -2687,11 +2687,9 @@
void setConfigurationLocales(Configuration conf, @NonNull LocaleListCompat locales) {
if (Build.VERSION.SDK_INT >= 24) {
Api24Impl.setLocales(conf, locales);
- } else if (Build.VERSION.SDK_INT >= 17) {
- Api17Impl.setLocale(conf, locales.get(0));
- Api17Impl.setLayoutDirection(conf, locales.get(0));
} else {
- conf.locale = locales.get(0);
+ conf.setLocale(locales.get(0));
+ conf.setLayoutDirection(locales.get(0));
}
}
@@ -2795,9 +2793,7 @@
}
if (newLocales != null && !currentLocales.equals(newLocales)) {
configChanges |= ActivityInfo.CONFIG_LOCALE;
- if (Build.VERSION.SDK_INT >= 17) {
- configChanges |= ActivityInfo.CONFIG_LAYOUT_DIRECTION;
- }
+ configChanges |= ActivityInfo.CONFIG_LAYOUT_DIRECTION;
}
if (DEBUG) {
@@ -2836,9 +2832,8 @@
// layout direction after recreating in Android S.
if (Build.VERSION.SDK_INT >= 31
&& (configChanges & ActivityInfo.CONFIG_LAYOUT_DIRECTION) != 0) {
- Api17Impl.setLayoutDirection(
- ((Activity) mHost).getWindow().getDecorView(),
- Api17Impl.getLayoutDirection(overrideConfig));
+ View view = ((Activity) mHost).getWindow().getDecorView();
+ view.setLayoutDirection(overrideConfig.getLayoutDirection());
}
ActivityCompat.recreate((Activity) mHost);
handled = true;
@@ -3908,8 +3903,8 @@
delta.smallestScreenWidthDp = change.smallestScreenWidthDp;
}
- if (Build.VERSION.SDK_INT >= 17) {
- Api17Impl.generateConfigDelta_densityDpi(base, change, delta);
+ if (base.densityDpi != change.densityDpi) {
+ delta.densityDpi = change.densityDpi;
}
// Assets sequence and window configuration are not supported.
@@ -3917,44 +3912,6 @@
return delta;
}
- @RequiresApi(17)
- static class Api17Impl {
- private Api17Impl() { }
-
- static void generateConfigDelta_densityDpi(@NonNull Configuration base,
- @NonNull Configuration change, @NonNull Configuration delta) {
- if (base.densityDpi != change.densityDpi) {
- delta.densityDpi = change.densityDpi;
- }
- }
-
- @DoNotInline
- static Context createConfigurationContext(@NonNull Context context,
- @NonNull Configuration overrideConfiguration) {
- return context.createConfigurationContext(overrideConfiguration);
- }
-
- @DoNotInline
- static void setLayoutDirection(Configuration configuration, Locale loc) {
- configuration.setLayoutDirection(loc);
- }
-
- @DoNotInline
- static void setLayoutDirection(View view, int layoutDirection) {
- view.setLayoutDirection(layoutDirection);
- }
-
- @DoNotInline
- static void setLocale(Configuration configuration, Locale loc) {
- configuration.setLocale(loc);
- }
-
- @DoNotInline
- static int getLayoutDirection(Configuration configuration) {
- return configuration.getLayoutDirection();
- }
- }
-
@RequiresApi(21)
static class Api21Impl {
private Api21Impl() { }
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatViewInflater.java b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatViewInflater.java
index a8936f8..317cd76 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatViewInflater.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatViewInflater.java
@@ -419,7 +419,7 @@
private void backportAccessibilityAttributes(@NonNull Context context, @NonNull View view,
@NonNull AttributeSet attrs) {
- if (Build.VERSION.SDK_INT < 19 || Build.VERSION.SDK_INT > 28) {
+ if (Build.VERSION.SDK_INT > 28) {
return;
}
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/view/menu/MenuItemImpl.java b/appcompat/appcompat/src/main/java/androidx/appcompat/view/menu/MenuItemImpl.java
index 6d20471..dbdc39a 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/view/menu/MenuItemImpl.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/view/menu/MenuItemImpl.java
@@ -25,7 +25,6 @@
import android.content.res.Resources;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
-import android.os.Build;
import android.util.Log;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.KeyEvent;
@@ -471,17 +470,7 @@
@Override
public CharSequence getTitleCondensed() {
- final CharSequence ctitle = mTitleCondensed != null ? mTitleCondensed : mTitle;
-
- if (Build.VERSION.SDK_INT < 18 && ctitle != null && !(ctitle instanceof String)) {
- // For devices pre-JB-MR2, where we have a non-String CharSequence, we need to
- // convert this to a String so that EventLog.writeEvent() does not throw an exception
- // in Activity.onMenuItemSelected()
- return ctitle.toString();
- } else {
- // Else, we just return the condensed title
- return ctitle;
- }
+ return mTitleCondensed != null ? mTitleCondensed : mTitle;
}
@Override
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/view/menu/MenuPopupHelper.java b/appcompat/appcompat/src/main/java/androidx/appcompat/view/menu/MenuPopupHelper.java
index 69c498c..aa98768 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/view/menu/MenuPopupHelper.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/view/menu/MenuPopupHelper.java
@@ -22,7 +22,6 @@
import android.content.Context;
import android.graphics.Point;
import android.graphics.Rect;
-import android.os.Build;
import android.view.Display;
import android.view.Gravity;
import android.view.View;
@@ -32,10 +31,8 @@
import android.widget.PopupWindow.OnDismissListener;
import androidx.annotation.AttrRes;
-import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.StyleRes;
import androidx.appcompat.R;
@@ -233,11 +230,7 @@
final Display display = windowManager.getDefaultDisplay();
final Point displaySize = new Point();
- if (Build.VERSION.SDK_INT >= 17) {
- Api17Impl.getRealSize(display, displaySize);
- } else {
- display.getSize(displaySize);
- }
+ display.getRealSize(displaySize);
final int smallestWidth = Math.min(displaySize.x, displaySize.y);
final int minSmallestWidthCascading = mContext.getResources().getDimensionPixelSize(
@@ -351,16 +344,4 @@
public ListView getListView() {
return getPopup().getListView();
}
-
- @RequiresApi(17)
- static class Api17Impl {
- private Api17Impl() {
- // This class is not instantiable.
- }
-
- @DoNotInline
- static void getRealSize(Display display, Point outSize) {
- display.getRealSize(outSize);
- }
- }
}
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatCheckBox.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatCheckBox.java
index 82d92e2..c7d4f79 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatCheckBox.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatCheckBox.java
@@ -117,14 +117,6 @@
setButtonDrawable(AppCompatResources.getDrawable(getContext(), resId));
}
- @Override
- public int getCompoundPaddingLeft() {
- final int value = super.getCompoundPaddingLeft();
- return mCompoundButtonHelper != null
- ? mCompoundButtonHelper.getCompoundPaddingLeft(value)
- : value;
- }
-
/**
* This should be accessed from {@link androidx.core.widget.CompoundButtonCompat}
*/
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatCompoundButtonHelper.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatCompoundButtonHelper.java
index 3513f72..bf9b410 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatCompoundButtonHelper.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatCompoundButtonHelper.java
@@ -20,7 +20,6 @@
import android.content.res.Resources;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
-import android.os.Build;
import android.util.AttributeSet;
import android.widget.CompoundButton;
@@ -144,15 +143,4 @@
}
}
- int getCompoundPaddingLeft(int superValue) {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
- // Before JB-MR1 the button drawable wasn't taken into account for padding. We'll
- // workaround that here
- Drawable buttonDrawable = CompoundButtonCompat.getButtonDrawable(mView);
- if (buttonDrawable != null) {
- superValue += buttonDrawable.getIntrinsicWidth();
- }
- }
- return superValue;
- }
}
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatDrawableManager.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatDrawableManager.java
index 6bd8f11..479200e 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatDrawableManager.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatDrawableManager.java
@@ -309,9 +309,7 @@
}
private void setPorterDuffColorFilter(Drawable d, int color, PorterDuff.Mode mode) {
- if (DrawableUtils.canSafelyMutateDrawable(d)) {
- d = d.mutate();
- }
+ d = d.mutate();
d.setColorFilter(getPorterDuffColorFilter(color, mode == null ? DEFAULT_MODE
: mode));
}
@@ -423,9 +421,7 @@
}
if (colorAttrSet) {
- if (DrawableUtils.canSafelyMutateDrawable(drawable)) {
- drawable = drawable.mutate();
- }
+ drawable = drawable.mutate();
final int color = getThemeAttrColor(context, colorAttr);
drawable.setColorFilter(getPorterDuffColorFilter(color, tintMode));
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatRadioButton.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatRadioButton.java
index 693f95f..dee338b 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatRadioButton.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatRadioButton.java
@@ -115,14 +115,6 @@
setButtonDrawable(AppCompatResources.getDrawable(getContext(), resId));
}
- @Override
- public int getCompoundPaddingLeft() {
- final int value = super.getCompoundPaddingLeft();
- return mCompoundButtonHelper != null
- ? mCompoundButtonHelper.getCompoundPaddingLeft(value)
- : value;
- }
-
/**
* This should be accessed from {@link androidx.core.widget.CompoundButtonCompat}
*/
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextHelper.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextHelper.java
index 4eb3371..8408ef6 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextHelper.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextHelper.java
@@ -548,12 +548,10 @@
applyCompoundDrawableTint(compoundDrawables[2], mDrawableRightTint);
applyCompoundDrawableTint(compoundDrawables[3], mDrawableBottomTint);
}
- if (Build.VERSION.SDK_INT >= 17) {
- if (mDrawableStartTint != null || mDrawableEndTint != null) {
- final Drawable[] compoundDrawables = Api17Impl.getCompoundDrawablesRelative(mView);
- applyCompoundDrawableTint(compoundDrawables[0], mDrawableStartTint);
- applyCompoundDrawableTint(compoundDrawables[2], mDrawableEndTint);
- }
+ if (mDrawableStartTint != null || mDrawableEndTint != null) {
+ final Drawable[] compoundDrawables = Api17Impl.getCompoundDrawablesRelative(mView);
+ applyCompoundDrawableTint(compoundDrawables[0], mDrawableStartTint);
+ applyCompoundDrawableTint(compoundDrawables[2], mDrawableEndTint);
}
}
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextViewAutoSizeHelper.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextViewAutoSizeHelper.java
index 446b6d8..684696a 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextViewAutoSizeHelper.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextViewAutoSizeHelper.java
@@ -34,7 +34,6 @@
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
-import android.view.View;
import android.widget.TextView;
import androidx.annotation.DoNotInline;
@@ -47,7 +46,6 @@
import androidx.core.view.ViewCompat;
import androidx.core.widget.TextViewCompat;
-import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
@@ -75,11 +73,6 @@
@SuppressLint("BanConcurrentHashMap")
private static java.util.concurrent.ConcurrentHashMap<String, Method>
sTextViewMethodByNameCache = new java.util.concurrent.ConcurrentHashMap<>();
- // Cache of TextView fields used via reflection; the key is the field name and the value is
- // the field itself or null if it can not be found.
- @SuppressLint("BanConcurrentHashMap")
- private static java.util.concurrent.ConcurrentHashMap<String, Field> sTextViewFieldByNameCache =
- new java.util.concurrent.ConcurrentHashMap<>();
// Use this to specify that any of the auto-size configuration int values have not been set.
static final float UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE = -1f;
// Ported from TextView#VERY_WIDE. Represents a maximum width in pixels the TextView takes when
@@ -647,14 +640,12 @@
setRawTextSize(TypedValue.applyDimension(unit, size, res.getDisplayMetrics()));
}
+ @SuppressLint("BanUncheckedReflection")
private void setRawTextSize(float size) {
if (size != mTextView.getPaint().getTextSize()) {
mTextView.getPaint().setTextSize(size);
- boolean isInLayout = false;
- if (Build.VERSION.SDK_INT >= 18) {
- isInLayout = Api18Impl.isInLayout(mTextView);
- }
+ boolean isInLayout = mTextView.isInLayout();
if (mTextView.getLayout() != null) {
// Do not auto-size right after setting the text size.
@@ -731,11 +722,18 @@
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return Api23Impl.createStaticLayoutForMeasuring(
text, alignment, availableWidth, maxLines, mTextView, mTempTextPaint, mImpl);
- } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
- return Api16Impl.createStaticLayoutForMeasuring(
- text, alignment, availableWidth, mTextView, mTempTextPaint);
} else {
- return createStaticLayoutForMeasuringPre16(text, alignment, availableWidth);
+ final float lineSpacingMultiplier = mTextView.getLineSpacingMultiplier();
+ final float lineSpacingAdd = mTextView.getLineSpacingExtra();
+ final boolean includePad = mTextView.getIncludeFontPadding();
+
+ // The layout could not be constructed using the builder so fall back to the
+ // most broad constructor.
+ return new StaticLayout(text, mTempTextPaint, availableWidth,
+ alignment,
+ lineSpacingMultiplier,
+ lineSpacingAdd,
+ includePad);
}
}
@@ -749,7 +747,7 @@
}
}
- final int maxLines = Build.VERSION.SDK_INT >= 16 ? Api16Impl.getMaxLines(mTextView) : -1;
+ final int maxLines = mTextView.getMaxLines();
initTempTextPaint(suggestedSizeInPx);
// Needs reflection call due to being private.
@@ -771,25 +769,7 @@
return true;
}
-
- private StaticLayout createStaticLayoutForMeasuringPre16(CharSequence text,
- Layout.Alignment alignment, int availableWidth) {
- // The default values have been inlined with the StaticLayout defaults.
-
- final float lineSpacingMultiplier = accessAndReturnWithDefault(mTextView,
- "mSpacingMult", 1.0f);
- final float lineSpacingAdd = accessAndReturnWithDefault(mTextView,
- "mSpacingAdd", 0.0f);
- final boolean includePad = accessAndReturnWithDefault(mTextView,
- "mIncludePad", true);
-
- return new StaticLayout(text, mTempTextPaint, availableWidth,
- alignment,
- lineSpacingMultiplier,
- lineSpacingAdd,
- includePad);
- }
-
+ @SuppressLint("BanUncheckedReflection")
@SuppressWarnings("unchecked")
// This is marked package-protected so that it doesn't require a synthetic accessor
// when being used from the Impl inner classes
@@ -814,22 +794,6 @@
return result;
}
- @SuppressWarnings("unchecked")
- private static <T> T accessAndReturnWithDefault(@NonNull Object object,
- @NonNull final String fieldName, @NonNull final T defaultValue) {
- try {
- final Field field = getTextViewField(fieldName);
- if (field == null) {
- return defaultValue;
- }
-
- return (T) field.get(object);
- } catch (IllegalAccessException e) {
- Log.w(TAG, "Failed to access TextView#" + fieldName + " member", e);
- return defaultValue;
- }
- }
-
@Nullable
private static Method getTextViewMethod(@NonNull final String methodName) {
try {
@@ -850,25 +814,6 @@
}
}
- @Nullable
- private static Field getTextViewField(@NonNull final String fieldName) {
- try {
- Field field = sTextViewFieldByNameCache.get(fieldName);
- if (field == null) {
- field = TextView.class.getDeclaredField(fieldName);
- if (field != null) {
- field.setAccessible(true);
- sTextViewFieldByNameCache.put(fieldName, field);
- }
- }
-
- return field;
- } catch (NoSuchFieldException e) {
- Log.w(TAG, "Failed to access TextView#" + fieldName + " member", e);
- return null;
- }
- }
-
/**
* @return {@code true} if this widget supports auto-sizing text and has been configured to
* auto-size.
@@ -928,50 +873,4 @@
return layoutBuilder.build();
}
}
-
- @RequiresApi(18)
- private static final class Api18Impl {
- private Api18Impl() {
- // This class is not instantiable.
- }
-
- @DoNotInline
- static boolean isInLayout(@NonNull View view) {
- return view.isInLayout();
- }
- }
-
- @RequiresApi(16)
- private static final class Api16Impl {
- private Api16Impl() {
- // This class is not instantiable.
- }
-
- @DoNotInline
- static int getMaxLines(@NonNull TextView textView) {
- return textView.getMaxLines();
- }
-
- @DoNotInline
- @NonNull
- static StaticLayout createStaticLayoutForMeasuring(
- @NonNull CharSequence text,
- @NonNull Layout.Alignment alignment,
- int availableWidth,
- @NonNull TextView textView,
- @NonNull TextPaint tempTextPaint
- ) {
- final float lineSpacingMultiplier = textView.getLineSpacingMultiplier();
- final float lineSpacingAdd = textView.getLineSpacingExtra();
- final boolean includePad = textView.getIncludeFontPadding();
-
- // The layout could not be constructed using the builder so fall back to the
- // most broad constructor.
- return new StaticLayout(text, tempTextPaint, availableWidth,
- alignment,
- lineSpacingMultiplier,
- lineSpacingAdd,
- includePad);
- }
- }
}
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/SwitchCompat.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/SwitchCompat.java
index c764cb6..24ca26b 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/SwitchCompat.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/SwitchCompat.java
@@ -48,11 +48,9 @@
import android.widget.Switch;
import android.widget.TextView;
-import androidx.annotation.DoNotInline;
import androidx.annotation.FloatRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
import androidx.appcompat.R;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.text.AllCapsTransformationMethod;
@@ -1139,9 +1137,7 @@
final float targetPosition = newCheckedState ? 1 : 0;
mPositionAnimator = ObjectAnimator.ofFloat(this, THUMB_POS, targetPosition);
mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION);
- if (Build.VERSION.SDK_INT >= 18) {
- Api18Impl.setAutoCancel(mPositionAnimator, true);
- }
+ mPositionAnimator.setAutoCancel(true);
mPositionAnimator.start();
}
@@ -1692,16 +1688,4 @@
}
}
}
-
- @RequiresApi(18)
- static class Api18Impl {
- private Api18Impl() {
- // This class is not instantiable.
- }
-
- @DoNotInline
- static void setAutoCancel(ObjectAnimator objectAnimator, boolean cancel) {
- objectAnimator.setAutoCancel(cancel);
- }
- }
}
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/Toolbar.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/Toolbar.java
index b48c66c..9198f6a 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/Toolbar.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/Toolbar.java
@@ -559,9 +559,7 @@
@Override
public void onRtlPropertiesChanged(int layoutDirection) {
- if (Build.VERSION.SDK_INT >= 17) {
- super.onRtlPropertiesChanged(layoutDirection);
- }
+ super.onRtlPropertiesChanged(layoutDirection);
ensureContentInsets();
mContentInsets.setDirection(layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL);
diff --git a/arch/core/core-runtime/src/main/java/androidx/arch/core/executor/DefaultTaskExecutor.java b/arch/core/core-runtime/src/main/java/androidx/arch/core/executor/DefaultTaskExecutor.java
index b164eb5..99433a0 100644
--- a/arch/core/core-runtime/src/main/java/androidx/arch/core/executor/DefaultTaskExecutor.java
+++ b/arch/core/core-runtime/src/main/java/androidx/arch/core/executor/DefaultTaskExecutor.java
@@ -82,7 +82,7 @@
private static Handler createAsync(@NonNull Looper looper) {
if (Build.VERSION.SDK_INT >= 28) {
return Api28Impl.createAsync(looper);
- } else if (Build.VERSION.SDK_INT >= 17) {
+ } else {
try {
// This constructor was added as private in JB MR1:
// https://android.googlesource.com/platform/frameworks/base/+/refs/heads/jb-mr1-release/core/java/android/os/Handler.java
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/task/MergeBaselineProfileTask.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/task/MergeBaselineProfileTask.kt
index 2f2b284..40b06ab 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/task/MergeBaselineProfileTask.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/task/MergeBaselineProfileTask.kt
@@ -20,6 +20,7 @@
import androidx.baselineprofile.gradle.utils.TASK_NAME_SUFFIX
import androidx.baselineprofile.gradle.utils.maybeRegister
import java.io.File
+import kotlin.io.path.Path
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.Project
@@ -262,7 +263,7 @@
logger.warn(
"""
A baseline profile was generated for the variant `${variantName.get()}`:
- file:///$absolutePath
+ ${Path(absolutePath).toUri()}
""".trimIndent()
)
}
@@ -312,7 +313,7 @@
logger.warn(
"""
A startup profile was generated for the variant `${variantName.get()}`:
- file:///$absolutePath
+ ${Path(absolutePath).toUri()}
""".trimIndent()
)
}
diff --git a/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPluginTest.kt b/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPluginTest.kt
index aa4cf94..0fec0ff 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPluginTest.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPluginTest.kt
@@ -35,6 +35,7 @@
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import java.io.File
+import kotlin.io.path.Path
import org.junit.Assume.assumeTrue
import org.junit.Rule
import org.junit.Test
@@ -68,6 +69,8 @@
"src/$variantName/$EXPECTED_PROFILE_FOLDER/startup-prof.txt"
)
+ private fun File.toUri() = Path(canonicalPath).toUri()
+
private fun mergedArtProfile(variantName: String): File {
// Task name folder in path was first observed in the update to AGP 8.3.0-alpha10.
// Before that, the folder was omitted in path.
@@ -111,7 +114,7 @@
gradleRunner.build("generateBaselineProfile") {
val notFound = it.lines().requireInOrder(
"A baseline profile was generated for the variant `release`:",
- "file:///${baselineProfileFile("main").canonicalPath}"
+ "${baselineProfileFile("main").toUri()}"
)
assertThat(notFound).isEmpty()
}
@@ -154,9 +157,9 @@
gradleRunner.build("generateBaselineProfile") {
val notFound = it.lines().requireInOrder(
"A baseline profile was generated for the variant `release`:",
- "file:///${baselineProfileFile("release").canonicalPath}",
+ "${baselineProfileFile("release").toUri()}",
"A startup profile was generated for the variant `release`:",
- "file:///${startupProfileFile("release").canonicalPath}"
+ "${startupProfileFile("release").toUri()}"
)
assertThat(notFound).isEmpty()
}
@@ -237,9 +240,9 @@
val notFound = it.lines().requireInOrder(
"A baseline profile was generated for the variant `$variantName`:",
- "file:///${baselineProfileFile(variantName).canonicalPath}",
+ "${baselineProfileFile(variantName).toUri()}",
"A startup profile was generated for the variant `$variantName`:",
- "file:///${startupProfileFile(variantName).canonicalPath}"
+ "${startupProfileFile(variantName).toUri()}"
)
assertWithMessage(
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
index 441f9ff..e888c7b 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
@@ -48,7 +48,7 @@
import org.junit.runners.Parameterized
import org.junit.runners.Parameterized.Parameters
-private const val tracingPerfettoVersion = "1.0.0-beta03" // TODO(224510255): get by 'reflection'
+private const val tracingPerfettoVersion = "1.0.0" // TODO(224510255): get by 'reflection'
private const val minSupportedSdk = Build.VERSION_CODES.R // TODO(234351579): Support API < 30
@RunWith(Parameterized::class)
diff --git a/benchmark/integration-tests/baselineprofile-producer/src/main/java/androidx/benchmark/integration/baselineprofile/producer/BaselineProfileTest.kt b/benchmark/integration-tests/baselineprofile-producer/src/main/java/androidx/benchmark/integration/baselineprofile/producer/BaselineProfileTest.kt
index 7b55cde..8e04606 100644
--- a/benchmark/integration-tests/baselineprofile-producer/src/main/java/androidx/benchmark/integration/baselineprofile/producer/BaselineProfileTest.kt
+++ b/benchmark/integration-tests/baselineprofile-producer/src/main/java/androidx/benchmark/integration/baselineprofile/producer/BaselineProfileTest.kt
@@ -43,6 +43,7 @@
fun standardBaselineProfile() = baselineRule.collect(
packageName = PACKAGE_NAME,
includeInStartupProfile = false,
+ maxIterations = 1,
profileBlock = {
startActivityAndWait(Intent(ACTION))
device.waitForIdle()
@@ -53,6 +54,7 @@
fun startupBaselineProfile() = baselineRule.collect(
packageName = PACKAGE_NAME,
includeInStartupProfile = true,
+ maxIterations = 1,
profileBlock = {
startActivityAndWait(Intent(ACTION))
device.waitForIdle()
diff --git a/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/ScanResultTest.kt b/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/ScanResultTest.kt
index 9236324..df5d8b1 100644
--- a/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/ScanResultTest.kt
+++ b/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/ScanResultTest.kt
@@ -17,11 +17,13 @@
package androidx.bluetooth
import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice as FwkBluetoothDevice
import android.bluetooth.BluetoothManager
import android.bluetooth.le.ScanResult as FwkScanResult
import android.content.Context
import android.os.Build
import android.os.ParcelUuid
+import androidx.bluetooth.utils.addressType
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.SdkSuppress
import androidx.test.rule.GrantPermissionRule
@@ -39,6 +41,7 @@
*/
@RunWith(JUnit4::class)
class ScanResultTest {
+
@Rule
@JvmField
val permissionRule: GrantPermissionRule = if (Build.VERSION.SDK_INT >= 31) {
@@ -53,6 +56,7 @@
private val bluetoothManager: BluetoothManager? =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager?
private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager?.adapter
+ private val bluetoothLe = BluetoothLe(context)
@Before
fun setUp() {
@@ -90,8 +94,13 @@
assertThat(BluetoothDevice(fwkBluetoothDevice).bondState)
.isEqualTo(scanResult.device.bondState)
assertThat(address).isEqualTo(scanResult.deviceAddress.address)
- assertThat(BluetoothAddress.ADDRESS_TYPE_UNKNOWN)
- .isEqualTo(scanResult.deviceAddress.addressType)
+ val expectedAddressType = if (Build.VERSION.SDK_INT >= 34) {
+ BluetoothAddress.ADDRESS_TYPE_PUBLIC
+ } else {
+ BluetoothAddress.ADDRESS_TYPE_UNKNOWN
+ }
+ assertThat(scanResult.deviceAddress.addressType)
+ .isEqualTo(expectedAddressType)
assertThat(true).isEqualTo(scanResult.isConnectable())
assertThat(timeStampNanos).isEqualTo(scanResult.timestampNanos)
assertThat(scanResult.getManufacturerSpecificData(1)).isNull()
@@ -129,4 +138,100 @@
assertThat(scanResult.device).isEqualTo(scanResult.device)
assertThat(scanResult.deviceAddress).isEqualTo(scanResult.deviceAddress)
}
+
+ @SdkSuppress(minSdkVersion = 34)
+ @Test
+ fun frameworkScanResultAddressTypeRandomStatic() {
+ val address = "F0:43:A8:23:10:11"
+ val fwkBluetoothDevice = bluetoothAdapter!!
+ .getRemoteLeDevice(address, FwkBluetoothDevice.ADDRESS_TYPE_RANDOM)
+ val rssi = 34
+ val periodicAdvertisingInterval = 8
+ val timeStampNanos: Long = 1
+
+ val fwkScanResult = FwkScanResult(
+ fwkBluetoothDevice,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ rssi,
+ periodicAdvertisingInterval,
+ null,
+ timeStampNanos
+ )
+
+ val bluetoothAddress = BluetoothAddress(
+ fwkScanResult.device.address,
+ fwkScanResult.device.addressType()
+ )
+
+ assertThat(bluetoothAddress.addressType)
+ .isEqualTo(BluetoothAddress.ADDRESS_TYPE_RANDOM_STATIC)
+ }
+
+ @SdkSuppress(minSdkVersion = 34)
+ @Test
+ fun frameworkScanResultAddressTypeRandomResolvable() {
+ val address = "40:01:02:03:04:05"
+ val fwkBluetoothDevice = bluetoothAdapter!!
+ .getRemoteLeDevice(address, FwkBluetoothDevice.ADDRESS_TYPE_RANDOM)
+ val rssi = 34
+ val periodicAdvertisingInterval = 8
+ val timeStampNanos: Long = 1
+
+ val fwkScanResult = FwkScanResult(
+ fwkBluetoothDevice,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ rssi,
+ periodicAdvertisingInterval,
+ null,
+ timeStampNanos
+ )
+
+ val bluetoothAddress = BluetoothAddress(
+ fwkScanResult.device.address,
+ fwkScanResult.device.addressType()
+ )
+
+ assertThat(bluetoothAddress.addressType)
+ .isEqualTo(BluetoothAddress.ADDRESS_TYPE_RANDOM_RESOLVABLE)
+ }
+
+ @SdkSuppress(minSdkVersion = 34)
+ @Test
+ fun frameworkScanResultAddressTypeRandomNonResolvable() {
+ val address = "00:01:02:03:04:05"
+ val fwkBluetoothDevice = bluetoothAdapter!!
+ .getRemoteLeDevice(address, FwkBluetoothDevice.ADDRESS_TYPE_RANDOM)
+ val rssi = 34
+ val periodicAdvertisingInterval = 8
+ val timeStampNanos: Long = 1
+
+ val fwkScanResult = FwkScanResult(
+ fwkBluetoothDevice,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ rssi,
+ periodicAdvertisingInterval,
+ null,
+ timeStampNanos
+ )
+
+ val bluetoothAddress = BluetoothAddress(
+ fwkScanResult.device.address,
+ fwkScanResult.device.addressType()
+ )
+
+ assertThat(bluetoothAddress.addressType)
+ .isEqualTo(BluetoothAddress.ADDRESS_TYPE_RANDOM_NON_RESOLVABLE)
+ }
}
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothAddress.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothAddress.kt
index 7846be5..c104d69 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothAddress.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothAddress.kt
@@ -44,15 +44,6 @@
/** Address type is unknown. */
const val ADDRESS_TYPE_UNKNOWN: Int = 0xFFFF
-
- /** Address type random static bits value */
- internal const val TYPE_RANDOM_STATIC_BITS_VALUE: Int = 3
-
- /** Address type random resolvable bits value */
- internal const val TYPE_RANDOM_RESOLVABLE_BITS_VALUE: Int = 1
-
- /** Address type random non resolvable bits value */
- internal const val TYPE_RANDOM_NON_RESOLVABLE_BITS_VALUE: Int = 0
}
@Target(
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/ScanResult.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/ScanResult.kt
index b9af5f7..2e3cf987 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/ScanResult.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/ScanResult.kt
@@ -22,6 +22,7 @@
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
+import androidx.bluetooth.utils.addressType
import java.util.UUID
/**
@@ -74,12 +75,10 @@
/** Remote Bluetooth device found. */
val device: BluetoothDevice = BluetoothDevice(fwkScanResult.device)
- // TODO(kihongs) Find a way to get address type from framework scan result
/** Bluetooth address for the remote device found. */
-
val deviceAddress: BluetoothAddress = BluetoothAddress(
fwkScanResult.device.address,
- BluetoothAddress.ADDRESS_TYPE_UNKNOWN
+ fwkScanResult.device.addressType()
)
/** Device timestamp when the advertisement was last seen. */
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/utils/FwkBluetoothDevice.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/utils/FwkBluetoothDevice.kt
new file mode 100644
index 0000000..3e39c67
--- /dev/null
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/utils/FwkBluetoothDevice.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.bluetooth.utils
+
+import android.bluetooth.BluetoothDevice as FwkBluetoothDevice
+import android.os.Build
+import android.os.Parcel
+import androidx.annotation.RequiresApi
+import androidx.bluetooth.BluetoothAddress
+
+/** Address type random static bits value */
+private const val ADDRESS_TYPE_RANDOM_STATIC_BITS_VALUE: Int = 3
+
+/** Address type random resolvable bits value */
+private const val ADDRESS_TYPE_RANDOM_RESOLVABLE_BITS_VALUE: Int = 1
+
+/** Address type random non resolvable bits value */
+private const val ADDRESS_TYPE_RANDOM_NON_RESOLVABLE_BITS_VALUE: Int = 0
+
+// mAddressType is added to the parcel in API 34
+internal fun FwkBluetoothDevice.addressType(): @BluetoothAddress.AddressType Int {
+ return if (Build.VERSION.SDK_INT >= 34) {
+ return addressType34()
+ } else {
+ BluetoothAddress.ADDRESS_TYPE_UNKNOWN
+ }
+}
+
+@RequiresApi(34)
+private fun FwkBluetoothDevice.addressType34(): @BluetoothAddress.AddressType Int {
+ val parcel = Parcel.obtain()
+ writeToParcel(parcel, 0)
+ parcel.setDataPosition(0)
+ parcel.readString() // Skip address
+ val mAddressType = parcel.readInt()
+ parcel.recycle()
+
+ return when (mAddressType) {
+ FwkBluetoothDevice.ADDRESS_TYPE_PUBLIC -> BluetoothAddress.ADDRESS_TYPE_PUBLIC
+ FwkBluetoothDevice.ADDRESS_TYPE_RANDOM ->
+ when (address.substring(0, 1).toInt(16).shr(2)) {
+ ADDRESS_TYPE_RANDOM_STATIC_BITS_VALUE ->
+ BluetoothAddress.ADDRESS_TYPE_RANDOM_STATIC
+
+ ADDRESS_TYPE_RANDOM_RESOLVABLE_BITS_VALUE ->
+ BluetoothAddress.ADDRESS_TYPE_RANDOM_RESOLVABLE
+
+ ADDRESS_TYPE_RANDOM_NON_RESOLVABLE_BITS_VALUE ->
+ BluetoothAddress.ADDRESS_TYPE_RANDOM_NON_RESOLVABLE
+
+ else -> BluetoothAddress.ADDRESS_TYPE_UNKNOWN
+ }
+
+ FwkBluetoothDevice.ADDRESS_TYPE_UNKNOWN -> BluetoothAddress.ADDRESS_TYPE_UNKNOWN
+ else -> BluetoothAddress.ADDRESS_TYPE_UNKNOWN
+ }
+}
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/utils/Utils.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/utils/Utils.kt
index 6dac023..ed4f1dd 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/utils/Utils.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/utils/Utils.kt
@@ -17,11 +17,7 @@
package androidx.bluetooth.utils
import android.bluetooth.BluetoothDevice as FwkBluetoothDevice
-import android.os.Build
-import android.os.Parcel
-import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
-import androidx.bluetooth.BluetoothAddress
import java.security.MessageDigest
import java.util.UUID
import kotlin.experimental.and
@@ -32,19 +28,7 @@
packageName: String,
fwkDevice: FwkBluetoothDevice
): UUID {
- return if (Build.VERSION.SDK_INT >= 34) {
- deviceId(packageName, fwkDevice.address, fwkDevice.addressType())
- } else {
- deviceId(packageName, fwkDevice.address, BluetoothAddress.ADDRESS_TYPE_UNKNOWN)
- }
-}
-
-private fun deviceId(
- packageName: String,
- address: String,
- @BluetoothAddress.AddressType addressType: Int
-): UUID {
- val name = packageName + address + addressType
+ val name = packageName + fwkDevice.address + fwkDevice.addressType()
val md = MessageDigest.getInstance("SHA-1")
md.update(name.toByteArray())
val hash = md.digest()
@@ -68,30 +52,3 @@
return UUID(msb, lsb)
}
-
-// mAddressType is added to the parcel in API 34
-@RequiresApi(34)
-private fun FwkBluetoothDevice.addressType(): @BluetoothAddress.AddressType Int {
- val parcel = Parcel.obtain()
- writeToParcel(parcel, 0)
- parcel.setDataPosition(0)
- parcel.readString() // Skip address
- val mAddressType = parcel.readInt()
- parcel.recycle()
-
- return when (mAddressType) {
- FwkBluetoothDevice.ADDRESS_TYPE_PUBLIC -> BluetoothAddress.ADDRESS_TYPE_PUBLIC
- FwkBluetoothDevice.ADDRESS_TYPE_RANDOM ->
- when (address.substring(0, 0).toInt(16).shr(2)) {
- BluetoothAddress.TYPE_RANDOM_STATIC_BITS_VALUE ->
- BluetoothAddress.ADDRESS_TYPE_RANDOM_STATIC
- BluetoothAddress.TYPE_RANDOM_RESOLVABLE_BITS_VALUE ->
- BluetoothAddress.ADDRESS_TYPE_RANDOM_RESOLVABLE
- BluetoothAddress.TYPE_RANDOM_NON_RESOLVABLE_BITS_VALUE ->
- BluetoothAddress.ADDRESS_TYPE_RANDOM_NON_RESOLVABLE
- else -> BluetoothAddress.ADDRESS_TYPE_UNKNOWN
- }
- FwkBluetoothDevice.ADDRESS_TYPE_UNKNOWN -> BluetoothAddress.ADDRESS_TYPE_UNKNOWN
- else -> BluetoothAddress.ADDRESS_TYPE_UNKNOWN
- }
-}
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/SdkResourceGenerator.kt b/buildSrc/public/src/main/kotlin/androidx/build/SdkResourceGenerator.kt
index 1a4dc97..991a22c 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/SdkResourceGenerator.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/SdkResourceGenerator.kt
@@ -61,7 +61,7 @@
@get:Input val agpDependency: String = AGP_LATEST
@get:Input
- val navigationRuntime: String = "androidx.navigation:navigation-runtime:2.4.0-alpha01"
+ val navigationRuntime: String = "androidx.navigation:navigation-runtime:2.4.0"
@get:Input abstract val kotlinStdlib: Property<String>
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompat.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompat.kt
index 4d1fddd..37150eb 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompat.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompat.kt
@@ -29,14 +29,18 @@
*/
@RequiresApi(21)
class DynamicRangeProfilesCompat internal constructor(
- private val mImpl: DynamicRangeProfilesCompatImpl
+ private val impl: DynamicRangeProfilesCompatImpl
) {
+ /** The set of supported dynamic ranges. */
+ val supportedDynamicRanges: Set<DynamicRange>
+ get() = impl.supportedDynamicRanges
+
/**
* Returns a set of supported [DynamicRange] that can be referenced in a single
* capture request.
*
* For example if a particular 10-bit output capable device returns (STANDARD,
- * HLG10, HDR10) as result from calling [getSupportedDynamicRanges] and
+ * HLG10, HDR10) as result from calling [supportedDynamicRanges] and
* [DynamicRangeProfiles.getProfileCaptureRequestConstraints]
* returns (STANDARD, HLG10) when given an argument
* of STANDARD. This means that the corresponding camera device will only accept and process
@@ -50,21 +54,12 @@
* @param dynamicRange The dynamic range that will be checked for constraints
* @return non-modifiable set of dynamic ranges
* @throws IllegalArgumentException If the dynamic range argument is not within the set
- * returned by [getSupportedDynamicRanges].
+ * returned by [supportedDynamicRanges].
*/
fun getDynamicRangeCaptureRequestConstraints(
dynamicRange: DynamicRange
): Set<DynamicRange> {
- return mImpl.getDynamicRangeCaptureRequestConstraints(dynamicRange)
- }
-
- /**
- * Returns a set of supported dynamic ranges.
- *
- * @return a non-modifiable set of dynamic ranges.
- */
- fun getSupportedDynamicRanges(): Set<DynamicRange> {
- return mImpl.getSupportedDynamicRanges()
+ return impl.getDynamicRangeCaptureRequestConstraints(dynamicRange)
}
/**
@@ -80,10 +75,10 @@
* @return `true` if the given profile is not suitable for latency sensitive use cases,
* `false` otherwise.
* @throws IllegalArgumentException If the dynamic range argument is not within the set
- * returned by [getSupportedDynamicRanges].
+ * returned by [supportedDynamicRanges].
*/
fun isExtraLatencyPresent(dynamicRange: DynamicRange): Boolean {
- return mImpl.isExtraLatencyPresent(dynamicRange)
+ return impl.isExtraLatencyPresent(dynamicRange)
}
/**
@@ -99,16 +94,15 @@
33, "DynamicRangesCompat can only be " +
"converted to DynamicRangeProfiles on API 33 or higher."
)
- return mImpl.unwrap()
+ return impl.unwrap()
}
internal interface DynamicRangeProfilesCompatImpl {
+ val supportedDynamicRanges: Set<DynamicRange>
fun getDynamicRangeCaptureRequestConstraints(
dynamicRange: DynamicRange
): Set<DynamicRange>
- fun getSupportedDynamicRanges(): Set<DynamicRange>
-
fun isExtraLatencyPresent(dynamicRange: DynamicRange): Boolean
fun unwrap(): DynamicRangeProfiles?
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatApi33Impl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatApi33Impl.kt
index 12a5687..0197afe 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatApi33Impl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatApi33Impl.kt
@@ -26,6 +26,10 @@
internal class DynamicRangeProfilesCompatApi33Impl(
private val dynamicRangeProfiles: DynamicRangeProfiles
) : DynamicRangeProfilesCompat.DynamicRangeProfilesCompatImpl {
+ override val supportedDynamicRanges: Set<DynamicRange>
+ get() = profileSetToDynamicRangeSet(
+ dynamicRangeProfiles.supportedProfiles
+ )
override fun getDynamicRangeCaptureRequestConstraints(
dynamicRange: DynamicRange
@@ -39,10 +43,6 @@
)
}
- override fun getSupportedDynamicRanges() = profileSetToDynamicRangeSet(
- dynamicRangeProfiles.supportedProfiles
- )
-
override fun isExtraLatencyPresent(dynamicRange: DynamicRange): Boolean {
val dynamicRangeProfile = dynamicRangeToFirstSupportedProfile(dynamicRange)
require(
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatBaseImpl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatBaseImpl.kt
index b9a0cad..2d5cc50 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatBaseImpl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatBaseImpl.kt
@@ -24,6 +24,9 @@
@RequiresApi(21)
internal class DynamicRangeProfilesCompatBaseImpl :
DynamicRangeProfilesCompat.DynamicRangeProfilesCompatImpl {
+ override val supportedDynamicRanges: Set<DynamicRange>
+ get() = SDR_ONLY
+
override fun getDynamicRangeCaptureRequestConstraints(
dynamicRange: DynamicRange
): Set<DynamicRange> {
@@ -34,10 +37,6 @@
return SDR_ONLY
}
- override fun getSupportedDynamicRanges(): Set<DynamicRange> {
- return SDR_ONLY
- }
-
override fun isExtraLatencyPresent(dynamicRange: DynamicRange): Boolean {
Preconditions.checkArgument(
DynamicRange.SDR == dynamicRange,
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/DynamicRangeResolver.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/DynamicRangeResolver.kt
index 947d31c..7871614 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/DynamicRangeResolver.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/DynamicRangeResolver.kt
@@ -54,7 +54,7 @@
}
// Get the supported dynamic ranges from the device
- val supportedDynamicRanges = dynamicRangesInfo.getSupportedDynamicRanges()
+ val supportedDynamicRanges = dynamicRangesInfo.supportedDynamicRanges
// Collect initial dynamic range constraints. This set will potentially shrink as we add
// more dynamic ranges. We start with the initial set of supported dynamic ranges to
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatTest.kt
index b06ab92..73172a9 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatTest.kt
@@ -64,7 +64,7 @@
fun canSupportDynamicRangeFromHlg10Profile() {
val dynamicRangeProfilesCompat =
DynamicRangeProfilesCompat.toDynamicRangesCompat(HLG10_UNCONSTRAINED)
- Truth.assertThat(dynamicRangeProfilesCompat?.getSupportedDynamicRanges())
+ Truth.assertThat(dynamicRangeProfilesCompat?.supportedDynamicRanges)
.contains(DynamicRange.HLG_10_BIT)
}
@@ -73,7 +73,7 @@
fun canSupportDynamicRangeFromHdr10Profile() {
val dynamicRangeProfilesCompat =
DynamicRangeProfilesCompat.toDynamicRangesCompat(HDR10_UNCONSTRAINED)
- Truth.assertThat(dynamicRangeProfilesCompat?.getSupportedDynamicRanges())
+ Truth.assertThat(dynamicRangeProfilesCompat?.supportedDynamicRanges)
.contains(DynamicRange.HDR10_10_BIT)
}
@@ -82,7 +82,7 @@
fun canSupportDynamicRangeFromHdr10PlusProfile() {
val dynamicRangeProfilesCompat =
DynamicRangeProfilesCompat.toDynamicRangesCompat(HDR10_PLUS_UNCONSTRAINED)
- Truth.assertThat(dynamicRangeProfilesCompat?.getSupportedDynamicRanges())
+ Truth.assertThat(dynamicRangeProfilesCompat?.supportedDynamicRanges)
.contains(DynamicRange.HDR10_PLUS_10_BIT)
}
@@ -91,7 +91,7 @@
fun canSupportDynamicRangeFromDolbyVision10bProfile() {
val dynamicRangeProfilesCompat =
DynamicRangeProfilesCompat.toDynamicRangesCompat(DOLBY_VISION_10B_UNCONSTRAINED)
- Truth.assertThat(dynamicRangeProfilesCompat?.getSupportedDynamicRanges())
+ Truth.assertThat(dynamicRangeProfilesCompat?.supportedDynamicRanges)
.contains(DynamicRange.DOLBY_VISION_10_BIT)
}
@@ -100,7 +100,7 @@
fun canSupportDynamicRangeFromDolbyVision8bProfile() {
val dynamicRangeProfilesCompat =
DynamicRangeProfilesCompat.toDynamicRangesCompat(DOLBY_VISION_8B_UNCONSTRAINED)
- Truth.assertThat(dynamicRangeProfilesCompat?.getSupportedDynamicRanges())
+ Truth.assertThat(dynamicRangeProfilesCompat?.supportedDynamicRanges)
.contains(DynamicRange.DOLBY_VISION_8_BIT)
}
@@ -203,7 +203,7 @@
val dynamicRangeProfilesCompat =
DynamicRangeProfilesCompat.fromCameraMetaData(cameraMetadata)
- Truth.assertThat(dynamicRangeProfilesCompat.getSupportedDynamicRanges())
+ Truth.assertThat(dynamicRangeProfilesCompat.supportedDynamicRanges)
.containsExactly(DynamicRange.SDR)
Truth.assertThat(
dynamicRangeProfilesCompat.getDynamicRangeCaptureRequestConstraints(DynamicRange.SDR)
@@ -225,10 +225,10 @@
DynamicRangeProfilesCompat.fromCameraMetaData(cameraMetadata)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
- Truth.assertThat(dynamicRangeProfilesCompat.getSupportedDynamicRanges())
+ Truth.assertThat(dynamicRangeProfilesCompat.supportedDynamicRanges)
.containsExactly(DynamicRange.SDR)
} else {
- Truth.assertThat(dynamicRangeProfilesCompat.getSupportedDynamicRanges())
+ Truth.assertThat(dynamicRangeProfilesCompat.supportedDynamicRanges)
.containsExactly(
DynamicRange.SDR, DynamicRange.DOLBY_VISION_8_BIT
)
diff --git a/cardview/cardview/src/main/java/androidx/cardview/widget/CardView.java b/cardview/cardview/src/main/java/androidx/cardview/widget/CardView.java
index 08b5580..b940212 100644
--- a/cardview/cardview/src/main/java/androidx/cardview/widget/CardView.java
+++ b/cardview/cardview/src/main/java/androidx/cardview/widget/CardView.java
@@ -86,8 +86,6 @@
static {
if (Build.VERSION.SDK_INT >= 21) {
IMPL = new CardViewApi21Impl();
- } else if (Build.VERSION.SDK_INT >= 17) {
- IMPL = new CardViewApi17Impl();
} else {
IMPL = new CardViewBaseImpl();
}
diff --git a/cardview/cardview/src/main/java/androidx/cardview/widget/CardViewApi17Impl.java b/cardview/cardview/src/main/java/androidx/cardview/widget/CardViewApi17Impl.java
deleted file mode 100644
index 49387fd..0000000
--- a/cardview/cardview/src/main/java/androidx/cardview/widget/CardViewApi17Impl.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.cardview.widget;
-
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.RectF;
-
-import androidx.annotation.RequiresApi;
-
-@RequiresApi(17)
-class CardViewApi17Impl extends CardViewBaseImpl {
-
- @Override
- public void initStatic() {
- RoundRectDrawableWithShadow.sRoundRectHelper =
- new RoundRectDrawableWithShadow.RoundRectHelper() {
- @Override
- public void drawRoundRect(Canvas canvas, RectF bounds, float cornerRadius,
- Paint paint) {
- canvas.drawRoundRect(bounds, cornerRadius, cornerRadius, paint);
- }
- };
- }
-}
diff --git a/cardview/cardview/src/main/java/androidx/cardview/widget/CardViewBaseImpl.java b/cardview/cardview/src/main/java/androidx/cardview/widget/CardViewBaseImpl.java
index bc6cd5d..c5a8dec 100644
--- a/cardview/cardview/src/main/java/androidx/cardview/widget/CardViewBaseImpl.java
+++ b/cardview/cardview/src/main/java/androidx/cardview/widget/CardViewBaseImpl.java
@@ -17,64 +17,17 @@
import android.content.Context;
import android.content.res.ColorStateList;
-import android.graphics.Canvas;
-import android.graphics.Paint;
import android.graphics.Rect;
-import android.graphics.RectF;
import androidx.annotation.Nullable;
class CardViewBaseImpl implements CardViewImpl {
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- final RectF mCornerRect = new RectF();
-
@Override
public void initStatic() {
- // Draws a round rect using 7 draw operations. This is faster than using
- // canvas.drawRoundRect before JBMR1 because API 11-16 used alpha mask textures to draw
- // shapes.
RoundRectDrawableWithShadow.sRoundRectHelper =
- new RoundRectDrawableWithShadow.RoundRectHelper() {
- @Override
- public void drawRoundRect(Canvas canvas, RectF bounds, float cornerRadius,
- Paint paint) {
- final float twoRadius = cornerRadius * 2;
- final float innerWidth = bounds.width() - twoRadius - 1;
- final float innerHeight = bounds.height() - twoRadius - 1;
- if (cornerRadius >= 1f) {
- // increment corner radius to account for half pixels.
- float roundedCornerRadius = cornerRadius + .5f;
- mCornerRect.set(-roundedCornerRadius, -roundedCornerRadius, roundedCornerRadius,
- roundedCornerRadius);
- int saved = canvas.save();
- canvas.translate(bounds.left + roundedCornerRadius,
- bounds.top + roundedCornerRadius);
- canvas.drawArc(mCornerRect, 180, 90, true, paint);
- canvas.translate(innerWidth, 0);
- canvas.rotate(90);
- canvas.drawArc(mCornerRect, 180, 90, true, paint);
- canvas.translate(innerHeight, 0);
- canvas.rotate(90);
- canvas.drawArc(mCornerRect, 180, 90, true, paint);
- canvas.translate(innerWidth, 0);
- canvas.rotate(90);
- canvas.drawArc(mCornerRect, 180, 90, true, paint);
- canvas.restoreToCount(saved);
- //draw top and bottom pieces
- canvas.drawRect(bounds.left + roundedCornerRadius - 1f, bounds.top,
- bounds.right - roundedCornerRadius + 1f,
- bounds.top + roundedCornerRadius, paint);
-
- canvas.drawRect(bounds.left + roundedCornerRadius - 1f,
- bounds.bottom - roundedCornerRadius,
- bounds.right - roundedCornerRadius + 1f, bounds.bottom, paint);
- }
- // center
- canvas.drawRect(bounds.left, bounds.top + cornerRadius,
- bounds.right, bounds.bottom - cornerRadius , paint);
- }
- };
+ (canvas, bounds, cornerRadius, paint) ->
+ canvas.drawRoundRect(bounds, cornerRadius, cornerRadius, paint);
}
@Override
diff --git a/compose/animation/animation/api/current.txt b/compose/animation/animation/api/current.txt
index 2e7140c..f8817a9 100644
--- a/compose/animation/animation/api/current.txt
+++ b/compose/animation/animation/api/current.txt
@@ -19,8 +19,6 @@
public sealed interface AnimatedContentTransitionScope<S> extends androidx.compose.animation.core.Transition.Segment<S> {
method public androidx.compose.ui.Alignment getContentAlignment();
method public default androidx.compose.animation.ExitTransition getKeepUntilTransitionsFinished(androidx.compose.animation.ExitTransition.Companion);
- method @SuppressCompatibility @androidx.compose.animation.ExperimentalAnimationApi public androidx.compose.animation.EnterTransition scaleInToFitContainer(optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale);
- method @SuppressCompatibility @androidx.compose.animation.ExperimentalAnimationApi public androidx.compose.animation.ExitTransition scaleOutToFitContainer(optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale);
method public androidx.compose.animation.EnterTransition slideIntoContainer(int towards, optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> animationSpec, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Integer> initialOffset);
method public androidx.compose.animation.ExitTransition slideOutOfContainer(int towards, optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> animationSpec, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Integer> targetOffset);
method public infix androidx.compose.animation.ContentTransform using(androidx.compose.animation.ContentTransform, androidx.compose.animation.SizeTransform? sizeTransform);
diff --git a/compose/animation/animation/api/restricted_current.txt b/compose/animation/animation/api/restricted_current.txt
index 2e7140c..f8817a9 100644
--- a/compose/animation/animation/api/restricted_current.txt
+++ b/compose/animation/animation/api/restricted_current.txt
@@ -19,8 +19,6 @@
public sealed interface AnimatedContentTransitionScope<S> extends androidx.compose.animation.core.Transition.Segment<S> {
method public androidx.compose.ui.Alignment getContentAlignment();
method public default androidx.compose.animation.ExitTransition getKeepUntilTransitionsFinished(androidx.compose.animation.ExitTransition.Companion);
- method @SuppressCompatibility @androidx.compose.animation.ExperimentalAnimationApi public androidx.compose.animation.EnterTransition scaleInToFitContainer(optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale);
- method @SuppressCompatibility @androidx.compose.animation.ExperimentalAnimationApi public androidx.compose.animation.ExitTransition scaleOutToFitContainer(optional androidx.compose.ui.Alignment alignment, optional androidx.compose.ui.layout.ContentScale contentScale);
method public androidx.compose.animation.EnterTransition slideIntoContainer(int towards, optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> animationSpec, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Integer> initialOffset);
method public androidx.compose.animation.ExitTransition slideOutOfContainer(int towards, optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> animationSpec, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Integer> targetOffset);
method public infix androidx.compose.animation.ContentTransform using(androidx.compose.animation.ContentTransform, androidx.compose.animation.SizeTransform? sizeTransform);
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/layoutanimation/ContainerTransform.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/layoutanimation/ContainerTransform.kt
deleted file mode 100644
index cf03291..0000000
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/layoutanimation/ContainerTransform.kt
+++ /dev/null
@@ -1,246 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.animation.demos.layoutanimation
-
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.animation.core.animateDpAsState
-import androidx.compose.animation.core.animateIntAsState
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.togetherWith
-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.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.Icon
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.RadioButton
-import androidx.compose.material.Text
-import androidx.compose.material.TextField
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material.icons.filled.AccountCircle
-import androidx.compose.material.icons.filled.Add
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.vector.rememberVectorPainter
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-
-@OptIn(ExperimentalAnimationApi::class)
-@Preview
-@Composable
-fun LocalContainerTransformDemo() {
- Box(Modifier.fillMaxSize()) {
- LazyColumn {
- items(20) {
- Box(
- Modifier
- .fillMaxWidth()
- .height(150.dp)
- .padding(15.dp)
- .background(MaterialTheme.colors.primary)
- )
- }
- }
- }
- var selectedAlignment by remember { mutableStateOf(Alignment.Center) }
- var contentScale by remember { mutableStateOf(ContentScale.FillWidth) }
- Column(
- Modifier.padding(top = 100.dp)
- ) {
- Column(
- Modifier
- .background(Color.LightGray, RoundedCornerShape(10.dp)),
- ) {
- Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
- RadioButton(
- selected = selectedAlignment == Alignment.TopStart,
- onClick = { selectedAlignment = Alignment.TopStart }
- )
- Text("TopStart", Modifier.padding(5.dp))
- RadioButton(
- selected = selectedAlignment == Alignment.TopCenter,
- onClick = { selectedAlignment = Alignment.TopCenter }
- )
- Text("TopCenter", Modifier.padding(5.dp))
- RadioButton(
- selected = selectedAlignment == Alignment.TopEnd,
- onClick = { selectedAlignment = Alignment.TopEnd }
- )
- Text("TopEnd", Modifier.padding(5.dp))
- }
- Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
- RadioButton(
- selected = selectedAlignment == Alignment.CenterStart,
- onClick = { selectedAlignment = Alignment.CenterStart }
- )
- Text("CenterStart", Modifier.padding(5.dp))
- RadioButton(
- selected = selectedAlignment == Alignment.Center,
- onClick = { selectedAlignment = Alignment.Center }
- )
- Text("Center", Modifier.padding(5.dp))
- RadioButton(
- selected = selectedAlignment == Alignment.CenterEnd,
- onClick = { selectedAlignment = Alignment.CenterEnd }
- )
- Text("CenterEnd", Modifier.padding(5.dp))
- }
- Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
- RadioButton(
- selected = selectedAlignment == Alignment.BottomStart,
- onClick = { selectedAlignment = Alignment.BottomStart }
- )
- Text("BottomStart", Modifier.padding(5.dp))
- RadioButton(
- selected = selectedAlignment == Alignment.BottomCenter,
- onClick = { selectedAlignment = Alignment.BottomCenter }
- )
- Text("BottomCenter", Modifier.padding(5.dp))
- RadioButton(
- selected = selectedAlignment == Alignment.BottomEnd,
- onClick = { selectedAlignment = Alignment.BottomEnd }
- )
- Text("BottomEnd", Modifier.padding(5.dp))
- }
- }
- Column(
- Modifier
- .background(Color.Gray, RoundedCornerShape(10.dp)),
- ) {
- Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
- RadioButton(
- selected = contentScale == ContentScale.FillWidth,
- onClick = { contentScale = ContentScale.FillWidth }
- )
- Text("FillWidth", Modifier.padding(5.dp))
- RadioButton(
- selected = contentScale == ContentScale.FillHeight,
- onClick = { contentScale = ContentScale.FillHeight }
- )
- Text("FillHeight", Modifier.padding(5.dp))
- RadioButton(
- selected = contentScale == ContentScale.FillBounds,
- onClick = { contentScale = ContentScale.FillBounds }
- )
- Text("FillBounds", Modifier.padding(5.dp))
- }
- Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
- RadioButton(
- selected = contentScale == ContentScale.Crop,
- onClick = { contentScale = ContentScale.Crop }
- )
- Text("Crop", Modifier.padding(5.dp))
- RadioButton(
- selected = contentScale == ContentScale.Fit,
- onClick = { contentScale = ContentScale.Fit }
- )
- Text("Fit", Modifier.padding(5.dp))
- RadioButton(
- selected = contentScale == ContentScale.Inside,
- onClick = { contentScale = ContentScale.Inside }
- )
- Text("Inside", Modifier.padding(5.dp))
- }
- }
- }
- Box(Modifier.fillMaxSize()) {
- var target by remember { mutableStateOf(ContainerState.FAB) }
- // Corner radius
- val cr by animateIntAsState(if (target == ContainerState.FAB) 50 else 0)
- val padding by animateDpAsState(if (target == ContainerState.FAB) 10.dp else 0.dp)
- AnimatedContent(
- target,
- label = "",
- transitionSpec = {
- fadeIn(tween(200, delayMillis = 100)) +
- scaleInToFitContainer(selectedAlignment, contentScale) togetherWith
- fadeOut(tween(100)) + scaleOutToFitContainer(selectedAlignment, contentScale)
- },
- modifier = Modifier
- .align(Alignment.BottomEnd)
- .padding(padding)
- .clip(RoundedCornerShape(cr))
- .background(Color.White)
- ) {
- if (it == ContainerState.FAB) {
- Icon(
- rememberVectorPainter(image = Icons.Default.Add),
- null,
- modifier = Modifier
- .clickable {
- target = ContainerState.FullScreen
- }
- .padding(20.dp))
- } else {
- Column(Modifier.fillMaxSize()) {
- Icon(
- rememberVectorPainter(image = Icons.AutoMirrored.Filled.ArrowBack),
- null,
- modifier = Modifier
- .clickable {
- target = ContainerState.FAB
- }
- .padding(20.dp))
- Spacer(Modifier.height(60.dp))
- Text("Page Title", fontSize = 20.sp, modifier = Modifier.padding(20.dp))
- Spacer(
- Modifier
- .fillMaxWidth()
- .height(2.dp)
- .background(Color.LightGray)
- )
- Row(
- Modifier
- .fillMaxWidth()
- .padding(20.dp)
- ) {
- Icon(rememberVectorPainter(image = Icons.Default.AccountCircle), null)
- Spacer(Modifier.width(20.dp))
- TextField(value = "Account Name", onValueChange = {})
- }
- }
- }
- }
- }
-}
-
-private enum class ContainerState {
- FAB,
- FullScreen
-}
diff --git a/compose/animation/animation/samples/src/main/java/androidx/compose/animation/samples/AnimatedContentSamples.kt b/compose/animation/animation/samples/src/main/java/androidx/compose/animation/samples/AnimatedContentSamples.kt
index c9c4f15..b9eeaff 100644
--- a/compose/animation/animation/samples/src/main/java/androidx/compose/animation/samples/AnimatedContentSamples.kt
+++ b/compose/animation/animation/samples/src/main/java/androidx/compose/animation/samples/AnimatedContentSamples.kt
@@ -22,7 +22,6 @@
import androidx.compose.animation.AnimatedContentTransitionScope.SlideDirection
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.ExitTransition
-import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.keyframes
@@ -55,7 +54,6 @@
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -309,30 +307,6 @@
}
}
-@OptIn(ExperimentalAnimationApi::class)
-@Suppress("UNUSED_VARIABLE")
-@Sampled
-@Composable
-fun ScaleInToFitContainerSample() {
- // enum class CartState { Expanded, Collapsed }
- // This is an example of scaling both the incoming content and outgoing content to fit in the
- // animating container size while animating alpha.
- val transitionSpec: AnimatedContentTransitionScope<CartState>.() -> ContentTransform = {
- // Fade in while scaling the content.
- fadeIn() + scaleInToFitContainer() togetherWith
- // Fade out outgoing content while scaling it. It is important
- // to combine `scaleOutToFitContainer` with another ExitTransition that defines
- // a timeframe for the exit (such as fade/shrink/slide/Hold).
- fadeOut() + scaleOutToFitContainer(
- // Default alignment is the content alignment defined in AnimatedContent
- Alignment.Center,
- // Content will be scaled based on the height of the content. Default content
- // scale is ContentScale.FillWidth.
- ContentScale.FillHeight
- )
- }
-}
-
private enum class CartState {
Expanded,
Collapsed
diff --git a/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt
index 0568edf..56d0163 100644
--- a/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt
+++ b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt
@@ -51,19 +51,13 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.layout.SubcomposeLayout
-import androidx.compose.ui.layout.boundsInRoot
-import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
@@ -72,7 +66,6 @@
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.round
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
@@ -789,334 +782,6 @@
}
@Test
- fun testScaleToFitDefault() {
- var target by mutableStateOf(1)
- var box1Coords: LayoutCoordinates? = null
- var box2Coords: LayoutCoordinates? = null
- var box1Disposed = true
- var box2Disposed = true
- rule.setContent {
- CompositionLocalProvider(LocalDensity provides Density(1f)) {
- AnimatedContent(
- targetState = target,
- transitionSpec = {
- if (1 isTransitioningTo 2) {
- fadeIn(tween(300)) + scaleInToFitContainer() togetherWith
- scaleOutToFitContainer()
- } else {
- fadeIn() + scaleInToFitContainer() togetherWith
- fadeOut(tween(150))
- } using SizeTransform { initialSize, targetSize ->
- keyframes {
- durationMillis = 300
- initialSize at 100 using LinearEasing
- targetSize at 200 using LinearEasing
- }
- }
- }) {
- if (it == 1) {
- Box(
- Modifier
- .onPlaced {
- box1Coords = it
- }
- .size(200.dp, 400.dp)) {
- DisposableEffect(key1 = Unit) {
- box1Disposed = false
- onDispose {
- box1Disposed = true
- }
- }
- }
- } else {
- Box(
- Modifier
- .onPlaced { box2Coords = it }
- .size(100.dp, 50.dp)) {
-
- DisposableEffect(key1 = Unit) {
- box2Disposed = false
- onDispose {
- box2Disposed = true
- }
- }
- }
- }
- }
- }
- }
-
- rule.waitForIdle()
- rule.mainClock.autoAdvance = false
- assertEquals(IntSize(200, 400), box1Coords?.size)
- assertNull(box2Coords)
-
- assertFalse(box1Disposed)
- assertTrue(box2Disposed)
-
- rule.runOnIdle {
- // Start transition from 1 -> 2, size 200,400 -> 100,50
- target = 2
- }
- rule.mainClock.advanceTimeByFrame()
- rule.waitForIdle()
-
- // Box1 doesn't have any other ExitTransition than scale, so it'll be disposed
- // after a couple of frames
- assertFalse(box1Disposed)
- assertFalse(box2Disposed)
-
- repeat(20) {
- rule.mainClock.advanceTimeByFrame()
-
- val playTime = 16 * it
- val bounds2 = box2Coords?.boundsInRoot()
- if (playTime <= 100) {
- assertEquals(Rect(0f, 0f, 200f, 100f), bounds2)
- } else if (playTime <= 200) {
- val fraction = (playTime - 100) / 100f
- val width = 200 * (1 - fraction) + 100 * fraction
- // Since we are testing default behavior, the scaling is based on width.
- val height = width / 100f * 50
- assertEquals(Offset.Zero, bounds2?.topLeft)
- assertEquals(width, bounds2?.width)
- assertEquals(height, bounds2?.height)
- } else {
- assertEquals(Rect(0f, 0f, 100f, 50f), bounds2)
- }
- }
-
- rule.runOnIdle {
- // Start transition from false -> true, size 100, 50 -> 200,400
- target = 1
- }
- rule.mainClock.advanceTimeByFrame()
- rule.waitForIdle()
-
- assertFalse(box1Disposed)
- assertFalse(box2Disposed)
-
- repeat(20) {
- rule.mainClock.advanceTimeByFrame()
- val playTime = 16 * it
- val bounds = box1Coords?.boundsInRoot()
- if (playTime <= 100) {
- assertEquals(100f, bounds?.width)
- assertFalse(box2Disposed)
- } else if (playTime <= 150) {
- val fraction = (playTime - 100) / 100f
- val width = 100 * (1 - fraction) + 200 * fraction
- // Since we are testing default behavior, the scaling is based on width.
- assertEquals(Offset.Zero, bounds?.topLeft)
- assertEquals(width, bounds?.width)
- } else {
- rule.waitForIdle()
- assertThat(box2Disposed)
- }
- }
- }
-
- @Test
- fun testScaleToFitCenterAlignment() {
- var target by mutableStateOf(true)
- var box1Coords: LayoutCoordinates? = null
- var box2Coords: LayoutCoordinates? = null
- var layoutDirection: LayoutDirection? = null
- rule.setContent {
- CompositionLocalProvider(LocalDensity provides Density(1f)) {
- layoutDirection = LocalLayoutDirection.current
- AnimatedContent(
- targetState = target,
- transitionSpec = {
- fadeIn() + scaleInToFitContainer(Alignment.Center) togetherWith
- fadeOut(tween(100)) using
- SizeTransform { _, _ ->
- tween(100, easing = LinearEasing)
- }
- }) {
- if (target) {
- Box(
- Modifier
- .onPlaced {
- box1Coords = it
- }
- .size(200.dp, 400.dp))
- } else {
- Box(
- Modifier
- .onPlaced { box2Coords = it }
- .size(100.dp, 50.dp))
- }
- }
- }
- }
-
- rule.waitForIdle()
- assertEquals(IntSize(200, 400), box1Coords?.size)
- assertNull(box2Coords)
-
- rule.runOnIdle {
- // Start transition from true -> false, size 200,400 -> 100,50
- target = false
- }
- rule.mainClock.advanceTimeByFrame()
- repeat(10) {
- rule.mainClock.advanceTimeByFrame()
- val playTime = 16 * it
- val bounds = box2Coords?.boundsInRoot()
- assertNotNull(bounds)
- val fraction = (playTime / 100f).coerceAtMost(1f)
- val width = 200 * (1 - fraction) + 100 * fraction
- val containerHeight = 400 * (1 - fraction) + 50 * fraction
- // Since we are testing default behavior, the scaling is based on width.
- val height = width / 100f * 50
- assertEquals(width, bounds!!.width, 0.01f)
- assertEquals(height, bounds.height, 0.01f)
- val offset = Alignment.Center.align(
- IntSize(width.roundToInt(), height.roundToInt()),
- IntSize(width.roundToInt(), containerHeight.roundToInt()), layoutDirection!!
- )
- assertEquals(offset, bounds.topLeft.round())
- }
- }
-
- @Test
- fun testScaleToFitBottomCenterAlignment() {
- var target by mutableStateOf(true)
- var box1Coords: LayoutCoordinates? = null
- var box2Coords: LayoutCoordinates? = null
- var layoutDirection: LayoutDirection? = null
- rule.setContent {
- CompositionLocalProvider(LocalDensity provides Density(1f)) {
- layoutDirection = LocalLayoutDirection.current
- AnimatedContent(
- targetState = target,
- transitionSpec = {
- fadeIn() + scaleInToFitContainer(
- Alignment.BottomCenter
- ) togetherWith
- fadeOut(tween(100)) using
- SizeTransform { _, _ ->
- tween(100, easing = LinearEasing)
- }
- }) {
- if (target) {
- Box(
- Modifier
- .onPlaced {
- box1Coords = it
- }
- .size(200.dp, 400.dp))
- } else {
- Box(
- Modifier
- .onPlaced { box2Coords = it }
- .size(100.dp, 50.dp))
- }
- }
- }
- }
-
- rule.waitForIdle()
- rule.mainClock.autoAdvance = false
- assertEquals(IntSize(200, 400), box1Coords?.size)
- assertNull(box2Coords)
-
- rule.runOnIdle {
- // Start transition from true -> false, size 200,400 -> 100,50
- target = false
- }
- rule.mainClock.advanceTimeByFrame()
- repeat(10) {
- rule.mainClock.advanceTimeByFrame()
- val playTime = 16 * it
- val bounds = box2Coords?.boundsInRoot()
- assertNotNull(bounds)
- val fraction = (playTime / 100f).coerceAtMost(1f)
- val width = 200 * (1 - fraction) + 100 * fraction
- val containerHeight = 400 * (1 - fraction) + 50 * fraction
- // Since we are testing default behavior, the scaling is based on width.
- val height = width / 100f * 50
- assertEquals(width, bounds!!.width, 0.01f)
- assertEquals(height, bounds.height, 0.01f)
- val offset = Alignment.BottomCenter.align(
- IntSize(width.roundToInt(), height.roundToInt()),
- IntSize(width.roundToInt(), containerHeight.roundToInt()), layoutDirection!!
- )
- assertEquals(offset, bounds.topLeft.round())
- }
- }
-
- @Test
- fun testScaleToFitInsideBottomEndAlignment() {
- var target by mutableStateOf(true)
- var box1Coords: LayoutCoordinates? = null
- var box2Coords: LayoutCoordinates? = null
- var layoutDirection: LayoutDirection? = null
- rule.setContent {
- CompositionLocalProvider(LocalDensity provides Density(1f)) {
- layoutDirection = LocalLayoutDirection.current
- AnimatedContent(
- targetState = target,
- transitionSpec = {
- fadeIn() + scaleInToFitContainer(
- Alignment.BottomEnd, ContentScale.Inside
- ) togetherWith
- fadeOut(tween(100)) using
- SizeTransform { _, _ ->
- tween(100, easing = LinearEasing)
- }
- }) {
- if (target) {
- Box(
- Modifier
- .onPlaced {
- box1Coords = it
- }
- .size(200.dp, 400.dp))
- } else {
- Box(
- Modifier
- .onPlaced { box2Coords = it }
- .size(100.dp, 50.dp))
- }
- }
- }
- }
-
- rule.waitForIdle()
- rule.mainClock.autoAdvance = false
- assertEquals(IntSize(200, 400), box1Coords?.size)
- assertNull(box2Coords)
-
- rule.runOnIdle {
- // Start transition from true -> false, size 200,400 -> 100,50
- target = false
- }
- rule.mainClock.advanceTimeByFrame()
- repeat(10) {
- rule.mainClock.advanceTimeByFrame()
- val playTime = 16 * it
- val bounds = box2Coords?.boundsInRoot()
- assertNotNull(bounds)
- val fraction = (playTime / 100f).coerceAtMost(1f)
- val width = 100f
- val containerWidth = 200 * (1 - fraction) + 100 * fraction
- val containerHeight = 400 * (1 - fraction) + 50 * fraction
- // Since we are testing default behavior, the scaling is based on width.
- val height = 50f
- assertEquals(width, bounds!!.width, 0.01f)
- assertEquals(height, bounds.height, 0.01f)
- val offset = Alignment.BottomEnd.align(
- IntSize(width.roundToInt(), height.roundToInt()),
- IntSize(containerWidth.roundToInt(), containerHeight.roundToInt()),
- layoutDirection!!
- )
- assertEquals(offset, bounds.topLeft.round())
- }
- }
-
- @Test
fun testRightEnterExitTransitionIsChosenDuringInterruption() {
var flag by mutableStateOf(false)
var fixedPosition: Offset? = null
@@ -1187,72 +852,6 @@
rule.waitForIdle()
}
- @Test
- fun testScaleToFitWithFitHeight() {
- var target by mutableStateOf(true)
- var box1Coords: LayoutCoordinates? = null
- var box2Coords: LayoutCoordinates? = null
- var layoutDirection: LayoutDirection? = null
- rule.setContent {
- CompositionLocalProvider(LocalDensity provides Density(1f)) {
- layoutDirection = LocalLayoutDirection.current
- AnimatedContent(
- targetState = target,
- transitionSpec = {
- fadeIn() + scaleInToFitContainer(
- Alignment.Center, ContentScale.FillHeight
- ) togetherWith fadeOut(tween(100)) using
- SizeTransform { _, _ ->
- tween(100, easing = LinearEasing)
- }
- }) {
- if (target) {
- Box(
- Modifier
- .onPlaced {
- box1Coords = it
- }
- .size(200.dp, 400.dp))
- } else {
- Box(
- Modifier
- .onPlaced { box2Coords = it }
- .size(100.dp, 250.dp))
- }
- }
- }
- }
-
- rule.waitForIdle()
- rule.mainClock.autoAdvance = false
- assertEquals(IntSize(200, 400), box1Coords?.size)
- assertNull(box2Coords)
-
- rule.runOnIdle {
- // Start transition from true -> false, size 200,400 -> 100,250
- target = false
- }
- rule.mainClock.advanceTimeByFrame()
- repeat(10) {
- rule.mainClock.advanceTimeByFrame()
- val playTime = 16 * it
- val bounds = box2Coords?.boundsInRoot()
- assertNotNull(bounds)
- val fraction = (playTime / 100f).coerceAtMost(1f)
- val height = 400 * (1 - fraction) + 250 * fraction
- val containerWidth = 200 * (1 - fraction) + 100 * fraction
- // Since we are testing default behavior, the scaling is based on width.
- val width = height / 250f * 100
- assertEquals(width, bounds!!.width, 0.01f)
- assertEquals(height, bounds.height, 0.01f)
- val offset = Alignment.Center.align(
- IntSize(width.roundToInt(), height.roundToInt()),
- IntSize(containerWidth.roundToInt(), height.roundToInt()), layoutDirection!!
- )
- assertEquals(offset, bounds.topLeft.round())
- }
- }
-
@OptIn(ExperimentalAnimationApi::class)
@Test
fun testExitHoldDefersUntilAllFinished() {
@@ -1327,51 +926,6 @@
assertTrue(box2EnterFinished)
}
- /**
- * This test checks that scaleInToFitContainer and scaleOutToFitContainer handle empty
- * content correctly.
- */
- @Test
- fun testAnimateToEmptyComposable() {
- var isEmpty by mutableStateOf(false)
- var targetSize: IntSize? = null
- rule.setContent {
- CompositionLocalProvider(LocalDensity provides Density(1f)) {
- AnimatedContent(targetState = isEmpty,
- transitionSpec = {
- scaleInToFitContainer() togetherWith scaleOutToFitContainer()
- },
- modifier = Modifier.layout { measurable, constraints ->
- measurable.measure(constraints).run {
- if (isLookingAhead) {
- targetSize = IntSize(width, height)
- }
- layout(width, height) {
- place(0, 0)
- }
- }
- }
- ) {
- if (!it) {
- Box(Modifier.size(200.dp))
- }
- }
- }
- }
- rule.runOnIdle {
- assertEquals(IntSize(200, 200), targetSize)
- isEmpty = true
- }
-
- rule.runOnIdle {
- assertEquals(IntSize.Zero, targetSize)
- isEmpty = !isEmpty
- }
- rule.runOnIdle {
- assertEquals(IntSize(200, 200), targetSize)
- }
- }
-
@OptIn(InternalAnimationApi::class)
private val Transition<*>.playTimeMillis get() = (playTimeNanos / 1_000_000L).toInt()
}
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedContent.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedContent.kt
index 24ce9d9..896308a 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedContent.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedContent.kt
@@ -1,3 +1,4 @@
+
/*
* Copyright 2021 The Android Open Source Project
*
@@ -13,10 +14,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-@file:OptIn(InternalAnimationApi::class, ExperimentalAnimationApi::class)
-
+@file:OptIn(InternalAnimationApi::class)
package androidx.compose.animation
-
import androidx.compose.animation.AnimatedContentTransitionScope.SlideDirection.Companion.Down
import androidx.compose.animation.AnimatedContentTransitionScope.SlideDirection.Companion.End
import androidx.compose.animation.AnimatedContentTransitionScope.SlideDirection.Companion.Left
@@ -37,7 +36,6 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutable
-import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
@@ -45,44 +43,30 @@
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
-import androidx.compose.ui.graphics.TransformOrigin
-import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.IntrinsicMeasureScope
import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.ParentDataModifier
import androidx.compose.ui.layout.Placeable
-import androidx.compose.ui.layout.ScaleFactor
import androidx.compose.ui.layout.layout
-import androidx.compose.ui.node.LayoutModifierNode
-import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.toSize
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
import androidx.compose.ui.util.fastMaxOfOrNull
-import androidx.compose.ui.util.fastRoundToInt
-import kotlinx.coroutines.CoroutineScope
-
/**
* [AnimatedContent] is a container that automatically animates its content when [targetState]
* changes. Its [content] for different target states is defined in a mapping between a target
@@ -161,7 +145,6 @@
content = content
)
}
-
/**
* [ContentTransform] defines how the target content (i.e. content associated with target state)
* enters [AnimatedContent] and how the initial content disappears.
@@ -212,7 +195,6 @@
* content with the same index, the target content will be placed on top.
*/
var targetContentZIndex by mutableFloatStateOf(targetContentZIndex)
-
/**
* [sizeTransform] manages the expanding and shrinking of the container if there is any size
* change as new content enters the [AnimatedContent] and old content leaves.
@@ -223,7 +205,6 @@
var sizeTransform: SizeTransform? = sizeTransform
internal set
}
-
/**
* This creates a [SizeTransform] with the provided [clip] and [sizeAnimationSpec]. By default,
* [clip] will be true. This means during the size animation, the content will be clipped to the
@@ -241,7 +222,6 @@
)
}
): SizeTransform = SizeTransformImpl(clip, sizeAnimationSpec)
-
/**
* [SizeTransform] defines how to transform from one size to another when the size of the content
* changes. When [clip] is true, the content will be clipped to the animation size.
@@ -255,14 +235,12 @@
* Whether the content should be clipped using the animated size.
*/
val clip: Boolean
-
/**
* This allows [FiniteAnimationSpec] to be defined based on the [initialSize] before the size
* animation and the [targetSize] of the animation.
*/
fun createAnimationSpec(initialSize: IntSize, targetSize: IntSize): FiniteAnimationSpec<IntSize>
}
-
/**
* Private implementation of SizeTransform interface.
*/
@@ -276,7 +254,6 @@
targetSize: IntSize
): FiniteAnimationSpec<IntSize> = sizeAnimationSpec(initialSize, targetSize)
}
-
/**
* This creates a [ContentTransform] using the provided [EnterTransition] and [exit], where the
* enter and exit transition will be running simultaneously.
@@ -285,14 +262,12 @@
* @sample androidx.compose.animation.samples.AnimatedContentTransitionSpecSample
*/
infix fun EnterTransition.togetherWith(exit: ExitTransition) = ContentTransform(this, exit)
-
@ExperimentalAnimationApi
@Deprecated(
"Infix fun EnterTransition.with(ExitTransition) has been renamed to" +
" togetherWith", ReplaceWith("togetherWith(exit)")
)
infix fun EnterTransition.with(exit: ExitTransition) = ContentTransform(this, exit)
-
/**
* [AnimatedContentTransitionScope] provides functions that are convenient and only applicable in the
* context of [AnimatedContent], such as [slideIntoContainer] and [slideOutOfContainer].
@@ -304,7 +279,6 @@
* @sample androidx.compose.animation.samples.AnimatedContentTransitionSpecSample
*/
infix fun ContentTransform.using(sizeTransform: SizeTransform?): ContentTransform
-
/**
* [SlideDirection] defines the direction of the slide in/out for [slideIntoContainer] and
* [slideOutOfContainer]. The supported directions are: [Left], [Right], [Up] and [Down].
@@ -320,7 +294,6 @@
val Start = SlideDirection(4)
val End = SlideDirection(5)
}
-
override fun toString(): String {
return when (this) {
Left -> "Left"
@@ -333,7 +306,6 @@
}
}
}
-
/**
* This defines a horizontal/vertical slide-in that is specific to [AnimatedContent] from the
* edge of the container. The offset amount is dynamically calculated based on the current
@@ -363,7 +335,6 @@
),
initialOffset: (offsetForFullSlide: Int) -> Int = { it }
): EnterTransition
-
/**
* This defines a horizontal/vertical exit transition to completely slide out of the
* [AnimatedContent] container. The offset amount is dynamically calculated based on the current
@@ -392,7 +363,6 @@
),
targetOffset: (offsetForFullSlide: Int) -> Int = { it }
): ExitTransition
-
/**
* [KeepUntilTransitionsFinished] defers the disposal of the exiting content till both enter and
* exit transitions have finished. It can be combined with other [ExitTransition]s using
@@ -408,81 +378,28 @@
*/
val ExitTransition.Companion.KeepUntilTransitionsFinished: ExitTransition
get() = KeepUntilTransitionsFinished
-
/**
* This returns the [Alignment] specified on [AnimatedContent].
*/
val contentAlignment: Alignment
-
- /**
- * [scaleInToFitContainer] defines an [EnterTransition] that scales the incoming content
- * based on the (potentially animating) container (i.e. [AnimatedContent]) size. [contentScale]
- * defines the scaling function. By default, the incoming content will be scaled based on its
- * width (i.e. [ContentScale.FillWidth]), so that the content fills the container's width.
- * [alignment] can be used to specify the alignment of the scaled content
- * within the container of AnimatedContent.
- *
- * [scaleInToFitContainer] will measure the content using the final (i.e. lookahead)
- * constraints, in order to obtain the final layout and apply scaling to that final layout
- * while the container is resizing.
- *
- * @sample androidx.compose.animation.samples.ScaleInToFitContainerSample
- */
- @ExperimentalAnimationApi
- fun scaleInToFitContainer(
- alignment: Alignment = contentAlignment,
- contentScale: ContentScale = ContentScale.FillWidth
- ): EnterTransition
-
- /**
- * [scaleOutToFitContainer] defines an [ExitTransition] that scales the outgoing content
- * based on the (potentially animating) container (i.e. [AnimatedContent]) size.
- * [contentScale] defines the scaling function. By default, the outgoing content will be scaled
- * using [ContentScale.FillWidth], so that it fits the container's width.
- * [alignment] can be used to specify the alignment of the scaled content
- * within the container of AnimatedContent.
- *
- * [scaleOutToFitContainer] will measure the content using the constraints cached
- * at the beginning of the exit animation so that the content does not get re-laid out during
- * the exit animation, and instead only scaling will be applied as the container resizes.
- *
- * **IMPORTANT**: [scaleOutToFitContainer] does NOT keep the exiting content from being
- * disposed. Therefore it relies on other ExitTransitions such as [fadeOut] to define a
- * timeframe for when should be active.
- *
- * @sample androidx.compose.animation.samples.ScaleInToFitContainerSample
- */
- @ExperimentalAnimationApi
- fun scaleOutToFitContainer(
- alignment: Alignment = contentAlignment,
- contentScale: ContentScale = ContentScale.FillWidth,
- ): ExitTransition
}
-
-internal class AnimatedContentRootScope<S> internal constructor(
+internal class AnimatedContentTransitionScopeImpl<S> internal constructor(
internal val transition: Transition<S>,
- lookaheadScope: LookaheadScope,
- internal val coroutineScope: CoroutineScope,
override var contentAlignment: Alignment,
internal var layoutDirection: LayoutDirection
-) : AnimatedContentTransitionScope<S>, LookaheadScope by lookaheadScope {
- lateinit var rootCoords: LayoutCoordinates
- lateinit var rootLookaheadCoords: LayoutCoordinates
-
+) : AnimatedContentTransitionScope<S> {
/**
* Initial state of a Transition Segment. This is the state that transition starts from.
*/
override val initialState: S
@Suppress("UnknownNullness")
get() = transition.segment.initialState
-
/**
* Target state of a Transition Segment. This is the state that transition will end on.
*/
override val targetState: S
@Suppress("UnknownNullness")
get() = transition.segment.targetState
-
/**
* Customizes the [SizeTransform] of a given [ContentTransform]. For example:
*
@@ -491,7 +408,6 @@
override infix fun ContentTransform.using(sizeTransform: SizeTransform?) = this.apply {
this.sizeTransform = sizeTransform
}
-
/**
* This defines a horizontal/vertical slide-in that is specific to [AnimatedContent] from the
* edge of the container. The offset amount is dynamically calculated based on the current
@@ -528,24 +444,19 @@
currentSize.width - calculateOffset(IntSize(it, it), currentSize).x
)
}
-
towards.isRight -> slideInHorizontally(animationSpec) {
initialOffset.invoke(-calculateOffset(IntSize(it, it), currentSize).x - it)
}
-
towards == Up -> slideInVertically(animationSpec) {
initialOffset.invoke(
currentSize.height - calculateOffset(IntSize(it, it), currentSize).y
)
}
-
towards == Down -> slideInVertically(animationSpec) {
initialOffset.invoke(-calculateOffset(IntSize(it, it), currentSize).y - it)
}
-
else -> EnterTransition.None
}
-
private val AnimatedContentTransitionScope.SlideDirection.isLeft: Boolean
get() {
return this == Left || this == Start && layoutDirection == LayoutDirection.Ltr ||
@@ -556,11 +467,9 @@
return this == Right || this == Start && layoutDirection == LayoutDirection.Rtl ||
this == End && layoutDirection == LayoutDirection.Ltr
}
-
private fun calculateOffset(fullSize: IntSize, currentSize: IntSize): IntOffset {
return contentAlignment.align(fullSize, currentSize, LayoutDirection.Ltr)
}
-
/**
* This defines a horizontal/vertical exit transition to completely slide out of the
* [AnimatedContent] container. The offset amount is dynamically calculated based on the current
@@ -596,69 +505,32 @@
val targetSize = targetSizeMap[transition.targetState]?.value ?: IntSize.Zero
targetOffset.invoke(-calculateOffset(IntSize(it, it), targetSize).x - it)
}
-
towards.isRight -> slideOutHorizontally(animationSpec) {
val targetSize = targetSizeMap[transition.targetState]?.value ?: IntSize.Zero
targetOffset.invoke(
-calculateOffset(IntSize(it, it), targetSize).x + targetSize.width
)
}
-
towards == Up -> slideOutVertically(animationSpec) {
val targetSize = targetSizeMap[transition.targetState]?.value ?: IntSize.Zero
targetOffset.invoke(-calculateOffset(IntSize(it, it), targetSize).y - it)
}
-
towards == Down -> slideOutVertically(animationSpec) {
val targetSize = targetSizeMap[transition.targetState]?.value ?: IntSize.Zero
targetOffset.invoke(
-calculateOffset(IntSize(it, it), targetSize).y + targetSize.height
)
}
-
else -> ExitTransition.None
}
}
-
- @ExperimentalAnimationApi
- override fun scaleInToFitContainer(
- alignment: Alignment,
- contentScale: ContentScale
- ): EnterTransition = EnterTransition(
- ScaleToFitTransitionKey, ScaleToFitInLookaheadElement(
- this@AnimatedContentRootScope,
- contentScale,
- alignment
- )
- )
-
- @ExperimentalAnimationApi
- override fun scaleOutToFitContainer(
- alignment: Alignment,
- contentScale: ContentScale
- ): ExitTransition = ExitTransition(
- ScaleToFitTransitionKey,
- ScaleToFitInLookaheadElement(
- this@AnimatedContentRootScope,
- contentScale,
- alignment
- )
- )
-
internal var measuredSize: IntSize by mutableStateOf(IntSize.Zero)
- internal val targetSizeMap = mutableMapOf<S, MutableState<IntSize>>()
+ internal val targetSizeMap = mutableMapOf<S, State<IntSize>>()
internal var animatedSize: State<IntSize>? = null
-
// Current size of the container. If there's any size animation, the current size will be
// read from the animation value, otherwise we'll use the current
- internal val currentSize: IntSize
+ private val currentSize: IntSize
get() = animatedSize?.value ?: measuredSize
-
- internal val targetSize: IntSize
- get() = requireNotNull(targetSizeMap[targetState]) {
- "Error: Target size for AnimatedContent has not been set."
- }.value
-
@Suppress("ComposableModifierFactory", "ModifierFactoryExtensionFunction")
@Composable
internal fun createSizeAnimationModifier(
@@ -668,47 +540,67 @@
val sizeTransform = rememberUpdatedState(contentTransform.sizeTransform)
if (transition.currentState == transition.targetState) {
shouldAnimateSize = false
- } else if (sizeTransform.value != null) {
- shouldAnimateSize = true
+ } else {
+ // TODO: CurrentSize is only relevant to enter/exit transition, not so much for sizeAnim
+ if (sizeTransform.value != null) {
+ shouldAnimateSize = true
+ }
}
-
return if (shouldAnimateSize) {
- val sizeAnimation =
- transition.createDeferredAnimation(IntSize.VectorConverter, "sizeTransform")
+ val sizeAnimation = transition.createDeferredAnimation(IntSize.VectorConverter)
remember(sizeAnimation) {
(if (sizeTransform.value?.clip == false) Modifier else Modifier.clipToBounds())
- .then(
- SizeModifierInLookaheadElement(
- this, sizeAnimation, sizeTransform
- )
- )
+ .then(SizeModifier(sizeAnimation, sizeTransform))
}
} else {
animatedSize = null
Modifier
}
}
-
// This helps track the target measurable without affecting the placement order. Target
// measurable needs to be measured first but placed last.
- internal data class ChildData<T>(var targetState: T) : ParentDataModifier {
+ internal data class ChildData(var isTarget: Boolean) : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any {
return this@ChildData
}
}
+ private inner class SizeModifier(
+ val sizeAnimation: Transition<S>.DeferredAnimation<IntSize, AnimationVector2D>,
+ val sizeTransform: State<SizeTransform?>,
+ ) : LayoutModifierWithPassThroughIntrinsics() {
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ val placeable = measurable.measure(constraints)
+ val size = sizeAnimation.animate(
+ transitionSpec = {
+ val initial = targetSizeMap[initialState]?.value ?: IntSize.Zero
+ val target = targetSizeMap[targetState]?.value ?: IntSize.Zero
+ sizeTransform.value?.createAnimationSpec(initial, target) ?: spring()
+ }
+ ) {
+ targetSizeMap[it]?.value ?: IntSize.Zero
+ }
+ animatedSize = size
+ val offset = contentAlignment.align(
+ IntSize(placeable.width, placeable.height), size.value, LayoutDirection.Ltr
+ )
+ return layout(size.value.width, size.value.height) {
+ placeable.place(offset)
+ }
+ }
+ }
}
-
/**
* Receiver scope for content lambda for AnimatedContent. In this scope,
* [transition][AnimatedVisibilityScope.transition] can be used to observe the state of the
* transition, or to add more enter/exit transition for the content.
*/
sealed interface AnimatedContentScope : AnimatedVisibilityScope
-
private class AnimatedContentScopeImpl internal constructor(
animatedVisibilityScope: AnimatedVisibilityScope
) : AnimatedContentScope, AnimatedVisibilityScope by animatedVisibilityScope
-
/**
* [AnimatedContent] is a container that automatically animates its content when
* [Transition.targetState] changes. Its [content] for different target states is defined in a
@@ -772,268 +664,145 @@
content: @Composable() AnimatedContentScope.(targetState: S) -> Unit
) {
val layoutDirection = LocalLayoutDirection.current
- val coroutineScope = rememberCoroutineScope()
- LookaheadScope {
- val rootScope = remember(this@AnimatedContent) {
- AnimatedContentRootScope(
- this@AnimatedContent, this@LookaheadScope,
- coroutineScope, contentAlignment, layoutDirection
- )
- }
- val currentlyVisible = remember(this) { mutableStateListOf(currentState) }
- val contentMap = remember(this) { mutableMapOf<S, @Composable() () -> Unit>() }
- val constraintsMap = remember { mutableMapOf<S, Constraints>() }
-
- // This is needed for tooling because it could change currentState directly,
- // as opposed to changing target only. When that happens we need to clear all the
- // visible content and only display the content for the new current state and target state.
- if (!currentlyVisible.contains(currentState)) {
+ val rootScope = remember(this) {
+ AnimatedContentTransitionScopeImpl(this, contentAlignment, layoutDirection)
+ }
+ // TODO: remove screen as soon as they are animated out
+ val currentlyVisible = remember(this) { mutableStateListOf(currentState) }
+ val contentMap = remember(this) { mutableMapOf<S, @Composable() () -> Unit>() }
+ // This is needed for tooling because it could change currentState directly,
+ // as opposed to changing target only. When that happens we need to clear all the
+ // visible content and only display the content for the new current state and target state.
+ if (!currentlyVisible.contains(currentState)) {
+ currentlyVisible.clear()
+ currentlyVisible.add(currentState)
+ }
+ if (currentState == targetState) {
+ if (currentlyVisible.size != 1 || currentlyVisible[0] != currentState) {
currentlyVisible.clear()
currentlyVisible.add(currentState)
}
-
- if (currentState == targetState) {
- if (currentlyVisible.size != 1 || currentlyVisible[0] != currentState) {
- currentlyVisible.clear()
- currentlyVisible.add(currentState)
- }
- if (contentMap.size != 1 || contentMap.containsKey(currentState)) {
- contentMap.clear()
- }
- val targetConstraints = constraintsMap[targetState]
- constraintsMap.clear()
- targetConstraints?.let { constraintsMap[targetState] = it }
- // TODO: Do we want to support changing contentAlignment amid animation?
- rootScope.contentAlignment = contentAlignment
- rootScope.layoutDirection = layoutDirection
- } else if (!currentlyVisible.contains(targetState)) {
- // Currently visible list always keeps the targetState at the end of the list, unless
- // it's already in the list in the case of interruption. This makes the composable
- // associated with the targetState get placed last, so the target composable will be
- // displayed on top of content associated with other states, unless zIndex is specified.
- // Replace the target with the same key if any.
- val id = currentlyVisible.indexOfFirst { contentKey(it) == contentKey(targetState) }
- if (id == -1) {
- currentlyVisible.add(targetState)
- } else {
- currentlyVisible[id] = targetState
- }
- }
- if (!contentMap.containsKey(targetState) || !contentMap.containsKey(currentState)) {
+ if (contentMap.size != 1 || contentMap.containsKey(currentState)) {
contentMap.clear()
- currentlyVisible.fastForEach { stateForContent ->
- contentMap[stateForContent] = {
- // Only update content transform when enter/exit _direction_ changes.
- val contentTransform = remember(stateForContent == targetState) {
- rootScope.transitionSpec()
- }
- PopulateContentFor(
- stateForContent, rootScope, contentTransform, currentlyVisible, content
- )
- }
- }
}
- val contentTransform = remember(rootScope, segment) { transitionSpec(rootScope) }
- val sizeModifier = rootScope.createSizeAnimationModifier(contentTransform)
- Layout(
- modifier = modifier
- .layout { measurable, constraints ->
- val placeable = measurable.measure(constraints)
- layout(placeable.width, placeable.height) {
- coordinates?.let {
- if (isLookingAhead) {
- rootScope.rootLookaheadCoords = it
- } else {
- rootScope.rootCoords = it
+ // TODO: Do we want to support changing contentAlignment amid animation?
+ rootScope.contentAlignment = contentAlignment
+ rootScope.layoutDirection = layoutDirection
+ }
+ // Currently visible list always keeps the targetState at the end of the list, unless it's
+ // already in the list in the case of interruption. This makes the composable associated with
+ // the targetState get placed last, so the target composable will be displayed on top of
+ // content associated with other states, unless zIndex is specified.
+ if (currentState != targetState && !currentlyVisible.contains(targetState)) {
+ // Replace the target with the same key if any
+ val id = currentlyVisible.indexOfFirst { contentKey(it) == contentKey(targetState) }
+ if (id == -1) {
+ currentlyVisible.add(targetState)
+ } else {
+ currentlyVisible[id] = targetState
+ }
+ }
+ if (!contentMap.containsKey(targetState) || !contentMap.containsKey(currentState)) {
+ contentMap.clear()
+ currentlyVisible.fastForEach { stateForContent ->
+ contentMap[stateForContent] = {
+ val specOnEnter = remember { transitionSpec(rootScope) }
+ // NOTE: enter and exit for this AnimatedVisibility will be using different spec,
+ // naturally.
+ val exit =
+ remember(segment.targetState == stateForContent) {
+ if (segment.targetState == stateForContent) {
+ ExitTransition.None
+ } else {
+ rootScope.transitionSpec().initialContentExit
+ }
+ }
+ val childData = remember {
+ AnimatedContentTransitionScopeImpl.ChildData(stateForContent == targetState)
+ }
+ // TODO: Will need a custom impl of this to: 1) get the signal for when
+ // the animation is finished, 2) get the target size properly
+ AnimatedEnterExitImpl(
+ this,
+ { it == stateForContent },
+ enter = specOnEnter.targetContentEnter,
+ exit = exit,
+ modifier = Modifier
+ .layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0, zIndex = specOnEnter.targetContentZIndex)
}
}
- placeable.place(0, 0)
+ .then(childData.apply { isTarget = stateForContent == targetState }),
+ shouldDisposeBlock = { currentState, targetState ->
+ currentState == EnterExitState.PostExit &&
+ targetState == EnterExitState.PostExit &&
+ !exit.data.hold
+ }
+ ) {
+ // TODO: Should Transition.AnimatedVisibility have an end listener?
+ DisposableEffect(this) {
+ onDispose {
+ currentlyVisible.remove(stateForContent)
+ rootScope.targetSizeMap.remove(stateForContent)
+ }
+ }
+ rootScope.targetSizeMap[stateForContent] =
+ (this as AnimatedVisibilityScopeImpl).targetSize
+ with(remember { AnimatedContentScopeImpl(this) }) {
+ content(stateForContent)
}
}
- .then(sizeModifier),
- content = {
- currentlyVisible.fastForEach {
- key(contentKey(it)) { contentMap[it]?.invoke() }
- }
- },
- measurePolicy = remember {
- AnimatedContentMeasurePolicy(
- rootScope, constraintsMap
- )
}
- )
- }
-}
-
-/**
- * Creates content for a specific state based on the current Transition, enter/exit and the content
- * lookup lambda.
- */
-@Composable
-private inline fun <S> Transition<S>.PopulateContentFor(
- stateForContent: S,
- rootScope: AnimatedContentRootScope<S>,
- contentTransform: ContentTransform,
- currentlyVisible: SnapshotStateList<S>,
- crossinline content: @Composable() AnimatedContentScope.(targetState: S) -> Unit
-) {
- var activeEnter by remember { mutableStateOf(contentTransform.targetContentEnter) }
- var activeExit by remember { mutableStateOf(ExitTransition.None) }
- val targetZIndex = remember { contentTransform.targetContentZIndex }
-
- val isEntering = targetState == stateForContent
- if (targetState == currentState) {
- // Transition finished, reset active enter & exit.
- activeEnter = EnterTransition.None
- activeExit = ExitTransition.None
- } else if (isEntering) {
- // If the previous enter transition never finishes when multiple
- // interruptions happen, avoid adding new enter transitions for simplicity.
- if (activeEnter == EnterTransition.None)
- activeEnter += contentTransform.targetContentEnter
- } else {
- // If the previous exit transition never finishes when multiple
- // interruptions happen, avoid adding new enter transitions for simplicity.
- if (activeExit == ExitTransition.None) {
- activeExit += contentTransform.initialContentExit
}
}
-
- val childData = remember { AnimatedContentRootScope.ChildData(stateForContent) }
- AnimatedEnterExitImpl(
- this,
- { it == stateForContent },
- enter = activeEnter,
- exit = activeExit,
- modifier = Modifier
- .layout { measurable, constraints ->
- val placeable = measurable.measure(constraints)
- layout(placeable.width, placeable.height) {
- placeable.place(0, 0, zIndex = targetZIndex)
+ val contentTransform = remember(rootScope, segment) { transitionSpec(rootScope) }
+ val sizeModifier = rootScope.createSizeAnimationModifier(contentTransform)
+ Layout(
+ modifier = modifier.then(sizeModifier),
+ content = {
+ currentlyVisible.fastForEach {
+ key(contentKey(it)) {
+ contentMap[it]?.invoke()
}
}
- .then(childData)
- .then(
- if (isEntering) {
- activeEnter[ScaleToFitTransitionKey]
- ?: activeExit[ScaleToFitTransitionKey] ?: Modifier
- } else {
- activeExit[ScaleToFitTransitionKey]
- ?: activeEnter[ScaleToFitTransitionKey] ?: Modifier
- }
- ),
- shouldDisposeBlock = { currentState, targetState ->
- currentState == EnterExitState.PostExit &&
- targetState == EnterExitState.PostExit && !activeExit.data.hold
},
- onLookaheadMeasured = {
- if (isEntering) rootScope.targetSizeMap.getOrPut(targetState) {
- mutableStateOf(it)
- }.value = it
- }
- ) {
- // TODO: Should Transition.AnimatedVisibility have an end listener?
- DisposableEffect(this) {
- onDispose {
- currentlyVisible.remove(stateForContent)
- rootScope.targetSizeMap.remove(stateForContent)
- }
- }
- with(remember { AnimatedContentScopeImpl(this) }) {
- content(stateForContent)
- }
- }
+ measurePolicy = remember { AnimatedContentMeasurePolicy(rootScope) }
+ )
}
-
-/**
- * This measure policy returns the target content size in the lookahead pass, and the max width
- * and height needed for all contents to fit during the main measure pass.
- *
- * The measure policy will measure all children with lookahead constraints. For outgoing content,
- * we will use the constraints recorded before the content started to exit. This enables the
- * outgoing content to not change constraints on its way out.
- */
-@Suppress("UNCHECKED_CAST")
-private class AnimatedContentMeasurePolicy<S>(
- val rootScope: AnimatedContentRootScope<S>,
- val constraintsMap: MutableMap<S, Constraints>
-) : MeasurePolicy {
+private class AnimatedContentMeasurePolicy(val rootScope: AnimatedContentTransitionScopeImpl<*>) :
+ MeasurePolicy {
override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult {
val placeables = arrayOfNulls<Placeable>(measurables.size)
// Measure the target composable first (but place it on top unless zIndex is specified)
- val targetState = rootScope.targetState
measurables.fastForEachIndexed { index, measurable ->
- if ((measurable.parentData as? AnimatedContentRootScope.ChildData<*>)
- ?.targetState == targetState
+ if ((measurable.parentData as? AnimatedContentTransitionScopeImpl.ChildData)
+ ?.isTarget == true
) {
- // Record lookahead constraints and always use it to measure target content.
- val lookaheadConstraints = if (isLookingAhead) {
- constraintsMap[targetState] = constraints
- constraints
- } else {
- requireNotNull(constraintsMap[targetState]) {
- "Lookahead pass was never done for target content."
- }
- }
- placeables[index] = measurable.measure(lookaheadConstraints)
+ placeables[index] = measurable.measure(constraints)
}
}
- // If no content is defined for target state, set the target size to zero
- rootScope.targetSizeMap.getOrPut(targetState) { mutableStateOf(IntSize.Zero) }
-
- val initialState = rootScope.initialState
// Measure the non-target composables after target, since these have no impact on
// container size in the size animation.
measurables.fastForEachIndexed { index, measurable ->
- val stateForContent =
- (measurable.parentData as? AnimatedContentRootScope.ChildData<*>)
- ?.targetState
if (placeables[index] == null) {
- val lookaheadConstraints =
- constraintsMap[stateForContent] ?: if (isLookingAhead) {
- constraintsMap[stateForContent as S] = constraints
- constraints
- } else {
- requireNotNull(constraintsMap[stateForContent as S]) {
- "Error: Lookahead pass never happened for state: $stateForContent"
- }
- }
- placeables[index] = measurable.measure(lookaheadConstraints).also {
- // If the initial state size isn't in the map, add it. This could be possible
- // when the initial state is specified to be different than target state upon
- // entering composition.
- if (stateForContent == initialState &&
- isLookingAhead &&
- !rootScope.targetSizeMap.containsKey(initialState)
- ) {
- rootScope.targetSizeMap[initialState] =
- mutableStateOf(IntSize(it.width, it.height))
- }
- }
+ placeables[index] = measurable.measure(constraints)
}
}
- val lookaheadSize = rootScope.targetSizeMap[targetState]!!.value
- val measuredWidth = if (isLookingAhead) {
- lookaheadSize.width
- } else {
- placeables.maxByOrNull { it?.width ?: 0 }?.width ?: 0
- }
- val measuredHeight = if (isLookingAhead) {
- lookaheadSize.height
- } else {
- placeables.maxByOrNull { it?.height ?: 0 }?.height ?: 0
- }
- rootScope.measuredSize = IntSize(measuredWidth, measuredHeight)
+ val maxWidth: Int = placeables.maxByOrNull { it?.width ?: 0 }?.width ?: 0
+ val maxHeight = placeables.maxByOrNull { it?.height ?: 0 }?.height ?: 0
+ rootScope.measuredSize = IntSize(maxWidth, maxHeight)
// Position the children.
- return layout(measuredWidth, measuredHeight) {
+ return layout(maxWidth, maxHeight) {
placeables.forEach { placeable ->
placeable?.let {
val offset = rootScope.contentAlignment.align(
IntSize(it.width, it.height),
- IntSize(measuredWidth, measuredHeight),
+ IntSize(maxWidth, maxHeight),
LayoutDirection.Ltr
)
it.place(offset.x, offset.y)
@@ -1041,178 +810,20 @@
}
}
}
-
override fun IntrinsicMeasureScope.minIntrinsicWidth(
measurables: List<IntrinsicMeasurable>,
height: Int
) = measurables.fastMaxOfOrNull { it.minIntrinsicWidth(height) } ?: 0
-
override fun IntrinsicMeasureScope.minIntrinsicHeight(
measurables: List<IntrinsicMeasurable>,
width: Int
) = measurables.fastMaxOfOrNull { it.minIntrinsicHeight(width) } ?: 0
-
override fun IntrinsicMeasureScope.maxIntrinsicWidth(
measurables: List<IntrinsicMeasurable>,
height: Int
) = measurables.fastMaxOfOrNull { it.maxIntrinsicWidth(height) } ?: 0
-
override fun IntrinsicMeasureScope.maxIntrinsicHeight(
measurables: List<IntrinsicMeasurable>,
width: Int
) = measurables.fastMaxOfOrNull { it.maxIntrinsicHeight(width) } ?: 0
}
-
-private class SizeModifierInLookaheadNode<S>(
- var rootScope: AnimatedContentRootScope<S>,
- var sizeAnimation: Transition<S>.DeferredAnimation<IntSize, AnimationVector2D>,
- var sizeTransform: State<SizeTransform?>,
-) : LayoutModifierNodeWithPassThroughIntrinsics() {
-
- override fun MeasureScope.measure(
- measurable: Measurable,
- constraints: Constraints,
- ): MeasureResult {
- val placeable = measurable.measure(constraints)
- val size = if (isLookingAhead) {
- val targetSize = IntSize(placeable.width, placeable.height)
- // lookahead pass
- rootScope.animatedSize = sizeAnimation.animate(
- transitionSpec = {
- val initial = rootScope.targetSizeMap[initialState]?.value ?: IntSize.Zero
- val target = rootScope.targetSizeMap[targetState]?.value ?: IntSize.Zero
- sizeTransform.value?.createAnimationSpec(initial, target) ?: spring()
- }
- ) {
- rootScope.targetSizeMap[it]?.value ?: IntSize.Zero
- }
- targetSize
- } else {
- rootScope.animatedSize!!.value
- }
- val offset = rootScope.contentAlignment.align(
- IntSize(placeable.width, placeable.height), size, LayoutDirection.Ltr
- )
- return layout(size.width, size.height) {
- placeable.place(offset)
- }
- }
-}
-
-private data class SizeModifierInLookaheadElement<S>(
- val rootScope: AnimatedContentRootScope<S>,
- val sizeAnimation: Transition<S>.DeferredAnimation<IntSize, AnimationVector2D>,
- val sizeTransform: State<SizeTransform?>,
-) : ModifierNodeElement<SizeModifierInLookaheadNode<S>>() {
- override fun create(): SizeModifierInLookaheadNode<S> {
- return SizeModifierInLookaheadNode(rootScope, sizeAnimation, sizeTransform)
- }
-
- override fun update(node: SizeModifierInLookaheadNode<S>) {
- node.rootScope = rootScope
- node.sizeTransform = sizeTransform
- node.sizeAnimation = sizeAnimation
- }
-
- override fun InspectorInfo.inspectableProperties() {
- name = "sizeTransform"
- properties["sizeTransform"] = sizeTransform
- properties["sizeAnimation"] = sizeAnimation
- }
-}
-
-private data class ScaleToFitInLookaheadElement(
- val rootScope: AnimatedContentRootScope<*>,
- val contentScale: ContentScale,
- val alignment: Alignment
-) : ModifierNodeElement<ScaleToFitInLookaheadNode>() {
- override fun create(): ScaleToFitInLookaheadNode =
- ScaleToFitInLookaheadNode(rootScope, contentScale, alignment)
-
- override fun update(node: ScaleToFitInLookaheadNode) {
- node.rootScope = rootScope
- node.contentScale = contentScale
- node.alignment = alignment
- }
-
- override fun InspectorInfo.inspectableProperties() {
- name = "scaleToFit"
- properties["rootScope"] = rootScope
- properties["scale"] = contentScale
- properties["alignment"] = alignment
- }
-}
-
-/**
- * Creates a Modifier Node to: 1) measure the layout with lookahead constraints, 2) scale the
- * resulting (potentially unfitting) layout based on the resizing container using the given
- * [contentScale] lambda.
- *
- * This node is designed to work in a lookahead scope, therefore it anticipates lookahead pass
- * before actual measure pass.
- */
-private class ScaleToFitInLookaheadNode(
- var rootScope: AnimatedContentRootScope<*>,
- var contentScale: ContentScale,
- var alignment: Alignment
-) : Modifier.Node(), LayoutModifierNode {
- private var lookaheadConstraints: Constraints = Constraints()
- set(value) {
- lookaheadPassOccurred = true
- field = value
- }
- get() {
- require(lookaheadPassOccurred) {
- "Error: Attempting to read lookahead constraints before lookahead pass."
- }
- return field
- }
- private var lookaheadPassOccurred = false
-
- override fun onDetach() {
- super.onDetach()
- lookaheadPassOccurred = false
- }
-
- override fun MeasureScope.measure(
- measurable: Measurable,
- constraints: Constraints
- ): MeasureResult {
- if (isLookingAhead) lookaheadConstraints = constraints
- // Measure with lookahead constraints.
- val placeable = measurable.measure(lookaheadConstraints)
- val contentSize = IntSize(placeable.width, placeable.height)
- val sizeToReport = if (isLookingAhead) {
- // report size of the target content, as that's what the content will be scaled to.
- rootScope.targetSize
- } else {
- // report current animated size && scale based on that and full size
- rootScope.currentSize
- }
- val resolvedScale =
- if (contentSize.width == 0 || contentSize.height == 0) {
- ScaleFactor(1f, 1f)
- } else
- contentScale.computeScaleFactor(contentSize.toSize(), sizeToReport.toSize())
- return layout(sizeToReport.width, sizeToReport.height) {
- val (x, y) = alignment.align(
- IntSize(
- (contentSize.width * resolvedScale.scaleX).fastRoundToInt(),
- (contentSize.height * resolvedScale.scaleY).fastRoundToInt()
- ),
- sizeToReport,
- layoutDirection
- )
- placeable.placeWithLayer(x, y) {
- scaleX = resolvedScale.scaleX
- scaleY = resolvedScale.scaleY
- transformOrigin = TransformOrigin(0f, 0f)
- }
- }
- }
-}
-
-/**
- * Fixed key to read customization out of EnterTransition and ExitTransition.
- */
-private val ScaleToFitTransitionKey = Any()
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/androidUnitTest/kotlin/androidx/compose/compiler/plugins/kotlin/ComposerParamSignatureTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/androidUnitTest/kotlin/androidx/compose/compiler/plugins/kotlin/ComposerParamSignatureTests.kt
index 190bd74..bbb3dc3 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/androidUnitTest/kotlin/androidx/compose/compiler/plugins/kotlin/ComposerParamSignatureTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/androidUnitTest/kotlin/androidx/compose/compiler/plugins/kotlin/ComposerParamSignatureTests.kt
@@ -1814,4 +1814,35 @@
) {
assertFalse(it.contains("INVOKESTATIC kotlin/jvm/internal/Reflection.property0 (Lkotlin/jvm/internal/PropertyReference0;)Lkotlin/reflect/KProperty0;"))
}
+
+ @Test
+ fun testComposableAdaptedFunctionReference() = validateBytecode(
+ """
+ class ScrollState {
+ fun test(index: Int, default: Int = 0): Int = 0
+ fun testExact(index: Int): Int = 0
+ }
+ fun scrollState(): ScrollState = TODO()
+
+ @Composable fun rememberFooInline() = fooInline(scrollState()::test)
+ @Composable fun rememberFoo() = foo(scrollState()::test)
+ @Composable fun rememberFooExactInline() = fooInline(scrollState()::testExact)
+ @Composable fun rememberFooExact() = foo(scrollState()::testExact)
+
+ @Composable
+ inline fun fooInline(block: (Int) -> Int) = block(0)
+
+ @Composable
+ fun foo(block: (Int) -> Int) = block(0)
+ """,
+ validate = {
+ // Validate that function references in inline calls are actually getting inlined
+ assertFalse(
+ it.contains("""INVOKESPECIAL Test_0Kt${'$'}rememberFooInline$1$1.<init> (Ljava/lang/Object;)V""")
+ )
+ assertFalse(
+ it.contains("""INVOKESPECIAL Test_0Kt${'$'}rememberFooExactInline$1$1.<init> (Ljava/lang/Object;)V""")
+ )
+ }
+ )
}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/jvmTest/kotlin/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/jvmTest/kotlin/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
index b8dceda..4164c6d 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/jvmTest/kotlin/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/jvmTest/kotlin/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
@@ -652,4 +652,28 @@
}
"""
)
+
+ @Test
+ fun testAdaptedFunctionRef() = verifyGoldenComposeIrTransform(
+ """
+ import androidx.compose.runtime.Composable
+
+ class ScrollState {
+ fun test(index: Int, default: Int = 0): Int = 0
+ fun testExact(index: Int): Int = 0
+ }
+ fun scrollState(): ScrollState = TODO()
+
+ @Composable fun rememberFooInline() = fooInline(scrollState()::test)
+ @Composable fun rememberFoo() = foo(scrollState()::test)
+ @Composable fun rememberFooExactInline() = fooInline(scrollState()::testExact)
+ @Composable fun rememberFooExact() = foo(scrollState()::testExact)
+
+ @Composable
+ inline fun fooInline(block: (Int) -> Int) = block(0)
+
+ @Composable
+ fun foo(block: (Int) -> Int) = block(0)
+ """,
+ )
}
diff --git "a/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.LambdaMemoizationTransformTests/testAdaptedFunctionRef\133useFir = false\135.txt" "b/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.LambdaMemoizationTransformTests/testAdaptedFunctionRef\133useFir = false\135.txt"
new file mode 100644
index 0000000..5785249
--- /dev/null
+++ "b/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.LambdaMemoizationTransformTests/testAdaptedFunctionRef\133useFir = false\135.txt"
@@ -0,0 +1,149 @@
+//
+// Source
+// ------------------------------------------
+
+import androidx.compose.runtime.Composable
+
+class ScrollState {
+ fun test(index: Int, default: Int = 0): Int = 0
+ fun testExact(index: Int): Int = 0
+}
+fun scrollState(): ScrollState = TODO()
+
+@Composable fun rememberFooInline() = fooInline(scrollState()::test)
+@Composable fun rememberFoo() = foo(scrollState()::test)
+@Composable fun rememberFooExactInline() = fooInline(scrollState()::testExact)
+@Composable fun rememberFooExact() = foo(scrollState()::testExact)
+
+@Composable
+inline fun fooInline(block: (Int) -> Int) = block(0)
+
+@Composable
+fun foo(block: (Int) -> Int) = block(0)
+
+//
+// Transformed IR
+// ------------------------------------------
+
+@StabilityInferred(parameters = 1)
+class ScrollState {
+ fun test(index: Int, default: Int = 0): Int {
+ return 0
+ }
+ fun testExact(index: Int): Int {
+ return 0
+ }
+ static val %stable: Int = 0
+}
+fun scrollState(): ScrollState {
+ return TODO()
+}
+@Composable
+fun rememberFooInline(%composer: Composer?, %changed: Int): Int {
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C(rememberFooInline)<fooInl...>:Test.kt")
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ val tmp0 = fooInline(<block>{
+ fun ScrollState.test(p0: Int): Int {
+ val tmp0_return = receiver.test(
+ index = p0
+ )
+ tmp0_return
+ }
+ scrollState()::test
+ }, %composer, 0)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ %composer.endReplaceableGroup()
+ return tmp0
+}
+@Composable
+fun rememberFoo(%composer: Composer?, %changed: Int): Int {
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C(rememberFoo)<scroll...>,<foo(sc...>:Test.kt")
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ val tmp0 = foo(<block>{
+ val tmp0 = scrollState()
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "CC(remember):Test.kt#9igjgp")
+ val tmp1_group = %composer.cache(%composer.changed(tmp0)) {
+ fun ScrollState.test(p0: Int): Int {
+ receiver.test(
+ index = p0
+ )
+ }
+ tmp0::test
+ }
+ %composer.endReplaceableGroup()
+ tmp1_group
+ }, %composer, 0)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ %composer.endReplaceableGroup()
+ return tmp0
+}
+@Composable
+fun rememberFooExactInline(%composer: Composer?, %changed: Int): Int {
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C(rememberFooExactInline)<fooInl...>:Test.kt")
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ val tmp0 = fooInline(scrollState()::testExact, %composer, 0)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ %composer.endReplaceableGroup()
+ return tmp0
+}
+@Composable
+fun rememberFooExact(%composer: Composer?, %changed: Int): Int {
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C(rememberFooExact)<scroll...>,<foo(sc...>:Test.kt")
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ val tmp0 = foo(<block>{
+ val tmp0 = scrollState()
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "CC(remember):Test.kt#9igjgp")
+ val tmp1_group = %composer.cache(%composer.changed(tmp0)) {
+ tmp0::testExact
+ }
+ %composer.endReplaceableGroup()
+ tmp1_group
+ }, %composer, 0)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ %composer.endReplaceableGroup()
+ return tmp0
+}
+@Composable
+fun fooInline(block: Function1<Int, Int>, %composer: Composer?, %changed: Int): Int {
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "CC(fooInline):Test.kt")
+ val tmp0 = block(0)
+ %composer.endReplaceableGroup()
+ return tmp0
+}
+@Composable
+fun foo(block: Function1<Int, Int>, %composer: Composer?, %changed: Int): Int {
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C(foo):Test.kt")
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ val tmp0 = block(0)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ %composer.endReplaceableGroup()
+ return tmp0
+}
diff --git "a/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.LambdaMemoizationTransformTests/testAdaptedFunctionRef\133useFir = true\135.txt" "b/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.LambdaMemoizationTransformTests/testAdaptedFunctionRef\133useFir = true\135.txt"
new file mode 100644
index 0000000..ab1b9ff
--- /dev/null
+++ "b/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.LambdaMemoizationTransformTests/testAdaptedFunctionRef\133useFir = true\135.txt"
@@ -0,0 +1,149 @@
+//
+// Source
+// ------------------------------------------
+
+import androidx.compose.runtime.Composable
+
+class ScrollState {
+ fun test(index: Int, default: Int = 0): Int = 0
+ fun testExact(index: Int): Int = 0
+}
+fun scrollState(): ScrollState = TODO()
+
+@Composable fun rememberFooInline() = fooInline(scrollState()::test)
+@Composable fun rememberFoo() = foo(scrollState()::test)
+@Composable fun rememberFooExactInline() = fooInline(scrollState()::testExact)
+@Composable fun rememberFooExact() = foo(scrollState()::testExact)
+
+@Composable
+inline fun fooInline(block: (Int) -> Int) = block(0)
+
+@Composable
+fun foo(block: (Int) -> Int) = block(0)
+
+//
+// Transformed IR
+// ------------------------------------------
+
+@StabilityInferred(parameters = 1)
+class ScrollState {
+ fun test(index: Int, default: Int = 0): Int {
+ return 0
+ }
+ fun testExact(index: Int): Int {
+ return 0
+ }
+ static val %stable: Int = 0
+}
+fun scrollState(): ScrollState {
+ return TODO()
+}
+@Composable
+fun rememberFooInline(%composer: Composer?, %changed: Int): Int {
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C(rememberFooInline)<fooInl...>:Test.kt")
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ val tmp0 = fooInline(<block>{
+ fun ScrollState.test(p0: Int): Int {
+ val tmp0_return = receiver.test(
+ index = p0
+ )
+ tmp0_return
+ }
+ scrollState()::test
+ }, %composer, 0)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ %composer.endReplaceableGroup()
+ return tmp0
+}
+@Composable
+fun rememberFoo(%composer: Composer?, %changed: Int): Int {
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C(rememberFoo)<test>,<foo(sc...>:Test.kt")
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ val tmp0 = foo(<block>{
+ val tmp0 = scrollState()
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "CC(remember):Test.kt#9igjgp")
+ val tmp1_group = %composer.cache(%composer.changed(tmp0)) {
+ fun ScrollState.test(p0: Int): Int {
+ receiver.test(
+ index = p0
+ )
+ }
+ tmp0::test
+ }
+ %composer.endReplaceableGroup()
+ tmp1_group
+ }, %composer, 0)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ %composer.endReplaceableGroup()
+ return tmp0
+}
+@Composable
+fun rememberFooExactInline(%composer: Composer?, %changed: Int): Int {
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C(rememberFooExactInline)<fooInl...>:Test.kt")
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ val tmp0 = fooInline(scrollState()::testExact, %composer, 0)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ %composer.endReplaceableGroup()
+ return tmp0
+}
+@Composable
+fun rememberFooExact(%composer: Composer?, %changed: Int): Int {
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C(rememberFooExact)<testEx...>,<foo(sc...>:Test.kt")
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ val tmp0 = foo(<block>{
+ val tmp0 = scrollState()
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "CC(remember):Test.kt#9igjgp")
+ val tmp1_group = %composer.cache(%composer.changed(tmp0)) {
+ tmp0::testExact
+ }
+ %composer.endReplaceableGroup()
+ tmp1_group
+ }, %composer, 0)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ %composer.endReplaceableGroup()
+ return tmp0
+}
+@Composable
+fun fooInline(block: Function1<Int, Int>, %composer: Composer?, %changed: Int): Int {
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "CC(fooInline):Test.kt")
+ val tmp0 = block(0)
+ %composer.endReplaceableGroup()
+ return tmp0
+}
+@Composable
+fun foo(block: Function1<Int, Int>, %composer: Composer?, %changed: Int): Int {
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C(foo):Test.kt")
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ val tmp0 = block(0)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ %composer.endReplaceableGroup()
+ return tmp0
+}
diff --git "a/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.RememberIntrinsicTransformTests/testRememberAdaptedFunctionReference\133useFir = false\135.txt" "b/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.RememberIntrinsicTransformTests/testRememberAdaptedFunctionReference\133useFir = false\135.txt"
index 05ba6c9..e276214 100644
--- "a/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.RememberIntrinsicTransformTests/testRememberAdaptedFunctionReference\133useFir = false\135.txt"
+++ "b/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.RememberIntrinsicTransformTests/testRememberAdaptedFunctionReference\133useFir = false\135.txt"
@@ -30,9 +30,12 @@
used(<block>{
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "CC(remember):Test.kt#9igjgp")
- val tmp0_group = %composer.cache(%dirty and 0b1110 == 0b0100) {
- effect()
- }
+ val tmp0_group = %composer.cache(%dirty and 0b1110 == 0b0100, <block>{
+ fun effect(): Int {
+ effect()
+ }
+ ::effect
+ })
%composer.endReplaceableGroup()
tmp0_group
})
diff --git "a/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.RememberIntrinsicTransformTests/testRememberAdaptedFunctionReference\133useFir = true\135.txt" "b/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.RememberIntrinsicTransformTests/testRememberAdaptedFunctionReference\133useFir = true\135.txt"
index 05ba6c9..e276214 100644
--- "a/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.RememberIntrinsicTransformTests/testRememberAdaptedFunctionReference\133useFir = true\135.txt"
+++ "b/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.RememberIntrinsicTransformTests/testRememberAdaptedFunctionReference\133useFir = true\135.txt"
@@ -30,9 +30,12 @@
used(<block>{
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "CC(remember):Test.kt#9igjgp")
- val tmp0_group = %composer.cache(%dirty and 0b1110 == 0b0100) {
- effect()
- }
+ val tmp0_group = %composer.cache(%dirty and 0b1110 == 0b0100, <block>{
+ fun effect(): Int {
+ effect()
+ }
+ ::effect
+ })
%composer.endReplaceableGroup()
tmp0_group
})
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt
index ff49298..90acac0 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt
@@ -62,7 +62,7 @@
import org.jetbrains.kotlin.ir.declarations.IrValueDeclaration
import org.jetbrains.kotlin.ir.declarations.IrValueParameter
import org.jetbrains.kotlin.ir.declarations.IrVariable
-import org.jetbrains.kotlin.ir.declarations.copyAttributes
+import org.jetbrains.kotlin.ir.expressions.IrBlock
import org.jetbrains.kotlin.ir.expressions.IrCall
import org.jetbrains.kotlin.ir.expressions.IrConstructorCall
import org.jetbrains.kotlin.ir.expressions.IrExpression
@@ -74,7 +74,6 @@
import org.jetbrains.kotlin.ir.expressions.IrTypeOperatorCall
import org.jetbrains.kotlin.ir.expressions.IrValueAccessExpression
import org.jetbrains.kotlin.ir.expressions.impl.IrCallImpl
-import org.jetbrains.kotlin.ir.expressions.impl.IrFunctionReferenceImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrGetObjectValueImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrInstanceInitializerCallImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrTypeOperatorCallImpl
@@ -458,17 +457,64 @@
return super.visitValueAccess(expression)
}
+ override fun visitBlock(expression: IrBlock): IrExpression {
+ val result = super.visitBlock(expression)
+
+ if (result is IrBlock && result.origin == IrStatementOrigin.ADAPTED_FUNCTION_REFERENCE) {
+ if (inlineLambdaInfo.isInlineFunctionExpression(expression)) {
+ // Do not memoize function references for inline lambdas
+ return result
+ }
+
+ val functionReference = result.statements.last()
+ if (functionReference !is IrFunctionReference) {
+ // Do not memoize if the expected shape doesn't match.
+ return result
+ }
+
+ return rememberFunctionReference(functionReference, expression)
+ }
+
+ return result
+ }
+
// Memoize the instance created by using the :: operator
override fun visitFunctionReference(expression: IrFunctionReference): IrExpression {
+ val result = super.visitFunctionReference(expression)
+
+ if (
+ inlineLambdaInfo.isInlineFunctionExpression(expression) ||
+ inlineLambdaInfo.isInlineLambda(expression.symbol.owner)
+ ) {
+ // Do not memoize function references used in inline parameters.
+ return result
+ }
+
+ if (expression.symbol.owner.origin == IrDeclarationOrigin.ADAPTER_FOR_CALLABLE_REFERENCE) {
+ // Adapted function reference (inexact function signature match) is handled in block
+ return result
+ }
+
+ if (result !is IrFunctionReference) {
+ // Do not memoize if the shape doesn't match
+ return result
+ }
+
+ return rememberFunctionReference(result, result)
+ }
+
+ private fun rememberFunctionReference(
+ reference: IrFunctionReference,
+ expression: IrExpression
+ ): IrExpression {
// Get the local captures for local function ref, to make sure we invalidate memoized
// reference if its capture is different.
- val localCaptures = if (expression.symbol.owner.isLocal) {
- declarationContextStack.recordLocalCapture(expression.symbol.owner)
+ val localCaptures = if (reference.symbol.owner.isLocal) {
+ declarationContextStack.recordLocalCapture(reference.symbol.owner)
} else {
null
}
- val result = super.visitFunctionReference(expression)
- val functionContext = currentFunctionContext ?: return result
+ val functionContext = currentFunctionContext ?: return expression
// The syntax <expr>::<method>(<params>) and ::<function>(<params>) is reserved for
// future use. Revisit implementation if this syntax is as a curry syntax in the future.
@@ -476,27 +522,27 @@
// receivers are treated below.
// Do not attempt memoization if the referenced function has context receivers.
- if (expression.symbol.owner.contextReceiverParametersCount > 0) {
- return result
+ if (reference.symbol.owner.contextReceiverParametersCount > 0) {
+ return expression
}
// Do not attempt memoization if value parameters are not null. This is to guard against
// unexpected IR shapes.
- for (i in 0 until expression.valueArgumentsCount) {
- if (expression.getValueArgument(i) != null) {
- return result
+ for (i in 0 until reference.valueArgumentsCount) {
+ if (reference.getValueArgument(i) != null) {
+ return expression
}
}
if (functionContext.canRemember) {
// Memoize the reference for <expr>::<method>
- val dispatchReceiver = expression.dispatchReceiver
- val extensionReceiver = expression.extensionReceiver
+ val dispatchReceiver = reference.dispatchReceiver
+ val extensionReceiver = reference.extensionReceiver
val hasReceiver = dispatchReceiver != null || extensionReceiver != null
val receiverIsStable =
dispatchReceiver.isNullOrStable() &&
- extensionReceiver.isNullOrStable()
+ extensionReceiver.isNullOrStable()
val captures = mutableListOf<IrValueDeclaration>()
if (localCaptures != null) {
@@ -526,28 +572,22 @@
tmp
}
+ // Patch reference receiver in place
+ reference.dispatchReceiver = tempDispatchReceiver?.let { irGet(it) }
+ reference.extensionReceiver = tempExtensionReceiver?.let { irGet(it) }
+
+rememberExpression(
functionContext,
- IrFunctionReferenceImpl(
- startOffset,
- endOffset,
- expression.type,
- expression.symbol,
- expression.typeArgumentsCount,
- expression.valueArgumentsCount,
- expression.reflectionTarget
- ).copyAttributes(expression).apply {
- this.dispatchReceiver = tempDispatchReceiver?.let { irGet(it) }
- this.extensionReceiver = tempExtensionReceiver?.let { irGet(it) }
- },
+ expression,
captures
)
}
} else if (dispatchReceiver == null && extensionReceiver == null) {
- return rememberExpression(functionContext, result, captures)
+ return rememberExpression(functionContext, expression, captures)
}
}
- return result
+
+ return expression
}
override fun visitTypeOperator(expression: IrTypeOperatorCall): IrExpression {
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrInlineReferenceLocator.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrInlineReferenceLocator.kt
index 852ae18..8c72dac 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrInlineReferenceLocator.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrInlineReferenceLocator.kt
@@ -46,10 +46,14 @@
class ComposeInlineLambdaLocator(private val context: IrPluginContext) {
private val inlineLambdaToParameter = mutableMapOf<IrFunctionSymbol, IrValueParameter>()
+ private val inlineFunctionExpressions = mutableSetOf<IrExpression>()
fun isInlineLambda(irFunction: IrFunction): Boolean =
irFunction.symbol in inlineLambdaToParameter.keys
+ fun isInlineFunctionExpression(expression: IrExpression): Boolean =
+ expression in inlineFunctionExpressions
+
fun preservesComposableScope(irFunction: IrFunction): Boolean =
inlineLambdaToParameter[irFunction.symbol]?.let {
!it.isCrossinline && !it.type.hasAnnotation(ComposeFqNames.DisallowComposableCalls)
@@ -66,7 +70,7 @@
declaration.acceptChildrenVoid(this)
val parent = declaration.parent as? IrFunction
if (parent?.isInlineFunctionCall(context) == true &&
- declaration.isInlineParameter()) {
+ declaration.isInlinedFunction()) {
declaration.defaultValue?.expression?.unwrapLambda()?.let {
inlineLambdaToParameter[it] = declaration
}
@@ -78,8 +82,9 @@
val function = expression.symbol.owner
if (function.isInlineFunctionCall(context)) {
for (parameter in function.valueParameters) {
- if (parameter.isInlineParameter()) {
+ if (parameter.isInlinedFunction()) {
expression.getValueArgument(parameter.index)
+ ?.also { inlineFunctionExpressions += it }
?.unwrapLambda()
?.let { inlineLambdaToParameter[it] = parameter }
}
@@ -119,7 +124,7 @@
// This is copied from JvmIrInlineUtils.kt in the Kotlin compiler, since we
// need to check for synthetic composable functions.
-private fun IrValueParameter.isInlineParameter(): Boolean =
+private fun IrValueParameter.isInlinedFunction(): Boolean =
index >= 0 && !isNoinline && (type.isFunction() || type.isSuspendFunction() ||
type.isSyntheticComposableFunction()) &&
// Parameters with default values are always nullable, so check the expression too.
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt
index 9980397..0b0f561 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt
@@ -522,8 +522,7 @@
val param = symbol.owner.valueParameters[i]
val isLambda = arg is IrFunctionExpression ||
(arg is IrBlock &&
- (arg.origin == IrStatementOrigin.LAMBDA ||
- arg.origin == IrStatementOrigin.ADAPTED_FUNCTION_REFERENCE))
+ (arg.origin == IrStatementOrigin.LAMBDA))
if (isLambda) {
arg.unwrapLambda()?.let {
returnTargetToCall[it] = this
@@ -881,7 +880,7 @@
lhs.print()
print("--")
}
- IrStatementOrigin.LAMBDA, IrStatementOrigin.ADAPTED_FUNCTION_REFERENCE -> {
+ IrStatementOrigin.LAMBDA -> {
val function = expression.statements[0] as IrFunction
function.printAsLambda()
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/BasicMarqueeTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/BasicMarqueeTest.kt
index ed67719..9cc7e7a 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/BasicMarqueeTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/BasicMarqueeTest.kt
@@ -922,6 +922,7 @@
focusManager = LocalFocusManager.current
TestMarqueeContent(
Modifier
+ .focusable() // extra focusable for initial focus.
.basicMarqueeWithTestParams(animationMode = WhileFocused)
.focusRequester(focusRequester)
.focusable()
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt
index 56d0bec..4c72289 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt
@@ -81,7 +81,9 @@
import androidx.test.filters.LargeTest
import androidx.test.filters.MediumTest
import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Correspondence
import com.google.common.truth.Truth.assertThat
+import kotlin.reflect.KClass
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.junit.After
@@ -97,6 +99,11 @@
@get:Rule
val rule = createComposeRule()
+ private val InstanceOf = Correspondence.from<Any, KClass<*>>(
+ { obj, clazz -> clazz?.isInstance(obj) ?: false },
+ "is an instance of"
+ )
+
@Before
fun before() {
isDebugInspectorInfoEnabled = true
@@ -1054,22 +1061,19 @@
val focusRequester = FocusRequester()
lateinit var focusManager: FocusManager
lateinit var inputModeManager: InputModeManager
- rule.setContent {
+ rule.setFocusableContent {
scope = rememberCoroutineScope()
focusManager = LocalFocusManager.current
inputModeManager = LocalInputModeManager.current
- Box {
- BasicText(
- "ClickableText",
- modifier = Modifier
- .testTag("myClickable")
- .focusRequester(focusRequester)
- .clickable(
- interactionSource = interactionSource,
- indication = null
- ) {}
- )
- }
+ Box {
+ BasicText(
+ "ClickableText",
+ modifier = Modifier
+ .testTag("myClickable")
+ .focusRequester(focusRequester)
+ .clickable(interactionSource = interactionSource, indication = null) {}
+ )
+ }
}
rule.runOnIdle {
@OptIn(ExperimentalComposeUiApi::class)
@@ -1092,8 +1096,9 @@
// Keyboard mode, so we should now be focused and see an interaction
rule.runOnIdle {
- assertThat(interactions).hasSize(1)
- assertThat(interactions.first()).isInstanceOf(FocusInteraction.Focus::class.java)
+ assertThat(interactions)
+ .comparingElementsUsing(InstanceOf)
+ .containsExactly(FocusInteraction.Focus::class)
}
rule.runOnIdle {
@@ -1101,12 +1106,12 @@
}
rule.runOnIdle {
- assertThat(interactions).hasSize(2)
- assertThat(interactions.first()).isInstanceOf(FocusInteraction.Focus::class.java)
- assertThat(interactions[1])
- .isInstanceOf(FocusInteraction.Unfocus::class.java)
- assertThat((interactions[1] as FocusInteraction.Unfocus).focus)
- .isEqualTo(interactions[0])
+ // TODO(b/308811852): Simplify the other assertions in FocusableTest, ClickableTest and
+ // CombinedClickable by using InstanceOf (like we do here).
+ assertThat(interactions)
+ .comparingElementsUsing(InstanceOf)
+ .containsExactly(FocusInteraction.Focus::class, FocusInteraction.Unfocus::class)
+ .inOrder()
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/CombinedClickableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/CombinedClickableTest.kt
index 3c6d481..0defe94 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/CombinedClickableTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/CombinedClickableTest.kt
@@ -1298,7 +1298,7 @@
val focusRequester = FocusRequester()
lateinit var focusManager: FocusManager
lateinit var inputModeManager: InputModeManager
- rule.setContent {
+ rule.setFocusableContent {
scope = rememberCoroutineScope()
focusManager = LocalFocusManager.current
inputModeManager = LocalInputModeManager.current
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/FocusableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/FocusableTest.kt
index 46b4931..80b1504 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/FocusableTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/FocusableTest.kt
@@ -97,7 +97,7 @@
@Test
fun focusable_defaultSemantics() {
- rule.setContent {
+ rule.setFocusableContent {
Box {
BasicText(
"focusableText",
@@ -115,7 +115,7 @@
@Test
fun focusable_disabledSemantics() {
- rule.setContent {
+ rule.setFocusableContent {
Box {
BasicText(
"focusableText",
@@ -133,7 +133,7 @@
@Test
fun focusable_focusAcquire() {
val (focusRequester, otherFocusRequester) = FocusRequester.createRefs()
- rule.setContent {
+ rule.setFocusableContent {
Box {
BasicText(
"focusableText",
@@ -176,7 +176,7 @@
lateinit var scope: CoroutineScope
- rule.setContent {
+ rule.setFocusableContent {
scope = rememberCoroutineScope()
Box {
BasicText(
@@ -236,7 +236,7 @@
lateinit var scope: CoroutineScope
- rule.setContent {
+ rule.setFocusableContent {
scope = rememberCoroutineScope()
Box {
if (emitFocusableText) {
@@ -284,7 +284,6 @@
}
}
- @OptIn(ExperimentalFoundationApi::class)
@Test
fun focusable_pins_whenItIsFocused() {
// Arrange.
@@ -296,7 +295,7 @@
return PinnedHandle {}
}
}
- rule.setContent {
+ rule.setFocusableContent {
CompositionLocalProvider(LocalPinnableContainer provides pinnableContainer) {
Box(
Modifier
@@ -318,7 +317,6 @@
}
}
- @OptIn(ExperimentalFoundationApi::class)
@Test
fun focusable_unpins_whenItIsUnfocused() {
// Arrange.
@@ -330,7 +328,7 @@
return PinnedHandle { onUnpinInvoked = true }
}
}
- rule.setContent {
+ rule.setFocusableContent {
CompositionLocalProvider(LocalPinnableContainer provides pinnableContainer) {
Box(
Modifier
@@ -386,7 +384,7 @@
}
val focusRequester = FocusRequester()
- rule.setContent {
+ rule.setFocusableContent {
with(rule.density) {
Box(
Modifier
@@ -422,7 +420,7 @@
lateinit var state: LazyListState
lateinit var coroutineScope: CoroutineScope
var items by mutableStateOf((1..20).toList())
- rule.setContent {
+ rule.setFocusableContent {
state = rememberLazyListState()
coroutineScope = rememberCoroutineScope()
LazyRow(
@@ -457,7 +455,7 @@
// Arrange.
var hasFocus = false
var itemVisible by mutableStateOf(true)
- rule.setContent {
+ rule.setFocusableContent {
SubcomposeLayout(
modifier = Modifier
.requiredSize(100.dp)
@@ -490,7 +488,6 @@
}
}
- @OptIn(ExperimentalFoundationApi::class)
@Test
fun focusable_updatePinnableContainer_staysPinned() {
// Arrange.
@@ -510,7 +507,7 @@
}
}
var pinnableContainer by mutableStateOf<PinnableContainer>(pinnableContainer1)
- rule.setContent {
+ rule.setFocusableContent {
CompositionLocalProvider(LocalPinnableContainer provides pinnableContainer) {
Box(
Modifier
@@ -544,7 +541,7 @@
val focusRequester = FocusRequester()
lateinit var state: FocusState
var key by mutableStateOf(0)
- rule.setContent {
+ rule.setFocusableContent {
ReusableContent(key) {
BasicText(
"focusableText",
@@ -594,7 +591,7 @@
)
}
- rule.setContent {
+ rule.setFocusableContent {
scope = rememberCoroutineScope()
if (moveContent) {
Box(Modifier.size(5.dp)) {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/FoundationTestUtils.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/FoundationTestUtils.kt
new file mode 100644
index 0000000..990aaec
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/FoundationTestUtils.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.unit.dp
+
+/**
+ * This function adds a parent composable which has size.
+ * [View.requestFocus()][android.view.View.requestFocus] will not take focus if the view has no
+ * size.
+ *
+ * @param extraItemForInitialFocus Includes an extra item that takes focus initially. This is
+ * useful in cases where we need tests that could be affected by initial focus. Eg. When there is
+ * only one focusable item and we clear focus, that item could end up being focused on again by the
+ * initial focus logic.
+ */
+internal fun ComposeContentTestRule.setFocusableContent(
+ extraItemForInitialFocus: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ setContent {
+ if (extraItemForInitialFocus) {
+ Row {
+ Box(modifier = Modifier.requiredSize(10.dp, 10.dp).focusable())
+ Box { content() }
+ }
+ } else {
+ Box(modifier = Modifier.requiredSize(100.dp, 100.dp)) { content() }
+ }
+ }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/selection/SelectableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/selection/SelectableTest.kt
index 5b384af..74078e3 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/selection/SelectableTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/selection/SelectableTest.kt
@@ -26,6 +26,7 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.setFocusableContent
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.getValue
@@ -548,7 +549,7 @@
lateinit var focusManager: FocusManager
lateinit var inputModeManager: InputModeManager
- rule.setContent {
+ rule.setFocusableContent {
scope = rememberCoroutineScope()
focusManager = LocalFocusManager.current
inputModeManager = LocalInputModeManager.current
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/selection/ToggleableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/selection/ToggleableTest.kt
index 4174820..312e6b5 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/selection/ToggleableTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/selection/ToggleableTest.kt
@@ -29,6 +29,7 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.setFocusableContent
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.getValue
@@ -640,7 +641,7 @@
lateinit var focusManager: FocusManager
lateinit var inputModeManager: InputModeManager
- rule.setContent {
+ rule.setFocusableContent {
scope = rememberCoroutineScope()
focusManager = LocalFocusManager.current
inputModeManager = LocalInputModeManager.current
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt
index 6860c1a..489b3e2 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt
@@ -19,6 +19,7 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.setFocusableContent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
@@ -370,7 +371,7 @@
lateinit var textLayoutResult: TextLayoutResult
val focusRequester = FocusRequester()
- setContent {
+ setContent(extraItemForInitialFocus = false) {
CoreTextField(
value = value,
modifier = Modifier.focusRequester(focusRequester),
@@ -400,7 +401,7 @@
lateinit var textLayoutResult: TextLayoutResult
val focusRequester = FocusRequester()
- setContent {
+ setContent(extraItemForInitialFocus = false) {
Box(Modifier.offset { offset }) {
CoreTextField(
value = value,
@@ -440,7 +441,7 @@
var value by mutableStateOf(TextFieldValue(""))
lateinit var textLayoutResult: TextLayoutResult
- setContent {
+ setContent(extraItemForInitialFocus = false) {
CoreTextField(
value = value,
modifier = Modifier.testTag(tag),
@@ -494,7 +495,7 @@
val focusRequester = FocusRequester()
val matrix = Matrix()
- setContent {
+ setContent(extraItemForInitialFocus = false) {
Box(Modifier.offset { offset }) {
CoreTextField(value = value,
modifier = Modifier.focusRequester(focusRequester),
@@ -539,8 +540,11 @@
}
}
- private fun setContent(content: @Composable () -> Unit) {
- rule.setContent {
+ private fun setContent(
+ extraItemForInitialFocus: Boolean = true,
+ content: @Composable () -> Unit
+ ) {
+ rule.setFocusableContent(extraItemForInitialFocus) {
focusManager = LocalFocusManager.current
CompositionLocalProvider(
LocalTextInputService provides textInputService,
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/BasicTextField2ImmIntegrationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/BasicTextField2ImmIntegrationTest.kt
index ce06e74..d6aedab 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/BasicTextField2ImmIntegrationTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/BasicTextField2ImmIntegrationTest.kt
@@ -17,6 +17,10 @@
package androidx.compose.foundation.text2
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text2.input.InputTransformation
import androidx.compose.foundation.text2.input.TextFieldBuffer
@@ -44,6 +48,7 @@
import androidx.compose.ui.test.requestFocus
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.FlakyTest
import androidx.test.filters.SmallTest
@@ -95,14 +100,14 @@
@Test
fun stopsBeingTextEditor_whenFocusLost() {
val state = TextFieldState()
- var focusManager: FocusManager? = null
+ lateinit var focusManager: FocusManager
inputMethodInterceptor.setTextFieldTestContent {
focusManager = LocalFocusManager.current
BasicTextField2(state, Modifier.testTag(Tag))
}
requestFocus(Tag)
rule.runOnIdle {
- focusManager!!.clearFocus()
+ focusManager.clearFocus()
}
inputMethodInterceptor.assertNoSessionActive()
}
@@ -215,6 +220,7 @@
commitText("hello", 1)
+ @Suppress("SpellCheckingInspection")
assertThat(state.text.toString()).isEqualTo("helloworld")
}
@@ -408,6 +414,12 @@
override val isWindowFocused = true
}
this.setContent {
- CompositionLocalProvider(LocalWindowInfo provides windowInfo, content)
+ CompositionLocalProvider(LocalWindowInfo provides windowInfo) {
+ Row {
+ // Extra focusable that takes initial focus when focus is cleared.
+ Box(Modifier.size(10.dp).focusable())
+ Box { content() }
+ }
+ }
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
index 2130384..5a07ca6 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
@@ -390,7 +390,7 @@
} else {
absoluteOffset
}
- positionedItems.addAll(
+ positionedItems.addAllFromArray(
line.position(relativeOffset, layoutWidth, layoutHeight)
)
}
@@ -405,7 +405,7 @@
currentMainAxis = firstLineScrollOffset
lines.fastForEach {
- positionedItems.addAll(it.position(currentMainAxis, layoutWidth, layoutHeight))
+ positionedItems.addAllFromArray(it.position(currentMainAxis, layoutWidth, layoutHeight))
currentMainAxis += it.mainAxisSizeWithSpacings
}
@@ -417,3 +417,10 @@
}
return positionedItems
}
+
+// Faster version of addAll that does not create a list for each array
+private fun <T> MutableList<T>.addAllFromArray(arr: Array<T>) {
+ for (item in arr) {
+ add(item)
+ }
+}
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index c4701d4..7ca2ade 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -597,43 +597,19 @@
property public abstract kotlin.ranges.IntRange yearRange;
}
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public enum DismissDirection {
- method public static androidx.compose.material3.DismissDirection valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
- method public static androidx.compose.material3.DismissDirection[] values();
- enum_constant public static final androidx.compose.material3.DismissDirection EndToStart;
- enum_constant public static final androidx.compose.material3.DismissDirection StartToEnd;
+ @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public enum DismissDirection {
+ method @Deprecated public static androidx.compose.material3.DismissDirection valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+ method @Deprecated public static androidx.compose.material3.DismissDirection[] values();
+ enum_constant @Deprecated public static final androidx.compose.material3.DismissDirection EndToStart;
+ enum_constant @Deprecated public static final androidx.compose.material3.DismissDirection StartToEnd;
}
- public final class DismissState {
- ctor public DismissState(androidx.compose.material3.DismissValue initialValue, androidx.compose.ui.unit.Density density, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissValue,java.lang.Boolean> confirmValueChange, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold);
- ctor @Deprecated public DismissState(androidx.compose.material3.DismissValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissValue,java.lang.Boolean> confirmValueChange, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold);
- method public suspend Object? dismiss(androidx.compose.material3.DismissDirection direction, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method public androidx.compose.material3.DismissValue getCurrentValue();
- method public androidx.compose.material3.DismissDirection? getDismissDirection();
- method public float getProgress();
- method public androidx.compose.material3.DismissValue getTargetValue();
- method public boolean isDismissed(androidx.compose.material3.DismissDirection direction);
- method public float requireOffset();
- method public suspend Object? reset(kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method public suspend Object? snapTo(androidx.compose.material3.DismissValue targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- property public final androidx.compose.material3.DismissValue currentValue;
- property public final androidx.compose.material3.DismissDirection? dismissDirection;
- property public final float progress;
- property public final androidx.compose.material3.DismissValue targetValue;
- field public static final androidx.compose.material3.DismissState.Companion Companion;
- }
-
- public static final class DismissState.Companion {
- method @Deprecated public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.DismissState,androidx.compose.material3.DismissValue> Saver(kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissValue,java.lang.Boolean> confirmValueChange, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold);
- method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.DismissState,androidx.compose.material3.DismissValue> Saver(kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissValue,java.lang.Boolean> confirmValueChange, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold, androidx.compose.ui.unit.Density density);
- }
-
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public enum DismissValue {
- method public static androidx.compose.material3.DismissValue valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
- method public static androidx.compose.material3.DismissValue[] values();
- enum_constant public static final androidx.compose.material3.DismissValue Default;
- enum_constant public static final androidx.compose.material3.DismissValue DismissedToEnd;
- enum_constant public static final androidx.compose.material3.DismissValue DismissedToStart;
+ @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public enum DismissValue {
+ method @Deprecated public static androidx.compose.material3.DismissValue valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+ method @Deprecated public static androidx.compose.material3.DismissValue[] values();
+ enum_constant @Deprecated public static final androidx.compose.material3.DismissValue Default;
+ enum_constant @Deprecated public static final androidx.compose.material3.DismissValue DismissedToEnd;
+ enum_constant @Deprecated public static final androidx.compose.material3.DismissValue DismissedToStart;
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class DisplayMode {
@@ -1559,15 +1535,45 @@
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class SwipeToDismissBoxDefaults {
- method @androidx.compose.runtime.Composable public kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> getFixedPositionalThreshold();
- property @androidx.compose.runtime.Composable public final kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> fixedPositionalThreshold;
+ method @androidx.compose.runtime.Composable public kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> getPositionalThreshold();
+ property @androidx.compose.runtime.Composable public final kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> positionalThreshold;
field public static final androidx.compose.material3.SwipeToDismissBoxDefaults INSTANCE;
}
public final class SwipeToDismissBoxKt {
- method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SwipeToDismiss(androidx.compose.material3.DismissState state, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> background, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> dismissContent, optional androidx.compose.ui.Modifier modifier, optional java.util.Set<? extends androidx.compose.material3.DismissDirection> directions);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SwipeToDismissBox(androidx.compose.material3.DismissState state, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> backgroundContent, optional androidx.compose.ui.Modifier modifier, optional java.util.Set<? extends androidx.compose.material3.DismissDirection> directions, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.DismissState rememberDismissState(optional androidx.compose.material3.DismissValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissValue,java.lang.Boolean> confirmValueChange, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold);
+ method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SwipeToDismiss(androidx.compose.material3.SwipeToDismissState state, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> background, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> dismissContent, optional androidx.compose.ui.Modifier modifier, optional java.util.Set<? extends androidx.compose.material3.SwipeToDismissValue> directions);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SwipeToDismissBox(androidx.compose.material3.SwipeToDismissState state, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> backgroundContent, optional androidx.compose.ui.Modifier modifier, optional boolean enableDismissFromStartToEnd, optional boolean enableDismissFromEndToStart, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.SwipeToDismissState rememberSwipeToDismissState(optional androidx.compose.material3.SwipeToDismissValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SwipeToDismissValue,java.lang.Boolean> confirmValueChange, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold);
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class SwipeToDismissState {
+ ctor public SwipeToDismissState(androidx.compose.material3.SwipeToDismissValue initialValue, androidx.compose.ui.unit.Density density, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SwipeToDismissValue,java.lang.Boolean> confirmValueChange, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold);
+ method public suspend Object? dismiss(androidx.compose.material3.SwipeToDismissValue direction, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public androidx.compose.material3.SwipeToDismissValue getCurrentValue();
+ method public androidx.compose.material3.SwipeToDismissValue getDismissDirection();
+ method @FloatRange(from=0.0, to=1.0) public float getProgress();
+ method public androidx.compose.material3.SwipeToDismissValue getTargetValue();
+ method @Deprecated public boolean isDismissed(androidx.compose.material3.DismissDirection direction);
+ method public float requireOffset();
+ method public suspend Object? reset(kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public suspend Object? snapTo(androidx.compose.material3.SwipeToDismissValue targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ property public final androidx.compose.material3.SwipeToDismissValue currentValue;
+ property public final androidx.compose.material3.SwipeToDismissValue dismissDirection;
+ property @FloatRange(from=0.0, to=1.0) public final float progress;
+ property public final androidx.compose.material3.SwipeToDismissValue targetValue;
+ field public static final androidx.compose.material3.SwipeToDismissState.Companion Companion;
+ }
+
+ public static final class SwipeToDismissState.Companion {
+ method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.SwipeToDismissState,androidx.compose.material3.SwipeToDismissValue> Saver(kotlin.jvm.functions.Function1<? super androidx.compose.material3.SwipeToDismissValue,java.lang.Boolean> confirmValueChange, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold, androidx.compose.ui.unit.Density density);
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public enum SwipeToDismissValue {
+ method public static androidx.compose.material3.SwipeToDismissValue valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+ method public static androidx.compose.material3.SwipeToDismissValue[] values();
+ enum_constant public static final androidx.compose.material3.SwipeToDismissValue EndToStart;
+ enum_constant public static final androidx.compose.material3.SwipeToDismissValue Settled;
+ enum_constant public static final androidx.compose.material3.SwipeToDismissValue StartToEnd;
}
@androidx.compose.runtime.Immutable public final class SwitchColors {
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index c4701d4..7ca2ade 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -597,43 +597,19 @@
property public abstract kotlin.ranges.IntRange yearRange;
}
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public enum DismissDirection {
- method public static androidx.compose.material3.DismissDirection valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
- method public static androidx.compose.material3.DismissDirection[] values();
- enum_constant public static final androidx.compose.material3.DismissDirection EndToStart;
- enum_constant public static final androidx.compose.material3.DismissDirection StartToEnd;
+ @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public enum DismissDirection {
+ method @Deprecated public static androidx.compose.material3.DismissDirection valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+ method @Deprecated public static androidx.compose.material3.DismissDirection[] values();
+ enum_constant @Deprecated public static final androidx.compose.material3.DismissDirection EndToStart;
+ enum_constant @Deprecated public static final androidx.compose.material3.DismissDirection StartToEnd;
}
- public final class DismissState {
- ctor public DismissState(androidx.compose.material3.DismissValue initialValue, androidx.compose.ui.unit.Density density, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissValue,java.lang.Boolean> confirmValueChange, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold);
- ctor @Deprecated public DismissState(androidx.compose.material3.DismissValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissValue,java.lang.Boolean> confirmValueChange, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold);
- method public suspend Object? dismiss(androidx.compose.material3.DismissDirection direction, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method public androidx.compose.material3.DismissValue getCurrentValue();
- method public androidx.compose.material3.DismissDirection? getDismissDirection();
- method public float getProgress();
- method public androidx.compose.material3.DismissValue getTargetValue();
- method public boolean isDismissed(androidx.compose.material3.DismissDirection direction);
- method public float requireOffset();
- method public suspend Object? reset(kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method public suspend Object? snapTo(androidx.compose.material3.DismissValue targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- property public final androidx.compose.material3.DismissValue currentValue;
- property public final androidx.compose.material3.DismissDirection? dismissDirection;
- property public final float progress;
- property public final androidx.compose.material3.DismissValue targetValue;
- field public static final androidx.compose.material3.DismissState.Companion Companion;
- }
-
- public static final class DismissState.Companion {
- method @Deprecated public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.DismissState,androidx.compose.material3.DismissValue> Saver(kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissValue,java.lang.Boolean> confirmValueChange, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold);
- method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.DismissState,androidx.compose.material3.DismissValue> Saver(kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissValue,java.lang.Boolean> confirmValueChange, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold, androidx.compose.ui.unit.Density density);
- }
-
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public enum DismissValue {
- method public static androidx.compose.material3.DismissValue valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
- method public static androidx.compose.material3.DismissValue[] values();
- enum_constant public static final androidx.compose.material3.DismissValue Default;
- enum_constant public static final androidx.compose.material3.DismissValue DismissedToEnd;
- enum_constant public static final androidx.compose.material3.DismissValue DismissedToStart;
+ @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public enum DismissValue {
+ method @Deprecated public static androidx.compose.material3.DismissValue valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+ method @Deprecated public static androidx.compose.material3.DismissValue[] values();
+ enum_constant @Deprecated public static final androidx.compose.material3.DismissValue Default;
+ enum_constant @Deprecated public static final androidx.compose.material3.DismissValue DismissedToEnd;
+ enum_constant @Deprecated public static final androidx.compose.material3.DismissValue DismissedToStart;
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class DisplayMode {
@@ -1559,15 +1535,45 @@
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class SwipeToDismissBoxDefaults {
- method @androidx.compose.runtime.Composable public kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> getFixedPositionalThreshold();
- property @androidx.compose.runtime.Composable public final kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> fixedPositionalThreshold;
+ method @androidx.compose.runtime.Composable public kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> getPositionalThreshold();
+ property @androidx.compose.runtime.Composable public final kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> positionalThreshold;
field public static final androidx.compose.material3.SwipeToDismissBoxDefaults INSTANCE;
}
public final class SwipeToDismissBoxKt {
- method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SwipeToDismiss(androidx.compose.material3.DismissState state, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> background, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> dismissContent, optional androidx.compose.ui.Modifier modifier, optional java.util.Set<? extends androidx.compose.material3.DismissDirection> directions);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SwipeToDismissBox(androidx.compose.material3.DismissState state, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> backgroundContent, optional androidx.compose.ui.Modifier modifier, optional java.util.Set<? extends androidx.compose.material3.DismissDirection> directions, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.DismissState rememberDismissState(optional androidx.compose.material3.DismissValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissValue,java.lang.Boolean> confirmValueChange, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold);
+ method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SwipeToDismiss(androidx.compose.material3.SwipeToDismissState state, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> background, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> dismissContent, optional androidx.compose.ui.Modifier modifier, optional java.util.Set<? extends androidx.compose.material3.SwipeToDismissValue> directions);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SwipeToDismissBox(androidx.compose.material3.SwipeToDismissState state, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> backgroundContent, optional androidx.compose.ui.Modifier modifier, optional boolean enableDismissFromStartToEnd, optional boolean enableDismissFromEndToStart, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.SwipeToDismissState rememberSwipeToDismissState(optional androidx.compose.material3.SwipeToDismissValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SwipeToDismissValue,java.lang.Boolean> confirmValueChange, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold);
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class SwipeToDismissState {
+ ctor public SwipeToDismissState(androidx.compose.material3.SwipeToDismissValue initialValue, androidx.compose.ui.unit.Density density, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SwipeToDismissValue,java.lang.Boolean> confirmValueChange, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold);
+ method public suspend Object? dismiss(androidx.compose.material3.SwipeToDismissValue direction, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public androidx.compose.material3.SwipeToDismissValue getCurrentValue();
+ method public androidx.compose.material3.SwipeToDismissValue getDismissDirection();
+ method @FloatRange(from=0.0, to=1.0) public float getProgress();
+ method public androidx.compose.material3.SwipeToDismissValue getTargetValue();
+ method @Deprecated public boolean isDismissed(androidx.compose.material3.DismissDirection direction);
+ method public float requireOffset();
+ method public suspend Object? reset(kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public suspend Object? snapTo(androidx.compose.material3.SwipeToDismissValue targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ property public final androidx.compose.material3.SwipeToDismissValue currentValue;
+ property public final androidx.compose.material3.SwipeToDismissValue dismissDirection;
+ property @FloatRange(from=0.0, to=1.0) public final float progress;
+ property public final androidx.compose.material3.SwipeToDismissValue targetValue;
+ field public static final androidx.compose.material3.SwipeToDismissState.Companion Companion;
+ }
+
+ public static final class SwipeToDismissState.Companion {
+ method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.SwipeToDismissState,androidx.compose.material3.SwipeToDismissValue> Saver(kotlin.jvm.functions.Function1<? super androidx.compose.material3.SwipeToDismissValue,java.lang.Boolean> confirmValueChange, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold, androidx.compose.ui.unit.Density density);
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public enum SwipeToDismissValue {
+ method public static androidx.compose.material3.SwipeToDismissValue valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+ method public static androidx.compose.material3.SwipeToDismissValue[] values();
+ enum_constant public static final androidx.compose.material3.SwipeToDismissValue EndToStart;
+ enum_constant public static final androidx.compose.material3.SwipeToDismissValue Settled;
+ enum_constant public static final androidx.compose.material3.SwipeToDismissValue StartToEnd;
}
@androidx.compose.runtime.Immutable public final class SwitchColors {
diff --git a/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/SwipeToDismissDemo.kt b/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/SwipeToDismissDemo.kt
index 6f691ed..f751e9f 100644
--- a/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/SwipeToDismissDemo.kt
+++ b/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/SwipeToDismissDemo.kt
@@ -16,10 +16,8 @@
package androidx.compose.material3.demos
-import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.animation.shrinkHorizontally
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
@@ -30,14 +28,13 @@
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Done
import androidx.compose.material3.Card
-import androidx.compose.material3.DismissDirection
-import androidx.compose.material3.DismissValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.SwipeToDismissBox
+import androidx.compose.material3.SwipeToDismissValue
import androidx.compose.material3.Text
-import androidx.compose.material3.rememberDismissState
+import androidx.compose.material3.rememberSwipeToDismissState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -86,79 +83,75 @@
var unread by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
- val dismissState = rememberDismissState(
+ val dismissState = rememberSwipeToDismissState(
confirmValueChange = {
- if (it == DismissValue.DismissedToEnd) unread = !unread
- it != DismissValue.DismissedToEnd
+ if (it == SwipeToDismissValue.StartToEnd) unread = !unread
+ it != SwipeToDismissValue.StartToEnd
},
positionalThreshold = { distance -> distance * .25f }
)
- val isDismissed = dismissState.isDismissed(DismissDirection.EndToStart)
- AnimatedVisibility(
- visible = !isDismissed,
- exit = shrinkHorizontally(shrinkTowards = Alignment.Start)
- ) {
- SwipeToDismissBox(
- state = dismissState,
- backgroundContent = {
- val direction = dismissState.dismissDirection ?: return@SwipeToDismissBox
- val color by animateColorAsState(
- when (dismissState.targetValue) {
- DismissValue.Default -> Color.LightGray
- DismissValue.DismissedToEnd -> Color.Green
- DismissValue.DismissedToStart -> Color.Red
- }
- )
- val alignment = when (direction) {
- DismissDirection.StartToEnd -> Alignment.CenterStart
- DismissDirection.EndToStart -> Alignment.CenterEnd
+ SwipeToDismissBox(
+ state = dismissState,
+ modifier = Modifier.padding(vertical = 4.dp),
+ backgroundContent = {
+ val direction = dismissState.dismissDirection
+ val color by animateColorAsState(
+ when (dismissState.targetValue) {
+ SwipeToDismissValue.Settled -> Color.LightGray
+ SwipeToDismissValue.StartToEnd -> Color.Green
+ SwipeToDismissValue.EndToStart -> Color.Red
}
- val icon = when (direction) {
- DismissDirection.StartToEnd -> Icons.Default.Done
- DismissDirection.EndToStart -> Icons.Default.Delete
- }
- val scale by animateFloatAsState(
- if (dismissState.targetValue == DismissValue.Default)
- 0.75f else 1f
- )
- Box(
- Modifier
- .fillMaxSize()
- .background(color)
- .padding(horizontal = 20.dp),
- contentAlignment = alignment
- ) {
- Icon(
- icon,
- contentDescription = "Localized description",
- modifier = Modifier.scale(scale)
- )
- }
- },
- modifier = Modifier.padding(vertical = 4.dp)
- ) {
- Card {
- ListItem(
- headlineContent = {
- Text(item, fontWeight = if (unread) FontWeight.Bold else null)
- },
- modifier = Modifier.semantics {
- // Provide accessible alternatives to swipe actions.
- val label = if (unread) "Mark Read" else "Mark Unread"
- customActions = listOf(
- CustomAccessibilityAction(label) { unread = !unread; true },
- CustomAccessibilityAction("Delete") {
- scope.launch {
- dismissState.dismiss(DismissDirection.EndToStart)
- }
- true
- }
- )
- },
- supportingContent = { Text("Swipe me left or right!") },
+ )
+ val alignment = when (direction) {
+ SwipeToDismissValue.StartToEnd,
+ SwipeToDismissValue.Settled -> Alignment.CenterStart
+ SwipeToDismissValue.EndToStart -> Alignment.CenterEnd
+ }
+ val icon = when (direction) {
+ SwipeToDismissValue.StartToEnd,
+ SwipeToDismissValue.Settled -> Icons.Default.Done
+ SwipeToDismissValue.EndToStart -> Icons.Default.Delete
+ }
+ val scale by animateFloatAsState(
+ if (dismissState.targetValue == SwipeToDismissValue.Settled)
+ 0.75f else 1f
+ )
+ Box(
+ Modifier
+ .fillMaxSize()
+ .background(color)
+ .padding(horizontal = 20.dp),
+ contentAlignment = alignment
+ ) {
+ Icon(
+ icon,
+ contentDescription = "Localized description",
+ modifier = Modifier.scale(scale)
)
}
}
+ ) {
+ Card {
+ ListItem(
+ headlineContent = {
+ Text(item, fontWeight = if (unread) FontWeight.Bold else null)
+ },
+ modifier = Modifier.semantics {
+ // Provide accessible alternatives to swipe actions.
+ val label = if (unread) "Mark Read" else "Mark Unread"
+ customActions = listOf(
+ CustomAccessibilityAction(label) { unread = !unread; true },
+ CustomAccessibilityAction("Delete") {
+ scope.launch {
+ dismissState.dismiss(SwipeToDismissValue.EndToStart)
+ }
+ true
+ }
+ )
+ },
+ supportingContent = { Text("Swipe me left or right!") },
+ )
+ }
}
}
}
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SwipeToDismissSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SwipeToDismissSamples.kt
index 81d3b8e..b883b78 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SwipeToDismissSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SwipeToDismissSamples.kt
@@ -22,15 +22,13 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Card
-import androidx.compose.material3.DismissValue.Default
-import androidx.compose.material3.DismissValue.DismissedToEnd
-import androidx.compose.material3.DismissValue.DismissedToStart
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.ListItem
import androidx.compose.material3.SwipeToDismissBox
+import androidx.compose.material3.SwipeToDismissValue
import androidx.compose.material3.Text
-import androidx.compose.material3.rememberDismissState
+import androidx.compose.material3.rememberSwipeToDismissState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
@@ -42,15 +40,15 @@
@Composable
@ExperimentalMaterial3Api
fun SwipeToDismissListItems() {
- val dismissState = rememberDismissState()
+ val dismissState = rememberSwipeToDismissState()
SwipeToDismissBox(
state = dismissState,
backgroundContent = {
val color by animateColorAsState(
when (dismissState.targetValue) {
- Default -> Color.LightGray
- DismissedToEnd -> Color.Green
- DismissedToStart -> Color.Red
+ SwipeToDismissValue.Settled -> Color.LightGray
+ SwipeToDismissValue.StartToEnd -> Color.Green
+ SwipeToDismissValue.EndToStart -> Color.Red
}
)
Box(Modifier.fillMaxSize().background(color))
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SearchBarTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SearchBarTest.kt
index 89721c6..b025a39 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SearchBarTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SearchBarTest.kt
@@ -17,6 +17,7 @@
package androidx.compose.material3
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
+import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@@ -70,6 +71,9 @@
val dispatcher = LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher
var active by remember { mutableStateOf(false) }
+ // Extra item for initial focus.
+ Box(Modifier.size(10.dp).focusable())
+
SearchBar(
modifier = Modifier.testTag(SearchBarTestTag),
query = "Query",
@@ -249,6 +253,9 @@
val dispatcher = LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher
var active by remember { mutableStateOf(false) }
+ // Extra item for initial focus.
+ Box(Modifier.size(10.dp).focusable())
+
DockedSearchBar(
modifier = Modifier.testTag(SearchBarTestTag),
query = "Query",
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SwipeToDismissTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SwipeToDismissTest.kt
index 0ce7603..05a5858 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SwipeToDismissTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SwipeToDismissTest.kt
@@ -69,7 +69,7 @@
fun swipeDismiss_testOffset_whenDefault() {
rule.setContent {
SwipeToDismissBox(
- state = rememberDismissState(DismissValue.Default),
+ state = rememberSwipeToDismissState(SwipeToDismissValue.Settled),
backgroundContent = { }
) {
Box(
@@ -87,7 +87,7 @@
fun swipeDismiss_testOffset_whenDismissedToEnd() {
rule.setContent {
SwipeToDismissBox(
- state = rememberDismissState(DismissValue.DismissedToEnd),
+ state = rememberSwipeToDismissState(SwipeToDismissValue.StartToEnd),
backgroundContent = { }
) {
Box(
@@ -106,8 +106,8 @@
fun swipeDismiss_testOffset_whenDismissedToStart() {
rule.setContent {
SwipeToDismissBox(
- state = rememberDismissState(DismissValue.DismissedToStart),
- backgroundContent = { }
+ state = rememberSwipeToDismissState(SwipeToDismissValue.EndToStart),
+ backgroundContent = { },
) {
Box(
Modifier
@@ -125,7 +125,7 @@
fun swipeDismiss_testBackgroundMatchesContentSize() {
rule.setContent {
SwipeToDismissBox(
- state = rememberDismissState(DismissValue.Default),
+ state = rememberSwipeToDismissState(SwipeToDismissValue.Settled),
backgroundContent = {
Box(
Modifier
@@ -142,14 +142,15 @@
@Test
fun swipeDismiss_dismissBySwipe_toEnd() {
- lateinit var dismissState: DismissState
+ lateinit var swipeToDismissState: SwipeToDismissState
rule.setContent {
- dismissState = rememberDismissState(DismissValue.Default)
+ swipeToDismissState = rememberSwipeToDismissState(SwipeToDismissValue.Settled)
SwipeToDismissBox(
- state = dismissState,
- backgroundContent = { },
+ state = swipeToDismissState,
modifier = Modifier.testTag(swipeDismissTag),
- directions = setOf(DismissDirection.StartToEnd)
+ enableDismissFromStartToEnd = true,
+ enableDismissFromEndToStart = false,
+ backgroundContent = { }
) { Box(Modifier.fillMaxSize()) }
}
@@ -158,20 +159,21 @@
advanceClock()
rule.runOnIdle {
- assertThat(dismissState.currentValue).isEqualTo(DismissValue.DismissedToEnd)
+ assertThat(swipeToDismissState.currentValue).isEqualTo(SwipeToDismissValue.StartToEnd)
}
}
@Test
fun swipeDismiss_dismissBySwipe_toStart() {
- lateinit var dismissState: DismissState
+ lateinit var swipeToDismissState: SwipeToDismissState
rule.setContent {
- dismissState = rememberDismissState(DismissValue.Default)
+ swipeToDismissState = rememberSwipeToDismissState(SwipeToDismissValue.Settled)
SwipeToDismissBox(
- state = dismissState,
- backgroundContent = { },
+ state = swipeToDismissState,
modifier = Modifier.testTag(swipeDismissTag),
- directions = setOf(DismissDirection.EndToStart)
+ enableDismissFromStartToEnd = false,
+ enableDismissFromEndToStart = true,
+ backgroundContent = { },
) { Box(Modifier.fillMaxSize()) }
}
@@ -180,21 +182,22 @@
advanceClock()
rule.runOnIdle {
- assertThat(dismissState.currentValue).isEqualTo(DismissValue.DismissedToStart)
+ assertThat(swipeToDismissState.currentValue).isEqualTo(SwipeToDismissValue.EndToStart)
}
}
@Test
fun swipeDismiss_dismissBySwipe_toEnd_rtl() {
- lateinit var dismissState: DismissState
+ lateinit var swipeToDismissState: SwipeToDismissState
rule.setContent {
- dismissState = rememberDismissState(DismissValue.Default)
+ swipeToDismissState = rememberSwipeToDismissState()
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
SwipeToDismissBox(
- state = dismissState,
- backgroundContent = { },
+ state = swipeToDismissState,
modifier = Modifier.testTag(swipeDismissTag),
- directions = setOf(DismissDirection.StartToEnd)
+ enableDismissFromStartToEnd = true,
+ enableDismissFromEndToStart = false,
+ backgroundContent = { },
) { Box(Modifier.fillMaxSize()) }
}
}
@@ -204,21 +207,22 @@
advanceClock()
rule.runOnIdle {
- assertThat(dismissState.currentValue).isEqualTo(DismissValue.DismissedToEnd)
+ assertThat(swipeToDismissState.currentValue).isEqualTo(SwipeToDismissValue.StartToEnd)
}
}
@Test
fun swipeDismiss_dismissBySwipe_toStart_rtl() {
- lateinit var dismissState: DismissState
+ lateinit var swipeToDismissState: SwipeToDismissState
rule.setContent {
- dismissState = rememberDismissState(DismissValue.Default)
+ swipeToDismissState = rememberSwipeToDismissState(SwipeToDismissValue.Settled)
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
SwipeToDismissBox(
- state = dismissState,
- backgroundContent = { },
+ state = swipeToDismissState,
modifier = Modifier.testTag(swipeDismissTag),
- directions = setOf(DismissDirection.EndToStart)
+ enableDismissFromStartToEnd = false,
+ enableDismissFromEndToStart = true,
+ backgroundContent = { },
) { Box(Modifier.fillMaxSize()) }
}
}
@@ -228,20 +232,21 @@
advanceClock()
rule.runOnIdle {
- assertThat(dismissState.currentValue).isEqualTo(DismissValue.DismissedToStart)
+ assertThat(swipeToDismissState.currentValue).isEqualTo(SwipeToDismissValue.EndToStart)
}
}
@Test
fun swipeDismiss_dismissBySwipe_disabled() {
- lateinit var dismissState: DismissState
+ lateinit var swipeToDismissState: SwipeToDismissState
rule.setContent {
- dismissState = rememberDismissState(DismissValue.Default)
+ swipeToDismissState = rememberSwipeToDismissState(SwipeToDismissValue.Settled)
SwipeToDismissBox(
- state = dismissState,
- backgroundContent = { },
+ state = swipeToDismissState,
modifier = Modifier.testTag(swipeDismissTag),
- directions = setOf()
+ enableDismissFromStartToEnd = false,
+ enableDismissFromEndToStart = false,
+ backgroundContent = { },
) { Box(Modifier.fillMaxSize()) }
}
@@ -250,7 +255,7 @@
advanceClock()
rule.runOnIdle {
- assertThat(dismissState.currentValue).isEqualTo(DismissValue.Default)
+ assertThat(swipeToDismissState.currentValue).isEqualTo(SwipeToDismissValue.Settled)
}
rule.onNodeWithTag(swipeDismissTag).performTouchInput { swipeLeft() }
@@ -258,7 +263,7 @@
advanceClock()
rule.runOnIdle {
- assertThat(dismissState.currentValue).isEqualTo(DismissValue.Default)
+ assertThat(swipeToDismissState.currentValue).isEqualTo(SwipeToDismissValue.Settled)
}
}
@@ -273,7 +278,7 @@
lateinit var lazyState: LazyListState
lateinit var scope: CoroutineScope
val amountOfItems = 100
- val composedItems = mutableMapOf<Int, DismissState>()
+ val composedItems = mutableMapOf<Int, SwipeToDismissState>()
rule.setContent {
scope = rememberCoroutineScope()
@@ -281,9 +286,9 @@
lazyState = rememberLazyListState()
LazyColumn(state = lazyState) {
items(amountOfItems, key = { item -> item }) { index ->
- composedItems[index] = rememberDismissState()
- val isDismissed = composedItems[index]!!
- .isDismissed(DismissDirection.EndToStart)
+ composedItems[index] = rememberSwipeToDismissState()
+ val isDismissed =
+ composedItems[index]!!.currentValue == SwipeToDismissValue.EndToStart
AnimatedVisibility(visible = !isDismissed) {
SwipeToDismissBox(
modifier = Modifier
@@ -320,7 +325,7 @@
// Dismiss an item so that the lazy layout is required to compose a new item
scope.launch {
- composedItems[initiallyVisibleItems - 1]!!.dismiss(DismissDirection.EndToStart)
+ composedItems[initiallyVisibleItems - 1]!!.dismiss(SwipeToDismissValue.EndToStart)
}
rule.waitForIdle()
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
index e9d0618..ad8a03e 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
@@ -28,13 +28,10 @@
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
import androidx.compose.material3.tokens.AssistChipTokens
import androidx.compose.material3.tokens.FilterChipTokens
import androidx.compose.material3.tokens.InputChipTokens
@@ -56,12 +53,18 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.offset
+import androidx.compose.ui.util.fastFirst
+import androidx.compose.ui.util.fastFirstOrNull
/**
* <a href="https://m3.material.io/components/chips/overview" class="external" target="_blank">Material Design assist chip</a>.
@@ -1778,31 +1781,80 @@
LocalContentColor provides labelColor,
LocalTextStyle provides labelTextStyle
) {
- Row(
- Modifier
- .width(IntrinsicSize.Max)
+ Layout(
+ modifier = Modifier
.defaultMinSize(minHeight = minHeight)
.padding(paddingValues),
- horizontalArrangement = Arrangement.Start,
- verticalAlignment = Alignment.CenterVertically
- ) {
- if (avatar != null) {
- avatar()
- } else if (leadingIcon != null) {
- CompositionLocalProvider(
- LocalContentColor provides leadingIconColor, content = leadingIcon
+ content = {
+ if (avatar != null || leadingIcon != null) {
+ Box(
+ modifier = Modifier
+ .layoutId(LeadingIconLayoutId),
+ contentAlignment = Alignment.Center,
+ content = {
+ if (avatar != null) {
+ avatar()
+ } else if (leadingIcon != null) {
+ CompositionLocalProvider(
+ LocalContentColor provides leadingIconColor,
+ content = leadingIcon
+ )
+ }
+ }
+ )
+ }
+ Row(
+ modifier = Modifier
+ .layoutId(LabelLayoutId)
+ .padding(HorizontalElementsPadding, 0.dp),
+ horizontalArrangement = Arrangement.Start,
+ verticalAlignment = Alignment.CenterVertically,
+ content = { label() }
)
+ if (trailingIcon != null) {
+ Box(
+ modifier = Modifier
+ .layoutId(TrailingIconLayoutId),
+ contentAlignment = Alignment.Center,
+ content = {
+ CompositionLocalProvider(
+ LocalContentColor provides trailingIconColor,
+ content = trailingIcon
+ )
+ }
+ )
+ }
}
- Spacer(Modifier.width(HorizontalElementsPadding))
- Row(
- modifier = Modifier.weight(1f),
- horizontalArrangement = Arrangement.Start,
- verticalAlignment = Alignment.CenterVertically
- ) { label() }
- Spacer(Modifier.width(HorizontalElementsPadding))
- if (trailingIcon != null) {
- CompositionLocalProvider(
- LocalContentColor provides trailingIconColor, content = trailingIcon
+ ) { measurables, constraints ->
+ val leadingIconPlaceable: Placeable? =
+ measurables.fastFirstOrNull { it.layoutId == LeadingIconLayoutId }
+ ?.measure(constraints.copy(minWidth = 0, minHeight = 0))
+ val leadingIconWidth = widthOrZero(leadingIconPlaceable)
+ val leadingIconHeight = heightOrZero(leadingIconPlaceable)
+
+ val trailingIconPlaceable: Placeable? =
+ measurables.fastFirstOrNull { it.layoutId == TrailingIconLayoutId }
+ ?.measure(constraints.copy(minWidth = 0, minHeight = 0))
+ val trailingIconWidth = widthOrZero(trailingIconPlaceable)
+ val trailingIconHeight = heightOrZero(trailingIconPlaceable)
+
+ val labelPlaceable = measurables.fastFirst { it.layoutId == LabelLayoutId }
+ .measure(
+ constraints.offset(horizontal = -(leadingIconWidth + trailingIconWidth))
+ )
+
+ val width = leadingIconWidth + labelPlaceable.width + trailingIconWidth
+ val height = maxOf(leadingIconHeight, labelPlaceable.height, trailingIconHeight)
+
+ layout(width, height) {
+ leadingIconPlaceable?.placeRelative(
+ 0,
+ Alignment.CenterVertically.align(leadingIconHeight, height)
+ )
+ labelPlaceable.placeRelative(leadingIconWidth, 0)
+ trailingIconPlaceable?.placeRelative(
+ leadingIconWidth + labelPlaceable.width,
+ Alignment.CenterVertically.align(trailingIconHeight, height)
)
}
}
@@ -2435,3 +2487,7 @@
* Returns the [PaddingValues] for the suggestion chip.
*/
private val SuggestionChipPadding = PaddingValues(horizontal = HorizontalElementsPadding)
+
+private const val LeadingIconLayoutId = "leadingIcon"
+private const val LabelLayoutId = "label"
+private const val TrailingIconLayoutId = "trailingIcon"
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeToDismissBox.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeToDismissBox.kt
index a22cbe7..ebbe7c7 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeToDismissBox.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeToDismissBox.kt
@@ -16,18 +16,13 @@
package androidx.compose.material3
+import androidx.annotation.FloatRange
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
-import androidx.compose.material3.DismissDirection.EndToStart
-import androidx.compose.material3.DismissDirection.StartToEnd
-import androidx.compose.material3.DismissState.Companion.Saver
-import androidx.compose.material3.DismissValue.Default
-import androidx.compose.material3.DismissValue.DismissedToEnd
-import androidx.compose.material3.DismissValue.DismissedToStart
+import androidx.compose.material3.SwipeToDismissState.Companion.Saver
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
@@ -52,7 +47,7 @@
* The directions in which a [SwipeToDismissBox] can be dismissed.
*/
@ExperimentalMaterial3Api
-enum class DismissDirection {
+enum class SwipeToDismissValue {
/**
* Can be dismissed by swiping in the reading direction.
*/
@@ -61,81 +56,38 @@
/**
* Can be dismissed by swiping in the reverse of the reading direction.
*/
- EndToStart
-}
-
-/**
- * Possible values of [DismissState].
- */
-@ExperimentalMaterial3Api
-enum class DismissValue {
- /**
- * Indicates the component has not been dismissed yet.
- */
- Default,
+ EndToStart,
/**
- * Indicates the component has been dismissed in the reading direction.
+ * Cannot currently be dismissed.
*/
- DismissedToEnd,
-
- /**
- * Indicates the component has been dismissed in the reverse of the reading direction.
- */
- DismissedToStart
+ Settled
}
/**
* State of the [SwipeToDismissBox] composable.
*
* @param initialValue The initial value of the state.
+ * @param density The density that this state can use to convert values to and from dp.
* @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
* @param positionalThreshold The positional threshold to be used when calculating the target state
* while a swipe is in progress and when settling after the swipe ends. This is the distance from
* the start of a transition. It will be, depending on the direction of the interaction, added or
* subtracted from/to the origin offset. It should always be a positive value.
*/
-@OptIn(ExperimentalMaterial3Api::class)
-class DismissState @Deprecated(
- message = "This constructor is deprecated. " +
- "Please use the constructor that provides a [Density]",
- replaceWith = ReplaceWith(
- "DismissState(" +
- "initialValue, LocalDensity.current, confirmValueChange, positionalThreshold)"
- )
-) constructor(
- initialValue: DismissValue,
- confirmValueChange: (DismissValue) -> Boolean = { true },
+@ExperimentalMaterial3Api
+class SwipeToDismissState(
+ initialValue: SwipeToDismissValue,
+ internal val density: Density,
+ confirmValueChange: (SwipeToDismissValue) -> Boolean = { true },
positionalThreshold: (totalDistance: Float) -> Float
) {
-
- /**
- * State of the [SwipeToDismissBox] composable.
- *
- * @param initialValue The initial value of the state.
- * @param density The density that this state can use to convert values to and from dp.
- * @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
- * @param positionalThreshold The positional threshold to be used when calculating the target state
- * while a swipe is in progress and when settling after the swipe ends. This is the distance from
- * the start of a transition. It will be, depending on the direction of the interaction, added or
- * subtracted from/to the origin offset. It should always be a positive value.
- */
- @Suppress("Deprecation")
- constructor(
- initialValue: DismissValue,
- density: Density,
- confirmValueChange: (DismissValue) -> Boolean = { true },
- positionalThreshold: (totalDistance: Float) -> Float
- ) : this(initialValue, confirmValueChange, positionalThreshold) {
- this.density = density
- }
-
internal val anchoredDraggableState = AnchoredDraggableState(
initialValue = initialValue,
animationSpec = AnchoredDraggableDefaults.AnimationSpec,
confirmValueChange = confirmValueChange,
positionalThreshold = positionalThreshold,
- velocityThreshold = { with(requireDensity()) { DismissThreshold.toPx() } }
+ velocityThreshold = { with(density) { DismissThreshold.toPx() } }
)
internal val offset: Float get() = anchoredDraggableState.offset
@@ -148,40 +100,53 @@
fun requireOffset(): Float = anchoredDraggableState.requireOffset()
/**
- * The current state value of the [DismissState].
+ * The current state value of the [SwipeToDismissState].
*/
- val currentValue: DismissValue get() = anchoredDraggableState.currentValue
+ val currentValue: SwipeToDismissValue get() = anchoredDraggableState.currentValue
/**
* The target state. This is the closest state to the current offset (taking into account
* positional thresholds). If no interactions like animations or drags are in progress, this
* will be the current state.
*/
- val targetValue: DismissValue get() = anchoredDraggableState.targetValue
+ val targetValue: SwipeToDismissValue get() = anchoredDraggableState.targetValue
/**
* The fraction of the progress going from currentValue to targetValue, within [0f..1f] bounds.
*/
+ @get:FloatRange(from = 0.0, to = 1.0)
val progress: Float get() = anchoredDraggableState.progress
/**
* The direction (if any) in which the composable has been or is being dismissed.
*
- * If the composable is settled at the default state, then this will be null. Use this to
- * change the background of the [SwipeToDismissBox] if you want different actions on each side.
+ * Use this to change the background of the [SwipeToDismissBox] if you want different actions on each
+ * side.
*/
- val dismissDirection: DismissDirection?
+ val dismissDirection: SwipeToDismissValue
get() = if (offset == 0f || offset.isNaN())
- null
- else if (offset > 0f) StartToEnd else EndToStart
+ SwipeToDismissValue.Settled
+ else if (offset > 0f) SwipeToDismissValue.StartToEnd else SwipeToDismissValue.EndToStart
/**
* Whether the component has been dismissed in the given [direction].
*
* @param direction The dismiss direction.
*/
+ @Deprecated(
+ message = "DismissDirection is no longer used by SwipeToDismissState. Please compare " +
+ "currentValue against SwipeToDismissValue instead.",
+ level = DeprecationLevel.HIDDEN
+ )
+ @Suppress("DEPRECATION")
fun isDismissed(direction: DismissDirection): Boolean {
- return currentValue == if (direction == StartToEnd) DismissedToEnd else DismissedToStart
+ return currentValue == (
+ if (direction == DismissDirection.StartToEnd) {
+ SwipeToDismissValue.StartToEnd
+ } else {
+ SwipeToDismissValue.EndToStart
+ }
+ )
}
/**
@@ -189,7 +154,7 @@
*
* @param targetValue The new target value
*/
- suspend fun snapTo(targetValue: DismissValue) {
+ suspend fun snapTo(targetValue: SwipeToDismissValue) {
anchoredDraggableState.snapTo(targetValue)
}
@@ -200,7 +165,9 @@
*
* @return the reason the reset animation ended
*/
- suspend fun reset() = anchoredDraggableState.animateTo(targetValue = Default)
+ suspend fun reset() = anchoredDraggableState.animateTo(
+ targetValue = SwipeToDismissValue.Settled
+ )
/**
* Dismiss the component in the given [direction], with an animation and suspend. This method
@@ -208,64 +175,32 @@
*
* @param direction The dismiss direction.
*/
- suspend fun dismiss(direction: DismissDirection) {
- val targetValue = if (direction == StartToEnd) DismissedToEnd else DismissedToStart
- anchoredDraggableState.animateTo(targetValue = targetValue)
- }
-
- internal var density: Density? = null
- private fun requireDensity() = requireNotNull(density) {
- "DismissState did not have a density attached. Are you using DismissState with " +
- "the SwipeDismiss component?"
+ suspend fun dismiss(direction: SwipeToDismissValue) {
+ anchoredDraggableState.animateTo(targetValue = direction)
}
companion object {
/**
- * The default [Saver] implementation for [DismissState].
+ * The default [Saver] implementation for [SwipeToDismissState].
*/
fun Saver(
- confirmValueChange: (DismissValue) -> Boolean,
+ confirmValueChange: (SwipeToDismissValue) -> Boolean,
positionalThreshold: (totalDistance: Float) -> Float,
density: Density
- ) =
- Saver<DismissState, DismissValue>(
- save = { it.currentValue },
- restore = {
- DismissState(
- it, density, confirmValueChange, positionalThreshold
- )
- }
- )
-
- /**
- * The default [Saver] implementation for [DismissState].
- */
- @Deprecated(
- message = "This function is deprecated. Please use the overload where Density is" +
- " provided.",
- replaceWith = ReplaceWith(
- "Saver(confirmValueChange, positionalThreshold, LocalDensity.current)"
- )
+ ) = Saver<SwipeToDismissState, SwipeToDismissValue>(
+ save = { it.currentValue },
+ restore = {
+ SwipeToDismissState(
+ it, density, confirmValueChange, positionalThreshold
+ )
+ }
)
- @Suppress("Deprecation")
- fun Saver(
- confirmValueChange: (DismissValue) -> Boolean,
- positionalThreshold: (totalDistance: Float) -> Float,
- ) =
- Saver<DismissState, DismissValue>(
- save = { it.currentValue },
- restore = {
- DismissState(
- it, confirmValueChange, positionalThreshold
- )
- }
- )
}
}
/**
- * Create and [remember] a [DismissState].
+ * Create and [remember] a [SwipeToDismissState].
*
* @param initialValue The initial value of the state.
* @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
@@ -276,12 +211,12 @@
*/
@Composable
@ExperimentalMaterial3Api
-fun rememberDismissState(
- initialValue: DismissValue = Default,
- confirmValueChange: (DismissValue) -> Boolean = { true },
+fun rememberSwipeToDismissState(
+ initialValue: SwipeToDismissValue = SwipeToDismissValue.Settled,
+ confirmValueChange: (SwipeToDismissValue) -> Boolean = { true },
positionalThreshold: (totalDistance: Float) -> Float =
- SwipeToDismissBoxDefaults.fixedPositionalThreshold,
-): DismissState {
+ SwipeToDismissBoxDefaults.positionalThreshold,
+): SwipeToDismissState {
val density = LocalDensity.current
return rememberSaveable(
saver = Saver(
@@ -290,7 +225,7 @@
positionalThreshold = positionalThreshold
)
) {
- DismissState(initialValue, density, confirmValueChange, positionalThreshold)
+ SwipeToDismissState(initialValue, density, confirmValueChange, positionalThreshold)
}
}
@@ -311,16 +246,26 @@
level = DeprecationLevel.WARNING,
message = "Use SwipeToDismissBox instead",
replaceWith =
- ReplaceWith("SwipeToDismissBox(state, background, modifier, directions, dismissContent)")
+ ReplaceWith("SwipeToDismissBox(state, background, modifier, " +
+ "enableDismissFromStartToEnd, enableDismissFromEndToStart, dismissContent)")
)
@ExperimentalMaterial3Api
fun SwipeToDismiss(
- state: DismissState,
+ state: SwipeToDismissState,
background: @Composable RowScope.() -> Unit,
dismissContent: @Composable RowScope.() -> Unit,
modifier: Modifier = Modifier,
- directions: Set<DismissDirection> = setOf(EndToStart, StartToEnd),
-) = SwipeToDismissBox(state, background, modifier, directions, dismissContent)
+ directions: Set<SwipeToDismissValue> = setOf(SwipeToDismissValue.EndToStart,
+ SwipeToDismissValue.StartToEnd
+ ),
+) = SwipeToDismissBox(
+ state = state,
+ backgroundContent = background,
+ modifier = modifier,
+ enableDismissFromStartToEnd = SwipeToDismissValue.StartToEnd in directions,
+ enableDismissFromEndToStart = SwipeToDismissValue.EndToStart in directions,
+ content = dismissContent
+)
/**
* A composable that can be dismissed by swiping left or right.
@@ -328,27 +273,23 @@
* @sample androidx.compose.material3.samples.SwipeToDismissListItems
*
* @param state The state of this component.
- * @param backgroundContent A composable that is stacked behind the content and is exposed when the
+ * @param backgroundContent A composable that is stacked behind the [content] and is exposed when the
* content is swiped. You can/should use the [state] to have different backgrounds on each side.
- * @param content The content that can be dismissed.
* @param modifier Optional [Modifier] for this component.
- * @param directions The set of directions in which the component can be dismissed.
+ * @param enableDismissFromStartToEnd Whether SwipeToDismissBox can be dismissed from start to end.
+ * @param enableDismissFromEndToStart Whether SwipeToDismissBox can be dismissed from end to start.
+ * @param content The content that can be dismissed.
*/
@Composable
@ExperimentalMaterial3Api
fun SwipeToDismissBox(
- state: DismissState,
+ state: SwipeToDismissState,
backgroundContent: @Composable RowScope.() -> Unit,
modifier: Modifier = Modifier,
- directions: Set<DismissDirection> = setOf(EndToStart, StartToEnd),
+ enableDismissFromStartToEnd: Boolean = true,
+ enableDismissFromEndToStart: Boolean = true,
content: @Composable RowScope.() -> Unit,
) {
- // b/278692145 Remove this once deprecated methods without density are removed
- val density = LocalDensity.current
- SideEffect {
- state.density = density
- }
-
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
Box(
@@ -356,7 +297,7 @@
.anchoredDraggable(
state = state.anchoredDraggableState,
orientation = Orientation.Horizontal,
- enabled = state.currentValue == Default,
+ enabled = state.currentValue == SwipeToDismissValue.Settled,
reverseDirection = isRtl,
),
propagateMinConstraints = true
@@ -367,66 +308,134 @@
)
Row(
content = content,
- modifier = Modifier.swipeDismissAnchors(state, directions)
+ modifier = Modifier.swipeToDismissAnchors(
+ state,
+ enableDismissFromStartToEnd,
+ enableDismissFromEndToStart
+ )
)
}
}
-/** Contains default values for [SwipeToDismissBox] and [DismissState]. */
+/** Contains default values for [SwipeToDismissBox] and [SwipeToDismissState]. */
@ExperimentalMaterial3Api
object SwipeToDismissBoxDefaults {
- /** Default positional threshold of 56.dp for [DismissState]. */
- val fixedPositionalThreshold: (totalDistance: Float) -> Float
+ /** Default positional threshold of 56.dp for [SwipeToDismissState]. */
+ val positionalThreshold: (totalDistance: Float) -> Float
@Composable get() = with(LocalDensity.current) {
{ 56.dp.toPx() }
}
}
+/**
+ * The directions in which a [SwipeToDismissBox] can be dismissed.
+ */
+@ExperimentalMaterial3Api
+@Deprecated(
+ message = "Dismiss direction is no longer used by SwipeToDismissState. Please use " +
+ "SwipeToDismissValue instead.",
+ level = DeprecationLevel.WARNING
+)
+enum class DismissDirection {
+ /**
+ * Can be dismissed by swiping in the reading direction.
+ */
+ StartToEnd,
+
+ /**
+ * Can be dismissed by swiping in the reverse of the reading direction.
+ */
+ EndToStart,
+}
+
+/**
+ * Possible values of [SwipeToDismissState].
+ */
+@ExperimentalMaterial3Api
+@Deprecated(
+ message = "DismissValue is no longer used by SwipeToDismissState. Please use " +
+ "SwipeToDismissValue instead.",
+ level = DeprecationLevel.WARNING
+)
+enum class DismissValue {
+ /**
+ * Indicates the component has not been dismissed yet.
+ */
+ Default,
+
+ /**
+ * Indicates the component has been dismissed in the reading direction.
+ */
+ DismissedToEnd,
+
+ /**
+ * Indicates the component has been dismissed in the reverse of the reading direction.
+ */
+ DismissedToStart
+}
+
private val DismissThreshold = 125.dp
@OptIn(ExperimentalMaterial3Api::class)
-private fun Modifier.swipeDismissAnchors(state: DismissState, directions: Set<DismissDirection>) =
- this then SwipeDismissAnchorsElement(state, directions)
+private fun Modifier.swipeToDismissAnchors(
+ state: SwipeToDismissState,
+ enableDismissFromStartToEnd: Boolean,
+ enableDismissFromEndToStart: Boolean
+) = this then SwipeToDismissAnchorsElement(
+ state,
+ enableDismissFromStartToEnd,
+ enableDismissFromEndToStart
+)
@OptIn(ExperimentalMaterial3Api::class)
-private class SwipeDismissAnchorsElement(
- private val state: DismissState,
- private val directions: Set<DismissDirection>,
-) : ModifierNodeElement<SwipeDismissAnchorsNode>() {
+private class SwipeToDismissAnchorsElement(
+ private val state: SwipeToDismissState,
+ private val enableDismissFromStartToEnd: Boolean,
+ private val enableDismissFromEndToStart: Boolean,
+) : ModifierNodeElement<SwipeToDismissAnchorsNode>() {
- override fun create() = SwipeDismissAnchorsNode(state, directions)
+ override fun create() = SwipeToDismissAnchorsNode(
+ state,
+ enableDismissFromStartToEnd,
+ enableDismissFromEndToStart,
+ )
- override fun update(node: SwipeDismissAnchorsNode) {
+ override fun update(node: SwipeToDismissAnchorsNode) {
node.state = state
- node.directions = directions
+ node.enableDismissFromStartToEnd = enableDismissFromStartToEnd
+ node.enableDismissFromEndToStart = enableDismissFromEndToStart
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
- other as SwipeDismissAnchorsElement
+ other as SwipeToDismissAnchorsElement
if (state != other.state) return false
- if (directions != other.directions) return false
+ if (enableDismissFromStartToEnd != other.enableDismissFromStartToEnd) return false
+ if (enableDismissFromEndToStart != other.enableDismissFromEndToStart) return false
return true
}
override fun hashCode(): Int {
var result = state.hashCode()
- result = 31 * result + directions.hashCode()
+ result = 31 * result + enableDismissFromStartToEnd.hashCode()
+ result = 31 * result + enableDismissFromEndToStart.hashCode()
return result
}
override fun InspectorInfo.inspectableProperties() {
debugInspectorInfo {
properties["state"] = state
- properties["directions"] = directions
+ properties["enableDismissFromStartToEnd"] = enableDismissFromStartToEnd
+ properties["enableDismissFromEndToStart"] = enableDismissFromEndToStart
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
-private class SwipeDismissAnchorsNode(
- var state: DismissState,
- var directions: Set<DismissDirection>
+private class SwipeToDismissAnchorsNode(
+ var state: SwipeToDismissState,
+ var enableDismissFromStartToEnd: Boolean,
+ var enableDismissFromEndToStart: Boolean,
) : Modifier.Node(), LayoutModifierNode {
private var didLookahead: Boolean = false
@@ -445,12 +454,12 @@
if (isLookingAhead || !didLookahead) {
val width = placeable.width.toFloat()
val newAnchors = DraggableAnchors {
- Default at 0f
- if (StartToEnd in directions) {
- DismissedToEnd at width
+ SwipeToDismissValue.Settled at 0f
+ if (enableDismissFromStartToEnd) {
+ SwipeToDismissValue.StartToEnd at width
}
- if (EndToStart in directions) {
- DismissedToStart at -width
+ if (enableDismissFromEndToStart) {
+ SwipeToDismissValue.EndToStart at -width
}
}
state.anchoredDraggableState.updateAnchors(newAnchors)
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt
index 51b9f61e..7b97f58 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt
@@ -162,8 +162,7 @@
return if ((value and 0x3fUL) == 0UL) {
((value shr 48) and 0xffUL).toFloat() / 255.0f
} else {
- Float16(((value shr 48) and 0xffffUL).toShort())
- .toFloat()
+ halfToFloat(((value shr 48) and 0xffffUL).toShort())
}
}
@@ -185,8 +184,7 @@
return if ((value and 0x3fUL) == 0UL) {
((value shr 40) and 0xffUL).toFloat() / 255.0f
} else {
- Float16(((value shr 32) and 0xffffUL).toShort())
- .toFloat()
+ halfToFloat(((value shr 32) and 0xffffUL).toShort())
}
}
@@ -208,8 +206,7 @@
return if ((value and 0x3fUL) == 0UL) {
((value shr 32) and 0xffUL).toFloat() / 255.0f
} else {
- Float16(((value shr 16) and 0xffffUL).toShort())
- .toFloat()
+ halfToFloat(((value shr 16) and 0xffffUL).toShort())
}
}
@@ -395,7 +392,7 @@
/**
* Create a [Color] by passing individual [red], [green], [blue], [alpha], and [colorSpace]
- * components. The default [color space][ColorSpace] is [SRGB][ColorSpaces.Srgb] and
+ * components. The default [color space][ColorSpace] is [sRGB][ColorSpaces.Srgb] and
* the default [alpha] is `1.0` (opaque). [colorSpace] must have a [ColorSpace.componentCount] of
* 3.
*/
@@ -407,7 +404,7 @@
alpha: Float = 1f,
colorSpace: ColorSpace = ColorSpaces.Srgb
): Color {
- require(
+ requirePrecondition(
red in colorSpace.getMinValue(0)..colorSpace.getMaxValue(0) &&
green in colorSpace.getMinValue(1)..colorSpace.getMaxValue(1) &&
blue in colorSpace.getMinValue(2)..colorSpace.getMaxValue(2) &&
@@ -419,41 +416,37 @@
if (colorSpace.isSrgb) {
val argb = (
((alpha * 255.0f + 0.5f).toInt() shl 24) or
- ((red * 255.0f + 0.5f).toInt() shl 16) or
- ((green * 255.0f + 0.5f).toInt() shl 8) or
- (blue * 255.0f + 0.5f).toInt()
- )
+ ((red * 255.0f + 0.5f).toInt() shl 16) or
+ ((green * 255.0f + 0.5f).toInt() shl 8) or
+ (blue * 255.0f + 0.5f).toInt()
+ )
return Color(value = (argb.toULong() and 0xffffffffUL) shl 32)
}
- require(colorSpace.componentCount == 3) {
+ requirePrecondition(colorSpace.componentCount == 3) {
"Color only works with ColorSpaces with 3 components"
}
val id = colorSpace.id
- require(id != ColorSpace.MinId) {
+ requirePrecondition(id != ColorSpace.MinId) {
"Unknown color space, please use a color space in ColorSpaces"
}
- val r = Float16(red)
- val g = Float16(green)
- val b = Float16(blue)
+ val r = floatToHalf(red)
+ val g = floatToHalf(green)
+ val b = floatToHalf(blue)
val a = (max(0.0f, min(alpha, 1.0f)) * 1023.0f + 0.5f).toInt()
// Suppress sign extension
return Color(
value = (
- ((r.halfValue.toULong() and 0xffffUL) shl 48) or (
- (g.halfValue.toULong() and 0xffffUL) shl 32
- ) or (
- (b.halfValue.toULong() and 0xffffUL) shl 16
- ) or (
- (a.toULong() and 0x3ffUL) shl 6
- ) or (
- id.toULong() and 0x3fUL
- )
- )
+ ((r.toULong() and 0xffffUL) shl 48) or
+ ((g.toULong() and 0xffffUL) shl 32) or
+ ((b.toULong() and 0xffffUL) shl 16) or
+ ((a.toULong() and 0x03ffUL) shl 6) or
+ (id.toULong() and 0x003fUL)
+ )
)
}
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Float16.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Float16.kt
index d48fce3..8aabeca6 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Float16.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Float16.kt
@@ -85,7 +85,7 @@
*
* This table shows that numbers higher than 1024 lose all fractional precision.
*/
[email protected]
+@JvmInline
internal value class Float16(val halfValue: Short) : Comparable<Float16> {
/**
@@ -94,11 +94,7 @@
*
* @param value The value to be represented by the `Float16`
*/
- constructor(value: Float) : this(
- floatToHalf(
- value
- )
- )
+ constructor(value: Float) : this(floatToHalf(value))
/**
* Constructs a newly allocated `Float16` object that
@@ -115,9 +111,7 @@
* @return The half-precision float value represented by this object
* converted to type `Byte`
*/
- fun toByte(): Byte {
- return toFloat().toInt().toByte()
- }
+ fun toByte(): Byte = toFloat().toInt().toByte()
/**
* Returns the value of this `Float16` as a `Short` after
@@ -126,9 +120,7 @@
* @return The half-precision float value represented by this object
* converted to type `Short`
*/
- fun toShort(): Short {
- return toFloat().toInt().toShort()
- }
+ fun toShort(): Short = toFloat().toInt().toShort()
/**
* Returns the value of this `Float16` as a `Int` after
@@ -137,9 +129,7 @@
* @return The half-precision float value represented by this object
* converted to type `Int`
*/
- fun toInt(): Int {
- return toFloat().toInt()
- }
+ fun toInt(): Int = toFloat().toInt()
/**
* Returns the value of this `Float16` as a `Long` after
@@ -148,9 +138,7 @@
* @return The half-precision float value represented by this object
* converted to type `Long`
*/
- fun toLong(): Long {
- return toFloat().toLong()
- }
+ fun toLong(): Long = toFloat().toLong()
/**
* Returns the value of this `Float16` as a `Float` after
@@ -161,9 +149,9 @@
*/
fun toFloat(): Float {
val bits = halfValue.toInt() and 0xffff
- val s = bits and FP16_SIGN_MASK
- val e = bits.ushr(FP16_EXPONENT_SHIFT) and FP16_EXPONENT_MASK
- val m = bits and FP16_SIGNIFICAND_MASK
+ val s = bits and Fp16SignMask
+ val e = bits.ushr(Fp16ExponentShift) and Fp16ExponentMask
+ val m = bits and Fp16SignificandMask
var outE = 0
var outM = 0
@@ -171,8 +159,8 @@
if (e == 0) { // Denormal or 0
if (m != 0) {
// Convert denorm fp16 into normalized fp32
- var o = floatFromBits(FP32_DENORMAL_MAGIC + m)
- o -= FP32_DENORMAL_FLOAT
+ var o = floatFromBits(Fp32DenormalMagic + m)
+ o -= Fp32DenormalFloat
return if (s == 0) o else -o
}
} else {
@@ -180,14 +168,14 @@
if (e == 0x1f) { // Infinite or NaN
outE = 0xff
if (outM != 0) { // SNaNs are quieted
- outM = outM or FP32_QNAN_MASK
+ outM = outM or Fp32QNaNMask
}
} else {
- outE = e - FP16_EXPONENT_BIAS + FP32_EXPONENT_BIAS
+ outE = e - Fp16ExponentBias + Fp32ExponentBias
}
}
- val out = s shl 16 or (outE shl FP32_EXPONENT_SHIFT) or outM
+ val out = s shl 16 or (outE shl Fp32ExponentShift) or outM
return floatFromBits(out)
}
@@ -198,9 +186,7 @@
* @return The half-precision float value represented by this object
* converted to type `Double`
*/
- fun toDouble(): Double {
- return toFloat().toDouble()
- }
+ fun toDouble(): Double = toFloat().toDouble()
/**
* Returns a representation of the half-precision float value
@@ -212,12 +198,10 @@
*
* @return The bits that represent the half-precision float value
*/
- fun toBits(): Int {
- return if (isNaN()) {
- NaN.halfValue.toInt()
- } else {
- halfValue.toInt() and 0xffff
- }
+ fun toBits(): Int = if (isNaN()) {
+ NaN.halfValue.toInt()
+ } else {
+ halfValue.toInt() and 0xffff
}
/**
@@ -226,9 +210,7 @@
*
* @return The bits that represent the half-precision float value
*/
- fun toRawBits(): Int {
- return halfValue.toInt() and 0xffff
- }
+ fun toRawBits(): Int = halfValue.toInt() and 0xffff
/**
* Returns a string representation of the specified half-precision
@@ -236,26 +218,24 @@
*
* @return A string representation of this `Float16` object
*/
- override fun toString(): String {
- return toFloat().toString()
- }
+ override fun toString(): String = toFloat().toString()
/**
* Compares to another half-precision float value. The following
* conditions apply during the comparison:
*
* * [NaN] is considered by this method to be equal to itself and greater
- * than all other half-precision float values (including `#PositiveInfinity`)
+ * than all other half-precision float values (including [PositiveInfinity])
* * [PositiveZero] is considered by this method to be greater than
* [NegativeZero].
*
* @param other The half-precision float value to compare to the half-precision value
* represented by this `Float16` object
*
- * @return The value `0` if `this` is numerically equal to `h`; a
- * value less than `0` if `this` is numerically less than `h`;
+ * @return The value `0` if `this` is numerically equal to [other]; a
+ * value less than `0` if `this` is numerically less than [other];
* and a value greater than `0` if `this` is numerically greater
- * than `h`
+ * than [other]
*/
override operator fun compareTo(other: Float16): Int {
if (isNaN()) {
@@ -263,9 +243,7 @@
} else if (other.isNaN()) {
return -1
}
- return toCompareValue(halfValue).compareTo(
- toCompareValue(other.halfValue)
- )
+ return toCompareValue(halfValue).compareTo(toCompareValue(other.halfValue))
}
/**
@@ -291,9 +269,9 @@
fun withSign(sign: Float16): Float16 =
Float16(
(
- sign.halfValue.toInt() and FP16_SIGN_MASK or
- (halfValue.toInt() and FP16_COMBINED)
- ).toShort()
+ sign.halfValue.toInt() and Fp16SignMask or
+ (halfValue.toInt() and Fp16Combined)
+ ).toShort()
)
/**
@@ -307,7 +285,7 @@
* the result is positive infinity (see [PositiveInfinity])
*/
fun absoluteValue(): Float16 {
- return Float16((halfValue.toInt() and FP16_COMBINED).toShort())
+ return Float16((halfValue.toInt() and Fp16Combined).toShort())
}
/**
@@ -330,7 +308,7 @@
var result = bits
if (e < 0x3c00) {
- result = result and FP16_SIGN_MASK
+ result = result and Fp16SignMask
result = result or (0x3c00 and if (e >= 0x3800) 0xffff else 0x0)
} else if (e < 0x6400) {
e = 25 - (e shr 10)
@@ -362,7 +340,7 @@
var result = bits
if (e < 0x3c00) {
- result = result and FP16_SIGN_MASK
+ result = result and Fp16SignMask
result = result or (0x3c00 and -((bits shr 15).inv() and if (e != 0) 1 else 0))
} else if (e < 0x6400) {
e = 25 - (e shr 10)
@@ -394,7 +372,7 @@
var result = bits
if (e < 0x3c00) {
- result = result and FP16_SIGN_MASK
+ result = result and Fp16SignMask
result = result or (0x3c00 and if (bits > 0x8000) 0xffff else 0x0)
} else if (e < 0x6400) {
e = 25 - (e shr 10)
@@ -425,7 +403,7 @@
var result = bits
if (e < 0x3c00) {
- result = result and FP16_SIGN_MASK
+ result = result and Fp16SignMask
} else if (e < 0x6400) {
e = 25 - (e shr 10)
val mask = (1 shl e) - 1
@@ -443,19 +421,15 @@
* returns [MinExponent] - 1.
*/
val exponent: Int
- get() {
- return (halfValue.toInt().ushr(FP16_EXPONENT_SHIFT) and FP16_EXPONENT_MASK) -
- FP16_EXPONENT_BIAS
- }
+ get() = (halfValue.toInt().ushr(Fp16ExponentShift) and Fp16ExponentMask) -
+ Fp16ExponentBias
/**
* The significand, or mantissa, used in the representation
* of this half-precision float value.
*/
val significand: Int
- get() {
- return halfValue.toInt() and FP16_SIGNIFICAND_MASK
- }
+ get() = halfValue.toInt() and Fp16SignificandMask
/**
* Returns true if this `Float16` value represents a Not-a-Number,
@@ -463,9 +437,7 @@
*
* @return True if the value is a NaN, false otherwise
*/
- fun isNaN(): Boolean {
- return halfValue.toInt() and FP16_COMBINED > FP16_EXPONENT_MAX
- }
+ fun isNaN(): Boolean = halfValue.toInt() and Fp16Combined > Fp16ExponentMax
/**
* Returns true if the half-precision float value represents
@@ -474,9 +446,7 @@
* @return True if the value is positive infinity or negative infinity,
* false otherwise
*/
- fun isInfinite(): Boolean {
- return halfValue.toInt() and FP16_COMBINED == FP16_EXPONENT_MAX
- }
+ fun isInfinite(): Boolean = halfValue.toInt() and Fp16Combined == Fp16ExponentMax
/**
* Returns false if the half-precision float value represents
@@ -485,9 +455,7 @@
* @return False if the value is positive infinity or negative infinity,
* true otherwise
*/
- fun isFinite(): Boolean {
- return halfValue.toInt() and FP16_COMBINED != FP16_EXPONENT_MAX
- }
+ fun isFinite(): Boolean = halfValue.toInt() and Fp16Combined != Fp16ExponentMax
/**
* Returns true if the half-precision float value is normalized
@@ -499,8 +467,8 @@
* @return True if the value is normalized, false otherwise
*/
fun isNormalized(): Boolean {
- return halfValue.toInt() and FP16_EXPONENT_MAX != 0 &&
- halfValue.toInt() and FP16_EXPONENT_MAX != FP16_EXPONENT_MAX
+ return halfValue.toInt() and Fp16ExponentMax != 0 &&
+ halfValue.toInt() and Fp16ExponentMax != Fp16ExponentMax
}
/**
@@ -532,9 +500,9 @@
val o = StringBuilder()
val bits = halfValue.toInt() and 0xffff
- val s = bits.ushr(FP16_SIGN_SHIFT)
- val e = bits.ushr(FP16_EXPONENT_SHIFT) and FP16_EXPONENT_MASK
- val m = bits and FP16_SIGNIFICAND_MASK
+ val s = bits.ushr(Fp16SignShift)
+ val e = bits.ushr(Fp16ExponentShift) and Fp16ExponentMask
+ val m = bits and Fp16SignificandMask
if (e == 0x1f) { // Infinite or NaN
if (m == 0) {
@@ -559,7 +527,7 @@
val significand = m.toString(16)
o.append(significand.replaceFirst("0{2,}$".toRegex(), ""))
o.append('p')
- o.append((e - FP16_EXPONENT_BIAS).toString())
+ o.append((e - Fp16ExponentBias).toString())
}
}
@@ -623,78 +591,118 @@
* Positive 0 of type half-precision float.
*/
val PositiveZero = Float16(0x0000.toShort())
+ }
+}
- private val One = Float16(1f)
- private val NegativeOne = Float16(-1f)
+private val One = Float16(1f)
+private val NegativeOne = Float16(-1f)
- private const val FP16_SIGN_SHIFT = 15
- private const val FP16_SIGN_MASK = 0x8000
- private const val FP16_EXPONENT_SHIFT = 10
- private const val FP16_EXPONENT_MASK = 0x1f
- private const val FP16_SIGNIFICAND_MASK = 0x3ff
- private const val FP16_EXPONENT_BIAS = 15
- private const val FP16_COMBINED = 0x7fff
- private const val FP16_EXPONENT_MAX = 0x7c00
+private const val Fp16SignShift = 15
+private const val Fp16SignMask = 0x8000
+private const val Fp16ExponentShift = 10
+private const val Fp16ExponentMask = 0x1f
+private const val Fp16SignificandMask = 0x3ff
+private const val Fp16ExponentBias = 15
+private const val Fp16Combined = 0x7fff
+private const val Fp16ExponentMax = 0x7c00
- private const val FP32_SIGN_SHIFT = 31
- private const val FP32_EXPONENT_SHIFT = 23
- private const val FP32_EXPONENT_MASK = 0xff
- private const val FP32_SIGNIFICAND_MASK = 0x7fffff
- private const val FP32_EXPONENT_BIAS = 127
- private const val FP32_QNAN_MASK = 0x400000
+private const val Fp32SignShift = 31
+private const val Fp32ExponentShift = 23
+private const val Fp32ExponentMask = 0xff
+private const val Fp32SignificandMask = 0x7fffff
+private const val Fp32ExponentBias = 127
+private const val Fp32QNaNMask = 0x400000
- private const val FP32_DENORMAL_MAGIC = 126 shl 23
- private val FP32_DENORMAL_FLOAT = floatFromBits(FP32_DENORMAL_MAGIC)
+private const val Fp32DenormalMagic = 126 shl 23
+private val Fp32DenormalFloat = floatFromBits(Fp32DenormalMagic)
- private fun toCompareValue(value: Short): Int {
- return if (value.toInt() and FP16_SIGN_MASK != 0) {
- 0x8000 - (value.toInt() and 0xffff)
+@Suppress("NOTHING_TO_INLINE")
+private inline fun toCompareValue(value: Short): Int {
+ return if (value.toInt() and Fp16SignMask != 0) {
+ 0x8000 - (value.toInt() and 0xffff)
+ } else {
+ value.toInt() and 0xffff
+ }
+}
+
+/**
+ * Convert a single-precision float to a half-precision float, stored as
+ * [Short] data type to hold its 16 bits.
+ */
+internal fun floatToHalf(f: Float): Short {
+ val bits = f.toRawBits()
+ val s = bits.ushr(Fp32SignShift)
+ var e = bits.ushr(Fp32ExponentShift) and Fp32ExponentMask
+ var m = bits and Fp32SignificandMask
+
+ var outE = 0
+ var outM = 0
+
+ if (e == 0xff) { // Infinite or NaN
+ outE = 0x1f
+ outM = if (m != 0) 0x200 else 0
+ } else {
+ e = e - Fp32ExponentBias + Fp16ExponentBias
+ if (e >= 0x1f) { // Overflow
+ outE = 0x31
+ } else if (e <= 0) { // Underflow
+ if (e < -10) {
+ // The absolute fp32 value is less than MIN_VALUE, flush to +/-0
} else {
- value.toInt() and 0xffff
+ // The fp32 value is a normalized float less than MIN_NORMAL,
+ // we convert to a denorm fp16
+ m = m or 0x800000 shr 1 - e
+ if (m and 0x1000 != 0) m += 0x2000
+ outM = m shr 13
}
- }
-
- private fun floatToHalf(f: Float): Short {
- val bits = f.toRawBits()
- val s = bits.ushr(FP32_SIGN_SHIFT)
- var e = bits.ushr(FP32_EXPONENT_SHIFT) and FP32_EXPONENT_MASK
- var m = bits and FP32_SIGNIFICAND_MASK
-
- var outE = 0
- var outM = 0
-
- if (e == 0xff) { // Infinite or NaN
- outE = 0x1f
- outM = if (m != 0) 0x200 else 0
- } else {
- e = e - FP32_EXPONENT_BIAS + FP16_EXPONENT_BIAS
- if (e >= 0x1f) { // Overflow
- outE = 0x31
- } else if (e <= 0) { // Underflow
- if (e < -10) {
- // The absolute fp32 value is less than MIN_VALUE, flush to +/-0
- } else {
- // The fp32 value is a normalized float less than MIN_NORMAL,
- // we convert to a denorm fp16
- m = m or 0x800000 shr 1 - e
- if (m and 0x1000 != 0) m += 0x2000
- outM = m shr 13
- }
- } else {
- outE = e
- outM = m shr 13
- if (m and 0x1000 != 0) {
- // Round to nearest "0.5" up
- var out = outE shl FP16_EXPONENT_SHIFT or outM
- out++
- return (out or (s shl FP16_SIGN_SHIFT)).toShort()
- }
- }
+ } else {
+ outE = e
+ outM = m shr 13
+ if (m and 0x1000 != 0) {
+ // Round to nearest "0.5" up
+ var out = outE shl Fp16ExponentShift or outM
+ out++
+ return (out or (s shl Fp16SignShift)).toShort()
}
-
- return (s shl FP16_SIGN_SHIFT or (outE shl FP16_EXPONENT_SHIFT) or outM).toShort()
}
}
+
+ return (s shl Fp16SignShift or (outE shl Fp16ExponentShift) or outM).toShort()
+}
+
+/**
+ * Convert a half-precision float to a single-precision float.
+ */
+internal fun halfToFloat(h: Short): Float {
+ val bits = h.toInt() and 0xffff
+ val s = bits and Fp16SignMask
+ val e = bits.ushr(Fp16ExponentShift) and Fp16ExponentMask
+ val m = bits and Fp16SignificandMask
+
+ var outE = 0
+ var outM = 0
+
+ if (e == 0) { // Denormal or 0
+ if (m != 0) {
+ // Convert denorm fp16 into normalized fp32
+ var o = floatFromBits(Fp32DenormalMagic + m)
+ o -= Fp32DenormalFloat
+ return if (s == 0) o else -o
+ }
+ } else {
+ outM = m shl 13
+ if (e == 0x1f) { // Infinite or NaN
+ outE = 0xff
+ if (outM != 0) { // SNaNs are quieted
+ outM = outM or Fp32QNaNMask
+ }
+ } else {
+ outE = e - Fp16ExponentBias + Fp32ExponentBias
+ }
+ }
+
+ val out = s shl 16 or (outE shl Fp32ExponentShift) or outM
+ return floatFromBits(out)
}
/**
@@ -712,7 +720,6 @@
if (x.isNaN() || y.isNaN()) {
return Float16.NaN
}
-
return if (x <= y) x else y
}
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/InlineClassHelper.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/InlineClassHelper.kt
new file mode 100644
index 0000000..724538e
--- /dev/null
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/InlineClassHelper.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.graphics
+
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+
+// This function exists so we do *not* inline the throw. It keeps
+// the call site much smaller and since it's the slow path anyway,
+// we don't mind the extra function call
+internal fun throwIllegalArgumentException(message: String) {
+ throw IllegalArgumentException(message)
+}
+
+// Like Kotlin's require() but without the .toString() call
+@Suppress("BanInlineOptIn") // same opt-in as using Kotlin's require()
+@OptIn(ExperimentalContracts::class)
+internal inline fun requirePrecondition(value: Boolean, lazyMessage: () -> String) {
+ contract {
+ returns() implies value
+ }
+ if (!value) {
+ throwIllegalArgumentException(lazyMessage())
+ }
+}
diff --git a/compose/ui/ui-test/api/current.txt b/compose/ui/ui-test/api/current.txt
index b1cd79d..666a657 100644
--- a/compose/ui/ui-test/api/current.txt
+++ b/compose/ui/ui-test/api/current.txt
@@ -3,6 +3,8 @@
public final class ActionsKt {
method public static androidx.compose.ui.test.SemanticsNodeInteraction performClick(androidx.compose.ui.test.SemanticsNodeInteraction);
+ method @SuppressCompatibility @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsNodeInteraction performCustomAccessibilityActionLabelled(androidx.compose.ui.test.SemanticsNodeInteraction, String label);
+ method @SuppressCompatibility @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsNodeInteraction performCustomAccessibilityActionWhere(androidx.compose.ui.test.SemanticsNodeInteraction, optional String? predicateDescription, kotlin.jvm.functions.Function1<? super java.lang.String,java.lang.Boolean> labelPredicate);
method @Deprecated public static androidx.compose.ui.test.SemanticsNodeInteraction performGesture(androidx.compose.ui.test.SemanticsNodeInteraction, kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.GestureScope,kotlin.Unit> block);
method @SuppressCompatibility @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsNodeInteraction performKeyInput(androidx.compose.ui.test.SemanticsNodeInteraction, kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.KeyInjectionScope,kotlin.Unit> block);
method @SuppressCompatibility @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsNodeInteraction performMouseInput(androidx.compose.ui.test.SemanticsNodeInteraction, kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.MouseInjectionScope,kotlin.Unit> block);
diff --git a/compose/ui/ui-test/api/restricted_current.txt b/compose/ui/ui-test/api/restricted_current.txt
index 78faf62..b8f4e37 100644
--- a/compose/ui/ui-test/api/restricted_current.txt
+++ b/compose/ui/ui-test/api/restricted_current.txt
@@ -3,6 +3,8 @@
public final class ActionsKt {
method public static androidx.compose.ui.test.SemanticsNodeInteraction performClick(androidx.compose.ui.test.SemanticsNodeInteraction);
+ method @SuppressCompatibility @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsNodeInteraction performCustomAccessibilityActionLabelled(androidx.compose.ui.test.SemanticsNodeInteraction, String label);
+ method @SuppressCompatibility @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsNodeInteraction performCustomAccessibilityActionWhere(androidx.compose.ui.test.SemanticsNodeInteraction, optional String? predicateDescription, kotlin.jvm.functions.Function1<? super java.lang.String,java.lang.Boolean> labelPredicate);
method @Deprecated public static androidx.compose.ui.test.SemanticsNodeInteraction performGesture(androidx.compose.ui.test.SemanticsNodeInteraction, kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.GestureScope,kotlin.Unit> block);
method @SuppressCompatibility @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsNodeInteraction performKeyInput(androidx.compose.ui.test.SemanticsNodeInteraction, kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.KeyInjectionScope,kotlin.Unit> block);
method @SuppressCompatibility @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.SemanticsNodeInteraction performMouseInput(androidx.compose.ui.test.SemanticsNodeInteraction, kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.MouseInjectionScope,kotlin.Unit> block);
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/actions/CustomAccessibilityActionsTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/actions/CustomAccessibilityActionsTest.kt
new file mode 100644
index 0000000..4bffee2
--- /dev/null
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/actions/CustomAccessibilityActionsTest.kt
@@ -0,0 +1,248 @@
+/*
+ * 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.test.actions
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.CustomAccessibilityAction
+import androidx.compose.ui.semantics.customActions
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performCustomAccessibilityActionLabelled
+import androidx.compose.ui.test.performCustomAccessibilityActionWhere
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFailsWith
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalTestApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CustomAccessibilityActionsTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ private val tag = "tag"
+
+ @Test
+ fun performCustomAccessibilityActionLabelled_failsWhenNoNodeMatches() {
+ rule.setContent {
+ Box(
+ Modifier.semantics {
+ customActions = listOf(CustomAccessibilityAction("action") { true })
+ }
+ )
+ }
+
+ val interaction = rule.onNodeWithTag(tag)
+ val error = assertFailsWith<AssertionError> {
+ interaction.performCustomAccessibilityActionLabelled("action")
+ }
+ assertThat(error).hasMessageThat().contains("could not find any node that satisfies")
+ }
+
+ @Test
+ fun performCustomAccessibilityActionLabelled_failsWhenNoActionMatches() {
+ rule.setContent {
+ Box(
+ Modifier
+ .testTag(tag)
+ .semantics {
+ customActions = listOf(CustomAccessibilityAction("action") { true })
+ }
+ )
+ }
+
+ val error = assertFailsWith<AssertionError> {
+ rule.onNodeWithTag(tag).performCustomAccessibilityActionLabelled("not action")
+ }
+ assertThat(error).hasMessageThat().startsWith(
+ "No custom accessibility actions matched [label is \"not action\"]"
+ )
+ }
+
+ @Test
+ fun performCustomAccessibilityActionLabelled_failsWhenMultipleActionsMatch() {
+ rule.setContent {
+ Box(
+ Modifier
+ .testTag(tag)
+ .semantics {
+ customActions = listOf(
+ CustomAccessibilityAction("action") { true },
+ CustomAccessibilityAction("action") { true },
+ )
+ }
+ )
+ }
+
+ val error = assertFailsWith<AssertionError> {
+ rule.onNodeWithTag(tag).performCustomAccessibilityActionLabelled("action")
+ }
+ assertThat(error).hasMessageThat().startsWith(
+ "Expected exactly one custom accessibility action to match [label is \"action\"], " +
+ "but found 2."
+ )
+ }
+
+ @Test
+ fun performCustomAccessibilityActionLabelled_invokesActionWhenExactlyOneActionMatches() {
+ var fooInvocationCount = 0
+ var barInvocationCount = 0
+ rule.setContent {
+ Box(
+ Modifier
+ .testTag(tag)
+ .semantics {
+ customActions = listOf(
+ CustomAccessibilityAction("foo") { fooInvocationCount++; true },
+ CustomAccessibilityAction("bar") { barInvocationCount++; true },
+ )
+ }
+ )
+ }
+
+ rule.onNodeWithTag(tag).performCustomAccessibilityActionLabelled("foo")
+
+ assertThat(fooInvocationCount).isEqualTo(1)
+ assertThat(barInvocationCount).isEqualTo(0)
+ }
+
+ @Test
+ fun performCustomAccessibilityActionLabelled_doesntFailWhenActionReturnsFalse() {
+ rule.setContent {
+ Box(
+ Modifier
+ .testTag(tag)
+ .semantics {
+ customActions = listOf(
+ CustomAccessibilityAction("action") { false },
+ )
+ }
+ )
+ }
+
+ rule.onNodeWithTag(tag).performCustomAccessibilityActionLabelled("action")
+ }
+
+ @Test
+ fun performCustomAccessibilityActionWhere_failsWhenNoNodeMatches() {
+ rule.setContent {
+ Box(
+ Modifier.semantics {
+ customActions = listOf(CustomAccessibilityAction("action") { true })
+ }
+ )
+ }
+
+ val interaction = rule.onNodeWithTag(tag)
+ val error = assertFailsWith<AssertionError> {
+ interaction.performCustomAccessibilityActionWhere("description") { true }
+ }
+ assertThat(error).hasMessageThat().contains("could not find any node that satisfies")
+ }
+
+ @Test
+ fun performCustomAccessibilityActionWhere_failsWhenNoActionMatches() {
+ rule.setContent {
+ Box(
+ Modifier
+ .testTag(tag)
+ .semantics {
+ customActions = listOf(CustomAccessibilityAction("action") { true })
+ }
+ )
+ }
+
+ val error = assertFailsWith<AssertionError> {
+ rule.onNodeWithTag(tag).performCustomAccessibilityActionWhere("description") { false }
+ }
+ assertThat(error).hasMessageThat().startsWith(
+ "No custom accessibility actions matched [description]"
+ )
+ }
+
+ @Test
+ fun performCustomAccessibilityActionWhere_failsWhenMultipleActionsMatch() {
+ rule.setContent {
+ Box(
+ Modifier
+ .testTag(tag)
+ .semantics {
+ customActions = listOf(
+ CustomAccessibilityAction("action") { true },
+ CustomAccessibilityAction("action") { true },
+ )
+ }
+ )
+ }
+
+ val error = assertFailsWith<AssertionError> {
+ rule.onNodeWithTag(tag).performCustomAccessibilityActionWhere("description") { true }
+ }
+ assertThat(error).hasMessageThat().startsWith(
+ "Expected exactly one custom accessibility action to match [description], " +
+ "but found 2."
+ )
+ }
+
+ @Test
+ fun performCustomAccessibilityActionWhere_invokesActionWhenExactlyOneActionMatches() {
+ var fooInvocationCount = 0
+ var barInvocationCount = 0
+ rule.setContent {
+ Box(
+ Modifier
+ .testTag(tag)
+ .semantics {
+ customActions = listOf(
+ CustomAccessibilityAction("foo") { fooInvocationCount++; true },
+ CustomAccessibilityAction("bar") { barInvocationCount++; true },
+ )
+ }
+ )
+ }
+
+ rule.onNodeWithTag(tag).performCustomAccessibilityActionWhere("description") { it == "foo" }
+
+ assertThat(fooInvocationCount).isEqualTo(1)
+ assertThat(barInvocationCount).isEqualTo(0)
+ }
+
+ @Test
+ fun performCustomAccessibilityActionWhere_doesntFailWhenActionReturnsFalse() {
+ rule.setContent {
+ Box(
+ Modifier
+ .testTag(tag)
+ .semantics {
+ customActions = listOf(
+ CustomAccessibilityAction("action") { false },
+ )
+ }
+ )
+ }
+
+ rule.onNodeWithTag(tag).performCustomAccessibilityActionWhere("description") { true }
+ }
+}
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt
index 5f9f27e..a646fa8 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt
@@ -22,8 +22,10 @@
import androidx.compose.ui.layout.boundsInParent
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.semantics.AccessibilityAction
+import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.ScrollAxisRange
import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsActions.CustomActions
import androidx.compose.ui.semantics.SemanticsActions.ScrollBy
import androidx.compose.ui.semantics.SemanticsActions.ScrollToIndex
import androidx.compose.ui.semantics.SemanticsNode
@@ -625,6 +627,70 @@
return this
}
+/**
+ * Finds the [CustomAccessibilityAction] in the node's [CustomActions] list whose label is equal
+ * to [label] and then invokes it.
+ *
+ * To use your own logic to find the action to perform instead of matching on the full label, use
+ * [performCustomAccessibilityActionWhere].
+ *
+ * @param label The exact label of the [CustomAccessibilityAction] to perform.
+ *
+ * @throws AssertionError If no [SemanticsNode] is found, or no [CustomAccessibilityAction] has
+ * [label], or more than one [CustomAccessibilityAction] has [label].
+ *
+ * @see performCustomAccessibilityActionWhere
+ */
+@ExperimentalTestApi
+fun SemanticsNodeInteraction.performCustomAccessibilityActionLabelled(
+ label: String
+): SemanticsNodeInteraction =
+ performCustomAccessibilityActionWhere("label is \"$label\"") { it == label }
+
+/**
+ * Finds the [CustomAccessibilityAction] in the node's [CustomActions] list whose label satisfies a
+ * predicate function and then invokes it.
+ *
+ * @param predicateDescription A description of [labelPredicate] that will be included in the error
+ * message if zero or >1 actions match.
+ * @param labelPredicate A predicate function used to select the [CustomAccessibilityAction] to
+ * perform.
+ *
+ * @throws AssertionError If no [SemanticsNode] is found, or no [CustomAccessibilityAction] matches
+ * [labelPredicate], or more than one [CustomAccessibilityAction] matches [labelPredicate].
+ *
+ * @see performCustomAccessibilityActionLabelled
+ */
+@ExperimentalTestApi
+fun SemanticsNodeInteraction.performCustomAccessibilityActionWhere(
+ predicateDescription: String? = null,
+ labelPredicate: (label: String) -> Boolean
+): SemanticsNodeInteraction {
+ val node = fetchSemanticsNode()
+ val actions = node.config[CustomActions]
+ val matchingActions = actions.filter { labelPredicate(it.label) }
+ if (matchingActions.isEmpty()) {
+ throw AssertionError(
+ buildGeneralErrorMessage(
+ "No custom accessibility actions matched [$predicateDescription].",
+ selector,
+ node
+ )
+ )
+ } else if (matchingActions.size > 1) {
+ throw AssertionError(
+ buildGeneralErrorMessage(
+ "Expected exactly one custom accessibility action to match" +
+ " [$predicateDescription], but found ${matchingActions.size}.",
+ selector,
+ node
+ )
+ )
+ }
+ matchingActions[0].action()
+ return this
+}
+
// TODO(200928505): get a more accurate indication if it is a lazy list
private val SemanticsNode.isLazyList: Boolean
get() = ScrollBy in config && ScrollToIndex in config
diff --git a/compose/ui/ui-text/lint-baseline.xml b/compose/ui/ui-text/lint-baseline.xml
index 26fe7b7..40b9c16 100644
--- a/compose/ui/ui-text/lint-baseline.xml
+++ b/compose/ui/ui-text/lint-baseline.xml
@@ -1,14 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.3.0-alpha04" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-alpha04)" variant="all" version="8.3.0-alpha04">
-
- <issue
- id="NewApi"
- message="Call requires API level 29 (current min is 21): `android.graphics.Paint#getBlendMode`"
- errorLine1=" assertThat(paragraph.textPaint.blendMode).isEqualTo(BlendMode.SrcOver)"
- errorLine2=" ~~~~~~~~~">
- <location
- file="src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt"/>
- </issue>
+<issues format="6" by="lint 8.3.0-alpha10" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-alpha10)" variant="all" version="8.3.0-alpha10">
<issue
id="NewApi"
@@ -40,10 +31,10 @@
<issue
id="NewApi"
message="Call requires API level 29 (current min is 21): `android.graphics.Paint#getBlendMode`"
- errorLine1=" assertThat(textPaint.blendMode).isEqualTo(BlendMode.SrcOver)"
- errorLine2=" ~~~~~~~~~">
+ errorLine1=" assertThat(paragraph.textPaint.blendMode).isEqualTo(BlendMode.SrcOver)"
+ errorLine2=" ~~~~~~~~~">
<location
- file="src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidTextPaintTest.kt"/>
+ file="src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt"/>
</issue>
<issue
@@ -57,6 +48,15 @@
<issue
id="NewApi"
+ message="Call requires API level 29 (current min is 21): `android.graphics.Paint#getBlendMode`"
+ errorLine1=" assertThat(textPaint.blendMode).isEqualTo(BlendMode.SrcOver)"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidTextPaintTest.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
message="Call requires API level 29 (current min is 21): `android.graphics.Paint#setBlendMode`"
errorLine1=" textPaint.blendMode = BlendMode.DstOver"
errorLine2=" ~~~~~~~~~">
@@ -66,6 +66,15 @@
<issue
id="NewApi"
+ message="Cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
+ errorLine1=" assertThat(textPaint.blendMode).isEqualTo(BlendMode.DstOver)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidTextPaintTest.kt"/>
+ </issue>
+
+ <issue
+ id="NewApi"
message="Call requires API level 29 (current min is 21): `android.graphics.Paint#getBlendMode`"
errorLine1=" assertThat(textPaint.blendMode).isEqualTo(BlendMode.DstOver)"
errorLine2=" ~~~~~~~~~">
@@ -74,12 +83,93 @@
</issue>
<issue
- id="NewApi"
- message="Cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
- errorLine1=" assertThat(textPaint.blendMode).isEqualTo(BlendMode.DstOver)"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ id="PrimitiveInCollection"
+ message="field paragraphEnds with type List<Integer>: replace with IntList"
+ errorLine1=" private val paragraphEnds: List<Int>"
+ errorLine2=" ~~~~~~~~~">
<location
- file="src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidTextPaintTest.kt"/>
+ file="${:compose:ui:ui-text*debug*MAIN*sourceProvider*0*javaDir*4}/androidx/compose/ui/text/android/LayoutHelper.android.kt"/>
+ </issue>
+
+ <issue
+ id="PrimitiveInCollection"
+ message="variable lineFeeds with type List<Integer>: replace with IntList"
+ errorLine1=" val lineFeeds = mutableListOf<Int>()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="${:compose:ui:ui-text*debug*MAIN*sourceProvider*0*javaDir*4}/androidx/compose/ui/text/android/LayoutHelper.android.kt"/>
+ </issue>
+
+ <issue
+ id="PrimitiveInCollection"
+ message="return type List<Integer> of breakInWords: replace with IntList"
+ errorLine1=" private fun breakInWords(layoutHelper: LayoutHelper): List<Int> {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="${:compose:ui:ui-text*debug*MAIN*sourceProvider*0*javaDir*4}/androidx/compose/ui/text/android/animation/SegmentBreaker.android.kt"/>
+ </issue>
+
+ <issue
+ id="PrimitiveInCollection"
+ message="variable words with type List<? extends Integer>: replace with IntList"
+ errorLine1=" val words = breakWithBreakIterator(text, BreakIterator.getLineInstance(Locale.getDefault()))"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="${:compose:ui:ui-text*debug*MAIN*sourceProvider*0*javaDir*4}/androidx/compose/ui/text/android/animation/SegmentBreaker.android.kt"/>
+ </issue>
+
+ <issue
+ id="PrimitiveInCollection"
+ message="variable set with type TreeSet<Integer>: replace with IntSet"
+ errorLine1=" val set = TreeSet<Int>().apply {"
+ errorLine2=" ^">
+ <location
+ file="${:compose:ui:ui-text*debug*MAIN*sourceProvider*0*javaDir*4}/androidx/compose/ui/text/android/animation/SegmentBreaker.android.kt"/>
+ </issue>
+
+ <issue
+ id="PrimitiveInCollection"
+ message="return type List<Integer> of breakWithBreakIterator: replace with IntList"
+ errorLine1=" private fun breakWithBreakIterator(text: CharSequence, breaker: BreakIterator): List<Int> {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="${:compose:ui:ui-text*debug*MAIN*sourceProvider*0*javaDir*4}/androidx/compose/ui/text/android/animation/SegmentBreaker.android.kt"/>
+ </issue>
+
+ <issue
+ id="PrimitiveInCollection"
+ message="variable res with type List<Integer>: replace with IntList"
+ errorLine1=" val res = mutableListOf(0)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="${:compose:ui:ui-text*debug*MAIN*sourceProvider*0*javaDir*4}/androidx/compose/ui/text/android/animation/SegmentBreaker.android.kt"/>
+ </issue>
+
+ <issue
+ id="PrimitiveInCollection"
+ message="return type List<Integer> of breakOffsets: replace with IntList"
+ errorLine1=" fun breakOffsets(layoutHelper: LayoutHelper, segmentType: SegmentType): List<Int> {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="${:compose:ui:ui-text*debug*MAIN*sourceProvider*0*javaDir*4}/androidx/compose/ui/text/android/animation/SegmentBreaker.android.kt"/>
+ </issue>
+
+ <issue
+ id="PrimitiveInCollection"
+ message="variable list with type List<? extends Float>: replace with FloatList"
+ errorLine1=" @Suppress("UNCHECKED_CAST")"
+ errorLine2=" ^">
+ <location
+ file="src/commonMain/kotlin/androidx/compose/ui/text/Savers.kt"/>
+ </issue>
+
+ <issue
+ id="PrimitiveInCollection"
+ message="return type List<FontStyle> of values: replace with IntList"
+ errorLine1=" fun values(): List<FontStyle> = listOf(Normal, Italic)"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="src/commonMain/kotlin/androidx/compose/ui/text/font/FontStyle.kt"/>
</issue>
<issue
@@ -93,11 +183,11 @@
<issue
id="PrimitiveInCollection"
- message="return type List<FontStyle> of values: replace with IntList"
- errorLine1=" fun values(): List<FontStyle> = listOf(Normal, Italic)"
+ message="return type List<TextAlign> of values: replace with IntList"
+ errorLine1=" fun values(): List<TextAlign> = listOf(Left, Right, Center, Justify, Start, End)"
errorLine2=" ~~~~~~~~~~~~~~~">
<location
- file="src/commonMain/kotlin/androidx/compose/ui/text/font/FontStyle.kt"/>
+ file="src/commonMain/kotlin/androidx/compose/ui/text/style/TextAlign.kt"/>
</issue>
<issue
@@ -127,94 +217,4 @@
file="src/jvmMain/kotlin/androidx/compose/ui/text/JvmAnnotatedString.jvm.kt"/>
</issue>
- <issue
- id="PrimitiveInCollection"
- message="field paragraphEnds with type List<Integer>: replace with IntList"
- errorLine1=" private val paragraphEnds: List<Int>"
- errorLine2=" ~~~~~~~~~">
- <location
- file="../../../text/text/src/main/java/androidx/compose/ui/text/android/LayoutHelper.kt"/>
- </issue>
-
- <issue
- id="PrimitiveInCollection"
- message="variable lineFeeds with type List<Integer>: replace with IntList"
- errorLine1=" val lineFeeds = mutableListOf<Int>()"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="../../../text/text/src/main/java/androidx/compose/ui/text/android/LayoutHelper.kt"/>
- </issue>
-
- <issue
- id="PrimitiveInCollection"
- message="variable list with type List<? extends Float>: replace with FloatList"
- errorLine1=" @Suppress("UNCHECKED_CAST")"
- errorLine2=" ^">
- <location
- file="src/commonMain/kotlin/androidx/compose/ui/text/Savers.kt"/>
- </issue>
-
- <issue
- id="PrimitiveInCollection"
- message="return type List<Integer> of breakInWords: replace with IntList"
- errorLine1=" private fun breakInWords(layoutHelper: LayoutHelper): List<Int> {"
- errorLine2=" ~~~~~~~~~">
- <location
- file="../../../text/text/src/main/java/androidx/compose/ui/text/android/animation/SegmentBreaker.kt"/>
- </issue>
-
- <issue
- id="PrimitiveInCollection"
- message="variable words with type List<? extends Integer>: replace with IntList"
- errorLine1=" val words = breakWithBreakIterator(text, BreakIterator.getLineInstance(Locale.getDefault()))"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="../../../text/text/src/main/java/androidx/compose/ui/text/android/animation/SegmentBreaker.kt"/>
- </issue>
-
- <issue
- id="PrimitiveInCollection"
- message="variable set with type TreeSet<Integer>: replace with IntSet"
- errorLine1=" val set = TreeSet<Int>().apply {"
- errorLine2=" ^">
- <location
- file="../../../text/text/src/main/java/androidx/compose/ui/text/android/animation/SegmentBreaker.kt"/>
- </issue>
-
- <issue
- id="PrimitiveInCollection"
- message="return type List<Integer> of breakWithBreakIterator: replace with IntList"
- errorLine1=" private fun breakWithBreakIterator(text: CharSequence, breaker: BreakIterator): List<Int> {"
- errorLine2=" ~~~~~~~~~">
- <location
- file="../../../text/text/src/main/java/androidx/compose/ui/text/android/animation/SegmentBreaker.kt"/>
- </issue>
-
- <issue
- id="PrimitiveInCollection"
- message="variable res with type List<Integer>: replace with IntList"
- errorLine1=" val res = mutableListOf(0)"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="../../../text/text/src/main/java/androidx/compose/ui/text/android/animation/SegmentBreaker.kt"/>
- </issue>
-
- <issue
- id="PrimitiveInCollection"
- message="return type List<Integer> of breakOffsets: replace with IntList"
- errorLine1=" fun breakOffsets(layoutHelper: LayoutHelper, segmentType: SegmentType): List<Int> {"
- errorLine2=" ~~~~~~~~~">
- <location
- file="../../../text/text/src/main/java/androidx/compose/ui/text/android/animation/SegmentBreaker.kt"/>
- </issue>
-
- <issue
- id="PrimitiveInCollection"
- message="return type List<TextAlign> of values: replace with IntList"
- errorLine1=" fun values(): List<TextAlign> = listOf(Left, Right, Center, Justify, Start, End)"
- errorLine2=" ~~~~~~~~~~~~~~~">
- <location
- file="src/commonMain/kotlin/androidx/compose/ui/text/style/TextAlign.kt"/>
- </issue>
-
</issues>
diff --git a/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/clock/TransitionClockTest.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/clock/TransitionClockTest.kt
index bffd5f9..ce1ad15 100644
--- a/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/clock/TransitionClockTest.kt
+++ b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/animation/clock/TransitionClockTest.kt
@@ -599,8 +599,7 @@
clock.setStateParameters(10.dp, 10.dp)
}
rule.runOnIdle {
- // When initial == target state, no animation is active.
- assertEquals(0, clock.getAnimatedProperties().size)
+ assertEquals(2, clock.getAnimatedProperties().size)
clock.setStateParameters(20.dp, 40.dp)
}
rule.runOnIdle {
@@ -620,8 +619,7 @@
rule.runOnIdle {
// Default clock state.
clock.getTransitions(100).let {
- // When initial == target state, no animation is active.
- assertEquals(0, it.size)
+ assertEquals(2, it.size)
}
// Change state
clock.setStateParameters(20.dp, 40.dp)
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
index 2813b41..ece23e5 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
@@ -2399,12 +2399,17 @@
// Arrange.
val focusRequester = FocusRequester()
setContent {
- Box(
- Modifier
- .testTag(tag)
- .focusRequester(focusRequester)
- .focusable()) {
- BasicText("focusable")
+ Row {
+ // Initially focused item.
+ Box(Modifier.size(10.dp).focusable())
+ Box(
+ Modifier
+ .testTag(tag)
+ .focusRequester(focusRequester)
+ .focusable()
+ ) {
+ BasicText("focusable")
+ }
}
}
rule.runOnIdle { focusRequester.requestFocus() }
@@ -2550,11 +2555,15 @@
fun testTextField_testFocusClearFocusAction() {
// Arrange.
setContent {
- BasicTextField(
- modifier = Modifier.testTag(tag),
- value = "value",
- onValueChange = {}
- )
+ Row {
+ // Initially focused item.
+ Box(Modifier.size(10.dp).focusable())
+ BasicTextField(
+ modifier = Modifier.testTag(tag),
+ value = "value",
+ onValueChange = {}
+ )
+ }
}
val textFieldId = rule.onNodeWithTag(tag)
.assert(expectValue(Focused, false))
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/ClearFocusExitTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/ClearFocusExitTest.kt
index ae6436a..331bb2a 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/ClearFocusExitTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/ClearFocusExitTest.kt
@@ -37,13 +37,13 @@
@get:Rule
val rule = createComposeRule()
- val focusRequester = FocusRequester()
- var clearTriggered = false
- lateinit var focusState: FocusState
- lateinit var focusManager: FocusManager
+ private val focusRequester = FocusRequester()
+ private var clearTriggered = false
+ private lateinit var focusState: FocusState
+ private lateinit var focusManager: FocusManager
@Test
- fun clearFocus_doesNotTriggersExit() {
+ fun clearFocus_doesNotTriggerExit() {
// Arrange.
rule.setFocusableContent {
focusManager = LocalFocusManager.current
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusManagerCompositionLocalTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusManagerCompositionLocalTest.kt
index 48f89f2..2799818 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusManagerCompositionLocalTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusManagerCompositionLocalTest.kt
@@ -18,17 +18,22 @@
import android.view.View
import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusStateImpl.Active
import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
import androidx.compose.ui.focus.FocusStateImpl.Inactive
+import androidx.compose.ui.input.InputMode.Companion.Keyboard
+import androidx.compose.ui.input.InputMode.Companion.Touch
+import androidx.compose.ui.input.InputModeManager
import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalInputModeManager
import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -39,24 +44,25 @@
@get:Rule
val rule = createComposeRule()
+ private lateinit var focusManager: FocusManager
+ private lateinit var inputModeManager: InputModeManager
+ private val focusStates = mutableListOf<FocusState>()
+
@Test
- fun clearFocus_singleLayout() {
+ fun clearFocus_singleLayout_focusIsRestoredAfterClear() {
// Arrange.
- lateinit var focusManager: FocusManager
- lateinit var focusState: FocusState
val focusRequester = FocusRequester()
- rule.setFocusableContent {
- focusManager = LocalFocusManager.current
+ rule.setTestContent(extraItemForInitialFocus = false) {
Box(
modifier = Modifier
.focusRequester(focusRequester)
- .onFocusChanged { focusState = it }
+ .onFocusChanged { focusStates += it }
.focusTarget()
)
}
rule.runOnIdle {
focusRequester.requestFocus()
- assertThat(focusState.isFocused).isTrue()
+ focusStates.clear()
}
// Act.
@@ -64,8 +70,16 @@
// Assert.
rule.runOnIdle {
- assertThat(focusManager.rootFocusState.isFocused).isTrue()
- assertThat(focusState.isFocused).isFalse()
+ when (inputModeManager.inputMode) {
+ Keyboard -> {
+ assertThat(focusStates).containsExactly(Inactive, Active).inOrder()
+ assertThat(focusManager.rootFocusState.hasFocus).isTrue()
+ }
+ Touch -> {
+ assertThat(focusStates).containsExactly(Inactive).inOrder()
+ assertThat(focusManager.rootFocusState.hasFocus).isFalse()
+ }
+ }
}
}
@@ -77,7 +91,7 @@
lateinit var parentFocusState: FocusState
lateinit var grandparentFocusState: FocusState
val focusRequester = FocusRequester()
- rule.setFocusableContent {
+ rule.setTestContent {
focusManager = LocalFocusManager.current
Box(
modifier = Modifier
@@ -119,96 +133,38 @@
@Test
fun takeFocus_whenRootIsInactive() {
// Arrange.
- lateinit var focusManager: FocusManager
- lateinit var focusState: FocusState
lateinit var view: View
- rule.setFocusableContent {
- focusManager = LocalFocusManager.current
+ rule.setTestContent(extraItemForInitialFocus = false) {
view = LocalView.current
Box(
modifier = Modifier
- .onFocusChanged { focusState = it }
+ .onFocusChanged { focusStates += it }
.focusTarget()
)
}
// Act.
- rule.runOnIdle { view.requestFocus() }
-
- // Assert.
rule.runOnIdle {
- assertThat(focusManager.rootFocusState).isEqualTo(Active)
- assertThat(focusState.isFocused).isFalse()
+ focusStates.clear()
+ view.requestFocus()
}
- }
-
- fun takeFocus_whenRootIsActive() {
- // Arrange.
- lateinit var focusManager: FocusManager
- lateinit var focusState: FocusState
- lateinit var view: View
- rule.setFocusableContent {
- focusManager = LocalFocusManager.current
- view = LocalView.current
- Box(
- modifier = Modifier
- .onFocusChanged { focusState = it }
- .focusTarget()
- )
- }
- rule.runOnIdle { focusManager.setRootFocusState(Active) }
-
- // Act.
- rule.runOnIdle { view.requestFocus() }
-
- // Assert.
- rule.runOnIdle {
- assertThat(focusManager.rootFocusState).isEqualTo(Active)
- assertThat(focusState.isFocused).isFalse()
- }
- }
-
- @Test
- fun takeFocus_whenRootIsActiveParent() {
- // Arrange.
- lateinit var focusManager: FocusManager
- lateinit var focusState: FocusState
- lateinit var view: View
- val focusRequester = FocusRequester()
- rule.setFocusableContent {
- focusManager = LocalFocusManager.current
- view = LocalView.current
- Box(
- modifier = Modifier
- .focusRequester(focusRequester)
- .onFocusChanged { focusState = it }
- .focusTarget()
- )
- }
- rule.runOnIdle { focusRequester.requestFocus() }
-
- // Act.
- rule.runOnIdle { view.requestFocus() }
// Assert.
rule.runOnIdle {
assertThat(focusManager.rootFocusState).isEqualTo(ActiveParent)
- assertThat(focusState.isFocused).isTrue()
+ assertThat(focusStates).containsExactly(Active)
}
}
@Test
fun releaseFocus_whenRootIsInactive() {
// Arrange.
- lateinit var focusManager: FocusManager
- lateinit var focusState: FocusState
lateinit var view: View
- rule.setFocusableContent {
- focusManager = LocalFocusManager.current
+ rule.setTestContent(extraItemForInitialFocus = false) {
view = LocalView.current
Box(
modifier = Modifier
- .onFocusChanged { focusState = it }
+ .onFocusChanged { focusStates += it }
.focusTarget()
)
}
@@ -219,55 +175,28 @@
// Assert.
rule.runOnIdle {
assertThat(focusManager.rootFocusState).isEqualTo(Inactive)
- assertThat(focusState.isFocused).isFalse()
+ assertThat(focusStates).containsExactly(Inactive)
}
}
- fun releaseFocus_whenRootIsActive() {
- // Arrange.
- lateinit var focusManager: FocusManager
- lateinit var focusState: FocusState
- lateinit var view: View
- rule.setFocusableContent {
- focusManager = LocalFocusManager.current
- view = LocalView.current
- Box(
- modifier = Modifier
- .onFocusChanged { focusState = it }
- .focusTarget()
- )
- }
- rule.runOnIdle { focusManager.setRootFocusState(Active) }
-
- // Act.
- rule.runOnIdle { view.clearFocus() }
-
- // Assert.
- rule.runOnIdle {
- assertThat(focusManager.rootFocusState).isEqualTo(Inactive)
- assertThat(focusState.isFocused).isFalse()
- }
- }
-
- @Ignore("b/257499180")
@Test
- fun releaseFocus_whenRootIsActiveParent() {
+ fun releaseFocus_whenOwnerFocusIsCleared() {
// Arrange.
- lateinit var focusManager: FocusManager
- lateinit var focusState: FocusState
lateinit var view: View
val focusRequester = FocusRequester()
- rule.setFocusableContent {
- focusManager = LocalFocusManager.current
+ rule.setTestContent(extraItemForInitialFocus = false) {
view = LocalView.current
Box(
modifier = Modifier
.focusRequester(focusRequester)
- .onFocusChanged { focusState = it }
+ .onFocusChanged { focusStates += it }
.focusTarget()
)
}
- rule.runOnIdle { focusRequester.requestFocus() }
+ rule.runOnIdle {
+ focusRequester.requestFocus()
+ focusStates.clear()
+ }
// Act.
rule.runOnIdle {
@@ -276,111 +205,107 @@
// Assert.
rule.runOnIdle {
- assertThat(focusManager.rootFocusState).isEqualTo(Inactive)
- assertThat(focusState.isFocused).isFalse()
+ when (inputModeManager.inputMode) {
+ Keyboard -> {
+ // Focus is re-assigned to the initially focused item (default focus).
+ assertThat(focusManager.rootFocusState).isEqualTo(ActiveParent)
+ assertThat(focusStates).containsExactly(Inactive, Active).inOrder()
+ }
+ Touch -> {
+ assertThat(focusManager.rootFocusState).isEqualTo(Inactive)
+ assertThat(focusStates).containsExactly(Inactive)
+ }
+ else -> error("Invalid input mode")
+ }
}
}
@Test
fun clearFocus_whenRootIsInactive() {
// Arrange.
- lateinit var focusManager: FocusManager
- lateinit var focusState: FocusState
val focusRequester = FocusRequester()
- rule.setFocusableContent {
- focusManager = LocalFocusManager.current
+ rule.setTestContent {
Box(
modifier = Modifier
.focusRequester(focusRequester)
- .onFocusChanged { focusState = it }
+ .onFocusChanged { focusStates += it }
.focusTarget()
)
}
+ rule.runOnIdle { focusStates.clear() }
// Act.
rule.runOnIdle { focusManager.clearFocus() }
// Assert.
rule.runOnIdle {
- assertThat(focusManager.rootFocusState).isEqualTo(Inactive)
- assertThat(focusState.isFocused).isFalse()
- }
- }
-
- @Ignore("b/257499180")
- @Test
- fun clearFocus_whenRootIsActive() {
- // Arrange.
- lateinit var focusManager: FocusManager
- lateinit var focusState: FocusState
- val focusRequester = FocusRequester()
- rule.setFocusableContent {
- focusManager = LocalFocusManager.current
- Box(
- modifier = Modifier
- .focusRequester(focusRequester)
- .onFocusChanged { focusState = it }
- .focusTarget()
- )
- }
- rule.runOnIdle { focusManager.setRootFocusState(Active) }
-
- // Act.
- rule.runOnIdle { focusManager.clearFocus() }
-
- // Assert.
- rule.runOnIdle {
- assertThat(focusManager.rootFocusState).isEqualTo(Inactive)
- assertThat(focusState.isFocused).isFalse()
+ when (inputModeManager.inputMode) {
+ Keyboard -> {
+ assertThat(focusManager.rootFocusState).isEqualTo(Inactive)
+ assertThat(focusStates).isEmpty()
+ }
+ Touch -> {
+ assertThat(focusManager.rootFocusState).isEqualTo(Inactive)
+ assertThat(focusStates).isEmpty()
+ }
+ else -> error("Invalid input mode")
+ }
}
}
@Test
fun clearFocus_whenRootIsActiveParent() {
// Arrange.
- lateinit var focusManager: FocusManager
- lateinit var focusState: FocusState
val focusRequester = FocusRequester()
- rule.setFocusableContent {
- focusManager = LocalFocusManager.current
+ rule.setTestContent(extraItemForInitialFocus = false) {
Box(
modifier = Modifier
.focusRequester(focusRequester)
- .onFocusChanged { focusState = it }
+ .onFocusChanged { focusStates += it }
.focusTarget()
)
}
- rule.runOnIdle { focusRequester.requestFocus() }
+ rule.runOnIdle {
+ focusRequester.requestFocus()
+ focusStates.clear()
+ }
// Act.
rule.runOnIdle { focusManager.clearFocus() }
// Assert.
rule.runOnIdle {
- // TODO(b/257499180): Compose should not hold focus state when clear focus is requested.
- assertThat(focusManager.rootFocusState).isEqualTo(Active)
- assertThat(focusState.isFocused).isFalse()
+ when (inputModeManager.inputMode) {
+ Keyboard -> {
+ assertThat(focusManager.rootFocusState).isEqualTo(ActiveParent)
+ assertThat(focusStates).containsExactly(Inactive, Active).inOrder()
+ }
+ Touch -> {
+ assertThat(focusManager.rootFocusState).isEqualTo(Inactive)
+ assertThat(focusStates).containsExactly(Inactive)
+ }
+ else -> error("Invalid input mode")
+ }
}
}
@Test
fun clearFocus_whenHierarchyHasCapturedFocus() {
// Arrange.
- lateinit var focusManager: FocusManager
- lateinit var focusState: FocusState
val focusRequester = FocusRequester()
- rule.setFocusableContent {
+ rule.setTestContent {
focusManager = LocalFocusManager.current
Box(
modifier = Modifier
.focusRequester(focusRequester)
- .onFocusChanged { focusState = it }
+ .onFocusChanged { focusStates += it }
.focusTarget()
)
}
rule.runOnIdle {
focusRequester.requestFocus()
focusRequester.captureFocus()
+ focusStates.clear()
}
// Act.
@@ -389,28 +314,27 @@
// Assert.
rule.runOnIdle {
assertThat(focusManager.rootFocusState).isEqualTo(ActiveParent)
- assertThat(focusState.isFocused).isTrue()
+ assertThat(focusStates).isEmpty()
}
}
@Test
fun clearFocus_forced_whenHierarchyHasCapturedFocus() {
// Arrange.
- lateinit var focusManager: FocusManager
- lateinit var focusState: FocusState
val focusRequester = FocusRequester()
- rule.setFocusableContent {
- focusManager = LocalFocusManager.current
+ rule.setTestContent(extraItemForInitialFocus = false) {
+
Box(
modifier = Modifier
.focusRequester(focusRequester)
- .onFocusChanged { focusState = it }
+ .onFocusChanged { focusStates += it }
.focusTarget()
)
}
rule.runOnIdle {
focusRequester.requestFocus()
focusRequester.captureFocus()
+ focusStates.clear()
}
// Act.
@@ -418,16 +342,32 @@
// Assert.
rule.runOnIdle {
- // TODO(b/257499180): Compose should clear focus and send focus to the root view.
- assertThat(focusManager.rootFocusState).isEqualTo(Active)
- assertThat(focusState.isFocused).isFalse()
+ when (inputModeManager.inputMode) {
+ Keyboard -> {
+ // Focus is re-assigned to the initially focused item (default focus).
+ assertThat(focusManager.rootFocusState).isEqualTo(ActiveParent)
+ assertThat(focusStates).containsExactly(Inactive, Active).inOrder()
+ }
+ Touch -> {
+ assertThat(focusManager.rootFocusState).isEqualTo(Inactive)
+ assertThat(focusStates).containsExactly(Inactive).inOrder()
+ }
+ else -> error("Invalid input mode")
+ }
}
}
private val FocusManager.rootFocusState: FocusState
get() = (this as FocusOwnerImpl).rootFocusNode.focusState
- private fun FocusManager.setRootFocusState(focusState: FocusStateImpl) {
- (this as FocusOwnerImpl).rootFocusNode.focusState = focusState
+ private fun ComposeContentTestRule.setTestContent(
+ extraItemForInitialFocus: Boolean = true,
+ content: @Composable () -> Unit
+ ) {
+ setFocusableContent(extraItemForInitialFocus) {
+ focusManager = LocalFocusManager.current
+ inputModeManager = LocalInputModeManager.current
+ content()
+ }
}
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTestUtils.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTestUtils.kt
index 20fdc7a..f8be78e2 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTestUtils.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTestUtils.kt
@@ -17,6 +17,7 @@
package androidx.compose.ui.focus
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.runtime.Composable
@@ -34,10 +35,25 @@
* This function adds a parent composable which has size.
* [View.requestFocus()][android.view.View.requestFocus] will not take focus if the view has no
* size.
+ *
+ * @param extraItemForInitialFocus Includes an extra item that takes focus initially. This is
+ * useful in cases where we need tests that could be affected by initial focus. Eg. When there is
+ * only one focusable item and we clear focus, that item could end up being focused on again by the
+ * initial focus logic.
*/
-internal fun ComposeContentTestRule.setFocusableContent(content: @Composable () -> Unit) {
+internal fun ComposeContentTestRule.setFocusableContent(
+ extraItemForInitialFocus: Boolean = true,
+ content: @Composable () -> Unit
+) {
setContent {
- Box(modifier = Modifier.requiredSize(100.dp, 100.dp)) { content() }
+ if (extraItemForInitialFocus) {
+ Row {
+ Box(modifier = Modifier.requiredSize(10.dp, 10.dp).focusTarget())
+ Box(modifier = Modifier.requiredSize(100.dp, 100.dp)) { content() }
+ }
+ } else {
+ Box(modifier = Modifier.requiredSize(100.dp, 100.dp)) { content() }
+ }
}
}
@@ -66,9 +82,9 @@
.focusProperties { canFocus = !deactivated }
.focusTarget(),
measurePolicy = remember(width, height) {
- MeasurePolicy { measurables, constraint ->
+ MeasurePolicy { measurableList, constraint ->
layout(width, height) {
- measurables.forEach {
+ measurableList.forEach {
val placeable = it.measure(constraint)
placeable.placeRelative(0, 0)
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTransactionsTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTransactionsTest.kt
index b7caf2c..cdb0b7d 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTransactionsTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTransactionsTest.kt
@@ -23,9 +23,14 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester.Companion.Cancel
import androidx.compose.ui.focus.FocusStateImpl.Active
+import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
import androidx.compose.ui.focus.FocusStateImpl.Inactive
+import androidx.compose.ui.input.InputMode.Companion.Keyboard
+import androidx.compose.ui.input.InputMode.Companion.Touch
+import androidx.compose.ui.input.InputModeManager
import androidx.compose.ui.platform.AndroidComposeView
import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalInputModeManager
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.dp
@@ -91,33 +96,57 @@
fun cancelTakeFocus_fromOnFocusChanged() {
// Arrange.
lateinit var focusManager: FocusManager
+ lateinit var inputModeManager: InputModeManager
lateinit var view: View
+ lateinit var focusState1: FocusState
+ lateinit var focusState2: FocusState
+ lateinit var focusState3: FocusState
val box = FocusRequester()
rule.setFocusableContent {
focusManager = LocalFocusManager.current
+ inputModeManager = LocalInputModeManager.current
view = LocalView.current
Box(
Modifier
.size(10.dp)
.focusRequester(box)
- .onFocusChanged { if (it.isFocused) focusManager.clearFocus() }
+ .onFocusChanged { focusState1 = it }
+ .onFocusChanged {
+ focusState2 = it
+ if (it.isFocused) focusManager.clearFocus()
+ }
+ .onFocusChanged { focusState3 = it }
.focusTarget()
)
}
// Act.
- rule.runOnIdle {
+ rule.runOnUiThread {
box.requestFocus()
}
// Assert.
rule.runOnIdle {
+ assertThat(focusState1).isEqualTo(Inactive)
+ // TODO(b/312524818): When a focus transaction is cancelled, we should re-notify
+ // all the focus event modifiers that were called in the previous transaction.
+ assertThat(focusState2).isEqualTo(Active) // Should be Inactive.
+ assertThat(focusState3).isEqualTo(Active) // Should be Inactive.
+
val root = view as AndroidComposeView
- val focusOwner = root.focusOwner as FocusOwnerImpl
- assertThat(focusOwner.rootFocusNode.focusState).isEqualTo(Inactive)
- // TODO(b/288096244): Find out why this is flaky.
- // assertThat(view.isFocused()).isFalse()
+
+ when (inputModeManager.inputMode) {
+ Keyboard -> {
+ assertThat(root.focusOwner.rootState).isEqualTo(ActiveParent)
+ assertThat(view.isFocused).isTrue()
+ }
+ Touch -> {
+ assertThat(root.focusOwner.rootState).isEqualTo(Inactive)
+ assertThat(view.isFocused).isFalse()
+ }
+ else -> error("invalid input mode")
+ }
}
}
@@ -152,14 +181,13 @@
// Assert.
rule.runOnIdle {
val root = view as AndroidComposeView
- val focusOwner = root.focusOwner as FocusOwnerImpl
- assertThat(focusOwner.rootFocusNode.focusState).isEqualTo(Inactive)
+ assertThat(root.focusOwner.rootState).isEqualTo(Inactive)
assertThat(view.isFocused).isFalse()
}
}
@Test
- fun rootFocusNodeIsActiveWhenViewIsFocused() {
+ fun rootFocusNodeHasFocusWhenViewIsFocused() {
lateinit var view: View
val focusRequester = FocusRequester()
rule.setFocusableContent {
@@ -174,9 +202,8 @@
// Assert.
val root = view as AndroidComposeView
- val focusOwner = root.focusOwner as FocusOwnerImpl
rule.runOnIdle {
- assertThat(focusOwner.rootFocusNode.focusState).isEqualTo(Active)
+ assertThat(root.focusOwner.rootState).isEqualTo(ActiveParent)
assertThat(view.isFocused).isTrue()
}
@@ -190,7 +217,7 @@
// Assert.
rule.runOnIdle {
- assertThat(focusOwner.rootFocusNode.focusState).isEqualTo(Active)
+ assertThat(root.focusOwner.rootState.hasFocus).isEqualTo(true)
assertThat(view.isFocused).isTrue()
}
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusViewInteropTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusViewInteropTest.kt
index b2c2ef2..125adfd 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusViewInteropTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusViewInteropTest.kt
@@ -18,6 +18,7 @@
import android.graphics.Rect as AndroidRect
import android.view.View
+import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.offset
@@ -25,6 +26,7 @@
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
@@ -101,6 +103,36 @@
)
}
+ @Test
+ fun requestFocus_returnsFalseWhenCancelled() {
+ // Arrange.
+ lateinit var view: View
+ rule.setContent {
+ view = LocalView.current
+ Box(
+ Modifier
+ .size(10.dp)
+ .focusProperties {
+ @OptIn(ExperimentalComposeUiApi::class)
+ enter = { FocusRequester.Cancel }
+ }
+ .focusGroup()
+ ) {
+ Box(
+ Modifier
+ .size(10.dp)
+ .focusable()
+ )
+ }
+ }
+
+ // Act.
+ val success = rule.runOnIdle { view.requestFocus() }
+
+ // Assert.
+ rule.runOnIdle { assertThat(success).isFalse() }
+ }
+
private fun View.getFocusedRect() = AndroidRect().run {
rule.runOnIdle {
getFocusedRect(this)
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchNextTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchNextTest.kt
index a622899..78c6445 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchNextTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchNextTest.kt
@@ -44,7 +44,7 @@
@Test
fun moveFocus_noFocusableItem() {
// Arrange.
- rule.setContentWithInitialRootFocus {}
+ rule.setContentForTest {}
// Act.
val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Next) }
@@ -57,7 +57,7 @@
fun moveFocus_oneDisabledFocusableItem() {
// Arrange.
val isItemFocused = mutableStateOf(false)
- rule.setContentWithInitialRootFocus {
+ rule.setContentForTest {
FocusableBox(isItemFocused, 0, 0, 10, 10, deactivated = true)
}
@@ -72,7 +72,7 @@
fun initialFocus_oneItem() {
// Arrange.
val isItemFocused = mutableStateOf(false)
- rule.setContentWithInitialRootFocus {
+ rule.setContentForTest {
FocusableBox(isItemFocused, 0, 0, 10, 10)
}
@@ -90,7 +90,7 @@
fun initialFocus_skipsDeactivatedItem() {
// Arrange.
val (firstItem, secondItem) = List(2) { mutableStateOf(false) }
- rule.setContentWithInitialRootFocus {
+ rule.setContentForTest {
Column {
FocusableBox(firstItem, 0, 0, 10, 10, deactivated = true)
FocusableBox(secondItem, 0, 0, 10, 10)
@@ -112,7 +112,7 @@
fun initialFocus_firstItemInCompositionOrderGetsFocus() {
// Arrange.
val (firstItem, secondItem) = List(2) { mutableStateOf(false) }
- rule.setContentWithInitialRootFocus {
+ rule.setContentForTest {
FocusableBox(firstItem, 10, 10, 10, 10)
FocusableBox(secondItem, 0, 0, 10, 10)
}
@@ -131,7 +131,7 @@
fun initialFocus_firstParentInCompositionOrderGetsFocus() {
// Arrange.
val (parent1, parent2, child1, child2) = List(4) { mutableStateOf(false) }
- rule.setContentWithInitialRootFocus {
+ rule.setContentForTest {
FocusableBox(parent1, 10, 10, 10, 10) {
FocusableBox(child1, 10, 10, 10, 10)
}
@@ -154,7 +154,7 @@
fun initialFocus_firstItemInCompositionOrderGetsFocus_evenIfAnotherNonParentIsPresent() {
// Arrange.
val (parent1, child1, item2) = List(3) { mutableStateOf(false) }
- rule.setContentWithInitialRootFocus {
+ rule.setContentForTest {
FocusableBox(parent1, 10, 10, 10, 10) {
FocusableBox(child1, 10, 10, 10, 10)
}
@@ -175,7 +175,7 @@
fun initialFocus_firstItemInCompositionOrderGetsFocus_evenIfThereIsAParentAtTheRoot() {
// Arrange.
val (parent1, child1, item1) = List(3) { mutableStateOf(false) }
- rule.setContentWithInitialRootFocus {
+ rule.setContentForTest {
FocusableBox(item1, 0, 0, 10, 10)
FocusableBox(parent1, 10, 10, 10, 10) {
FocusableBox(child1, 10, 10, 10, 10)
@@ -196,7 +196,7 @@
fun focusMovesToSecondItem() {
// Arrange.
val (item1, item2, item3) = List(3) { mutableStateOf(false) }
- rule.setContentForTest {
+ rule.setContentForTest(initializeFocus = true) {
FocusableBox(item1, 0, 0, 10, 10, initialFocus)
FocusableBox(item2, 10, 0, 10, 10)
FocusableBox(item3, 20, 0, 10, 10)
@@ -216,7 +216,7 @@
fun focusMovesToThirdItem_skipsDeactivatedItem() {
// Arrange.
val (item1, item2, item3, item4) = List(4) { mutableStateOf(false) }
- rule.setContentForTest {
+ rule.setContentForTest(initializeFocus = true) {
FocusableBox(item1, 0, 0, 10, 10, initialFocus)
FocusableBox(item2, 10, 0, 10, 10, deactivated = true)
FocusableBox(item3, 10, 0, 10, 10)
@@ -237,7 +237,7 @@
fun focusMovesToThirdItem() {
// Arrange.
val (item1, item2, item3) = List(3) { mutableStateOf(false) }
- rule.setContentForTest {
+ rule.setContentForTest(initializeFocus = true) {
FocusableBox(item1, 0, 0, 10, 10)
FocusableBox(item2, 10, 0, 10, 10, initialFocus)
FocusableBox(item3, 20, 0, 10, 10)
@@ -257,7 +257,7 @@
fun focusMovesToFourthItem() {
// Arrange.
val (item1, item2, item3, item4) = List(4) { mutableStateOf(false) }
- rule.setContentForTest {
+ rule.setContentForTest(initializeFocus = true) {
FocusableBox(item1, 0, 0, 10, 10)
FocusableBox(item2, 0, 0, 10, 10, deactivated = true)
FocusableBox(item3, 10, 0, 10, 10, initialFocus)
@@ -278,7 +278,7 @@
fun focusWrapsAroundToFirstItem() {
// Arrange.
val (item1, item2, item3) = List(3) { mutableStateOf(false) }
- rule.setContentForTest {
+ rule.setContentForTest(initializeFocus = true) {
FocusableBox(item1, 0, 0, 10, 10)
FocusableBox(item2, 10, 0, 10, 10)
FocusableBox(item3, 20, 0, 10, 10, initialFocus)
@@ -298,7 +298,7 @@
fun focusWrapsAroundToFirstItem_skippingLastDeactivatedItem() {
// Arrange.
val (item1, item2, item3, item4) = List(4) { mutableStateOf(false) }
- rule.setContentForTest {
+ rule.setContentForTest(initializeFocus = true) {
FocusableBox(item1, 0, 0, 10, 10)
FocusableBox(item2, 10, 0, 10, 10)
FocusableBox(item3, 20, 0, 10, 10, initialFocus)
@@ -319,7 +319,7 @@
fun focusWrapsAroundToFirstItem_skippingFirstDeactivatedItem() {
// Arrange.
val (item1, item2, item3, item4) = List(4) { mutableStateOf(false) }
- rule.setContentForTest {
+ rule.setContentForTest(initializeFocus = true) {
FocusableBox(item1, 10, 0, 10, 10, deactivated = true)
FocusableBox(item2, 0, 0, 10, 10)
FocusableBox(item3, 10, 0, 10, 10)
@@ -340,7 +340,7 @@
fun focusMovesToChildOfDeactivatedItem() {
// Arrange.
val (item1, item2, item3, child) = List(4) { mutableStateOf(false) }
- rule.setContentForTest {
+ rule.setContentForTest(initializeFocus = true) {
FocusableBox(item1, 0, 0, 10, 10, initialFocus)
FocusableBox(item2, 10, 0, 10, 10, deactivated = true) {
FocusableBox(child, 10, 0, 10, 10)
@@ -362,7 +362,7 @@
fun focusMovesToGrandChildOfDeactivatedItem() {
// Arrange.
val (item1, item2, item3, child, grandchild) = List(5) { mutableStateOf(false) }
- rule.setContentForTest {
+ rule.setContentForTest(initializeFocus = true) {
FocusableBox(item1, 0, 0, 10, 10, initialFocus)
FocusableBox(item2, 10, 0, 10, 10, deactivated = true) {
FocusableBox(child, 10, 0, 10, 10, deactivated = true) {
@@ -386,7 +386,7 @@
fun focusMovesToNextSiblingOfDeactivatedItem_evenThoughThereIsACloserNonSibling() {
// Arrange.
val (item1, item2, item3, child1, child2) = List(5) { mutableStateOf(false) }
- rule.setContentForTest {
+ rule.setContentForTest(initializeFocus = true) {
FocusableBox(item1, 0, 0, 10, 10)
FocusableBox(item2, 10, 0, 10, 10, deactivated = true) {
FocusableBox(child1, 10, 0, 10, 10, initialFocus)
@@ -409,7 +409,7 @@
fun focusNextOrderAmongChildrenOfMultipleParents() {
// Arrange.
val focusState = List(12) { mutableStateOf(false) }
- rule.setContentForTest {
+ rule.setContentForTest(initializeFocus = true) {
Column {
Row {
FocusableBox(focusState[0], 0, 0, 10, 10, initialFocus)
@@ -446,7 +446,7 @@
fun focusNextOrderAmongChildrenAtMultipleLevels() {
// Arrange.
val focusState = List(14) { mutableStateOf(false) }
- rule.setContentForTest {
+ rule.setContentForTest(initializeFocus = true) {
Column {
FocusableBox(focusState[0], 0, 0, 10, 10, initialFocus)
FocusableBox(focusState[1], 0, 10, 10, 10)
@@ -488,7 +488,7 @@
val (parent3, child6) = List(2) { mutableStateOf(false) }
val (parent4, child7, child8, child9, child10) = List(5) { mutableStateOf(false) }
val (parent5, child11) = List(2) { mutableStateOf(false) }
- rule.setContentWithInitialRootFocus {
+ rule.setContentForTest {
FocusableBox(parent1, 0, 0, 10, 10) {
FocusableBox(child1, 0, 0, 10, 10)
FocusableBox(child2, 20, 0, 10, 10)
@@ -553,25 +553,16 @@
rule.runOnIdle { assertThat(parent1.value).isTrue() }
}
- private fun ComposeContentTestRule.setContentForTest(composable: @Composable () -> Unit) {
- setContent {
- focusManager = LocalFocusManager.current
- composable()
- }
- rule.runOnIdle { initialFocus.requestFocus() }
- }
-
- private fun ComposeContentTestRule.setContentWithInitialRootFocus(
+ private fun ComposeContentTestRule.setContentForTest(
+ initializeFocus: Boolean = false,
composable: @Composable () -> Unit
) {
setContent {
focusManager = LocalFocusManager.current
composable()
}
- rule.runOnIdle {
- with(focusManager as FocusOwner) {
- focusTransactionManager.withNewTransaction { takeFocus() }
- }
+ if (initializeFocus) {
+ rule.runOnIdle { initialFocus.requestFocus() }
}
}
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchPreviousTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchPreviousTest.kt
index 613421e..9d248a0 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchPreviousTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchPreviousTest.kt
@@ -44,7 +44,7 @@
@Test
fun moveFocus_noFocusableItem() {
// Arrange.
- rule.setContentWithInitialRootFocus {}
+ rule.setContentForTest {}
// Act.
val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Previous) }
@@ -57,7 +57,7 @@
fun moveFocus_oneDisabledFocusableItem() {
// Arrange.
val isItemFocused = mutableStateOf(false)
- rule.setContentWithInitialRootFocus {
+ rule.setContentForTest {
FocusableBox(isItemFocused, 0, 0, 10, 10, deactivated = true)
}
@@ -72,7 +72,7 @@
fun initialFocus_oneItem() {
// Arrange.
val isItemFocused = mutableStateOf(false)
- rule.setContentWithInitialRootFocus {
+ rule.setContentForTest {
FocusableBox(isItemFocused, 0, 0, 10, 10)
}
@@ -90,7 +90,7 @@
fun initialFocus_skipsDeactivatedItem() {
// Arrange.
val (firstItem, secondItem) = List(2) { mutableStateOf(false) }
- rule.setContentWithInitialRootFocus {
+ rule.setContentForTest {
Column {
FocusableBox(firstItem, 0, 0, 10, 10)
FocusableBox(secondItem, 0, 0, 10, 10, deactivated = true)
@@ -112,7 +112,7 @@
fun initialFocus_lastItemInCompositionOrderGetsFocus() {
// Arrange.
val (firstItem, secondItem) = List(2) { mutableStateOf(false) }
- rule.setContentWithInitialRootFocus {
+ rule.setContentForTest {
FocusableBox(firstItem, 10, 10, 10, 10)
FocusableBox(secondItem, 0, 0, 10, 10)
}
@@ -131,7 +131,7 @@
fun initialFocus_lastChildInCompositionOrderGetsFocus() {
// Arrange.
val (parent1, parent2, child1, child2) = List(4) { mutableStateOf(false) }
- rule.setContentWithInitialRootFocus {
+ rule.setContentForTest {
FocusableBox(parent1, 10, 10, 10, 10) {
FocusableBox(child1, 10, 10, 10, 10)
}
@@ -154,7 +154,7 @@
fun initialFocus_lastItemInCompositionOrderGetsFocus_evenIfAnotherNonParentIsPresent() {
// Arrange.
val (parent1, child1, item1) = List(3) { mutableStateOf(false) }
- rule.setContentWithInitialRootFocus {
+ rule.setContentForTest {
FocusableBox(item1, 0, 0, 10, 10)
FocusableBox(parent1, 10, 10, 10, 10) {
FocusableBox(child1, 10, 10, 10, 10)
@@ -175,7 +175,7 @@
fun initialFocus_lastItemInCompositionOrderGetsFocus_evenIfThereIsAParentAtTheRoot() {
// Arrange.
val (parent1, child1, item2) = List(3) { mutableStateOf(false) }
- rule.setContentWithInitialRootFocus {
+ rule.setContentForTest {
FocusableBox(parent1, 10, 10, 10, 10) {
FocusableBox(child1, 10, 10, 10, 10)
FocusableBox(item2, 0, 0, 10, 10)
@@ -196,7 +196,7 @@
fun focusMovesToSecondItem() {
// Arrange.
val (item1, item2, item3) = List(3) { mutableStateOf(false) }
- rule.setContentForTest {
+ rule.setContentForTest(initializeFocus = true) {
FocusableBox(item1, 0, 0, 10, 10)
FocusableBox(item2, 10, 0, 10, 10)
FocusableBox(item3, 20, 0, 10, 10, initialFocus)
@@ -216,7 +216,7 @@
fun focusMovesToSecondItem_skipsDeactivatedItem() {
// Arrange.
val (item1, item2, item3, item4) = List(4) { mutableStateOf(false) }
- rule.setContentForTest {
+ rule.setContentForTest(initializeFocus = true) {
FocusableBox(item1, 0, 0, 10, 10)
FocusableBox(item2, 10, 0, 10, 10)
FocusableBox(item3, 10, 0, 10, 10, deactivated = true)
@@ -237,7 +237,7 @@
fun focusMovesToFirstItem() {
// Arrange.
val (item1, item2, item3) = List(3) { mutableStateOf(false) }
- rule.setContentForTest {
+ rule.setContentForTest(initializeFocus = true) {
FocusableBox(item1, 0, 0, 10, 10)
FocusableBox(item2, 10, 0, 10, 10, initialFocus)
FocusableBox(item3, 20, 0, 10, 10)
@@ -257,7 +257,7 @@
fun focusMovesToFirstItem_ignoresDeactivated() {
// Arrange.
val (item1, item2, item3, item4) = List(4) { mutableStateOf(false) }
- rule.setContentForTest {
+ rule.setContentForTest(initializeFocus = true) {
FocusableBox(item1, 0, 0, 10, 10)
FocusableBox(item2, 10, 0, 10, 10, initialFocus)
FocusableBox(item3, 20, 0, 10, 10, deactivated = true)
@@ -278,7 +278,7 @@
fun focusMovesToParent() {
// Arrange.
val (parent, child1, child2, child3) = List(4) { mutableStateOf(false) }
- rule.setContentForTest {
+ rule.setContentForTest(initializeFocus = true) {
FocusableBox(parent, 0, 0, 10, 10) {
FocusableBox(child1, 10, 0, 10, 10, initialFocus)
FocusableBox(child2, 20, 0, 10, 10)
@@ -300,7 +300,7 @@
fun focusMovesToParent_ignoresDeactivated() {
// Arrange.
val (item, parent, child1, child2) = List(4) { mutableStateOf(false) }
- rule.setContentForTest {
+ rule.setContentForTest(initializeFocus = true) {
FocusableBox(item, 0, 0, 10, 10)
FocusableBox(parent, 0, 0, 10, 10, deactivated = true) {
FocusableBox(child1, 10, 0, 10, 10, initialFocus)
@@ -322,7 +322,7 @@
fun focusMovesToParent_ignoresDeactivated_andWrapsAround() {
// Arrange.
val (parent, child1, child2, child3) = List(4) { mutableStateOf(false) }
- rule.setContentForTest {
+ rule.setContentForTest(initializeFocus = true) {
FocusableBox(parent, 0, 0, 10, 10, deactivated = true) {
FocusableBox(child1, 10, 0, 10, 10, initialFocus)
FocusableBox(child2, 20, 0, 10, 10)
@@ -344,7 +344,7 @@
fun focusWrapsAroundToLastItem() {
// Arrange.
val (item1, item2, item3) = List(3) { mutableStateOf(false) }
- rule.setContentForTest {
+ rule.setContentForTest(initializeFocus = true) {
FocusableBox(item1, 0, 0, 10, 10, initialFocus)
FocusableBox(item2, 10, 0, 10, 10)
FocusableBox(item3, 20, 0, 10, 10)
@@ -364,7 +364,7 @@
fun focusWrapsAroundToLastItem_skippingFirstDeactivatedItem() {
// Arrange.
val (item1, item2, item3, item4) = List(4) { mutableStateOf(false) }
- rule.setContentForTest {
+ rule.setContentForTest(initializeFocus = true) {
FocusableBox(item1, 20, 0, 10, 10, deactivated = true)
FocusableBox(item2, 0, 0, 10, 10, initialFocus)
FocusableBox(item3, 10, 0, 10, 10)
@@ -385,7 +385,7 @@
fun focusWrapsAroundToLastItem_skippingLastDeactivatedItem() {
// Arrange.
val (item1, item2, item3, item4) = List(4) { mutableStateOf(false) }
- rule.setContentForTest {
+ rule.setContentForTest(initializeFocus = true) {
FocusableBox(item1, 0, 0, 10, 10, initialFocus)
FocusableBox(item2, 10, 0, 10, 10)
FocusableBox(item3, 20, 0, 10, 10)
@@ -406,7 +406,7 @@
fun focusMovesToChildOfDeactivatedItem() {
// Arrange.
val (item1, item2, item3, child) = List(4) { mutableStateOf(false) }
- rule.setContentForTest {
+ rule.setContentForTest(initializeFocus = true) {
FocusableBox(item1, 0, 0, 10, 10)
FocusableBox(item2, 10, 0, 10, 10, deactivated = true) {
FocusableBox(child, 10, 0, 10, 10)
@@ -428,7 +428,7 @@
fun focusMovesToGrandChildOfDeactivatedItem() {
// Arrange.
val (item1, item2, item3, child, grandchild) = List(5) { mutableStateOf(false) }
- rule.setContentForTest {
+ rule.setContentForTest(initializeFocus = true) {
FocusableBox(item1, 0, 0, 10, 10)
FocusableBox(item2, 10, 0, 10, 10, deactivated = true) {
FocusableBox(child, 10, 0, 10, 10, deactivated = true) {
@@ -452,7 +452,7 @@
fun focusMovesToNextSiblingOfDeactivatedItem_evenThoughThereIsACloserNonSibling() {
// Arrange.
val (item1, item2, item3, child1, child2) = List(5) { mutableStateOf(false) }
- rule.setContentForTest {
+ rule.setContentForTest(initializeFocus = true) {
FocusableBox(item1, 10, 0, 10, 10)
FocusableBox(item2, 0, 0, 10, 10, deactivated = true) {
FocusableBox(child1, 0, 0, 10, 10)
@@ -475,7 +475,7 @@
fun focusNextOrderAmongChildrenOfMultipleParents() {
// Arrange.
val focusState = List(12) { mutableStateOf(false) }
- rule.setContentForTest {
+ rule.setContentForTest(initializeFocus = true) {
Column {
Row {
FocusableBox(focusState[0], 0, 0, 10, 10)
@@ -511,7 +511,7 @@
fun focusNextOrderAmongChildrenAtMultipleLevels() {
// Arrange.
val focusState = List(14) { mutableStateOf(false) }
- rule.setContentForTest {
+ rule.setContentForTest(initializeFocus = true) {
Column {
FocusableBox(focusState[0], 0, 0, 10, 10)
FocusableBox(focusState[1], 0, 10, 10, 10)
@@ -554,7 +554,7 @@
val (parent4, child7, child8, child11, child12) = List(5) { mutableStateOf(false) }
val (child9, child10) = List(2) { mutableStateOf(false) }
val (parent5, child13) = List(2) { mutableStateOf(false) }
- rule.setContentWithInitialRootFocus {
+ rule.setContentForTest {
FocusableBox(parent1, 0, 0, 10, 10) {
FocusableBox(child1, 0, 0, 10, 10)
FocusableBox(child2, 20, 0, 10, 10)
@@ -624,25 +624,16 @@
rule.runOnIdle { assertThat(child11.value).isTrue() }
}
- private fun ComposeContentTestRule.setContentForTest(composable: @Composable () -> Unit) {
- setContent {
- focusManager = LocalFocusManager.current
- composable()
- }
- rule.runOnIdle { initialFocus.requestFocus() }
- }
-
- private fun ComposeContentTestRule.setContentWithInitialRootFocus(
+ private fun ComposeContentTestRule.setContentForTest(
+ initializeFocus: Boolean = false,
composable: @Composable () -> Unit
) {
setContent {
focusManager = LocalFocusManager.current
composable()
}
- rule.runOnIdle {
- with(focusManager as FocusOwner) {
- focusTransactionManager.withNewTransaction { takeFocus() }
- }
+ if (initializeFocus) {
+ rule.runOnIdle { initialFocus.requestFocus() }
}
}
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt
index bfeff0b..e3843b0 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt
@@ -17,13 +17,18 @@
package androidx.compose.ui.focus
import android.view.View
+import android.view.View.FOCUS_DOWN
+import android.view.View.FOCUS_UP
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.size
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
+import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
@@ -63,19 +68,16 @@
}
}
- @Ignore("Enable this test after the owner propagates focus to the hierarchy (b/152535715)")
@Test
fun whenOwnerGainsFocus_focusModifiersAreUpdated() {
// Arrange.
lateinit var ownerView: View
lateinit var focusState: FocusState
- val focusRequester = FocusRequester()
- rule.setFocusableContent {
+ rule.setFocusableContent(extraItemForInitialFocus = false) {
ownerView = LocalView.current
Box(
modifier = Modifier
.onFocusChanged { focusState = it }
- .focusRequester(focusRequester)
.focusTarget()
)
}
@@ -91,6 +93,106 @@
}
}
+ @Test
+ fun callingRequestFocusDownWhenOwnerAlreadyHasFocus() {
+ // Arrange.
+ lateinit var ownerView: View
+ lateinit var focusState1: FocusState
+ lateinit var focusState2: FocusState
+ lateinit var focusState3: FocusState
+ val focusRequester = FocusRequester()
+ rule.setFocusableContent(extraItemForInitialFocus = false) {
+ ownerView = LocalView.current
+ Column {
+ Box(
+ modifier = Modifier
+ .size(10.dp)
+ .onFocusChanged { focusState1 = it }
+ .focusTarget()
+ )
+ Box(
+ modifier = Modifier
+ .size(10.dp)
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState2 = it }
+ .focusTarget()
+ )
+ Box(
+ modifier = Modifier
+ .size(10.dp)
+ .onFocusChanged { focusState3 = it }
+ .focusTarget()
+ )
+ }
+ }
+ rule.runOnIdle {
+ focusRequester.requestFocus()
+ }
+
+ // Act.
+ val focusRequested = rule.runOnIdle {
+ ownerView.requestFocus(FOCUS_DOWN)
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusRequested).isTrue()
+ assertThat(focusState1.isFocused).isFalse()
+ assertThat(focusState2.isFocused).isTrue()
+ assertThat(focusState3.isFocused).isFalse()
+ }
+ }
+
+ @Test
+ fun callingRequestFocusUpWhenOwnerAlreadyHasFocus() {
+ // Arrange.
+ lateinit var ownerView: View
+ lateinit var focusState1: FocusState
+ lateinit var focusState2: FocusState
+ lateinit var focusState3: FocusState
+ val focusRequester = FocusRequester()
+ rule.setFocusableContent(extraItemForInitialFocus = false) {
+ ownerView = LocalView.current
+ Column {
+ Box(
+ modifier = Modifier
+ .size(10.dp)
+ .onFocusChanged { focusState1 = it }
+ .focusTarget()
+ )
+ Box(
+ modifier = Modifier
+ .size(10.dp)
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState2 = it }
+ .focusTarget()
+ )
+ Box(
+ modifier = Modifier
+ .size(10.dp)
+ .onFocusChanged { focusState3 = it }
+ .focusTarget()
+ )
+ }
+ }
+ rule.runOnIdle {
+ focusRequester.requestFocus()
+ }
+
+ // Act.
+ val focusRequested = rule.runOnIdle {
+ ownerView.requestFocus(FOCUS_UP)
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusRequested).isTrue()
+ assertThat(focusState1.isFocused).isFalse()
+ assertThat(focusState2.isFocused).isTrue()
+ assertThat(focusState3.isFocused).isFalse()
+ }
+ }
+
@Ignore("Enable this test after the owner propagates focus to the hierarchy (b/152535715)")
@Test
fun whenWindowGainsFocus_focusModifiersAreUpdated() {
@@ -129,6 +231,7 @@
ownerView = LocalView.current
Box(
modifier = Modifier
+ .size(10.dp)
.onFocusChanged { focusState = it }
.focusRequester(focusRequester)
.focusTarget()
@@ -180,6 +283,25 @@
}
@Test
+ fun viewDoesNotTakeFocus_whenThereAreNoFocusableItems() {
+ // Arrange.
+ lateinit var ownerView: View
+ rule.setFocusableContent(extraItemForInitialFocus = false) {
+ ownerView = LocalView.current
+ Box {}
+ }
+
+ // Act.
+ val success = rule.runOnIdle { ownerView.requestFocus() }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(success).isFalse()
+ assertThat(ownerView.isFocused).isFalse()
+ }
+ }
+
+ @Test
fun clickingOnNonClickableSpaceInAppWhenViewIsFocused_doesNotChangeViewFocus() {
// Arrange.
val nonClickable = "notClickable"
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalImplicitEnterTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalImplicitEnterTest.kt
index cd43471..30f4e1a 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalImplicitEnterTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalImplicitEnterTest.kt
@@ -152,7 +152,7 @@
* |________|
*/
@Test
- fun moveFocusEnter_blockFocusChange() {
+ fun moveFocus_skipsItemWithCustomEnter() {
// Arrange.
val (up, down, left, right, parent) = List(5) { mutableStateOf(false) }
val child = mutableStateOf(false)
@@ -185,15 +185,95 @@
// Assert.
rule.runOnIdle {
- assertThat(movedFocusSuccessfully).isFalse()
+ assertThat(movedFocusSuccessfully).isTrue()
assertThat(directionSentToEnter).isEqualTo(focusDirection)
assertThat(child.value).isFalse()
assertThat(parent.value).isFalse()
when (focusDirection) {
- Left -> assertThat(right.value).isTrue()
- Right -> assertThat(left.value).isTrue()
- Up -> assertThat(down.value).isTrue()
- Down -> assertThat(up.value).isTrue()
+ Left -> assertThat(left.value).isTrue()
+ Right -> assertThat(right.value).isTrue()
+ Up -> assertThat(left.value).isTrue()
+ Down -> assertThat(left.value).isTrue()
+ }
+ }
+ }
+
+ /**
+ * _________ | _________
+ * | Up | | | Up |
+ * |________| | |________|
+ * ________________ | ________________
+ * | parent | | | parent |
+ * | _________ | __________ | __________ | _________ |
+ * | | child0 | | | focused | | | focused | | | child0 | |
+ * | |________| | |_________| | |_________| | |________| |
+ * |_______________| | |_______________|
+ * _________ | _________
+ * | Down | | | Down |
+ * |________| | |________|
+ * |
+ * moveFocus(Left) | moveFocus(Right)
+ * |
+ * ---------------------------------------------|--------------------------------------------
+ * | __________
+ * | | focused |
+ * | |_________|
+ * ________________ | ________________
+ * | parent | | | parent |
+ * _________ | _________ | _________ | _________ | _________ | _________
+ * | Left | | | child0 | | | Right | | | Left | | | child0 | | | Right |
+ * |________| | |________| | |________| | |________| | |________| | |________|
+ * |_______________| | |_______________|
+ * __________ |
+ * | focused | |
+ * |_________| |
+ * |
+ * moveFocus(Up) | moveFocus(Down)
+ * |
+ */
+ @Test
+ fun moveFocusEnter_blockFocusChange_appropriateOtherItemIsFocused() {
+ // Arrange.
+ val (up, down, left, right, parent) = List(5) { mutableStateOf(false) }
+ val child = mutableStateOf(false)
+ var (upItem, downItem, leftItem, rightItem, childItem) = FocusRequester.createRefs()
+ var directionSentToEnter: FocusDirection? = null
+ val customFocusEnter = Modifier.focusProperties {
+ enter = {
+ directionSentToEnter = it
+ Cancel
+ }
+ }
+ when (focusDirection) {
+ Left -> rightItem = initialFocus
+ Right -> leftItem = initialFocus
+ Up -> downItem = initialFocus
+ Down -> upItem = initialFocus
+ }
+ rule.setContentForTest {
+ if (focusDirection != Up) FocusableBox(up, 30, 0, 10, 10, upItem)
+ if (focusDirection != Left) FocusableBox(left, 0, 30, 10, 10, leftItem)
+ FocusableBox(parent, 20, 20, 30, 30, deactivated = true, modifier = customFocusEnter) {
+ FocusableBox(child, 10, 10, 10, 10, childItem)
+ }
+ if (focusDirection != Right) FocusableBox(right, 60, 30, 10, 10, rightItem)
+ if (focusDirection != Down) FocusableBox(down, 30, 60, 10, 10, downItem)
+ }
+
+ // Act.
+ val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(focusDirection) }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(movedFocusSuccessfully).isTrue()
+ assertThat(directionSentToEnter).isEqualTo(focusDirection)
+ assertThat(child.value).isFalse()
+ assertThat(parent.value).isFalse()
+ when (focusDirection) {
+ Left -> assertThat(up.value).isTrue()
+ Right -> assertThat(up.value).isTrue()
+ Up -> assertThat(left.value).isTrue()
+ Down -> assertThat(left.value).isTrue()
}
}
}
@@ -205,7 +285,7 @@
* ________________
* | |
* _________ | empty | _________
- * | Left | | lazylist | | Right |
+ * | Left | | lazyList | | Right |
* |________| | | |________|
* |_______________|
* _________
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalInitialFocusTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalInitialFocusTest.kt
index bfccce6..a08a472 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalInitialFocusTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalInitialFocusTest.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui.focus
-import android.view.View
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.text.BasicText
@@ -29,7 +28,6 @@
import androidx.compose.ui.focus.FocusDirection.Companion.Right
import androidx.compose.ui.focus.FocusDirection.Companion.Up
import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.filters.MediumTest
@@ -54,7 +52,6 @@
}
private val focusDirection = param.focusDirection
- lateinit var view: View
private lateinit var focusManager: FocusManager
companion object {
@@ -214,11 +211,9 @@
private fun ComposeContentTestRule.setContentForTest(composable: @Composable () -> Unit) {
setContent {
- view = LocalView.current
focusManager = LocalFocusManager.current
composable()
}
- rule.runOnIdle { view.requestFocus() }
}
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt
index e350b81..07764c7 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt
@@ -84,7 +84,6 @@
@Test
fun noFocusable_doesNotDeliverEvent() {
// Arrange.
- var error: IllegalStateException? = null
rule.setContent {
Box(
modifier = Modifier.onFocusAwareEvent {
@@ -95,25 +94,15 @@
}
// Act.
- try {
- rule.onRoot().performFocusAwareInput(sentEvent)
- } catch (exception: IllegalStateException) {
- error = exception
- }
+ rule.onRoot().performFocusAwareInput(sentEvent)
// Assert.
- assertThat(receivedEvent).isNull()
- when (nodeType) {
- KeyInput, InterruptedSoftKeyboardInput ->
- assertThat(error!!.message).contains("do not have an active focus target")
- RotaryInput -> assertThat(error).isNull()
- }
+ rule.runOnIdle { assertThat(receivedEvent).isNull() }
}
@Test
fun unfocusedFocusable_doesNotDeliverEvent() {
// Arrange.
- var error: IllegalStateException? = null
rule.setFocusableContent {
Box(
modifier = Modifier
@@ -126,18 +115,10 @@
}
// Act.
- try {
- rule.onRoot().performFocusAwareInput(sentEvent)
- } catch (exception: IllegalStateException) {
- error = exception
- }
+ rule.onRoot().performFocusAwareInput(sentEvent)
// Assert.
- assertThat(receivedEvent).isNull()
- when (nodeType) {
- KeyInput -> assertThat(error!!.message).contains("do not have an active focus target")
- InterruptedSoftKeyboardInput, RotaryInput -> assertThat(receivedEvent).isNull()
- }
+ rule.runOnIdle { assertThat(receivedEvent).isNull() }
}
@Test
@@ -526,7 +507,7 @@
}
private fun ComposeContentTestRule.setContentWithInitialFocus(content: @Composable () -> Unit) {
- setFocusableContent(content)
+ setFocusableContent(content = content)
runOnIdle { initialFocus.requestFocus() }
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/key/ProcessKeyInputTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/key/ProcessKeyInputTest.kt
index 70124ae..f87436d 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/key/ProcessKeyInputTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/key/ProcessKeyInputTest.kt
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package androidx.compose.ui.input.key
import android.view.KeyEvent as AndroidKeyEvent
@@ -39,7 +38,6 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
-import kotlin.test.assertFailsWith
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -51,44 +49,45 @@
val rule = createComposeRule()
@Test
- fun noRootFocusTarget_throwsException() {
+ fun noFocusTarget_doesNotTriggerOnKeyEvent() {
// Arrange.
- rule.setContent {
- Box(modifier = Modifier.onKeyEvent { false })
+ var receivedKeyEvent: KeyEvent? = null
+ rule.setFocusableContent {
+ Box(
+ Modifier.onKeyEvent {
+ receivedKeyEvent = it
+ true
+ }
+ )
}
// Act.
- assertFailsWith<IllegalStateException> {
- rule.onRoot().performKeyPress(keyEvent(KeyCodeA, KeyDown))
- }
+ rule.onRoot().performKeyPress(keyEvent(KeyCodeA, KeyDown))
+
+ // Assert.
+ rule.runOnIdle { assertThat(receivedKeyEvent).isNull() }
}
@Test
- fun noFocusTarget_throwsException() {
+ fun focusTargetNotFocused_doesNotTriggerOnKeyEvent() {
// Arrange.
+ var receivedKeyEvent: KeyEvent? = null
rule.setFocusableContent {
- Box(modifier = Modifier.onKeyEvent { true })
+ Box(
+ Modifier
+ .focusTarget()
+ .onKeyEvent {
+ receivedKeyEvent = it
+ true
+ }
+ )
}
// Act.
- assertFailsWith<IllegalStateException> {
- rule.onRoot().performKeyPress(keyEvent(KeyCodeA, KeyDown))
- }
- }
+ rule.onRoot().performKeyPress(keyEvent(KeyCodeA, KeyDown))
- @Test
- fun focusTargetNotFocused_throwsException() {
- // Arrange.
- rule.setFocusableContent {
- Box(modifier = Modifier
- .focusTarget()
- .onKeyEvent { true })
- }
-
- // Act.
- assertFailsWith<IllegalStateException> {
- rule.onRoot().performKeyPress(keyEvent(KeyCodeA, KeyDown))
- }
+ // Assert.
+ rule.runOnIdle { assertThat(receivedKeyEvent).isNull() }
}
@Test
@@ -681,7 +680,10 @@
* The [KeyEvent] is usually created by the system. This function creates an instance of
* [KeyEvent] that can be used in tests.
*/
- private fun keyEvent(keycode: Int, keyEventType: KeyEventType): KeyEvent {
+ private fun keyEvent(
+ @Suppress("SameParameterValue") keycode: Int,
+ keyEventType: KeyEventType
+ ): KeyEvent {
val action = when (keyEventType) {
KeyDown -> ACTION_DOWN
KeyUp -> ACTION_UP
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
index 69c453e..4de0f4c 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
@@ -19,7 +19,6 @@
package androidx.compose.ui.layout
import androidx.activity.ComponentActivity
-import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector2D
@@ -53,7 +52,6 @@
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
-import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@@ -64,7 +62,6 @@
import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
@@ -103,7 +100,6 @@
import kotlin.math.roundToInt
import kotlin.random.Random
import kotlin.test.assertNotNull
-import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.junit.Ignore
import org.junit.Rule
@@ -2290,83 +2286,6 @@
}
}
- @Test
- fun forceMeasureLookaheadRootInParentsMeasurePass() {
- var show by mutableStateOf(false)
- var lookaheadOffset: Offset? = null
- var offset: Offset? = null
- rule.setContent {
- CompositionLocalProvider(LocalDensity provides Density(1f)) {
- // Mutate this state in measure
- Box(Modifier.fillMaxSize()) {
- val size by produceState(initialValue = 200) {
- delay(500)
- value = 600 - value
- }
- LazyColumn(Modifier.layout { measurable, _ ->
- // Mutate this state in measure. This state will later be used in descendant's
- // composition.
- show = size > 300
- measurable.measure(Constraints.fixed(size, size)).run {
- layout(width, height) { place(0, 0) }
- }
- }) {
- item {
- SubcomposeLayout(Modifier.fillMaxSize()) {
- val placeable = subcompose(Unit) {
- // read the value to force a recomposition
- Box(
- Modifier.requiredSize(222.dp)
- ) {
- AnimatedContent(show, Modifier.requiredSize(200.dp)) {
- if (it) {
- Row(
- Modifier
- .fillMaxSize()
- .layout { measurable, constraints ->
- val p = measurable.measure(constraints)
- layout(p.width, p.height) {
- coordinates
- ?.positionInRoot()
- .let {
- if (isLookingAhead) {
- lookaheadOffset = it
- } else {
- offset = it
- }
- }
- p.place(0, 0)
- }
- }) {}
- } else {
- Row(
- Modifier.size(10.dp)
- ) {}
- }
- }
- }
- }[0].measure(Constraints(0, 2000, 0, 2000))
- // Measure with the same constraints to ensure the child (i.e. Box)
- // gets no constraints change and hence starts forceMeasureSubtree
- // from there
- layout(700, 800) {
- placeable.place(0, 0)
- }
- }
- }
- }
- }
- }
- }
- rule.waitUntil(2000) {
- show
- }
- rule.waitForIdle()
-
- assertEquals(Offset(-150f, 0f), lookaheadOffset)
- assertEquals(Offset(-150f, 0f), offset)
- }
-
@OptIn(ExperimentalComposeUiApi::class)
@Test
fun lookaheadSizeTrackedWhenModifierChanges() {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/MeasureOnlyTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/MeasureOnlyTest.kt
index f95f492..c64186f 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/MeasureOnlyTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/MeasureOnlyTest.kt
@@ -18,14 +18,9 @@
import android.view.View
import android.view.View.MeasureSpec
import androidx.activity.ComponentActivity
-import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -34,19 +29,15 @@
import androidx.compose.ui.background
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.test.junit4.createAndroidComposeRule
-import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.constrainHeight
import androidx.compose.ui.unit.constrainWidth
-import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
-import kotlin.test.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -287,64 +278,4 @@
assertThat(view.height).isEqualTo(10)
}
}
-
- /**
- * When a descendant affects the root size, the root should resize when the
- * descendant changes size.
- */
- @Test
- fun remeasureRootWithLookahead() {
- var largeContent by mutableStateOf(false)
- var lookaheadSize: IntSize? = null
- rule.setContent {
- // Forces the box to change size, so that the containing AndroidView will get a
- // different set of measureSpec in `onMeasure`.
- Box(Modifier.size(if (largeContent) 200.dp else 100.dp)) {
- AndroidView(factory = { context ->
- ComposeView(context).apply {
- setContent {
- CompositionLocalProvider(LocalDensity provides Density(1f)) {
- Box(Modifier.requiredSize(300.dp)) {
- LazyColumn(Modifier.size(300.dp)) {
- item {
- AnimatedContent(largeContent, Modifier.fillMaxSize()) {
- if (it) {
- Box(
- Modifier
- .layout { measurable, constraints ->
- val placeable = measurable
- .measure(constraints)
- if (isLookingAhead)
- lookaheadSize = IntSize(
- placeable.width,
- placeable.height
- )
- layout(
- placeable.width,
- placeable.height
- ) {
- placeable.place(0, 0)
- }
- }
- .size(200.dp))
- } else {
- Box(Modifier.size(100.dp))
- }
- }
- }
- }
- }
- }
- }
- }
- })
- }
- }
- rule.runOnIdle {
- largeContent = true
- }
- rule.runOnIdle {
- assertEquals(lookaheadSize, IntSize(300, 200))
- }
- }
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt
index bec5a59..592ab66 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt
@@ -21,10 +21,10 @@
/**
* Remove the root modifier nodes as they are not relevant from the perspective of the tests.
- * There are 5 nodes:
- * KeyInputNode, FocusTargetNode, RotaryInputNode, SemanticsNode and DragAndDropNode.
+ * There are 5 nodes: FocusTargetNode, FocusPropertiesNode, KeyInputNode, RotaryInputNode,
+ * SemanticsNode and DragAndDropNode.
*/
-internal fun <T> List<T>.trimRootModifierNodes(): List<T> = dropLast(5)
+internal fun <T> List<T>.trimRootModifierNodes(): List<T> = dropLast(6)
internal fun Modifier.elementOf(node: Modifier.Node): Modifier {
return this.then(ElementOf { node })
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/WindowInfoCompositionLocalTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/WindowInfoCompositionLocalTest.kt
index 2a2d199..f539ca4 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/WindowInfoCompositionLocalTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/WindowInfoCompositionLocalTest.kt
@@ -18,9 +18,11 @@
import android.view.KeyEvent
import android.view.View
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.focusTarget
import androidx.compose.ui.focus.setFocusableContent
import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
@@ -53,7 +55,6 @@
rule.setContent {
BasicText("Main Window")
windowInfo = LocalWindowInfo.current
- @Suppress("DEPRECATION")
WindowFocusObserver { if (it) windowFocusGain.countDown() }
}
@@ -201,21 +202,16 @@
assertThat(mainWindowInfo.isWindowFocused).isTrue()
}
- @OptIn(ExperimentalComposeUiApi::class)
@Test
fun windowInfo_providesKeyModifiers() {
- lateinit var mainWindowInfo: WindowInfo
lateinit var ownerView: View
-
var keyModifiers = PointerKeyboardModifiers(0)
rule.setFocusableContent {
ownerView = LocalView.current
- mainWindowInfo = LocalWindowInfo.current
-
- keyModifiers = mainWindowInfo.keyboardModifiers
+ keyModifiers = LocalWindowInfo.current.keyboardModifiers
+ Box(Modifier.focusTarget())
}
-
assertThat(keyModifiers.packedValue).isEqualTo(0)
(rule as AndroidComposeTestRule<*, *>).runOnUiThread {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/focus/FocusInteropUtils.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/focus/FocusInteropUtils.android.kt
new file mode 100644
index 0000000..922d23c
--- /dev/null
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/focus/FocusInteropUtils.android.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.focus
+
+import android.view.ViewGroup
+import androidx.compose.ui.unit.LayoutDirection
+
+/**
+ * Converts an android focus direction to a compose [focus direction][FocusDirection].
+ */
+internal fun toFocusDirection(androidDirection: Int): FocusDirection? = when (androidDirection) {
+ ViewGroup.FOCUS_UP -> FocusDirection.Up
+ ViewGroup.FOCUS_DOWN -> FocusDirection.Down
+ ViewGroup.FOCUS_LEFT -> FocusDirection.Left
+ ViewGroup.FOCUS_RIGHT -> FocusDirection.Right
+ ViewGroup.FOCUS_FORWARD -> FocusDirection.Next
+ ViewGroup.FOCUS_BACKWARD -> FocusDirection.Previous
+ else -> null
+}
+
+/**
+ * Converts a compose [focus direction][FocusDirection] to an android focus direction.
+ */
+internal fun FocusDirection.toAndroidFocusDirection(): Int? = when (this) {
+ FocusDirection.Up -> ViewGroup.FOCUS_UP
+ FocusDirection.Down -> ViewGroup.FOCUS_DOWN
+ FocusDirection.Left -> ViewGroup.FOCUS_LEFT
+ FocusDirection.Right -> ViewGroup.FOCUS_RIGHT
+ FocusDirection.Next -> ViewGroup.FOCUS_FORWARD
+ FocusDirection.Previous -> ViewGroup.FOCUS_BACKWARD
+ else -> null
+ }
+
+/**
+ * Convert an Android layout direction to a compose [layout direction][LayoutDirection].
+ */
+internal fun toLayoutDirection(androidLayoutDirection: Int): LayoutDirection? {
+ return when (androidLayoutDirection) {
+ android.util.LayoutDirection.LTR -> LayoutDirection.Ltr
+ android.util.LayoutDirection.RTL -> LayoutDirection.Rtl
+ else -> null
+ }
+}
+
+/**
+ * focus search in the Android framework wraps around for 1D focus search, but not for 2D focus
+ * search. This is a helper function that can be used to determine whether we should wrap around.
+ */
+internal fun supportsWrapAroundFocus(androidDirection: Int): Boolean = when (androidDirection) {
+ ViewGroup.FOCUS_FORWARD, ViewGroup.FOCUS_BACKWARD -> true
+ else -> false
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index e2bd716..c47b2b9 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -22,7 +22,6 @@
import android.os.Build
import android.os.Looper
import android.os.SystemClock
-import android.util.Log
import android.util.LongSparseArray
import android.util.SparseArray
import android.view.DragEvent
@@ -80,6 +79,7 @@
import androidx.compose.ui.draganddrop.DragAndDropTransferData
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusDirection.Companion.Down
+import androidx.compose.ui.focus.FocusDirection.Companion.Enter
import androidx.compose.ui.focus.FocusDirection.Companion.Exit
import androidx.compose.ui.focus.FocusDirection.Companion.Left
import androidx.compose.ui.focus.FocusDirection.Companion.Next
@@ -88,6 +88,11 @@
import androidx.compose.ui.focus.FocusDirection.Companion.Up
import androidx.compose.ui.focus.FocusOwner
import androidx.compose.ui.focus.FocusOwnerImpl
+import androidx.compose.ui.focus.requestFocus
+import androidx.compose.ui.focus.supportsWrapAroundFocus
+import androidx.compose.ui.focus.toAndroidFocusDirection
+import androidx.compose.ui.focus.toFocusDirection
+import androidx.compose.ui.focus.toLayoutDirection
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Canvas
@@ -95,19 +100,21 @@
import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.setFrom
+import androidx.compose.ui.graphics.toAndroidRect
+import androidx.compose.ui.graphics.toComposeRect
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.PlatformHapticFeedback
import androidx.compose.ui.input.InputMode.Companion.Keyboard
import androidx.compose.ui.input.InputMode.Companion.Touch
import androidx.compose.ui.input.InputModeManager
import androidx.compose.ui.input.InputModeManagerImpl
+import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.Key.Companion.Back
import androidx.compose.ui.input.key.Key.Companion.DirectionCenter
import androidx.compose.ui.input.key.Key.Companion.DirectionDown
import androidx.compose.ui.input.key.Key.Companion.DirectionLeft
import androidx.compose.ui.input.key.Key.Companion.DirectionRight
import androidx.compose.ui.input.key.Key.Companion.DirectionUp
-import androidx.compose.ui.input.key.Key.Companion.Enter
import androidx.compose.ui.input.key.Key.Companion.Escape
import androidx.compose.ui.input.key.Key.Companion.NumPadEnter
import androidx.compose.ui.input.key.Key.Companion.PageDown
@@ -220,12 +227,15 @@
private val semanticsModifier = EmptySemanticsElement
- override val focusOwner: FocusOwner = FocusOwnerImpl { registerOnEndApplyChangesListener(it) }
-
- private val dragAndDropModifierOnDragListener = DragAndDropModifierOnDragListener(
- ::startDrag
+ override val focusOwner: FocusOwner = FocusOwnerImpl(
+ onRequestApplyChangesListener = ::registerOnEndApplyChangesListener,
+ onRequestFocusForOwner = ::onRequestFocusForOwner,
+ onClearFocusForOwner = ::onClearFocusForOwner,
+ layoutDirection = ::layoutDirection
)
+ private val dragAndDropModifierOnDragListener = DragAndDropModifierOnDragListener(::startDrag)
+
override val dragAndDropManager: DragAndDropManager = dragAndDropModifierOnDragListener
private val _windowInfo: WindowInfoImpl = WindowInfoImpl()
@@ -238,8 +248,10 @@
val focusDirection = getFocusDirection(it)
if (focusDirection == null || it.type != KeyDown) return@onKeyEvent false
- // Consume the key event if we moved focus.
- focusOwner.moveFocus(focusDirection)
+ // Consume the key event if we moved focus or if focus search or requestFocus is cancelled.
+ focusOwner.focusSearch(focusDirection, null) { focusTargetNode ->
+ focusTargetNode.requestFocus(focusDirection) ?: true
+ } ?: true
}
private val rotaryInputModifier = Modifier.onRotaryScrollEvent {
@@ -256,8 +268,8 @@
it.modifier = Modifier
.then(semanticsModifier)
.then(rotaryInputModifier)
- .then(focusOwner.modifier)
.then(keyInputModifier)
+ .then(focusOwner.modifier)
.then(dragAndDropModifierOnDragListener.modifier)
}
@@ -460,7 +472,11 @@
// Backed by mutableStateOf so that the ambient provider recomposes when it changes
override var layoutDirection by mutableStateOf(
- context.resources.configuration.localeLayoutDirection
+ // We don't use the attached View's layout direction here since that layout direction may not
+ // be resolved since composables may be composed without attaching to the RootViewImpl.
+ // In Jetpack Compose, use the locale layout direction (i.e. layoutDirection came from
+ // configuration) as a default layout direction.
+ toLayoutDirection(context.resources.configuration.layoutDirection) ?: LayoutDirection.Ltr
)
private set
@@ -646,14 +662,65 @@
showLayoutBounds = getIsShowingLayoutBounds()
}
+ override fun focusSearch(direction: Int): View? = if (focusOwner.rootState.hasFocus) {
+ // When the compose hierarchy is focused, it intercepts the key events that trigger focus
+ // search. So focus search should never find a compose hierarchy that has focus.
+ //
+ // However there is a case where we don't consume the key events. When all the components
+ // have been visited, and/or focus can't be moved within the compose hierarchy, the key
+ // events are returned to the framework so it can perform a search among other views. This
+ // focus search could land back on this view.
+ //
+ // Ideally just returning "this" to focus search should cause it to call requestFocus with
+ // the previously focused rect, and we would find the next item. However the framework does
+ // not call request focus on this view because it already has focus.
+ //
+ // To fix this issue, we manually clear focus and return this. The view with default focus
+ // might be assigned focus for a while, but requestFocus will be called which will then
+ // transfer focus to this view.
+ //
+ // There is an additional special case here. Focus wraps around only for 1D focus search
+ // and not for 2D focus search. So we clear focus only if focus search was triggered by
+ // a 1D focus search.
+ if (supportsWrapAroundFocus(direction)) clearFocus()
+ this
+ } else {
+ // TODO(b/261190892) run a mixed focus search that searches between composables and
+ // child views and chooses an appropriate result.
+ // We give the embedded children a chance to take focus before the compose view.
+ super.focusSearch(direction) ?: this
+ }
+
+ override fun requestFocus(direction: Int, previouslyFocusedRect: Rect?): Boolean {
+ if (focusOwner.rootState.hasFocus) return true
+ return focusOwner.takeFocus(
+ focusDirection = toFocusDirection(direction) ?: Enter,
+ previouslyFocusedRect = previouslyFocusedRect?.toComposeRect()
+ )
+ }
+
+ private fun onRequestFocusForOwner(
+ focusDirection: FocusDirection?,
+ previouslyFocusedRect: androidx.compose.ui.geometry.Rect?
+ ): Boolean {
+ return super.requestFocus(
+ focusDirection?.toAndroidFocusDirection() ?: FOCUS_DOWN,
+ @Suppress("DEPRECATION")
+ previouslyFocusedRect?.toAndroidRect()
+ )
+ }
+
+ private fun onClearFocusForOwner() {
+ if (isFocused || hasFocus()) {
+ super.clearFocus()
+ }
+ }
+
override fun onFocusChanged(gainFocus: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect)
- Log.d(FocusTag, "Owner FocusChanged($gainFocus)")
- focusOwner.focusTransactionManager.withExistingTransaction(
- onCancelled = { if (gainFocus) clearFocus() else requestFocus() }
- ) {
- if (gainFocus) focusOwner.takeFocus() else focusOwner.releaseFocus()
- }
+ if (!gainFocus) {
+ focusOwner.releaseFocus()
+ }
}
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
@@ -1227,7 +1294,7 @@
// focus.
DirectionUp, PageUp -> Up
DirectionDown, PageDown -> Down
- DirectionCenter, Enter, NumPadEnter -> FocusDirection.Enter
+ DirectionCenter, Key.Enter, NumPadEnter -> Enter
Back, Escape -> Exit
else -> null
}
@@ -1808,10 +1875,7 @@
// If we get such a call, don't try to write to a property delegate
// that hasn't been initialized yet.
if (superclassInitComplete) {
- layoutDirectionFromInt(layoutDirection).let {
- this.layoutDirection = it
- focusOwner.layoutDirection = it
- }
+ this.layoutDirection = toLayoutDirection(layoutDirection) ?: LayoutDirection.Ltr
}
}
@@ -1975,7 +2039,6 @@
override fun shouldDelayChildPressedState(): Boolean = false
companion object {
- private const val FocusTag = "Compose Focus"
private const val MaximumLayerCacheSize = 10
private var systemPropertiesClass: Class<*>? = null
private var getBooleanMethod: Method? = null
@@ -2034,25 +2097,6 @@
}
/**
- * Return the layout direction set by the [Locale][java.util.Locale].
- *
- * A convenience getter that translates [Configuration.getLayoutDirection] result into
- * [LayoutDirection] instance.
- */
-internal val Configuration.localeLayoutDirection: LayoutDirection
- // We don't use the attached View's layout direction here since that layout direction may not
- // be resolved since the composables may be composed without attaching to the RootViewImpl.
- // In Jetpack Compose, use the locale layout direction (i.e. layoutDirection came from
- // configuration) as a default layout direction.
- get() = layoutDirectionFromInt(layoutDirection)
-
-private fun layoutDirectionFromInt(layoutDirection: Int): LayoutDirection = when (layoutDirection) {
- android.util.LayoutDirection.LTR -> LayoutDirection.Ltr
- android.util.LayoutDirection.RTL -> LayoutDirection.Rtl
- else -> LayoutDirection.Ltr
-}
-
-/**
* These classes are here to ensure that the classes that use this API will get verified and can be
* AOT compiled. It is expected that this class will soft-fail verification, but the classes
* which use this method will pass.
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 a1df9ce..bce1f6e 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
@@ -49,9 +49,6 @@
import androidx.collection.ArrayMap
import androidx.collection.ArraySet
import androidx.collection.SparseArrayCompat
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.R
import androidx.compose.ui.geometry.Offset
@@ -314,7 +311,7 @@
// traversal with granularity switches to the next node
private var previousTraversedNode: Int? = null
private val subtreeChangedLayoutNodes = ArraySet<LayoutNode>()
- private val boundsUpdateChannel = Channel<Unit>(Channel.CONFLATED)
+ private val boundsUpdateChannel = Channel<Unit>(1)
private var currentSemanticsNodesInvalidated = true
@VisibleForTesting
internal var contentCaptureForceEnabledForTesting = false
@@ -355,8 +352,10 @@
internal var idToBeforeMap = HashMap<Int, Int>()
internal var idToAfterMap = HashMap<Int, Int>()
internal val ExtraDataTestTraversalBeforeVal =
+ @Suppress("SpellCheckingInspection")
"android.view.accessibility.extra.EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL"
internal val ExtraDataTestTraversalAfterVal =
+ @Suppress("SpellCheckingInspection")
"android.view.accessibility.extra.EXTRA_DATA_TEST_TRAVERSALAFTER_VAL"
private val urlSpanCache = URLSpanCache()
@@ -1847,7 +1846,11 @@
AccessibilityNodeInfoCompat.ACTION_CLEAR_FOCUS -> {
return if (node.unmergedConfig.getOrNull(SemanticsProperties.Focused) == true) {
- view.focusOwner.clearFocus()
+ view.focusOwner.clearFocus(
+ force = false,
+ refreshFocusEvents = true,
+ clearOwnerFocus = true
+ )
true
} else {
false
@@ -2219,7 +2222,6 @@
* recent layout changes and sends events to the accessibility and content capture framework in
* batches separated by a 100ms delay.
*/
- @OptIn(ExperimentalComposeUiApi::class)
internal suspend fun boundsUpdatesEventLoop() {
try {
val subtreeChangedSemanticsNodesIds = ArraySet<Int>()
@@ -3793,4 +3795,4 @@
@get:ExperimentalComposeUiApi
@set:ExperimentalComposeUiApi
@ExperimentalComposeUiApi
-var DisableContentCapture: Boolean by mutableStateOf(false)
+var DisableContentCapture: Boolean = false
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusInvalidationManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusInvalidationManager.kt
index 9843404f..c80645c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusInvalidationManager.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusInvalidationManager.kt
@@ -26,7 +26,8 @@
* onApplyChangesListener when nodes are scheduled for invalidation.
*/
internal class FocusInvalidationManager(
- private val onRequestApplyChangesListener: (() -> Unit) -> Unit
+ private val onRequestApplyChangesListener: (() -> Unit) -> Unit,
+ private val invalidateOwnerFocusState: () -> Unit
) {
private var focusTargetNodes = mutableSetOf<FocusTargetNode>()
private var focusEventNodes = mutableSetOf<FocusEventModifierNode>()
@@ -138,6 +139,8 @@
focusTargetNodes.clear()
focusTargetsWithInvalidatedFocusEvents.clear()
+ invalidateOwnerFocusState()
+
check(focusPropertiesNodes.isEmpty()) { "Unprocessed FocusProperties nodes" }
check(focusEventNodes.isEmpty()) { "Unprocessed FocusEvent nodes" }
check(focusTargetNodes.isEmpty()) { "Unprocessed FocusTarget nodes" }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwner.kt
index 39a0722..f50a0ef 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwner.kt
@@ -20,7 +20,6 @@
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.rotary.RotaryScrollEvent
-import androidx.compose.ui.unit.LayoutDirection
/**
* The focus owner provides some internal APIs that are not exposed by focus manager.
@@ -34,11 +33,6 @@
val modifier: Modifier
/**
- * The owner sets the layoutDirection that is then used during focus search.
- */
- var layoutDirection: LayoutDirection
-
- /**
* This manager provides a way to ensure that only one focus transaction is running at a time.
* We use this to prevent re-entrant focus operations. Starting a new transaction automatically
* cancels the previous transaction and reverts any focus state changes made during that
@@ -47,12 +41,52 @@
val focusTransactionManager: FocusTransactionManager
/**
+ * This function is called to ask the owner to request focus from the framework.
+ * eg. If a composable calls requestFocus and the root view does not have focus, this function
+ * can be used to request focus for the view.
+ *
+ * @param focusDirection If this focus request was triggered by a call to moveFocus or using the
+ * keyboard, provide the owner with the direction of focus change.
+ *
+ * @param previouslyFocusedRect The bounds of the currently focused item.
+ *
+ * @return true if the owner successfully requested focus from the framework. False otherwise.
+ */
+ fun requestFocusForOwner(focusDirection: FocusDirection?, previouslyFocusedRect: Rect?): Boolean
+
+ /**
+ * This function searches the compose hierarchy for the next focus target based on the supplied
+ * parameters.
+ *
+ * @param focusDirection the direction to search for the focus target.
+ *
+ * @param focusedRect the bounds of the currently focused item.
+ *
+ * @param onFound This lambda is called with the focus search result.
+ *
+ * @return true, if a suitable [FocusTargetNode] was found, false if no [FocusTargetNode] was
+ * found, and null if the focus search was cancelled.
+ */
+ fun focusSearch(
+ focusDirection: FocusDirection,
+ focusedRect: Rect?,
+ onFound: (FocusTargetNode) -> Boolean
+ ): Boolean?
+
+ /**
* The [Owner][androidx.compose.ui.node.Owner] calls this function when it gains focus. This
* informs the [focus manager][FocusOwnerImpl] that the
* [Owner][androidx.compose.ui.node.Owner] gained focus, and that it should propagate this
* focus to one of the focus modifiers in the component hierarchy.
+ *
+ * @param focusDirection the direction to search for the focus target.
+ *
+ * @param previouslyFocusedRect the bounds of the currently focused item.
+ *
+ * @return true, if a suitable [FocusTargetNode] was found and it took focus, false if no
+ * [FocusTargetNode] was found or if the focus search was cancelled.
*/
- fun takeFocus()
+ fun takeFocus(focusDirection: FocusDirection, previouslyFocusedRect: Rect?): Boolean
/**
* The [Owner][androidx.compose.ui.node.Owner] calls this function when it loses focus. This
@@ -71,10 +105,13 @@
* @param refreshFocusEvents: Whether we should send an event up the hierarchy to update
* the associated onFocusEvent nodes.
*
+ * @param clearOwnerFocus whether we should also clear focus from the owner. This is usually
+ * true, unless focus is being temporarily cleared (eg. to implement focus wrapping).
+ *
* This could be used to clear focus when a user clicks on empty space outside a focusable
* component.
*/
- fun clearFocus(force: Boolean, refreshFocusEvents: Boolean)
+ fun clearFocus(force: Boolean, refreshFocusEvents: Boolean, clearOwnerFocus: Boolean)
/**
* Searches for the currently focused item, and returns its coordinates as a rect.
@@ -110,4 +147,9 @@
* Schedule a FocusProperties node to be invalidated after onApplyChanges.
*/
fun scheduleInvalidation(node: FocusPropertiesModifierNode)
+
+ /**
+ * The focus state of the root focus node.
+ */
+ val rootState: FocusState
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
index 0a7515b..c9e4fcf 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
@@ -28,10 +28,6 @@
import androidx.compose.ui.focus.FocusDirection.Companion.Previous
import androidx.compose.ui.focus.FocusRequester.Companion.Cancel
import androidx.compose.ui.focus.FocusRequester.Companion.Default
-import androidx.compose.ui.focus.FocusStateImpl.Active
-import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
-import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Inactive
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.KeyEventType.Companion.KeyDown
@@ -56,11 +52,20 @@
* The focus manager is used by different [Owner][androidx.compose.ui.node.Owner] implementations
* to control focus.
*/
-internal class FocusOwnerImpl(onRequestApplyChangesListener: (() -> Unit) -> Unit) : FocusOwner {
+internal class FocusOwnerImpl(
+ onRequestApplyChangesListener: (() -> Unit) -> Unit,
+ private val onRequestFocusForOwner:
+ (focusDirection: FocusDirection?, previouslyFocusedRect: Rect?) -> Boolean,
+ private val onClearFocusForOwner: () -> Unit,
+ private val layoutDirection: (() -> LayoutDirection)
+) : FocusOwner {
internal var rootFocusNode = FocusTargetNode()
- private val focusInvalidationManager = FocusInvalidationManager(onRequestApplyChangesListener)
+ private val focusInvalidationManager = FocusInvalidationManager(
+ onRequestApplyChangesListener,
+ ::invalidateOwnerFocusState
+ )
override val focusTransactionManager: FocusTransactionManager = FocusTransactionManager()
@@ -69,21 +74,38 @@
* list that contains the modifiers required by the focus system. (Eg, a root focus modifier).
*/
// TODO(b/168831247): return an empty Modifier when there are no focusable children.
- override val modifier: Modifier = object : ModifierNodeElement<FocusTargetNode>() {
- override fun create() = rootFocusNode
+ override val modifier: Modifier = Modifier
+ // The root focus target is not focusable, and acts like a focus group.
+ // We could save an allocation here by making FocusTargetNode implement
+ // FocusPropertiesModifierNode but to do that we would have to allocate
+ // a focus properties object. This way only the root node has this extra allocation.
+ .focusProperties { canFocus = false }
+ .then(
+ object : ModifierNodeElement<FocusTargetNode>() {
+ override fun create() = rootFocusNode
+ override fun update(node: FocusTargetNode) {}
+ override fun InspectorInfo.inspectableProperties() { name = "RootFocusTarget" }
+ override fun hashCode(): Int = rootFocusNode.hashCode()
+ override fun equals(other: Any?) = other === this
+ }
+ )
- override fun update(node: FocusTargetNode) {}
-
- override fun InspectorInfo.inspectableProperties() {
- name = "RootFocusTarget"
- }
-
- override fun hashCode(): Int = rootFocusNode.hashCode()
-
- override fun equals(other: Any?) = other === this
- }
-
- override lateinit var layoutDirection: LayoutDirection
+ /**
+ * This function is called to ask the owner to request focus from the framework.
+ * eg. If a composable calls requestFocus and the root view does not have focus, this function
+ * can be used to request focus for the view.
+ *
+ * @param focusDirection If this focus request was triggered by a call to moveFocus or using the
+ * keyboard, provide the owner with the direction of focus change.
+ *
+ * @param previouslyFocusedRect The bounds of the currently focused item.
+ *
+ * @return true if the owner successfully requested focus from the framework. False otherwise.
+ */
+ override fun requestFocusForOwner(
+ focusDirection: FocusDirection?,
+ previouslyFocusedRect: Rect?
+ ): Boolean = onRequestFocusForOwner(focusDirection, previouslyFocusedRect)
/**
* Keeps track of which keys have received DOWN events without UP events – i.e. which keys are
@@ -99,14 +121,19 @@
* informs the [focus manager][FocusOwnerImpl] that the
* [Owner][androidx.compose.ui.node.Owner] gained focus, and that it should propagate this
* focus to one of the focus modifiers in the component hierarchy.
+ *
+ * @param focusDirection the direction to search for the focus target.
+ *
+ * @param previouslyFocusedRect the bounds of the currently focused item.
+ *
+ * @return true, if a suitable [FocusTargetNode] was found and it took focus, false if no
+ * [FocusTargetNode] was found or if the focus search was cancelled.
*/
- override fun takeFocus() {
- // If the focus state is not Inactive, it indicates that the focus state is already
- // set (possibly by dispatchWindowFocusChanged). So we don't update the state.
- if (rootFocusNode.focusState == Inactive) {
- rootFocusNode.focusState = Active
- // TODO(b/152535715): propagate focus to children based on child focusability.
- // moveFocus(FocusDirection.Enter)
+ override fun takeFocus(focusDirection: FocusDirection, previouslyFocusedRect: Rect?): Boolean {
+ return focusTransactionManager.withExistingTransaction {
+ focusSearch(focusDirection, previouslyFocusedRect) {
+ it.requestFocus(focusDirection) ?: false
+ } ?: false
}
}
@@ -117,7 +144,9 @@
* all the focus modifiers in the component hierarchy.
*/
override fun releaseFocus() {
- rootFocusNode.clearFocus(forced = true, refreshFocusEvents = true)
+ focusTransactionManager.withExistingTransaction {
+ rootFocusNode.clearFocus(forced = true, refreshFocusEvents = true)
+ }
}
/**
@@ -130,30 +159,26 @@
* component.
*/
override fun clearFocus(force: Boolean) {
- clearFocus(force, refreshFocusEvents = true)
+ clearFocus(force, refreshFocusEvents = true, clearOwnerFocus = true)
}
@OptIn(ExperimentalComposeUiApi::class)
- override fun clearFocus(force: Boolean, refreshFocusEvents: Boolean) {
- focusTransactionManager.withNewTransaction {
+ override fun clearFocus(force: Boolean, refreshFocusEvents: Boolean, clearOwnerFocus: Boolean) {
+ val clearedFocusSuccessfully = focusTransactionManager.withNewTransaction(
+ onCancelled = { return@withNewTransaction }
+ ) {
// Don't clear focus if an item on the focused path has a custom exit specified.
if (!force) {
when (rootFocusNode.performCustomClearFocus(Exit)) {
- Redirected, Cancelled, RedirectCancelled -> return
+ Redirected, Cancelled, RedirectCancelled -> return@withNewTransaction false
None -> { /* Do nothing. */ }
}
}
+ return@withNewTransaction rootFocusNode.clearFocus(force, refreshFocusEvents)
+ }
- // If this hierarchy had focus before clearing it, it indicates that the host view has
- // focus. So after clearing focus within the compose hierarchy, we should restore focus
- // to the root focus modifier to maintain consistency with the host view.
- val rootInitialState = rootFocusNode.focusState
- if (rootFocusNode.clearFocus(force, refreshFocusEvents)) {
- rootFocusNode.focusState = when (rootInitialState) {
- Active, ActiveParent, Captured -> Active
- Inactive -> Inactive
- }
- }
+ if (clearedFocusSuccessfully && clearOwnerFocus) {
+ onClearFocusForOwner.invoke()
}
}
@@ -162,38 +187,45 @@
*
* @return true if focus was moved successfully. false if the focused item is unchanged.
*/
- @OptIn(ExperimentalComposeUiApi::class)
override fun moveFocus(focusDirection: FocusDirection): Boolean {
+ // moveFocus is an API that was added to compose, but isn't available in the classic view
+ // system, so for now we only search among compose items and don't support moveFocus for
+ // interop scenarios.
+ val movedFocus = focusSearch(focusDirection, null) {
+ it.requestFocus(focusDirection) ?: false
+ } ?: return false
- // If there is no active node in this sub-hierarchy, we can't move focus.
- val source = rootFocusNode.findActiveFocusNode() ?: return false
+ // To wrap focus around, we clear focus and request initial focus.
+ if (!movedFocus && focusDirection.supportsWrapAroundFocus()) {
+ clearFocus(force = false, refreshFocusEvents = true, clearOwnerFocus = false)
+ return takeFocus(focusDirection, previouslyFocusedRect = null)
+ }
- // Check if a custom focus traversal order is specified.
- source.customFocusSearch(focusDirection, layoutDirection).also {
- if (it !== Default) {
- return it !== Cancel && it.focus()
+ return movedFocus
+ }
+
+ override fun focusSearch(
+ focusDirection: FocusDirection,
+ focusedRect: Rect?,
+ onFound: (FocusTargetNode) -> Boolean
+ ): Boolean? {
+ val source = rootFocusNode.findActiveFocusNode()?.also {
+ // Check if a custom focus traversal order is specified.
+ when (val customDestination = it.customFocusSearch(focusDirection, layoutDirection())) {
+ @OptIn(ExperimentalComposeUiApi::class)
+ Cancel -> return null
+ Default -> { /* Do Nothing */ }
+ else -> return customDestination.findFocusTargetNode(onFound)
}
}
- var isCancelled = false
- val foundNextItem =
- rootFocusNode.focusSearch(focusDirection, layoutDirection) { destination ->
- if (destination == source) return@focusSearch false
- checkNotNull(destination.nearestAncestor(Nodes.FocusTarget)) {
- "Focus search landed at the root."
- }
- // If we found a potential next item, move focus to it.
- // Returning true ends focus search.
- focusTransactionManager.withNewTransaction {
- when (destination.performCustomRequestFocus(focusDirection)) {
- Redirected -> true
- Cancelled, RedirectCancelled -> { isCancelled = true; true }
- None -> destination.performRequestFocus()
- }
- }
+ return rootFocusNode.focusSearch(focusDirection, layoutDirection(), focusedRect) {
+ when (it) {
+ source -> false
+ rootFocusNode -> error("Focus search landed at the root.")
+ else -> onFound(it)
}
- // If we didn't find a potential next item, try to wrap around.
- return !isCancelled && (foundNextItem || wrapAroundFocus(focusDirection))
+ }
}
/**
@@ -207,11 +239,9 @@
if (!validateKeyEvent(keyEvent)) return false
val activeFocusTarget = rootFocusNode.findActiveFocusNode()
- checkNotNull(activeFocusTarget) {
- "Event can't be processed because we do not have an active focus target."
- }
- val focusedKeyInputNode = activeFocusTarget.lastLocalKeyInputNode()
- ?: activeFocusTarget.nearestAncestor(Nodes.KeyInput)?.node
+ val focusedKeyInputNode = activeFocusTarget?.lastLocalKeyInputNode()
+ ?: activeFocusTarget?.nearestAncestor(Nodes.KeyInput)?.node
+ ?: rootFocusNode.nearestAncestor(Nodes.KeyInput)?.node
focusedKeyInputNode?.traverseAncestors(
type = Nodes.KeyInput,
@@ -270,6 +300,18 @@
focusInvalidationManager.scheduleInvalidation(node)
}
+ /**
+ * At the end of the invalidations, we need to ensure that the focus system is in a valid state.
+ */
+ private fun invalidateOwnerFocusState() {
+ // If an active item is removed, we currently clear focus from the hierarchy. We don't
+ // clear focus from the root because that could cause initial focus logic to be re-run.
+ // Now that all the invalidations are complete, we run owner.clearFocus() if needed.
+ if (rootFocusNode.focusState == FocusStateImpl.Inactive) {
+ onClearFocusForOwner()
+ }
+ }
+
private inline fun <reified T : DelegatableNode> DelegatableNode.traverseAncestors(
type: NodeKind<T>,
onPreVisit: (T) -> Unit,
@@ -289,6 +331,9 @@
return rootFocusNode.findActiveFocusNode()?.focusRect()
}
+ override val rootState: FocusState
+ get() = rootFocusNode.focusState
+
private fun DelegatableNode.lastLocalKeyInputNode(): Modifier.Node? {
var focusedKeyInputNode: Modifier.Node? = null
visitLocalDescendants(Nodes.FocusTarget or Nodes.KeyInput) { modifierNode ->
@@ -299,26 +344,13 @@
return focusedKeyInputNode
}
- // TODO(b/144116848): This is a hack to make Next/Previous wrap around. This must be
- // replaced by code that sends the move request back to the view system. The view system
- // will then pass focus to other views, and ultimately return back to this compose view.
- private fun wrapAroundFocus(focusDirection: FocusDirection): Boolean {
- // Wrap is not supported when this sub-hierarchy doesn't have focus.
- if (!rootFocusNode.focusState.hasFocus || rootFocusNode.focusState.isFocused) return false
-
- // Next and Previous wraps around.
- when (focusDirection) {
- Next, Previous -> {
- // Clear Focus to send focus the root node.
- clearFocus(force = false)
- if (!rootFocusNode.focusState.isFocused) return false
-
- // Wrap around by calling moveFocus after the root gains focus.
- return moveFocus(focusDirection)
- }
- // We only wrap-around for 1D Focus search.
- else -> return false
- }
+ /**
+ * focus search in the Android framework wraps around for 1D focus search, but not for 2D focus
+ * search. This is a helper function that can be used to determine whether we should wrap around.
+ */
+ private fun FocusDirection.supportsWrapAroundFocus(): Boolean = when (this) {
+ Next, Previous -> true
+ else -> false
}
// TODO(b/307580000) Factor this out into a class to manage key inputs.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt
index 97d7357..8c11a94 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt
@@ -24,7 +24,6 @@
import androidx.compose.ui.node.Nodes
import androidx.compose.ui.node.visitChildren
-@Suppress("ConstPropertyName")
private const val FocusRequesterNotInitialized = """
FocusRequester is not initialized. Here are some possible fixes:
@@ -34,7 +33,6 @@
response to some event. Eg Modifier.clickable { focusRequester.requestFocus() }
"""
-@Suppress("ConstPropertyName")
private const val InvalidFocusRequesterInvocation = """
Please check whether the focusRequester is FocusRequester.Cancel or FocusRequester.Default
before invoking any functions on the focusRequester.
@@ -66,16 +64,15 @@
}
// TODO(b/245755256): Consider making this API Public.
- internal fun focus(): Boolean {
+ internal fun focus(): Boolean = findFocusTargetNode { it.requestFocus() }
+
+ internal fun findFocusTargetNode(onFound: (FocusTargetNode) -> Boolean): Boolean {
@OptIn(ExperimentalComposeUiApi::class)
return findFocusTarget { focusTarget ->
- val focusProperties = focusTarget.fetchFocusProperties()
- if (focusProperties.canFocus) {
- focusTarget.requestFocus()
+ if (focusTarget.fetchFocusProperties().canFocus) {
+ onFound(focusTarget)
} else {
- focusTarget.findChildCorrespondingToFocusEnter(Enter) {
- it.requestFocus()
- }
+ focusTarget.findChildCorrespondingToFocusEnter(Enter, onFound)
}
}
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequesterModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequesterModifierNode.kt
index 4d95db3..cb36d0e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequesterModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequesterModifierNode.kt
@@ -38,8 +38,7 @@
@OptIn(ExperimentalComposeUiApi::class)
fun FocusRequesterModifierNode.requestFocus(): Boolean {
visitSelfAndChildren(Nodes.FocusTarget) { focusTarget ->
- val focusProperties = focusTarget.fetchFocusProperties()
- return if (focusProperties.canFocus) {
+ return if (focusTarget.fetchFocusProperties().canFocus) {
focusTarget.requestFocus()
} else {
focusTarget.findChildCorrespondingToFocusEnter(Enter) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetNode.kt
index ea6ba20..489b51e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetNode.kt
@@ -84,13 +84,22 @@
// Clear focus from the current FocusTarget.
// This currently clears focus from the entire hierarchy, but we can change the
// implementation so that focus is sent to the immediate focus parent.
- Active, Captured -> requireOwner().focusOwner.clearFocus(force = true)
-
- // If an ActiveParent is deactivated, the entire subtree containing focus is
- // deactivated, which means the Active node will also receive an onReset() call.
- // This triggers a clearFocus call, which will notify all the focus event nodes
- // associated with this FocusTargetNode.
- ActiveParent, Inactive -> {}
+ Active, Captured -> {
+ requireOwner().focusOwner.clearFocus(
+ force = true,
+ refreshFocusEvents = true,
+ clearOwnerFocus = false
+ )
+ // We don't clear the owner's focus yet, because this could trigger an initial
+ // focus scenario after the focus is cleared. Instead, we schedule invalidation
+ // after onApplyChanges. The FocusInvalidationManager contains the invalidation
+ // logic and calls clearFocus() on the owner after all the nodes in the hierarchy
+ // are invalidated.
+ invalidateFocusTarget()
+ }
+ // This node might be reused, so reset the state to Inactive.
+ ActiveParent -> requireTransactionManager().withNewTransaction { focusState = Inactive }
+ Inactive -> {}
}
// This node might be reused, so we reset its state.
committedFocusState = null
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTransactions.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTransactions.kt
index 88cc1ee..d585d28 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTransactions.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTransactions.kt
@@ -30,6 +30,7 @@
import androidx.compose.ui.node.Nodes.FocusTarget
import androidx.compose.ui.node.nearestAncestor
import androidx.compose.ui.node.observeReads
+import androidx.compose.ui.node.requireOwner
/**
* Request focus for this node.
@@ -39,12 +40,14 @@
* [FocusNode][FocusTargetNode]'s parent [FocusNode][FocusTargetNode].
*/
@OptIn(ExperimentalComposeUiApi::class)
-internal fun FocusTargetNode.requestFocus(): Boolean {
+internal fun FocusTargetNode.requestFocus(): Boolean = requestFocus(Enter) ?: false
+
+internal fun FocusTargetNode.requestFocus(focusDirection: FocusDirection): Boolean? {
return requireTransactionManager().withNewTransaction {
- when (performCustomRequestFocus(Enter)) {
+ when (performCustomRequestFocus(focusDirection)) {
None -> performRequestFocus()
Redirected -> true
- Cancelled, RedirectCancelled -> false
+ Cancelled, RedirectCancelled -> null
}
}
}
@@ -244,7 +247,7 @@
}
private fun FocusTargetNode.requestFocusForOwner(): Boolean {
- return coordinator?.layoutNode?.owner?.requestFocus() ?: error("Owner not initialized.")
+ return requireOwner().focusOwner.requestFocusForOwner(null, null)
}
private fun FocusTargetNode.requireActiveChild(): FocusTargetNode {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt
index e0f38c5..7660e54 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt
@@ -91,6 +91,7 @@
*
* @param focusDirection The requested direction to move focus.
* @param layoutDirection Whether the layout is RTL or LTR.
+ * @param previouslyFocusedRect The bounds of the previously focused item.
* @param onFound This lambda is invoked if focus search finds the next focus node.
* @return if no focus node is found, we return false. If we receive a cancel, we return null
* otherwise we return the result of [onFound].
@@ -99,16 +100,19 @@
internal fun FocusTargetNode.focusSearch(
focusDirection: FocusDirection,
layoutDirection: LayoutDirection,
+ previouslyFocusedRect: Rect?,
onFound: (FocusTargetNode) -> Boolean
-): Boolean {
+): Boolean? {
return when (focusDirection) {
Next, Previous -> oneDimensionalFocusSearch(focusDirection, onFound)
- Left, Right, Up, Down -> twoDimensionalFocusSearch(focusDirection, onFound) ?: false
+ Left, Right, Up, Down ->
+ twoDimensionalFocusSearch(focusDirection, previouslyFocusedRect, onFound)
@OptIn(ExperimentalComposeUiApi::class)
Enter -> {
// we search among the children of the active item.
val direction = when (layoutDirection) { Rtl -> Left; Ltr -> Right }
- findActiveFocusNode()?.twoDimensionalFocusSearch(direction, onFound) ?: false
+ findActiveFocusNode()
+ ?.twoDimensionalFocusSearch(direction, previouslyFocusedRect, onFound)
}
@OptIn(ExperimentalComposeUiApi::class)
Exit -> findActiveFocusNode()?.findNonDeactivatedParent().let {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt
index 987cc9a..d883a49 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt
@@ -51,10 +51,17 @@
*/
internal fun FocusTargetNode.twoDimensionalFocusSearch(
direction: FocusDirection,
+ previouslyFocusedRect: Rect?,
onFound: (FocusTargetNode) -> Boolean
): Boolean? {
when (focusState) {
- Inactive -> return if (fetchFocusProperties().canFocus) onFound.invoke(this) else false
+ Inactive -> return if (fetchFocusProperties().canFocus) {
+ onFound.invoke(this)
+ } else if (previouslyFocusedRect == null) {
+ findChildCorrespondingToFocusEnter(direction, onFound)
+ } else {
+ searchChildren(previouslyFocusedRect, direction, onFound)
+ }
ActiveParent -> {
val focusedChild = activeChild ?: error(NoActiveChild)
// For 2D focus search we only search among siblings. You have to use DPad Center or
@@ -66,15 +73,20 @@
ActiveParent -> {
// If the focusedChild is an intermediate parent, we search among its children.
- val found = focusedChild.twoDimensionalFocusSearch(direction, onFound)
+ val found = focusedChild
+ .twoDimensionalFocusSearch(direction, previouslyFocusedRect, onFound)
if (found != false) return found
// We search among the siblings of the parent.
- return generateAndSearchChildren(focusedChild.activeNode(), direction, onFound)
+ return generateAndSearchChildren(
+ focusedChild.activeNode().focusRect(),
+ direction,
+ onFound
+ )
}
// Search for the next eligible sibling.
Active, Captured ->
- return generateAndSearchChildren(focusedChild, direction, onFound)
+ return generateAndSearchChildren(focusedChild.focusRect(), direction, onFound)
Inactive -> error(NoActiveChild)
}
}
@@ -132,7 +144,7 @@
// Search among your children for the next child.
// If the next child is not found, generate more children by requesting a beyondBoundsLayout.
private fun FocusTargetNode.generateAndSearchChildren(
- focusedItem: FocusTargetNode,
+ focusedItem: Rect,
direction: FocusDirection,
onFound: (FocusTargetNode) -> Boolean
): Boolean {
@@ -152,7 +164,7 @@
}
private fun FocusTargetNode.searchChildren(
- focusedItem: FocusTargetNode,
+ focusedItem: Rect,
direction: FocusDirection,
onFound: (FocusTargetNode) -> Boolean
): Boolean {
@@ -162,7 +174,7 @@
}
}
while (children.isNotEmpty()) {
- val nextItem = children.findBestCandidate(focusedItem.focusRect(), direction)
+ val nextItem = children.findBestCandidate(focusedItem, direction)
?: return false
// If the result is not deactivated, this is a valid next item.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index 223a4012..9df114d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -64,7 +64,6 @@
/**
* Enable to log changes to the LayoutNode tree. This logging is quite chatty.
*/
-@Suppress("ConstPropertyName")
private const val DebugChanges = false
private val DefaultDensity = Density(1f)
@@ -1061,7 +1060,11 @@
private fun invalidateFocusOnDetach() {
nodes.tailToHead(FocusTarget) {
if (it.focusState.isFocused) {
- requireOwner().focusOwner.clearFocus(force = true, refreshFocusEvents = false)
+ requireOwner().focusOwner.clearFocus(
+ force = true,
+ refreshFocusEvents = false,
+ clearOwnerFocus = true
+ )
it.scheduleInvalidationForFocusEvents()
}
}
@@ -1373,6 +1376,7 @@
/**
* Constant used by [placeOrder].
*/
+ @Suppress("ConstPropertyName")
internal const val NotPlacedPlaceOrder = Int.MAX_VALUE
/**
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
index 34ee53c3..4e6de06 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
@@ -1093,9 +1093,13 @@
fun shouldSharePointerInputWithSiblings(): Boolean {
val start = headNode(Nodes.PointerInput.includeSelfInTraversal) ?: return false
- start.visitLocalDescendants(Nodes.PointerInput) {
- if (it.sharePointerInputWithSiblings()) return true
+
+ if (start.isAttached) {
+ start.visitLocalDescendants(Nodes.PointerInput) {
+ if (it.sharePointerInputWithSiblings()) return true
+ }
}
+
return false
}
diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
index f0c5115..596a478 100644
--- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
+++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
@@ -76,7 +76,6 @@
import androidx.compose.ui.node.RootForTest
import androidx.compose.ui.semantics.EmptySemanticsElement
import androidx.compose.ui.semantics.SemanticsOwner
-import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.text.input.TextInputService
import androidx.compose.ui.text.platform.FontLoader
@@ -92,7 +91,6 @@
@OptIn(
ExperimentalComposeUiApi::class,
- ExperimentalTextApi::class,
InternalCoreApi::class,
InternalComposeUiApi::class
)
@@ -100,7 +98,7 @@
private val platformInputService: PlatformInput,
private val component: PlatformComponent,
density: Density = Density(1f, 1f),
- coroutineContext: CoroutineContext,
+ override val coroutineContext: CoroutineContext,
val isPopup: Boolean = false,
val isFocusable: Boolean = true,
val onDismissRequest: (() -> Unit)? = null,
@@ -126,12 +124,12 @@
private val semanticsModifier = EmptySemanticsElement
- override val focusOwner: FocusOwner = FocusOwnerImpl {
- registerOnEndApplyChangesListener(it)
- }.apply {
- // TODO(demin): support RTL [onRtlPropertiesChanged]
- layoutDirection = LayoutDirection.Ltr
- }
+ override val focusOwner: FocusOwner = FocusOwnerImpl(
+ onRequestApplyChangesListener = ::registerOnEndApplyChangesListener,
+ onRequestFocusForOwner = { _, _ -> true }, // TODO request focus from framework.
+ onClearFocusForOwner = {}, // TODO clear focus from framework.
+ layoutDirection = { layoutDirection } // TODO(demin): support RTL [onRtlPropertiesChanged].
+ )
// TODO: Set the input mode. For now we don't support touch mode, (always in Key mode).
private val _inputModeManager = InputModeManagerImpl(
@@ -188,8 +186,6 @@
.onKeyEvent(onKeyEvent)
}
- override val coroutineContext: CoroutineContext = coroutineContext
-
override val rootForTest = this
override val snapshotObserver = OwnerSnapshotObserver { command ->
@@ -204,7 +200,8 @@
snapshotObserver.startObserving()
root.attach(this)
focusOwner.focusTransactionManager.withNewTransaction {
- focusOwner.takeFocus()
+ // TODO instead of taking focus here, call this when the owner gets focused.
+ focusOwner.takeFocus(Enter, previouslyFocusedRect = null)
}
}
diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/Barrier.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/Barrier.java
index 2007e11..15d7e02 100644
--- a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/Barrier.java
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/Barrier.java
@@ -18,7 +18,6 @@
import android.content.Context;
import android.content.res.TypedArray;
-import android.os.Build;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.View;
@@ -158,29 +157,19 @@
private void updateType(ConstraintWidget widget, int type, boolean isRtl) {
mResolvedType = type;
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
- // Pre JB MR1, left/right should take precedence, unless they are
- // not defined and somehow a corresponding start/end constraint exists
+
+ if (isRtl) {
+ if (mIndicatedType == START) {
+ mResolvedType = RIGHT;
+ } else if (mIndicatedType == END) {
+ mResolvedType = LEFT;
+ }
+ } else {
if (mIndicatedType == START) {
mResolvedType = LEFT;
} else if (mIndicatedType == END) {
mResolvedType = RIGHT;
}
- } else {
- // Post JB MR1, if start/end are defined, they take precedence over left/right
- if (isRtl) {
- if (mIndicatedType == START) {
- mResolvedType = RIGHT;
- } else if (mIndicatedType == END) {
- mResolvedType = LEFT;
- }
- } else {
- if (mIndicatedType == START) {
- mResolvedType = LEFT;
- } else if (mIndicatedType == END) {
- mResolvedType = RIGHT;
- }
- }
}
if (widget instanceof androidx.constraintlayout.core.widgets.Barrier) {
androidx.constraintlayout.core.widgets.Barrier barrier =
diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java
index 24710a1..bfc5c8b 100644
--- a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java
@@ -1327,11 +1327,6 @@
int resolvedGuideBegin = layoutParams.mResolvedGuideBegin;
int resolvedGuideEnd = layoutParams.mResolvedGuideEnd;
float resolvedGuidePercent = layoutParams.mResolvedGuidePercent;
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
- resolvedGuideBegin = layoutParams.guideBegin;
- resolvedGuideEnd = layoutParams.guideEnd;
- resolvedGuidePercent = layoutParams.guidePercent;
- }
if (resolvedGuidePercent != UNSET) {
guideline.setGuidePercent(resolvedGuidePercent);
} else if (resolvedGuideBegin != UNSET) {
@@ -1349,33 +1344,6 @@
int resolveGoneRightMargin = layoutParams.mResolveGoneRightMargin;
float resolvedHorizontalBias = layoutParams.mResolvedHorizontalBias;
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
- // Pre JB MR1, left/right should take precedence, unless they are
- // not defined and somehow a corresponding start/end constraint exists
- resolvedLeftToLeft = layoutParams.leftToLeft;
- resolvedLeftToRight = layoutParams.leftToRight;
- resolvedRightToLeft = layoutParams.rightToLeft;
- resolvedRightToRight = layoutParams.rightToRight;
- resolveGoneLeftMargin = layoutParams.goneLeftMargin;
- resolveGoneRightMargin = layoutParams.goneRightMargin;
- resolvedHorizontalBias = layoutParams.horizontalBias;
-
- if (resolvedLeftToLeft == UNSET && resolvedLeftToRight == UNSET) {
- if (layoutParams.startToStart != UNSET) {
- resolvedLeftToLeft = layoutParams.startToStart;
- } else if (layoutParams.startToEnd != UNSET) {
- resolvedLeftToRight = layoutParams.startToEnd;
- }
- }
- if (resolvedRightToLeft == UNSET && resolvedRightToRight == UNSET) {
- if (layoutParams.endToStart != UNSET) {
- resolvedRightToLeft = layoutParams.endToStart;
- } else if (layoutParams.endToEnd != UNSET) {
- resolvedRightToRight = layoutParams.endToEnd;
- }
- }
- }
-
// Circular constraint
if (layoutParams.circleConstraint != UNSET) {
ConstraintWidget target = idToWidget.get(layoutParams.circleConstraint);
diff --git a/core/core-ktx/src/main/java/androidx/core/os/Bundle.kt b/core/core-ktx/src/main/java/androidx/core/os/Bundle.kt
index 9bda3bb..601fbf8 100644
--- a/core/core-ktx/src/main/java/androidx/core/os/Bundle.kt
+++ b/core/core-ktx/src/main/java/androidx/core/os/Bundle.kt
@@ -91,8 +91,8 @@
is Serializable -> putSerializable(key, value)
else -> {
- if (Build.VERSION.SDK_INT >= 18 && value is IBinder) {
- BundleApi18ImplKt.putBinder(this, key, value)
+ if (value is IBinder) {
+ this.putBinder(key, value)
} else if (Build.VERSION.SDK_INT >= 21 && value is Size) {
BundleApi21ImplKt.putSize(this, key, value)
} else if (Build.VERSION.SDK_INT >= 21 && value is SizeF) {
@@ -111,13 +111,6 @@
*/
public fun bundleOf(): Bundle = Bundle(0)
-@RequiresApi(18)
-private object BundleApi18ImplKt {
- @DoNotInline
- @JvmStatic
- fun putBinder(bundle: Bundle, key: String, value: IBinder?) = bundle.putBinder(key, value)
-}
-
@RequiresApi(21)
private object BundleApi21ImplKt {
@DoNotInline
diff --git a/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java b/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
index 454cfcd..c5a78b2 100644
--- a/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
@@ -686,12 +686,6 @@
// Add an action so that we start getting the view
builder.addAction(new NotificationCompat.Action(null, "action", null));
- // Before Jellybean, there was no big view; expect null
- if (Build.VERSION.SDK_INT < 16) {
- assertNull(builder.createHeadsUpContentView());
- return;
- }
-
// Expect the standard big notification template
RemoteViews standardView = builder.createBigContentView();
assertNotNull(standardView);
diff --git a/core/core/src/androidTest/java/androidx/core/content/res/ResourcesCompatTest.java b/core/core/src/androidTest/java/androidx/core/content/res/ResourcesCompatTest.java
index 16c5ef7..e877a26 100644
--- a/core/core/src/androidTest/java/androidx/core/content/res/ResourcesCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/content/res/ResourcesCompatTest.java
@@ -179,8 +179,7 @@
// For pre-v15 devices we should get a drawable that corresponds to the density of the
// current device. For v15+ devices we should get a drawable that corresponds to the
// density requested in the API call.
- final int expectedSizeForMediumDensity = (SDK_INT < 15) ?
- mResources.getDimensionPixelSize(R.dimen.density_aware_size) : 12;
+ final int expectedSizeForMediumDensity = 12;
assertEquals("Unthemed density-aware drawable load: medium width",
expectedSizeForMediumDensity, unthemedDrawableForMediumDensity.getIntrinsicWidth());
assertEquals("Unthemed density-aware drawable load: medium height",
@@ -190,11 +189,8 @@
final Drawable unthemedDrawableForHighDensity =
ResourcesCompat.getDrawableForDensity(mResources, R.drawable.density_aware_drawable,
DisplayMetrics.DENSITY_HIGH, null);
- // For pre-v15 devices we should get a drawable that corresponds to the density of the
- // current device. For v15+ devices we should get a drawable that corresponds to the
- // density requested in the API call.
- final int expectedSizeForHighDensity = (SDK_INT < 15) ?
- mResources.getDimensionPixelSize(R.dimen.density_aware_size) : 21;
+
+ final int expectedSizeForHighDensity = 21;
assertEquals("Unthemed density-aware drawable load: high width",
expectedSizeForHighDensity, unthemedDrawableForHighDensity.getIntrinsicWidth());
assertEquals("Unthemed density-aware drawable load: high height",
@@ -203,11 +199,8 @@
final Drawable unthemedDrawableForXHighDensity =
ResourcesCompat.getDrawableForDensity(mResources, R.drawable.density_aware_drawable,
DisplayMetrics.DENSITY_XHIGH, null);
- // For pre-v15 devices we should get a drawable that corresponds to the density of the
- // current device. For v15+ devices we should get a drawable that corresponds to the
- // density requested in the API call.
- final int expectedSizeForXHighDensity = (SDK_INT < 15) ?
- mResources.getDimensionPixelSize(R.dimen.density_aware_size) : 32;
+
+ final int expectedSizeForXHighDensity = 32;
assertEquals("Unthemed density-aware drawable load: xhigh width",
expectedSizeForXHighDensity, unthemedDrawableForXHighDensity.getIntrinsicWidth());
assertEquals("Unthemed density-aware drawable load: xhigh height",
@@ -216,11 +209,8 @@
final Drawable unthemedDrawableForXXHighDensity =
ResourcesCompat.getDrawableForDensity(mResources, R.drawable.density_aware_drawable,
DisplayMetrics.DENSITY_XXHIGH, null);
- // For pre-v15 devices we should get a drawable that corresponds to the density of the
- // current device. For v15+ devices we should get a drawable that corresponds to the
- // density requested in the API call.
- final int expectedSizeForXXHighDensity = (SDK_INT < 15) ?
- mResources.getDimensionPixelSize(R.dimen.density_aware_size) : 54;
+
+ final int expectedSizeForXXHighDensity = 54;
assertEquals("Unthemed density-aware drawable load: xxhigh width",
expectedSizeForXXHighDensity, unthemedDrawableForXXHighDensity.getIntrinsicWidth());
assertEquals("Unthemed density-aware drawable load: xxhigh height",
diff --git a/core/core/src/androidTest/java/androidx/core/graphics/TypefaceCompatUtilTest.java b/core/core/src/androidTest/java/androidx/core/graphics/TypefaceCompatUtilTest.java
index 245f6c5..e282798 100644
--- a/core/core/src/androidTest/java/androidx/core/graphics/TypefaceCompatUtilTest.java
+++ b/core/core/src/androidTest/java/androidx/core/graphics/TypefaceCompatUtilTest.java
@@ -20,7 +20,6 @@
import android.content.ContentUris;
import android.content.Context;
import android.net.Uri;
-import android.os.Build;
import androidx.annotation.RequiresApi;
import androidx.core.provider.MockFontProvider;
@@ -43,10 +42,6 @@
@Test
@RequiresApi(19)
public void testMmapNullPfd() {
- if (Build.VERSION.SDK_INT < 19) {
- // The API tested here requires SDK level 19.
- return;
- }
final Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
.authority(MockFontProvider.AUTHORITY).build();
final Uri fileUri = ContentUris.withAppendedId(uri, MockFontProvider.INVALID_FONT_FILE_ID);
diff --git a/core/core/src/androidTest/java/androidx/core/location/LocationCompatTest.java b/core/core/src/androidTest/java/androidx/core/location/LocationCompatTest.java
index 2c9770b..6cf5ee3 100644
--- a/core/core/src/androidTest/java/androidx/core/location/LocationCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/location/LocationCompatTest.java
@@ -25,7 +25,6 @@
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import android.location.Location;
-import android.os.Build;
import android.os.SystemClock;
import androidx.test.filters.SmallTest;
@@ -40,16 +39,10 @@
@Test
public void testGetElapsedRealtimeNanos() {
long locationElapsedRealtimeNs;
- if (Build.VERSION.SDK_INT >= 17) {
- locationElapsedRealtimeNs = SystemClock.elapsedRealtimeNanos();
- } else {
- locationElapsedRealtimeNs = MILLISECONDS.toNanos(SystemClock.elapsedRealtime());
- }
+ locationElapsedRealtimeNs = SystemClock.elapsedRealtimeNanos();
Location location = new Location("");
- if (Build.VERSION.SDK_INT >= 17) {
- location.setElapsedRealtimeNanos(locationElapsedRealtimeNs);
- }
+ location.setElapsedRealtimeNanos(locationElapsedRealtimeNs);
location.setTime(System.currentTimeMillis());
assertTrue(NANOSECONDS.toMillis(Math.abs(
@@ -62,9 +55,7 @@
long locationElapsedRealtimeMs = SystemClock.elapsedRealtime();
Location location = new Location("");
- if (Build.VERSION.SDK_INT >= 17) {
- location.setElapsedRealtimeNanos(MILLISECONDS.toNanos(locationElapsedRealtimeMs));
- }
+ location.setElapsedRealtimeNanos(MILLISECONDS.toNanos(locationElapsedRealtimeMs));
location.setTime(System.currentTimeMillis());
assertTrue(Math.abs(
diff --git a/core/core/src/androidTest/java/androidx/core/view/MarginLayoutParamsCompatTest.java b/core/core/src/androidTest/java/androidx/core/view/MarginLayoutParamsCompatTest.java
index 77676e7..7289810 100644
--- a/core/core/src/androidTest/java/androidx/core/view/MarginLayoutParamsCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/view/MarginLayoutParamsCompatTest.java
@@ -37,14 +37,8 @@
MarginLayoutParamsCompat.getLayoutDirection(mlp));
MarginLayoutParamsCompat.setLayoutDirection(mlp, ViewCompat.LAYOUT_DIRECTION_RTL);
- if (Build.VERSION.SDK_INT >= 17) {
- assertEquals("RTL layout direction", ViewCompat.LAYOUT_DIRECTION_RTL,
- MarginLayoutParamsCompat.getLayoutDirection(mlp));
- } else {
- assertEquals("Still LTR layout direction on older devices",
- ViewCompat.LAYOUT_DIRECTION_LTR,
- MarginLayoutParamsCompat.getLayoutDirection(mlp));
- }
+ assertEquals("RTL layout direction", ViewCompat.LAYOUT_DIRECTION_RTL,
+ MarginLayoutParamsCompat.getLayoutDirection(mlp));
MarginLayoutParamsCompat.setLayoutDirection(mlp, ViewCompat.LAYOUT_DIRECTION_LTR);
assertEquals("Back to LTR layout direction", ViewCompat.LAYOUT_DIRECTION_LTR,
diff --git a/core/core/src/androidTest/res/values-hdpi/dimens.xml b/core/core/src/androidTest/res/values-hdpi/dimens.xml
deleted file mode 100755
index 3126a6e..0000000
--- a/core/core/src/androidTest/res/values-hdpi/dimens.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<resources>
- <dimen name="density_aware_size">14dip</dimen>
-</resources>
\ No newline at end of file
diff --git a/core/core/src/androidTest/res/values-mdpi/dimens.xml b/core/core/src/androidTest/res/values-mdpi/dimens.xml
deleted file mode 100755
index ada2cae..0000000
--- a/core/core/src/androidTest/res/values-mdpi/dimens.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<resources>
- <dimen name="density_aware_size">12dip</dimen>
-</resources>
\ No newline at end of file
diff --git a/core/core/src/androidTest/res/values-xhdpi/dimens.xml b/core/core/src/androidTest/res/values-xhdpi/dimens.xml
deleted file mode 100755
index 21125b8..0000000
--- a/core/core/src/androidTest/res/values-xhdpi/dimens.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<resources>
- <dimen name="density_aware_size">16dip</dimen>
-</resources>
\ No newline at end of file
diff --git a/core/core/src/androidTest/res/values-xxhdpi/dimens.xml b/core/core/src/androidTest/res/values-xxhdpi/dimens.xml
deleted file mode 100755
index aaee862..0000000
--- a/core/core/src/androidTest/res/values-xxhdpi/dimens.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<resources>
- <dimen name="density_aware_size">18dip</dimen>
-</resources>
\ No newline at end of file
diff --git a/core/core/src/main/java/androidx/core/accessibilityservice/AccessibilityServiceInfoCompat.java b/core/core/src/main/java/androidx/core/accessibilityservice/AccessibilityServiceInfoCompat.java
index 9f4cd4e..d1dc409 100644
--- a/core/core/src/main/java/androidx/core/accessibilityservice/AccessibilityServiceInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/accessibilityservice/AccessibilityServiceInfoCompat.java
@@ -280,14 +280,7 @@
*/
@SuppressWarnings("deprecation")
public static int getCapabilities(@NonNull AccessibilityServiceInfo info) {
- if (Build.VERSION.SDK_INT >= 18) {
- return info.getCapabilities();
- } else {
- if (info.getCanRetrieveWindowContent()) {
- return CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT;
- }
- return 0;
- }
+ return info.getCapabilities();
}
/**
diff --git a/core/core/src/main/java/androidx/core/app/NotificationCompat.java b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
index 0cd62f5..bf3668fc 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationCompat.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
@@ -2219,10 +2219,6 @@
*/
@SuppressLint("BuilderSetStyle") // This API is copied from Notification.Builder
public @Nullable RemoteViews createBigContentView() {
- // Before Jellybean, there was no "big" notification view
- if (Build.VERSION.SDK_INT < 16) {
- return null;
- }
// If the user setCustomBigContentView(), return it if appropriate for the style.
if (mBigContentView != null && useExistingRemoteView()) {
return mBigContentView;
diff --git a/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java b/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java
index 927e008..9f30326 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java
@@ -157,9 +157,7 @@
mContentView = b.mContentView;
mBigContentView = b.mBigContentView;
}
- if (Build.VERSION.SDK_INT >= 17) {
- Api17Impl.setShowWhen(mBuilder, b.mShowWhen);
- }
+ mBuilder.setShowWhen(b.mShowWhen);
if (Build.VERSION.SDK_INT >= 19) {
if (Build.VERSION.SDK_INT < 21) {
final List<String> people = combineLists(getPeople(b.mPersonList), b.mPeople);
@@ -597,23 +595,6 @@
/**
* A class for wrapping calls to {@link NotificationCompatBuilder} methods which
- * were added in API 17; these calls must be wrapped to avoid performance issues.
- * See the UnsafeNewApiCall lint rule for more details.
- */
- @RequiresApi(17)
- static class Api17Impl {
- private Api17Impl() {
- }
-
- @DoNotInline
- static Notification.Builder setShowWhen(Notification.Builder builder, boolean show) {
- return builder.setShowWhen(show);
- }
-
- }
-
- /**
- * A class for wrapping calls to {@link NotificationCompatBuilder} methods which
* were added in API 19; these calls must be wrapped to avoid performance issues.
* See the UnsafeNewApiCall lint rule for more details.
*/
diff --git a/core/core/src/main/java/androidx/core/app/ShareCompat.java b/core/core/src/main/java/androidx/core/app/ShareCompat.java
index 7f4b1d0..818210a 100644
--- a/core/core/src/main/java/androidx/core/app/ShareCompat.java
+++ b/core/core/src/main/java/androidx/core/app/ShareCompat.java
@@ -16,8 +16,6 @@
package androidx.core.app;
-import static android.os.Build.VERSION.SDK_INT;
-
import static androidx.core.util.Preconditions.checkNotNull;
import android.app.Activity;
@@ -246,12 +244,6 @@
+ shareIntent.getContext().getClass().getName());
provider.setShareIntent(shareIntent.getIntent());
item.setActionProvider(provider);
-
- if (SDK_INT < 16) {
- if (!item.hasSubMenu()) {
- item.setIntent(shareIntent.createChooserIntent());
- }
- }
}
/**
diff --git a/core/core/src/main/java/androidx/core/content/ContextCompat.java b/core/core/src/main/java/androidx/core/content/ContextCompat.java
index 61c464a..8464b94 100644
--- a/core/core/src/main/java/androidx/core/content/ContextCompat.java
+++ b/core/core/src/main/java/androidx/core/content/ContextCompat.java
@@ -907,12 +907,12 @@
// The Android framework supports per-app locales on API 33, so we assume the
// configuration has been updated after API 32.
- if (Build.VERSION.SDK_INT <= 32 && Build.VERSION.SDK_INT >= 17) {
+ if (Build.VERSION.SDK_INT <= 32) {
if (!locales.isEmpty()) {
Configuration newConfig = new Configuration(
context.getResources().getConfiguration());
ConfigurationCompat.setLocales(newConfig, locales);
- return Api17Impl.createConfigurationContext(context, newConfig);
+ return context.createConfigurationContext(newConfig);
}
}
return context;
@@ -1006,24 +1006,18 @@
SERVICES.put(TelecomManager.class, TELECOM_SERVICE);
SERVICES.put(TvInputManager.class, TV_INPUT_SERVICE);
}
- if (Build.VERSION.SDK_INT >= 19) {
- SERVICES.put(AppOpsManager.class, APP_OPS_SERVICE);
- SERVICES.put(CaptioningManager.class, CAPTIONING_SERVICE);
- SERVICES.put(ConsumerIrManager.class, CONSUMER_IR_SERVICE);
- SERVICES.put(PrintManager.class, PRINT_SERVICE);
- }
- if (Build.VERSION.SDK_INT >= 18) {
- SERVICES.put(BluetoothManager.class, BLUETOOTH_SERVICE);
- }
- if (Build.VERSION.SDK_INT >= 17) {
- SERVICES.put(DisplayManager.class, DISPLAY_SERVICE);
- SERVICES.put(UserManager.class, USER_SERVICE);
- }
- if (Build.VERSION.SDK_INT >= 16) {
- SERVICES.put(InputManager.class, INPUT_SERVICE);
- SERVICES.put(MediaRouter.class, MEDIA_ROUTER_SERVICE);
- SERVICES.put(NsdManager.class, NSD_SERVICE);
- }
+
+ SERVICES.put(AppOpsManager.class, APP_OPS_SERVICE);
+ SERVICES.put(CaptioningManager.class, CAPTIONING_SERVICE);
+ SERVICES.put(ConsumerIrManager.class, CONSUMER_IR_SERVICE);
+ SERVICES.put(PrintManager.class, PRINT_SERVICE);
+ SERVICES.put(BluetoothManager.class, BLUETOOTH_SERVICE);
+ SERVICES.put(DisplayManager.class, DISPLAY_SERVICE);
+ SERVICES.put(UserManager.class, USER_SERVICE);
+
+ SERVICES.put(InputManager.class, INPUT_SERVICE);
+ SERVICES.put(MediaRouter.class, MEDIA_ROUTER_SERVICE);
+ SERVICES.put(NsdManager.class, NSD_SERVICE);
SERVICES.put(AccessibilityManager.class, ACCESSIBILITY_SERVICE);
SERVICES.put(AccountManager.class, ACCOUNT_SERVICE);
SERVICES.put(ActivityManager.class, ACTIVITY_SERVICE);
@@ -1056,18 +1050,6 @@
}
}
- @RequiresApi(17)
- static class Api17Impl {
- private Api17Impl() {
- // This class is not instantiable.
- }
-
- @DoNotInline
- static Context createConfigurationContext(Context obj, Configuration config) {
- return obj.createConfigurationContext(config);
- }
- }
-
@RequiresApi(19)
static class Api19Impl {
private Api19Impl() {
diff --git a/core/core/src/main/java/androidx/core/content/pm/ShortcutManagerCompat.java b/core/core/src/main/java/androidx/core/content/pm/ShortcutManagerCompat.java
index f8d72df..03e4741 100644
--- a/core/core/src/main/java/androidx/core/content/pm/ShortcutManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/content/pm/ShortcutManagerCompat.java
@@ -831,8 +831,7 @@
final boolean isHorizontal) {
final ActivityManager am = (ActivityManager)
context.getSystemService(Context.ACTIVITY_SERVICE);
- final boolean isLowRamDevice =
- Build.VERSION.SDK_INT < 19 || am == null || am.isLowRamDevice();
+ final boolean isLowRamDevice = am == null || am.isLowRamDevice();
final int iconDimensionDp = Math.max(1, isLowRamDevice
? DEFAULT_MAX_ICON_DIMENSION_LOWRAM_DP : DEFAULT_MAX_ICON_DIMENSION_DP);
final DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
diff --git a/core/core/src/main/java/androidx/core/content/res/ConfigurationHelper.java b/core/core/src/main/java/androidx/core/content/res/ConfigurationHelper.java
index ca5094d..dcfb64c 100644
--- a/core/core/src/main/java/androidx/core/content/res/ConfigurationHelper.java
+++ b/core/core/src/main/java/androidx/core/content/res/ConfigurationHelper.java
@@ -16,8 +16,6 @@
package androidx.core.content.res;
-import static android.os.Build.VERSION.SDK_INT;
-
import android.content.res.Configuration;
import android.content.res.Resources;
@@ -38,10 +36,6 @@
* is computed and returned.</p>
*/
public static int getDensityDpi(@NonNull Resources resources) {
- if (SDK_INT >= 17) {
- return resources.getConfiguration().densityDpi;
- } else {
- return resources.getDisplayMetrics().densityDpi;
- }
+ return resources.getConfiguration().densityDpi;
}
}
diff --git a/core/core/src/main/java/androidx/core/graphics/BitmapCompat.java b/core/core/src/main/java/androidx/core/graphics/BitmapCompat.java
index b10563e..74ad5c7 100644
--- a/core/core/src/main/java/androidx/core/graphics/BitmapCompat.java
+++ b/core/core/src/main/java/androidx/core/graphics/BitmapCompat.java
@@ -54,10 +54,7 @@
* @see Bitmap#hasMipMap()
*/
public static boolean hasMipMap(@NonNull Bitmap bitmap) {
- if (Build.VERSION.SDK_INT >= 17) {
- return Api17Impl.hasMipMap(bitmap);
- }
- return false;
+ return bitmap.hasMipMap();
}
/**
@@ -81,9 +78,7 @@
* @see Bitmap#setHasMipMap(boolean)
*/
public static void setHasMipMap(@NonNull Bitmap bitmap, boolean hasMipMap) {
- if (Build.VERSION.SDK_INT >= 17) {
- Api17Impl.setHasMipMap(bitmap, hasMipMap);
- }
+ bitmap.setHasMipMap(hasMipMap);
}
/**
@@ -334,23 +329,6 @@
// This class is not instantiable.
}
- @RequiresApi(17)
- static class Api17Impl {
- private Api17Impl() {
- // This class is not instantiable.
- }
-
- @DoNotInline
- static boolean hasMipMap(Bitmap bitmap) {
- return bitmap.hasMipMap();
- }
-
- @DoNotInline
- static void setHasMipMap(Bitmap bitmap, boolean hasMipMap) {
- bitmap.setHasMipMap(hasMipMap);
- }
- }
-
@RequiresApi(19)
static class Api19Impl {
private Api19Impl() {
diff --git a/core/core/src/main/java/androidx/core/hardware/display/DisplayManagerCompat.java b/core/core/src/main/java/androidx/core/hardware/display/DisplayManagerCompat.java
index 4f6a511..396cadf 100644
--- a/core/core/src/main/java/androidx/core/hardware/display/DisplayManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/hardware/display/DisplayManagerCompat.java
@@ -18,14 +18,10 @@
import android.content.Context;
import android.hardware.display.DisplayManager;
-import android.os.Build;
import android.view.Display;
-import android.view.WindowManager;
-import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
/**
* Helper for accessing features in {@link android.hardware.display.DisplayManager}.
@@ -73,17 +69,9 @@
@Nullable
@SuppressWarnings("deprecation")
public Display getDisplay(int displayId) {
- if (Build.VERSION.SDK_INT >= 17) {
- return Api17Impl.getDisplay(
- (DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE), displayId);
- }
-
- Display display = ((WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE))
- .getDefaultDisplay();
- if (display.getDisplayId() == displayId) {
- return display;
- }
- return null;
+ DisplayManager displayManager =
+ (DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE);
+ return displayManager.getDisplay(displayId);
}
/**
@@ -94,14 +82,7 @@
@SuppressWarnings("deprecation")
@NonNull
public Display[] getDisplays() {
- if (Build.VERSION.SDK_INT >= 17) {
- return Api17Impl.getDisplays(
- (DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE));
- }
-
- Display display = ((WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE))
- .getDefaultDisplay();
- return new Display[] { display };
+ return ((DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE)).getDisplays();
}
/**
@@ -123,33 +104,6 @@
@NonNull
@SuppressWarnings("deprecation")
public Display[] getDisplays(@Nullable String category) {
- if (Build.VERSION.SDK_INT >= 17) {
- return Api17Impl.getDisplays(
- (DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE));
- }
- if (category == null) {
- return new Display[0];
- }
-
- Display display = ((WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE))
- .getDefaultDisplay();
- return new Display[]{display};
- }
-
- @RequiresApi(17)
- static class Api17Impl {
- private Api17Impl() {
- // This class is not instantiable.
- }
-
- @DoNotInline
- static Display getDisplay(DisplayManager displayManager, int displayId) {
- return displayManager.getDisplay(displayId);
- }
-
- @DoNotInline
- static Display[] getDisplays(DisplayManager displayManager) {
- return displayManager.getDisplays();
- }
+ return ((DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE)).getDisplays();
}
}
diff --git a/core/core/src/main/java/androidx/core/location/LocationCompat.java b/core/core/src/main/java/androidx/core/location/LocationCompat.java
index 634dea0..883ff8a 100644
--- a/core/core/src/main/java/androidx/core/location/LocationCompat.java
+++ b/core/core/src/main/java/androidx/core/location/LocationCompat.java
@@ -16,14 +16,12 @@
package androidx.core.location;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import android.annotation.SuppressLint;
import android.location.Location;
import android.os.Build.VERSION;
import android.os.Bundle;
-import android.os.SystemClock;
import androidx.annotation.DoNotInline;
import androidx.annotation.FloatRange;
@@ -111,11 +109,7 @@
* location derivation is different from the system clock, the results may be inaccurate.
*/
public static long getElapsedRealtimeNanos(@NonNull Location location) {
- if (VERSION.SDK_INT >= 17) {
- return Api17Impl.getElapsedRealtimeNanos(location);
- } else {
- return MILLISECONDS.toNanos(getElapsedRealtimeMillis(location));
- }
+ return location.getElapsedRealtimeNanos();
}
/**
@@ -124,21 +118,7 @@
* @see #getElapsedRealtimeNanos(Location)
*/
public static long getElapsedRealtimeMillis(@NonNull Location location) {
- if (VERSION.SDK_INT >= 17) {
- return NANOSECONDS.toMillis(Api17Impl.getElapsedRealtimeNanos(location));
- } else {
- long timeDeltaMs = System.currentTimeMillis() - location.getTime();
- long elapsedRealtimeMs = SystemClock.elapsedRealtime();
- if (timeDeltaMs < 0) {
- // don't return an elapsed realtime from the future
- return elapsedRealtimeMs;
- } else if (timeDeltaMs > elapsedRealtimeMs) {
- // don't return an elapsed realtime from before boot
- return 0;
- } else {
- return elapsedRealtimeMs - timeDeltaMs;
- }
- }
+ return NANOSECONDS.toMillis(location.getElapsedRealtimeNanos());
}
/**
@@ -517,16 +497,7 @@
* @see android.location.LocationManager#addTestProvider
*/
public static boolean isMock(@NonNull Location location) {
- if (VERSION.SDK_INT >= 18) {
- return Api18Impl.isMock(location);
- } else {
- Bundle extras = location.getExtras();
- if (extras == null) {
- return false;
- }
-
- return extras.getBoolean(EXTRA_IS_MOCK, false);
- }
+ return location.isFromMockProvider();
}
/**
@@ -537,9 +508,9 @@
* boolean extra with the key {@link #EXTRA_IS_MOCK} to mark the location as mock. Be aware that
* this will overwrite any prior extra value under the same key.
*/
+ @SuppressLint("BanUncheckedReflection")
public static void setMock(@NonNull Location location, boolean mock) {
- if (VERSION.SDK_INT >= 18) {
- try {
+ try {
getSetIsFromMockProviderMethod().invoke(location, mock);
} catch (NoSuchMethodException e) {
Error error = new NoSuchMethodError();
@@ -552,25 +523,6 @@
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
- } else {
- Bundle extras = location.getExtras();
- if (extras == null) {
- if (mock) {
- extras = new Bundle();
- extras.putBoolean(EXTRA_IS_MOCK, true);
- location.setExtras(extras);
- }
- } else {
- if (mock) {
- extras.putBoolean(EXTRA_IS_MOCK, true);
- } else {
- extras.remove(EXTRA_IS_MOCK);
- if (extras.isEmpty()) {
- location.setExtras(null);
- }
- }
- }
- }
}
@RequiresApi(34)
@@ -960,30 +912,6 @@
}
}
- @RequiresApi(18)
- private static class Api18Impl {
-
- private Api18Impl() {
- }
-
- @DoNotInline
- static boolean isMock(Location location) {
- return location.isFromMockProvider();
- }
- }
-
- @RequiresApi(17)
- private static class Api17Impl {
-
- private Api17Impl() {
- }
-
- @DoNotInline
- static long getElapsedRealtimeNanos(Location location) {
- return location.getElapsedRealtimeNanos();
- }
- }
-
private static Method getSetIsFromMockProviderMethod() throws NoSuchMethodException {
if (sSetIsFromMockProviderMethod == null) {
sSetIsFromMockProviderMethod = Location.class.getDeclaredMethod("setIsFromMockProvider",
diff --git a/core/core/src/main/java/androidx/core/os/BundleCompat.java b/core/core/src/main/java/androidx/core/os/BundleCompat.java
index 94555cf..3aea8a8 100644
--- a/core/core/src/main/java/androidx/core/os/BundleCompat.java
+++ b/core/core/src/main/java/androidx/core/os/BundleCompat.java
@@ -21,7 +21,6 @@
import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcelable;
-import android.util.Log;
import android.util.SparseArray;
import androidx.annotation.DoNotInline;
@@ -29,8 +28,6 @@
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
import java.util.ArrayList;
/**
@@ -192,11 +189,7 @@
*/
@Nullable
public static IBinder getBinder(@NonNull Bundle bundle, @Nullable String key) {
- if (Build.VERSION.SDK_INT >= 18) {
- return Api18Impl.getBinder(bundle, key);
- } else {
- return BeforeApi18Impl.getBinder(bundle, key);
- }
+ return bundle.getBinder(key);
}
/**
@@ -209,11 +202,7 @@
*/
public static void putBinder(@NonNull Bundle bundle, @Nullable String key,
@Nullable IBinder binder) {
- if (Build.VERSION.SDK_INT >= 18) {
- Api18Impl.putBinder(bundle, key, binder);
- } else {
- BeforeApi18Impl.putBinder(bundle, key, binder);
- }
+ bundle.putBinder(key, binder);
}
@RequiresApi(33)
@@ -246,84 +235,4 @@
return in.getSparseParcelableArray(key, clazz);
}
}
-
- @RequiresApi(18)
- static class Api18Impl {
- private Api18Impl() {
- // This class is not instantiable.
- }
-
- @DoNotInline
- static IBinder getBinder(Bundle bundle, String key) {
- return bundle.getBinder(key);
- }
-
- @DoNotInline
- static void putBinder(Bundle bundle, String key, IBinder value) {
- bundle.putBinder(key, value);
- }
- }
-
- @SuppressLint("BanUncheckedReflection") // Only called prior to API 18
- static class BeforeApi18Impl {
- private static final String TAG = "BundleCompat";
-
- private static Method sGetIBinderMethod;
- private static boolean sGetIBinderMethodFetched;
-
- private static Method sPutIBinderMethod;
- private static boolean sPutIBinderMethodFetched;
-
- private BeforeApi18Impl() {
- // This class is not instantiable.
- }
-
- @SuppressWarnings("JavaReflectionMemberAccess")
- public static IBinder getBinder(Bundle bundle, String key) {
- if (!sGetIBinderMethodFetched) {
- try {
- sGetIBinderMethod = Bundle.class.getMethod("getIBinder", String.class);
- sGetIBinderMethod.setAccessible(true);
- } catch (NoSuchMethodException e) {
- Log.i(TAG, "Failed to retrieve getIBinder method", e);
- }
- sGetIBinderMethodFetched = true;
- }
-
- if (sGetIBinderMethod != null) {
- try {
- return (IBinder) sGetIBinderMethod.invoke(bundle, key);
- } catch (InvocationTargetException | IllegalAccessException
- | IllegalArgumentException e) {
- Log.i(TAG, "Failed to invoke getIBinder via reflection", e);
- sGetIBinderMethod = null;
- }
- }
- return null;
- }
-
- @SuppressWarnings("JavaReflectionMemberAccess")
- public static void putBinder(Bundle bundle, String key, IBinder binder) {
- if (!sPutIBinderMethodFetched) {
- try {
- sPutIBinderMethod =
- Bundle.class.getMethod("putIBinder", String.class, IBinder.class);
- sPutIBinderMethod.setAccessible(true);
- } catch (NoSuchMethodException e) {
- Log.i(TAG, "Failed to retrieve putIBinder method", e);
- }
- sPutIBinderMethodFetched = true;
- }
-
- if (sPutIBinderMethod != null) {
- try {
- sPutIBinderMethod.invoke(bundle, key, binder);
- } catch (InvocationTargetException | IllegalAccessException
- | IllegalArgumentException e) {
- Log.i(TAG, "Failed to invoke putIBinder via reflection", e);
- sPutIBinderMethod = null;
- }
- }
- }
- }
}
diff --git a/core/core/src/main/java/androidx/core/os/ConfigurationCompat.java b/core/core/src/main/java/androidx/core/os/ConfigurationCompat.java
index e5e80dc..d4438ce 100644
--- a/core/core/src/main/java/androidx/core/os/ConfigurationCompat.java
+++ b/core/core/src/main/java/androidx/core/os/ConfigurationCompat.java
@@ -57,20 +57,7 @@
@NonNull Configuration configuration, @NonNull LocaleListCompat locales) {
if (SDK_INT >= 24) {
Api24Impl.setLocales(configuration, locales);
- } else if (SDK_INT >= 17) {
- Api17Impl.setLocale(configuration, locales);
- }
- }
-
- @RequiresApi(17)
- static class Api17Impl {
- private Api17Impl() {
- // This class is not instantiable.
- }
-
- @DoNotInline
- static void setLocale(
- @NonNull Configuration configuration, @NonNull LocaleListCompat locales) {
+ } else {
if (!locales.isEmpty()) {
configuration.setLocale(locales.get(0));
}
diff --git a/core/core/src/main/java/androidx/core/os/ProcessCompat.java b/core/core/src/main/java/androidx/core/os/ProcessCompat.java
index 6343cf3..f2c38d0 100644
--- a/core/core/src/main/java/androidx/core/os/ProcessCompat.java
+++ b/core/core/src/main/java/androidx/core/os/ProcessCompat.java
@@ -54,12 +54,8 @@
public static boolean isApplicationUid(int uid) {
if (Build.VERSION.SDK_INT >= 24) {
return Api24Impl.isApplicationUid(uid);
- } else if (Build.VERSION.SDK_INT >= 17) {
- return Api17Impl.isApplicationUid(uid);
- } else if (Build.VERSION.SDK_INT == 16) {
- return Api16Impl.isApplicationUid(uid);
} else {
- return true;
+ return Api17Impl.isApplicationUid(uid);
}
}
@@ -76,7 +72,6 @@
}
}
- @RequiresApi(17)
static class Api17Impl {
private static final Object sResolvedLock = new Object();
@@ -114,43 +109,4 @@
return true;
}
}
-
- @RequiresApi(16)
- static class Api16Impl {
- private static final Object sResolvedLock = new Object();
-
- private static Method sMethodUserIdIsAppMethod;
- private static boolean sResolved;
-
- private Api16Impl() {
- // This class is non-instantiable.
- }
-
- @SuppressLint("PrivateApi")
- @SuppressWarnings("CatchAndPrintStackTrace")
- static boolean isApplicationUid(int uid) {
- // In JELLY_BEAN_MR1, the equivalent isApp(int) hidden method was available on hidden
- // class android.os.UserId.
- try {
- synchronized (sResolvedLock) {
- if (!sResolved) {
- sResolved = true;
- sMethodUserIdIsAppMethod = Class.forName("android.os.UserId")
- .getDeclaredMethod("isApp", int.class);
- }
- }
- if (sMethodUserIdIsAppMethod != null) {
- Boolean result = (Boolean) sMethodUserIdIsAppMethod.invoke(null, uid);
- if (result == null) {
- // This should never happen, as the method returns a boolean primitive.
- throw new NullPointerException();
- }
- return result;
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- return true;
- }
- }
}
diff --git a/core/core/src/main/java/androidx/core/os/TraceCompat.java b/core/core/src/main/java/androidx/core/os/TraceCompat.java
index f5022c6..7689534 100644
--- a/core/core/src/main/java/androidx/core/os/TraceCompat.java
+++ b/core/core/src/main/java/androidx/core/os/TraceCompat.java
@@ -81,7 +81,7 @@
public static boolean isEnabled() {
if (Build.VERSION.SDK_INT >= 29) {
return Api29Impl.isEnabled();
- } else if (Build.VERSION.SDK_INT >= 18) {
+ } else {
try {
return (boolean) sIsTagEnabledMethod.invoke(null, sTraceTagApp);
} catch (Exception e) {
@@ -105,9 +105,7 @@
* most 127 Unicode code units long.
*/
public static void beginSection(@NonNull String sectionName) {
- if (Build.VERSION.SDK_INT >= 18) {
- Api18Impl.beginSection(sectionName);
- }
+ Trace.beginSection(sectionName);
}
/**
@@ -118,9 +116,7 @@
* thread.
*/
public static void endSection() {
- if (Build.VERSION.SDK_INT >= 18) {
- Api18Impl.endSection();
- }
+ Trace.endSection();
}
/**
@@ -136,7 +132,7 @@
public static void beginAsyncSection(@NonNull String methodName, int cookie) {
if (Build.VERSION.SDK_INT >= 29) {
Api29Impl.beginAsyncSection(methodName, cookie);
- } else if (Build.VERSION.SDK_INT >= 18) {
+ } else {
try {
sAsyncTraceBeginMethod.invoke(null, sTraceTagApp, methodName, cookie);
} catch (Exception e) {
@@ -156,7 +152,7 @@
public static void endAsyncSection(@NonNull String methodName, int cookie) {
if (Build.VERSION.SDK_INT >= 29) {
Api29Impl.endAsyncSection(methodName, cookie);
- } else if (Build.VERSION.SDK_INT >= 18) {
+ } else {
try {
sAsyncTraceEndMethod.invoke(null, sTraceTagApp, methodName, cookie);
} catch (Exception e) {
@@ -175,7 +171,7 @@
public static void setCounter(@NonNull String counterName, int counterValue) {
if (Build.VERSION.SDK_INT >= 29) {
Api29Impl.setCounter(counterName, counterValue);
- } else if (Build.VERSION.SDK_INT >= 18) {
+ } else {
try {
sTraceCounterMethod.invoke(null, sTraceTagApp, counterName, counterValue);
} catch (Exception e) {
@@ -213,21 +209,4 @@
Trace.setCounter(counterName, counterValue);
}
}
-
- @RequiresApi(18)
- static class Api18Impl {
- private Api18Impl() {
- // This class is not instantiable.
- }
-
- @DoNotInline
- static void beginSection(String sectionName) {
- Trace.beginSection(sectionName);
- }
-
- @DoNotInline
- static void endSection() {
- Trace.endSection();
- }
- }
}
diff --git a/core/core/src/main/java/androidx/core/provider/FontProvider.java b/core/core/src/main/java/androidx/core/provider/FontProvider.java
index 6b2101b..1559dff 100644
--- a/core/core/src/main/java/androidx/core/provider/FontProvider.java
+++ b/core/core/src/main/java/androidx/core/provider/FontProvider.java
@@ -238,9 +238,7 @@
void close();
static ContentQueryWrapper make(Context context, Uri uri) {
- if (Build.VERSION.SDK_INT < 16) {
- return new ContentQueryWrapperBaseImpl(context);
- } else if (Build.VERSION.SDK_INT < 24) {
+ if (Build.VERSION.SDK_INT < 24) {
return new ContentQueryWrapperApi16Impl(context, uri);
} else {
return new ContentQueryWrapperApi24Impl(context, uri);
@@ -248,25 +246,6 @@
}
}
- private static class ContentQueryWrapperBaseImpl implements ContentQueryWrapper {
- private ContentResolver mResolver;
- ContentQueryWrapperBaseImpl(Context context) {
- mResolver = context.getContentResolver();
- }
-
- @Override
- public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
- String sortOrder, CancellationSignal cancellationSignal) {
- return mResolver.query(uri, projection, selection, selectionArgs, sortOrder);
- }
-
- @Override
- public void close() {
- mResolver = null;
- }
- }
-
- @RequiresApi(16)
private static class ContentQueryWrapperApi16Impl implements ContentQueryWrapper {
private final ContentProviderClient mClient;
ContentQueryWrapperApi16Impl(Context context, Uri uri) {
diff --git a/core/core/src/main/java/androidx/core/text/TextUtilsCompat.java b/core/core/src/main/java/androidx/core/text/TextUtilsCompat.java
index 3085998..bdcd81ee 100644
--- a/core/core/src/main/java/androidx/core/text/TextUtilsCompat.java
+++ b/core/core/src/main/java/androidx/core/text/TextUtilsCompat.java
@@ -16,14 +16,10 @@
package androidx.core.text;
-import static android.os.Build.VERSION.SDK_INT;
-
import android.text.TextUtils;
-import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
import androidx.core.view.ViewCompat;
import java.util.Locale;
@@ -32,9 +28,6 @@
* Backwards compatible version of {@link TextUtils}.
*/
public final class TextUtilsCompat {
- private static final Locale ROOT = new Locale("", "");
- private static final String ARAB_SCRIPT_SUBTAG = "Arab";
- private static final String HEBR_SCRIPT_SUBTAG = "Hebr";
/**
* Html-encode the string.
@@ -44,40 +37,7 @@
*/
@NonNull
public static String htmlEncode(@NonNull String s) {
- if (SDK_INT >= 17) {
- return TextUtils.htmlEncode(s);
- } else {
- StringBuilder sb = new StringBuilder();
- char c;
- for (int i = 0; i < s.length(); i++) {
- c = s.charAt(i);
- switch (c) {
- case '<':
- sb.append("<"); //$NON-NLS-1$
- break;
- case '>':
- sb.append(">"); //$NON-NLS-1$
- break;
- case '&':
- sb.append("&"); //$NON-NLS-1$
- break;
- case '\'':
- //http://www.w3.org/TR/xhtml1
- // The named character reference ' (the apostrophe, U+0027) was
- // introduced in XML 1.0 but does not appear in HTML. Authors should
- // therefore use ' instead of ' to work as expected in HTML 4
- // user agents.
- sb.append("'"); //$NON-NLS-1$
- break;
- case '"':
- sb.append("""); //$NON-NLS-1$
- break;
- default:
- sb.append(c);
- }
- }
- return sb.toString();
- }
+ return TextUtils.htmlEncode(s);
}
/**
@@ -89,58 +49,9 @@
* {@link ViewCompat#LAYOUT_DIRECTION_RTL}.
*/
public static int getLayoutDirectionFromLocale(@Nullable Locale locale) {
- if (SDK_INT >= 17) {
- return Api17Impl.getLayoutDirectionFromLocale(locale);
- } else {
- if (locale != null && !locale.equals(ROOT)) {
- final String scriptSubtag = ICUCompat.maximizeAndGetScript(locale);
- if (scriptSubtag == null) return getLayoutDirectionFromFirstChar(locale);
-
- // This is intentionally limited to Arabic and Hebrew scripts, since older
- // versions of Android platform only considered those scripts to be right-to-left.
- if (scriptSubtag.equalsIgnoreCase(ARAB_SCRIPT_SUBTAG)
- || scriptSubtag.equalsIgnoreCase(HEBR_SCRIPT_SUBTAG)) {
- return ViewCompat.LAYOUT_DIRECTION_RTL;
- }
- }
- return ViewCompat.LAYOUT_DIRECTION_LTR;
- }
- }
-
- /**
- * Fallback algorithm to detect the locale direction. Rely on the first char of the
- * localized locale name. This will not work if the localized locale name is in English
- * (this is the case for ICU 4.4 and "Urdu" script)
- *
- * @param locale the {@link Locale} for which we want the layout direction, maybe be
- * {@code null}.
- * @return the layout direction, either {@link ViewCompat#LAYOUT_DIRECTION_LTR} or
- * {@link ViewCompat#LAYOUT_DIRECTION_RTL}.
- */
- private static int getLayoutDirectionFromFirstChar(@NonNull Locale locale) {
- switch(Character.getDirectionality(locale.getDisplayName(locale).charAt(0))) {
- case Character.DIRECTIONALITY_RIGHT_TO_LEFT:
- case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC:
- return ViewCompat.LAYOUT_DIRECTION_RTL;
-
- case Character.DIRECTIONALITY_LEFT_TO_RIGHT:
- default:
- return ViewCompat.LAYOUT_DIRECTION_LTR;
- }
+ return TextUtils.getLayoutDirectionFromLocale(locale);
}
private TextUtilsCompat() {
}
-
- @RequiresApi(17)
- static class Api17Impl {
- private Api17Impl() {
- // This class is not instantiable.
- }
-
- @DoNotInline
- static int getLayoutDirectionFromLocale(Locale locale) {
- return TextUtils.getLayoutDirectionFromLocale(locale);
- }
- }
}
diff --git a/core/core/src/main/java/androidx/core/view/GravityCompat.java b/core/core/src/main/java/androidx/core/view/GravityCompat.java
index 5935241..b7e1215 100644
--- a/core/core/src/main/java/androidx/core/view/GravityCompat.java
+++ b/core/core/src/main/java/androidx/core/view/GravityCompat.java
@@ -17,14 +17,10 @@
package androidx.core.view;
-import static android.os.Build.VERSION.SDK_INT;
-
import android.graphics.Rect;
import android.view.Gravity;
-import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
/**
* Compatibility shim for accessing newer functionality from {@link Gravity}.
@@ -65,11 +61,7 @@
*/
public static void apply(int gravity, int w, int h, @NonNull Rect container,
@NonNull Rect outRect, int layoutDirection) {
- if (SDK_INT >= 17) {
- Api17Impl.apply(gravity, w, h, container, outRect, layoutDirection);
- } else {
- Gravity.apply(gravity, w, h, container, outRect);
- }
+ Gravity.apply(gravity, w, h, container, outRect, layoutDirection);
}
/**
@@ -99,11 +91,7 @@
*/
public static void apply(int gravity, int w, int h, @NonNull Rect container,
int xAdj, int yAdj, @NonNull Rect outRect, int layoutDirection) {
- if (SDK_INT >= 17) {
- Api17Impl.apply(gravity, w, h, container, xAdj, yAdj, outRect, layoutDirection);
- } else {
- Gravity.apply(gravity, w, h, container, xAdj, yAdj, outRect);
- }
+ Gravity.apply(gravity, w, h, container, xAdj, yAdj, outRect, layoutDirection);
}
/**
@@ -128,11 +116,7 @@
*/
public static void applyDisplay(int gravity, @NonNull Rect display, @NonNull Rect inoutObj,
int layoutDirection) {
- if (SDK_INT >= 17) {
- Api17Impl.applyDisplay(gravity, display, inoutObj, layoutDirection);
- } else {
- Gravity.applyDisplay(gravity, display, inoutObj);
- }
+ Gravity.applyDisplay(gravity, display, inoutObj, layoutDirection);
}
/**
@@ -147,38 +131,9 @@
* @return gravity converted to absolute (horizontal) values.
*/
public static int getAbsoluteGravity(int gravity, int layoutDirection) {
- if (SDK_INT >= 17) {
- return Gravity.getAbsoluteGravity(gravity, layoutDirection);
- } else {
- // Just strip off the relative bit to get LEFT/RIGHT.
- return gravity & ~RELATIVE_LAYOUT_DIRECTION;
- }
+ return Gravity.getAbsoluteGravity(gravity, layoutDirection);
}
private GravityCompat() {
}
-
- @RequiresApi(17)
- static class Api17Impl {
- private Api17Impl() {
- // This class is not instantiable.
- }
-
- @DoNotInline
- static void apply(int gravity, int w, int h, Rect container, Rect outRect,
- int layoutDirection) {
- Gravity.apply(gravity, w, h, container, outRect, layoutDirection);
- }
-
- @DoNotInline
- static void apply(int gravity, int w, int h, Rect container, int xAdj, int yAdj,
- Rect outRect, int layoutDirection) {
- Gravity.apply(gravity, w, h, container, xAdj, yAdj, outRect, layoutDirection);
- }
-
- @DoNotInline
- static void applyDisplay(int gravity, Rect display, Rect inoutObj, int layoutDirection) {
- Gravity.applyDisplay(gravity, display, inoutObj, layoutDirection);
- }
- }
}
diff --git a/core/core/src/main/java/androidx/core/view/MarginLayoutParamsCompat.java b/core/core/src/main/java/androidx/core/view/MarginLayoutParamsCompat.java
index 4237d43..8039d09 100644
--- a/core/core/src/main/java/androidx/core/view/MarginLayoutParamsCompat.java
+++ b/core/core/src/main/java/androidx/core/view/MarginLayoutParamsCompat.java
@@ -17,14 +17,10 @@
package androidx.core.view;
-import static android.os.Build.VERSION.SDK_INT;
-
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
/**
* Helper for accessing API features in
@@ -44,11 +40,7 @@
* @return the margin along the starting edge in pixels
*/
public static int getMarginStart(@NonNull ViewGroup.MarginLayoutParams lp) {
- if (SDK_INT >= 17) {
- return Api17Impl.getMarginStart(lp);
- } else {
- return lp.leftMargin;
- }
+ return lp.getMarginStart();
}
/**
@@ -63,11 +55,7 @@
* @return the margin along the ending edge in pixels
*/
public static int getMarginEnd(@NonNull ViewGroup.MarginLayoutParams lp) {
- if (SDK_INT >= 17) {
- return Api17Impl.getMarginEnd(lp);
- } else {
- return lp.rightMargin;
- }
+ return lp.getMarginEnd();
}
/**
@@ -82,11 +70,7 @@
* @param marginStart the desired start margin in pixels
*/
public static void setMarginStart(@NonNull ViewGroup.MarginLayoutParams lp, int marginStart) {
- if (SDK_INT >= 17) {
- Api17Impl.setMarginStart(lp, marginStart);
- } else {
- lp.leftMargin = marginStart;
- }
+ lp.setMarginStart(marginStart);
}
/**
@@ -101,11 +85,7 @@
* @param marginEnd the desired end margin in pixels
*/
public static void setMarginEnd(@NonNull ViewGroup.MarginLayoutParams lp, int marginEnd) {
- if (SDK_INT >= 17) {
- Api17Impl.setMarginEnd(lp, marginEnd);
- } else {
- lp.rightMargin = marginEnd;
- }
+ lp.setMarginEnd(marginEnd);
}
/**
@@ -114,11 +94,7 @@
* @return true if either marginStart or marginEnd has been set.
*/
public static boolean isMarginRelative(@NonNull ViewGroup.MarginLayoutParams lp) {
- if (SDK_INT >= 17) {
- return Api17Impl.isMarginRelative(lp);
- } else {
- return false;
- }
+ return lp.isMarginRelative();
}
/**
@@ -129,11 +105,7 @@
*/
public static int getLayoutDirection(@NonNull ViewGroup.MarginLayoutParams lp) {
int result;
- if (SDK_INT >= 17) {
- result = Api17Impl.getLayoutDirection(lp);
- } else {
- result = ViewCompat.LAYOUT_DIRECTION_LTR;
- }
+ result = lp.getLayoutDirection();
if ((result != ViewCompat.LAYOUT_DIRECTION_LTR)
&& (result != ViewCompat.LAYOUT_DIRECTION_RTL)) {
@@ -154,9 +126,7 @@
*/
public static void setLayoutDirection(@NonNull ViewGroup.MarginLayoutParams lp,
int layoutDirection) {
- if (SDK_INT >= 17) {
- Api17Impl.setLayoutDirection(lp, layoutDirection);
- }
+ lp.setLayoutDirection(layoutDirection);
}
/**
@@ -165,60 +135,10 @@
*/
public static void resolveLayoutDirection(@NonNull ViewGroup.MarginLayoutParams lp,
int layoutDirection) {
- if (SDK_INT >= 17) {
- Api17Impl.resolveLayoutDirection(lp, layoutDirection);
- }
+ lp.resolveLayoutDirection(layoutDirection);
}
private MarginLayoutParamsCompat() {
}
- @RequiresApi(17)
- static class Api17Impl {
- private Api17Impl() {
- // This class is not instantiable.
- }
-
- @DoNotInline
- static int getMarginStart(ViewGroup.MarginLayoutParams marginLayoutParams) {
- return marginLayoutParams.getMarginStart();
- }
-
- @DoNotInline
- static int getMarginEnd(ViewGroup.MarginLayoutParams marginLayoutParams) {
- return marginLayoutParams.getMarginEnd();
- }
-
- @DoNotInline
- static void setMarginStart(ViewGroup.MarginLayoutParams marginLayoutParams, int start) {
- marginLayoutParams.setMarginStart(start);
- }
-
- @DoNotInline
- static void setMarginEnd(ViewGroup.MarginLayoutParams marginLayoutParams, int end) {
- marginLayoutParams.setMarginEnd(end);
- }
-
- @DoNotInline
- static boolean isMarginRelative(ViewGroup.MarginLayoutParams marginLayoutParams) {
- return marginLayoutParams.isMarginRelative();
- }
-
- @DoNotInline
- static int getLayoutDirection(ViewGroup.MarginLayoutParams marginLayoutParams) {
- return marginLayoutParams.getLayoutDirection();
- }
-
- @DoNotInline
- static void setLayoutDirection(ViewGroup.MarginLayoutParams marginLayoutParams,
- int layoutDirection) {
- marginLayoutParams.setLayoutDirection(layoutDirection);
- }
-
- @DoNotInline
- static void resolveLayoutDirection(ViewGroup.MarginLayoutParams marginLayoutParams,
- int layoutDirection) {
- marginLayoutParams.resolveLayoutDirection(layoutDirection);
- }
- }
}
diff --git a/core/core/src/main/java/androidx/core/view/ViewGroupCompat.java b/core/core/src/main/java/androidx/core/view/ViewGroupCompat.java
index 207e55f..3034440 100644
--- a/core/core/src/main/java/androidx/core/view/ViewGroupCompat.java
+++ b/core/core/src/main/java/androidx/core/view/ViewGroupCompat.java
@@ -113,10 +113,7 @@
* @see #setLayoutMode(ViewGroup, int)
*/
public static int getLayoutMode(@NonNull ViewGroup group) {
- if (Build.VERSION.SDK_INT >= 18) {
- return Api18Impl.getLayoutMode(group);
- }
- return LAYOUT_MODE_CLIP_BOUNDS;
+ return group.getLayoutMode();
}
/**
@@ -130,9 +127,7 @@
* @see #getLayoutMode(ViewGroup)
*/
public static void setLayoutMode(@NonNull ViewGroup group, int mode) {
- if (Build.VERSION.SDK_INT >= 18) {
- Api18Impl.setLayoutMode(group, mode);
- }
+ group.setLayoutMode(mode);
}
/**
@@ -191,23 +186,6 @@
return ViewCompat.SCROLL_AXIS_NONE;
}
- @RequiresApi(18)
- static class Api18Impl {
- private Api18Impl() {
- // This class is not instantiable.
- }
-
- @DoNotInline
- static int getLayoutMode(ViewGroup viewGroup) {
- return viewGroup.getLayoutMode();
- }
-
- @DoNotInline
- static void setLayoutMode(ViewGroup viewGroup, int layoutMode) {
- viewGroup.setLayoutMode(layoutMode);
- }
- }
-
@RequiresApi(21)
static class Api21Impl {
private Api21Impl() {
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java
index fec6650..26c0417 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java
@@ -18,6 +18,7 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+import android.annotation.SuppressLint;
import android.os.Build;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
@@ -450,6 +451,7 @@
* <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_UNDEFINED}
* </ul>
*/
+ @SuppressLint("WrongConstant")
@ContentChangeType
public static int getContentChangeTypes(@NonNull AccessibilityEvent event) {
if (Build.VERSION.SDK_INT >= 19) {
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
index f8d31ba..f980b7b 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
@@ -2607,9 +2607,6 @@
}
private List<Integer> extrasIntList(String key) {
- if (Build.VERSION.SDK_INT < 19) {
- return new ArrayList<Integer>();
- }
ArrayList<Integer> list = Api19Impl.getExtras(mInfo)
.getIntegerArrayList(key);
if (list == null) {
@@ -3792,9 +3789,7 @@
* @param viewId The id resource name.
*/
public void setViewIdResourceName(String viewId) {
- if (Build.VERSION.SDK_INT >= 18) {
- mInfo.setViewIdResourceName(viewId);
- }
+ mInfo.setViewIdResourceName(viewId);
}
/**
@@ -3810,11 +3805,7 @@
* @return The id resource name.
*/
public String getViewIdResourceName() {
- if (Build.VERSION.SDK_INT >= 18) {
- return mInfo.getViewIdResourceName();
- } else {
- return null;
- }
+ return mInfo.getViewIdResourceName();
}
/**
@@ -4158,9 +4149,7 @@
* @param labeled The view for which this info serves as a label.
*/
public void setLabelFor(View labeled) {
- if (Build.VERSION.SDK_INT >= 17) {
- mInfo.setLabelFor(labeled);
- }
+ mInfo.setLabelFor(labeled);
}
/**
@@ -4178,9 +4167,7 @@
* @param virtualDescendantId The id of the virtual descendant.
*/
public void setLabelFor(View root, int virtualDescendantId) {
- if (Build.VERSION.SDK_INT >= 17) {
- mInfo.setLabelFor(root, virtualDescendantId);
- }
+ mInfo.setLabelFor(root, virtualDescendantId);
}
/**
@@ -4190,11 +4177,7 @@
* @return The labeled info.
*/
public AccessibilityNodeInfoCompat getLabelFor() {
- if (Build.VERSION.SDK_INT >= 17) {
- return AccessibilityNodeInfoCompat.wrapNonNullInstance(mInfo.getLabelFor());
- } else {
- return null;
- }
+ return AccessibilityNodeInfoCompat.wrapNonNullInstance(mInfo.getLabelFor());
}
/**
@@ -4204,9 +4187,7 @@
* @param label The view that labels this node's source.
*/
public void setLabeledBy(View label) {
- if (Build.VERSION.SDK_INT >= 17) {
- mInfo.setLabeledBy(label);
- }
+ mInfo.setLabeledBy(label);
}
/**
@@ -4229,9 +4210,7 @@
* @param virtualDescendantId The id of the virtual descendant.
*/
public void setLabeledBy(View root, int virtualDescendantId) {
- if (Build.VERSION.SDK_INT >= 17) {
- mInfo.setLabeledBy(root, virtualDescendantId);
- }
+ mInfo.setLabeledBy(root, virtualDescendantId);
}
/**
@@ -4241,11 +4220,7 @@
* @return The label.
*/
public AccessibilityNodeInfoCompat getLabeledBy() {
- if (Build.VERSION.SDK_INT >= 17) {
- return AccessibilityNodeInfoCompat.wrapNonNullInstance(mInfo.getLabeledBy());
- } else {
- return null;
- }
+ return AccessibilityNodeInfoCompat.wrapNonNullInstance(mInfo.getLabeledBy());
}
/**
@@ -4452,9 +4427,7 @@
* @throws IllegalStateException If called from an AccessibilityService.
*/
public void setTextSelection(int start, int end) {
- if (Build.VERSION.SDK_INT >= 18) {
- mInfo.setTextSelection(start, end);
- }
+ mInfo.setTextSelection(start, end);
}
/**
@@ -4463,11 +4436,7 @@
* @return The text selection start if there is selection or -1.
*/
public int getTextSelectionStart() {
- if (Build.VERSION.SDK_INT >= 18) {
- return mInfo.getTextSelectionStart();
- } else {
- return -1;
- }
+ return mInfo.getTextSelectionStart();
}
/**
@@ -4476,11 +4445,7 @@
* @return The text selection end if there is selection or -1.
*/
public int getTextSelectionEnd() {
- if (Build.VERSION.SDK_INT >= 18) {
- return mInfo.getTextSelectionEnd();
- } else {
- return -1;
- }
+ return mInfo.getTextSelectionEnd();
}
/**
@@ -4661,11 +4626,7 @@
* @return True if the node is editable, false otherwise.
*/
public boolean isEditable() {
- if (Build.VERSION.SDK_INT >= 18) {
- return mInfo.isEditable();
- } else {
- return false;
- }
+ return mInfo.isEditable();
}
/**
@@ -4681,9 +4642,7 @@
* @throws IllegalStateException If called from an AccessibilityService.
*/
public void setEditable(boolean editable) {
- if (Build.VERSION.SDK_INT >= 18) {
- mInfo.setEditable(editable);
- }
+ mInfo.setEditable(editable);
}
/**
@@ -4977,11 +4936,7 @@
* @return Whether the refresh succeeded.
*/
public boolean refresh() {
- if (Build.VERSION.SDK_INT >= 18) {
- return mInfo.refresh();
- } else {
- return false;
- }
+ return mInfo.refresh();
}
/**
diff --git a/core/core/src/main/java/androidx/core/widget/TextViewCompat.java b/core/core/src/main/java/androidx/core/widget/TextViewCompat.java
index b63d1df..df34fd0 100644
--- a/core/core/src/main/java/androidx/core/widget/TextViewCompat.java
+++ b/core/core/src/main/java/androidx/core/widget/TextViewCompat.java
@@ -120,14 +120,7 @@
public static void setCompoundDrawablesRelative(@NonNull TextView textView,
@Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end,
@Nullable Drawable bottom) {
- if (Build.VERSION.SDK_INT >= 18) {
- Api17Impl.setCompoundDrawablesRelative(textView, start, top, end, bottom);
- } else if (Build.VERSION.SDK_INT >= 17) {
- boolean rtl = Api17Impl.getLayoutDirection(textView) == View.LAYOUT_DIRECTION_RTL;
- textView.setCompoundDrawables(rtl ? end : start, top, rtl ? start : end, bottom);
- } else {
- textView.setCompoundDrawables(start, top, end, bottom);
- }
+ textView.setCompoundDrawablesRelative(start, top, end, bottom);
}
/**
@@ -152,16 +145,7 @@
public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
@Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end,
@Nullable Drawable bottom) {
- if (Build.VERSION.SDK_INT >= 18) {
- Api17Impl.setCompoundDrawablesRelativeWithIntrinsicBounds(textView, start, top, end,
- bottom);
- } else if (Build.VERSION.SDK_INT >= 17) {
- boolean rtl = Api17Impl.getLayoutDirection(textView) == View.LAYOUT_DIRECTION_RTL;
- textView.setCompoundDrawablesWithIntrinsicBounds(rtl ? end : start, top,
- rtl ? start : end, bottom);
- } else {
- textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom);
- }
+ textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
}
/**
@@ -185,16 +169,7 @@
public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
@DrawableRes int start, @DrawableRes int top, @DrawableRes int end,
@DrawableRes int bottom) {
- if (Build.VERSION.SDK_INT >= 18) {
- Api17Impl.setCompoundDrawablesRelativeWithIntrinsicBounds(textView, start, top, end,
- bottom);
- } else if (Build.VERSION.SDK_INT >= 17) {
- boolean rtl = Api17Impl.getLayoutDirection(textView) == View.LAYOUT_DIRECTION_RTL;
- textView.setCompoundDrawablesWithIntrinsicBounds(rtl ? end : start, top,
- rtl ? start : end, bottom);
- } else {
- textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom);
- }
+ textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
}
/**
@@ -235,22 +210,7 @@
*/
@NonNull
public static Drawable[] getCompoundDrawablesRelative(@NonNull TextView textView) {
- if (Build.VERSION.SDK_INT >= 18) {
- return Api17Impl.getCompoundDrawablesRelative(textView);
- }
- if (Build.VERSION.SDK_INT >= 17) {
- final boolean rtl = Api17Impl.getLayoutDirection(textView) == View.LAYOUT_DIRECTION_RTL;
- final Drawable[] compounds = textView.getCompoundDrawables();
- if (rtl) {
- // If we're on RTL, we need to invert the horizontal result like above
- final Drawable start = compounds[2];
- final Drawable end = compounds[0];
- compounds[0] = start;
- compounds[2] = end;
- }
- return compounds;
- }
- return textView.getCompoundDrawables();
+ return textView.getCompoundDrawablesRelative();
}
/**
@@ -815,9 +775,7 @@
builder.setBreakStrategy(Api23Impl.getBreakStrategy(textView));
builder.setHyphenationFrequency(Api23Impl.getHyphenationFrequency(textView));
}
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
- builder.setTextDirection(getTextDirectionHeuristic(textView));
- }
+ builder.setTextDirection(getTextDirectionHeuristic(textView));
return builder.build();
}
}
@@ -833,9 +791,7 @@
// There is no way of setting text direction heuristics to TextView.
// Convert to the View's text direction int values.
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
- Api17Impl.setTextDirection(textView, getTextDirection(params.getTextDirection()));
- }
+ textView.setTextDirection(getTextDirection(params.getTextDirection()));
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
float paintTextScaleX = params.getTextPaint().getTextScaleX();
@@ -910,7 +866,7 @@
// have LTR digits, but some locales, such as those written in the Adlam or N'Ko
// scripts, have RTL digits.
final DecimalFormatSymbols symbols =
- Api24Impl.getInstance(Api17Impl.getTextLocale(textView));
+ Api24Impl.getInstance(textView.getTextLocale());
final String zero = Api28Impl.getDigitStrings(symbols)[0];
// In case the zero digit is multi-codepoint, just use the first codepoint to
// determine direction.
@@ -927,10 +883,10 @@
// Always need to resolve layout direction first
final boolean defaultIsRtl =
- (Api17Impl.getLayoutDirection(textView) == View.LAYOUT_DIRECTION_RTL);
+ (textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL);
// Now, we can select the heuristic
- switch (Api17Impl.getTextDirection(textView)) {
+ switch (textView.getTextDirection()) {
default:
case TEXT_DIRECTION_FIRST_STRONG:
return (defaultIsRtl ? TextDirectionHeuristics.FIRSTSTRONG_RTL :
@@ -953,7 +909,6 @@
/**
* Convert TextDirectionHeuristic to TextDirection int values
*/
- @RequiresApi(18)
private static int getTextDirection(@NonNull TextDirectionHeuristic heuristic) {
if (heuristic == TextDirectionHeuristics.FIRSTSTRONG_RTL) {
return TEXT_DIRECTION_FIRST_STRONG;
@@ -1045,56 +1000,6 @@
return null;
}
- @RequiresApi(17)
- static class Api17Impl {
- private Api17Impl() {
- // This class is not instantiable.
- }
-
- @DoNotInline
- static void setCompoundDrawablesRelative(TextView textView, Drawable start, Drawable top,
- Drawable end, Drawable bottom) {
- textView.setCompoundDrawablesRelative(start, top, end, bottom);
- }
-
- @DoNotInline
- static int getLayoutDirection(View view) {
- return view.getLayoutDirection();
- }
-
- @DoNotInline
- static void setCompoundDrawablesRelativeWithIntrinsicBounds(TextView textView,
- Drawable start, Drawable top, Drawable end, Drawable bottom) {
- textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
- }
-
- @DoNotInline
- static void setCompoundDrawablesRelativeWithIntrinsicBounds(TextView textView, int start,
- int top, int end, int bottom) {
- textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
- }
-
- @DoNotInline
- static Drawable[] getCompoundDrawablesRelative(TextView textView) {
- return textView.getCompoundDrawablesRelative();
- }
-
- @DoNotInline
- static void setTextDirection(View view, int textDirection) {
- view.setTextDirection(textDirection);
- }
-
- @DoNotInline
- static Locale getTextLocale(TextView textView) {
- return textView.getTextLocale();
- }
-
- @DoNotInline
- static int getTextDirection(View view) {
- return view.getTextDirection();
- }
- }
-
@RequiresApi(26)
static class Api26Impl {
private Api26Impl() {
diff --git a/emoji/emoji/src/main/java/androidx/emoji/text/EmojiCompat.java b/emoji/emoji/src/main/java/androidx/emoji/text/EmojiCompat.java
index da9477f..b42c6a3 100644
--- a/emoji/emoji/src/main/java/androidx/emoji/text/EmojiCompat.java
+++ b/emoji/emoji/src/main/java/androidx/emoji/text/EmojiCompat.java
@@ -281,8 +281,7 @@
if (config.mInitCallbacks != null && !config.mInitCallbacks.isEmpty()) {
mInitCallbacks.addAll(config.mInitCallbacks);
}
- mHelper = Build.VERSION.SDK_INT < 19 ? new CompatInternal(this) : new CompatInternal19(
- this);
+ mHelper = new CompatInternal(this);
loadMetadata();
}
@@ -1201,49 +1200,7 @@
}
}
- /**
- * Internal helper class to behave no-op for certain functions.
- */
- private static class CompatInternal {
- final EmojiCompat mEmojiCompat;
-
- CompatInternal(EmojiCompat emojiCompat) {
- mEmojiCompat = emojiCompat;
- }
-
- void loadMetadata() {
- // Moves into LOAD_STATE_SUCCESS state immediately.
- mEmojiCompat.onMetadataLoadSuccess();
- }
-
- boolean hasEmojiGlyph(@NonNull final CharSequence sequence) {
- // Since no metadata is loaded, EmojiCompat cannot detect or render any emojis.
- return false;
- }
-
- boolean hasEmojiGlyph(@NonNull final CharSequence sequence, final int metadataVersion) {
- // Since no metadata is loaded, EmojiCompat cannot detect or render any emojis.
- return false;
- }
-
- CharSequence process(@NonNull final CharSequence charSequence,
- @IntRange(from = 0) final int start, @IntRange(from = 0) final int end,
- @IntRange(from = 0) final int maxEmojiCount, boolean replaceAll) {
- // Returns the given charSequence as it is.
- return charSequence;
- }
-
- void updateEditorInfoAttrs(@NonNull final EditorInfo outAttrs) {
- // Does not add any EditorInfo attributes.
- }
-
- String getAssetSignature() {
- return "";
- }
- }
-
- @RequiresApi(19)
- private static final class CompatInternal19 extends CompatInternal {
+ private static final class CompatInternal {
/**
* Responsible to process a CharSequence and add the spans. @{code Null} until the time the
* metadata is loaded.
@@ -1254,13 +1211,13 @@
* Keeps the information about emojis. Null until the time the data is loaded.
*/
private volatile MetadataRepo mMetadataRepo;
+ final EmojiCompat mEmojiCompat;
- CompatInternal19(EmojiCompat emojiCompat) {
- super(emojiCompat);
+ CompatInternal(EmojiCompat emojiCompat) {
+ mEmojiCompat = emojiCompat;
}
- @Override
void loadMetadata() {
try {
final MetadataRepoLoaderCallback callback = new MetadataRepoLoaderCallback() {
@@ -1299,30 +1256,25 @@
mEmojiCompat.onMetadataLoadSuccess();
}
- @Override
boolean hasEmojiGlyph(@NonNull CharSequence sequence) {
return mProcessor.getEmojiMetadata(sequence) != null;
}
- @Override
boolean hasEmojiGlyph(@NonNull CharSequence sequence, int metadataVersion) {
final EmojiMetadata emojiMetadata = mProcessor.getEmojiMetadata(sequence);
return emojiMetadata != null && emojiMetadata.getCompatAdded() <= metadataVersion;
}
- @Override
CharSequence process(@NonNull CharSequence charSequence, int start, int end,
int maxEmojiCount, boolean replaceAll) {
return mProcessor.process(charSequence, start, end, maxEmojiCount, replaceAll);
}
- @Override
void updateEditorInfoAttrs(@NonNull EditorInfo outAttrs) {
outAttrs.extras.putInt(EDITOR_INFO_METAVERSION_KEY, mMetadataRepo.getMetadataVersion());
outAttrs.extras.putBoolean(EDITOR_INFO_REPLACE_ALL_KEY, mEmojiCompat.mReplaceAll);
}
- @Override
String getAssetSignature() {
final String sha = mMetadataRepo.getMetadataList().sourceSha();
return sha == null ? "" : sha;
diff --git a/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiEditTextHelper.java b/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiEditTextHelper.java
index de5847c..c41335f 100644
--- a/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiEditTextHelper.java
+++ b/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiEditTextHelper.java
@@ -15,7 +15,6 @@
*/
package androidx.emoji2.viewsintegration;
-import android.os.Build;
import android.text.method.KeyListener;
import android.text.method.NumberKeyListener;
import android.view.inputmethod.EditorInfo;
@@ -104,11 +103,7 @@
public EmojiEditTextHelper(@NonNull EditText editText,
boolean expectInitializedEmojiCompat) {
Preconditions.checkNotNull(editText, "editText cannot be null");
- if (Build.VERSION.SDK_INT < 19) {
- mHelper = new HelperInternal();
- } else {
- mHelper = new HelperInternal19(editText, expectInitializedEmojiCompat);
- }
+ mHelper = new HelperInternal19(editText, expectInitializedEmojiCompat);
}
/**
diff --git a/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiTextViewHelper.java b/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiTextViewHelper.java
index 7748ba2..6ac961b 100644
--- a/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiTextViewHelper.java
+++ b/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiTextViewHelper.java
@@ -15,7 +15,6 @@
*/
package androidx.emoji2.viewsintegration;
-import android.os.Build;
import android.text.InputFilter;
import android.text.method.PasswordTransformationMethod;
import android.text.method.TransformationMethod;
@@ -95,9 +94,7 @@
*/
public EmojiTextViewHelper(@NonNull TextView textView, boolean expectInitializedEmojiCompat) {
Preconditions.checkNotNull(textView, "textView cannot be null");
- if (Build.VERSION.SDK_INT < 19) {
- mHelper = new HelperInternal();
- } else if (!expectInitializedEmojiCompat) {
+ if (!expectInitializedEmojiCompat) {
mHelper = new SkippingHelper19(textView);
} else {
mHelper = new HelperInternal19(textView);
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/DefaultEmojiCompatConfigTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/DefaultEmojiCompatConfigTest.java
index eda1b9d..c565086 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/DefaultEmojiCompatConfigTest.java
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/DefaultEmojiCompatConfigTest.java
@@ -72,9 +72,6 @@
@SuppressWarnings("deprecation")
private boolean providerOnSystem() {
- if (Build.VERSION.SDK_INT < 19) {
- return false;
- }
List<ResolveInfo> result = ApplicationProvider.getApplicationContext()
.getPackageManager().queryIntentContentProviders(generateIntent(), 0);
for (ResolveInfo resolveInfo : result) {
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 3f6f2af..a593eb4 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java
@@ -463,8 +463,7 @@
if (config.mInitCallbacks != null && !config.mInitCallbacks.isEmpty()) {
mInitCallbacks.addAll(config.mInitCallbacks);
}
- mHelper = Build.VERSION.SDK_INT < 19 ? new CompatInternal(this) : new CompatInternal19(
- this);
+ mHelper = new CompatInternal(this);
loadMetadata();
}
@@ -1646,64 +1645,7 @@
}
}
- /**
- * Internal helper class to behave no-op for certain functions.
- */
- private static class CompatInternal {
- final EmojiCompat mEmojiCompat;
-
- CompatInternal(EmojiCompat emojiCompat) {
- mEmojiCompat = emojiCompat;
- }
-
- void loadMetadata() {
- // Moves into LOAD_STATE_SUCCESS state immediately.
- mEmojiCompat.onMetadataLoadSuccess();
- }
-
- boolean hasEmojiGlyph(@NonNull final CharSequence sequence) {
- // Since no metadata is loaded, EmojiCompat cannot detect or render any emojis.
- return false;
- }
-
- boolean hasEmojiGlyph(@NonNull final CharSequence sequence, final int metadataVersion) {
- // Since no metadata is loaded, EmojiCompat cannot detect or render any emojis.
- return false;
- }
-
- int getEmojiStart(@NonNull final CharSequence cs, @IntRange(from = 0) final int offset) {
- // Since no metadata is loaded, EmojiCompat cannot detect any emojis.
- return -1;
- }
-
- int getEmojiEnd(@NonNull final CharSequence cs, @IntRange(from = 0) final int offset) {
- // Since no metadata is loaded, EmojiCompat cannot detect any emojis.
- return -1;
- }
-
- CharSequence process(@NonNull final CharSequence charSequence,
- @IntRange(from = 0) final int start, @IntRange(from = 0) final int end,
- @IntRange(from = 0) final int maxEmojiCount, boolean replaceAll) {
- // Returns the given charSequence as it is.
- return charSequence;
- }
-
- void updateEditorInfoAttrs(@NonNull final EditorInfo outAttrs) {
- // Does not add any EditorInfo attributes.
- }
-
- String getAssetSignature() {
- return "";
- }
-
- @CodepointSequenceMatchResult
- public int getEmojiMatch(CharSequence sequence, int metadataVersion) {
- return EMOJI_UNSUPPORTED;
- }
- }
-
- @RequiresApi(19)
- private static final class CompatInternal19 extends CompatInternal {
+ private static final class CompatInternal {
/**
* Responsible to process a CharSequence and add the spans. @{code Null} until the time the
* metadata is loaded.
@@ -1714,13 +1656,12 @@
* Keeps the information about emojis. Null until the time the data is loaded.
*/
private volatile MetadataRepo mMetadataRepo;
+ private final EmojiCompat mEmojiCompat;
-
- CompatInternal19(EmojiCompat emojiCompat) {
- super(emojiCompat);
+ CompatInternal(EmojiCompat emojiCompat) {
+ mEmojiCompat = emojiCompat;
}
- @Override
void loadMetadata() {
try {
final MetadataRepoLoaderCallback callback = new MetadataRepoLoaderCallback() {
@@ -1761,45 +1702,37 @@
mEmojiCompat.onMetadataLoadSuccess();
}
- @Override
boolean hasEmojiGlyph(@NonNull CharSequence sequence) {
return mProcessor.getEmojiMatch(sequence) == EMOJI_SUPPORTED;
}
- @Override
boolean hasEmojiGlyph(@NonNull CharSequence sequence, int metadataVersion) {
int emojiMatch = mProcessor.getEmojiMatch(sequence, metadataVersion);
return emojiMatch == EMOJI_SUPPORTED;
}
- @Override
public int getEmojiMatch(CharSequence sequence, int metadataVersion) {
return mProcessor.getEmojiMatch(sequence, metadataVersion);
}
- @Override
int getEmojiStart(@NonNull final CharSequence sequence, final int offset) {
return mProcessor.getEmojiStart(sequence, offset);
}
- @Override
int getEmojiEnd(@NonNull final CharSequence sequence, final int offset) {
return mProcessor.getEmojiEnd(sequence, offset);
}
- @Override
CharSequence process(@NonNull CharSequence charSequence, int start, int end,
int maxEmojiCount, boolean replaceAll) {
return mProcessor.process(charSequence, start, end, maxEmojiCount, replaceAll);
}
- @Override
void updateEditorInfoAttrs(@NonNull EditorInfo outAttrs) {
outAttrs.extras.putInt(EDITOR_INFO_METAVERSION_KEY, mMetadataRepo.getMetadataVersion());
outAttrs.extras.putBoolean(EDITOR_INFO_REPLACE_ALL_KEY, mEmojiCompat.mReplaceAll);
}
- @Override
String getAssetSignature() {
final String sha = mMetadataRepo.getMetadataList().sourceSha();
return sha == null ? "" : sha;
diff --git a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
index ca2a19f..4381628 100644
--- a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
+++ b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
@@ -50,7 +50,6 @@
import org.junit.After;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -1443,10 +1442,6 @@
* <p>This does not check the image itself for similarity/equality.
*/
private void assertBitmapsEquivalent(File expectedImageFile, File actualImageFile) {
- if (Build.VERSION.SDK_INT < 16 && expectedImageFile.getName().endsWith("webp")) {
- // BitmapFactory can't parse WebP files on API levels before 16: b/254571189
- return;
- }
if (Build.VERSION.SDK_INT < 26
&& expectedImageFile.getName().equals(WEBP_WITHOUT_EXIF_WITH_ANIM_DATA)) {
// BitmapFactory can't parse animated WebP files on API levels before 26: b/259964971
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentContainerView.kt b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentContainerView.kt
index 43f6429..e9dd1a8 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentContainerView.kt
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentContainerView.kt
@@ -18,7 +18,6 @@
import android.animation.LayoutTransition
import android.content.Context
import android.graphics.Canvas
-import android.os.Build
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
@@ -178,13 +177,6 @@
* @attr ref android.R.styleable#ViewGroup_animateLayoutChanges
*/
public override fun setLayoutTransition(transition: LayoutTransition?) {
- if (Build.VERSION.SDK_INT < 18) {
- // Transitions on APIs below 18 are using an empty LayoutTransition as a replacement
- // for suppressLayout(true) and null LayoutTransition to then unsuppress it. If the
- // API is below 18, we should allow FrameLayout to handle this call.
- super.setLayoutTransition(transition)
- return
- }
throw UnsupportedOperationException(
"FragmentContainerView does not support Layout Transitions or " +
"animateLayoutChanges=\"true\"."
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/AppWidgetSession.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/AppWidgetSession.kt
index fdc700d..09be273 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/AppWidgetSession.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/AppWidgetSession.kt
@@ -45,7 +45,8 @@
import androidx.glance.state.GlanceState
import androidx.glance.state.GlanceStateDefinition
import kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.CompletableJob
+import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
/**
@@ -100,6 +101,7 @@
private var glanceState by mutableStateOf(initialGlanceState, neverEqualPolicy())
private var options by mutableStateOf(initialOptions, neverEqualPolicy())
private var lambdas = mapOf<String, List<LambdaAction>>()
+ private val parentJob = Job()
internal val lastRemoteViews = MutableStateFlow<RemoteViews?>(null)
@@ -226,7 +228,9 @@
lambdas[event.key]?.forEach { it.block() }
} ?: Log.w(TAG, "Triggering Action(${event.key}) for session($key) failed")
}
- is WaitForReady -> event.resume.send(Unit)
+ is WaitForReady -> {
+ event.job.apply { if (isActive) complete() }
+ }
else -> {
throw IllegalArgumentException(
"Sent unrecognized event type ${event.javaClass} to AppWidgetSession"
@@ -235,6 +239,16 @@
}
}
+ override fun onClosed() {
+ // Normally when we are closed, any pending events are processed before the channel is
+ // shutdown. However, it is possible that the Worker for this session will die before
+ // processing the remaining events. So when this session is closed, we will immediately
+ // resume all waiters without waiting for their events to be processed. If the Worker lives
+ // long enough to process their events, it will have no effect because their Jobs are no
+ // longer active.
+ parentJob.cancel()
+ }
+
suspend fun updateGlance() {
sendEvent(UpdateGlanceState)
}
@@ -247,11 +261,17 @@
sendEvent(RunLambda(key))
}
- suspend fun waitForReady() {
- WaitForReady().let {
- sendEvent(it)
- it.resume.receive()
- }
+ /**
+ * Returns a Job that can be used to wait until the session is ready (i.e. has finished
+ * processEmittableTree for the first time and is now receiving events). You can wait on the
+ * session to be ready by calling [Job.join] on the returned [Job]. When the session is ready,
+ * join will resume successfully (Job is completed). If the session is closed before it is
+ * ready, we call [Job.cancel] and the call to join resumes with [CancellationException].
+ */
+ suspend fun waitForReady(): Job {
+ val event = WaitForReady(Job(parentJob))
+ sendEvent(event)
+ return event.job
}
private fun notifyWidgetOfError(context: Context, throwable: Throwable) {
@@ -276,7 +296,5 @@
@VisibleForTesting
internal class RunLambda(val key: String)
@VisibleForTesting
- internal class WaitForReady(
- val resume: Channel<Unit> = Channel(Channel.CONFLATED)
- )
+ internal class WaitForReady(val job: CompletableJob)
}
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt
index dac5826..1ce033a 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt
@@ -31,6 +31,7 @@
import androidx.glance.appwidget.state.getAppWidgetState
import androidx.glance.session.GlanceSessionManager
import androidx.glance.session.SessionManager
+import androidx.glance.session.SessionManagerScope
import androidx.glance.state.GlanceState
import androidx.glance.state.GlanceStateDefinition
import androidx.glance.state.PreferencesGlanceStateDefinition
@@ -120,7 +121,9 @@
*/
internal suspend fun deleted(context: Context, appWidgetId: Int) {
val glanceId = AppWidgetId(appWidgetId)
- sessionManager.closeSession(glanceId.toSessionKey())
+ sessionManager.runWithLock {
+ closeSession(glanceId.toSessionKey())
+ }
try {
onDelete(context, glanceId)
} catch (cancelled: CancellationException) {
@@ -144,10 +147,12 @@
) {
Tracing.beginGlanceAppWidgetUpdate()
val glanceId = AppWidgetId(appWidgetId)
- if (!sessionManager.isSessionRunning(context, glanceId.toSessionKey())) {
- sessionManager.startSession(context, AppWidgetSession(this, glanceId, options))
- } else {
- val session = sessionManager.getSession(glanceId.toSessionKey()) as AppWidgetSession
+ sessionManager.runWithLock {
+ if (!isSessionRunning(context, glanceId.toSessionKey())) {
+ startSession(context, AppWidgetSession(this@GlanceAppWidget, glanceId, options))
+ return@runWithLock
+ }
+ val session = getSession(glanceId.toSessionKey()) as AppWidgetSession
session.updateGlance()
}
}
@@ -163,14 +168,9 @@
options: Bundle? = null,
) {
val glanceId = AppWidgetId(appWidgetId)
- val session = if (!sessionManager.isSessionRunning(context, glanceId.toSessionKey())) {
- AppWidgetSession(this, glanceId, options).also { session ->
- sessionManager.startSession(context, session)
- }
- } else {
- sessionManager.getSession(glanceId.toSessionKey()) as AppWidgetSession
+ sessionManager.getOrCreateAppWidgetSession(context, glanceId, options) { session ->
+ session.runLambda(actionKey)
}
- session.runLambda(actionKey)
}
/**
@@ -189,10 +189,7 @@
return
}
val glanceId = AppWidgetId(appWidgetId)
- if (!sessionManager.isSessionRunning(context, glanceId.toSessionKey())) {
- sessionManager.startSession(context, AppWidgetSession(this, glanceId, options))
- } else {
- val session = sessionManager.getSession(glanceId.toSessionKey()) as AppWidgetSession
+ sessionManager.getOrCreateAppWidgetSession(context, glanceId, options) { session ->
session.updateAppWidgetOptions(options)
}
}
@@ -230,6 +227,19 @@
AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, rv)
}
}
+
+ private suspend fun SessionManager.getOrCreateAppWidgetSession(
+ context: Context,
+ glanceId: AppWidgetId,
+ options: Bundle? = null,
+ block: suspend SessionManagerScope.(AppWidgetSession) -> Unit
+ ) = runWithLock {
+ if (!isSessionRunning(context, glanceId.toSessionKey())) {
+ startSession(context, AppWidgetSession(this@GlanceAppWidget, glanceId, options))
+ }
+ val session = getSession(glanceId.toSessionKey()) as AppWidgetSession
+ block(session)
+ }
}
@RestrictTo(Scope.LIBRARY_GROUP)
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceRemoteViewsService.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceRemoteViewsService.kt
index 1e91d1c..52058e4 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceRemoteViewsService.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceRemoteViewsService.kt
@@ -21,12 +21,14 @@
import android.content.Intent
import android.net.Uri
import android.os.Build
+import android.util.Log
import android.widget.RemoteViews
import android.widget.RemoteViewsService
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.glance.session.GlanceSessionManager
+import kotlinx.coroutines.channels.ClosedSendChannelException
import kotlinx.coroutines.runBlocking
/**
@@ -51,6 +53,7 @@
companion object {
const val EXTRA_VIEW_ID = "androidx.glance.widget.extra.view_id"
const val EXTRA_SIZE_INFO = "androidx.glance.widget.extra.size_info"
+ const val TAG = "GlanceRemoteViewService"
// An in-memory store containing items to be returned via the adapter when requested.
private val InMemoryStore = RemoteCollectionItemsInMemoryStore()
@@ -107,26 +110,49 @@
private fun loadData() {
runBlocking {
val glanceId = AppWidgetId(appWidgetId)
- // If session is already running, data must have already been loaded into the store
- // during composition.
- if (!GlanceSessionManager.isSessionRunning(context, glanceId.toSessionKey())) {
- startSessionAndWaitUntilReady(glanceId)
+ try {
+ startSessionIfNeededAndWaitUntilReady(glanceId)
+ } catch (e: ClosedSendChannelException) {
+ // This catch should no longer be necessary.
+ // Because we use SessionManager.runWithLock, we are guaranteed that the session
+ // we create won't be closed by concurrent calls to SessionManager. Currently,
+ // the only way a session would be closed is if there is an error in the
+ // composition that happens between the call to `startSession` and
+ // `waitForReady()` In that case, the composition error will be logged by
+ // GlanceAppWidget.onCompositionError, but could still cause
+ // ClosedSendChannelException. This is pretty unlikely, however keeping this
+ // here to avoid crashes in that scenario.
+ Log.e(TAG, "Error when trying to start session for list items", e)
}
}
}
- private suspend fun startSessionAndWaitUntilReady(glanceId: AppWidgetId) {
+ private suspend fun startSessionIfNeededAndWaitUntilReady(glanceId: AppWidgetId) {
+ val job = getGlanceAppWidget()?.let { widget ->
+ GlanceSessionManager.runWithLock {
+ if (isSessionRunning(context, glanceId.toSessionKey())) {
+ // If session is already running, data must have already been loaded into
+ // the store during composition.
+ return@runWithLock null
+ }
+ startSession(context, AppWidgetSession(widget, glanceId))
+ val session = getSession(glanceId.toSessionKey()) as AppWidgetSession
+ session.waitForReady()
+ }
+ } ?: UnmanagedSessionReceiver.getSession(appWidgetId)?.waitForReady()
+ // The following join() may throw CancellationException if the session is closed before
+ // it is ready. This will have the effect of cancelling the runBlocking scope.
+ job?.join()
+ }
+
+ private fun getGlanceAppWidget(): GlanceAppWidget? {
val appWidgetManager = AppWidgetManager.getInstance(context)
val providerInfo = appWidgetManager.getAppWidgetInfo(appWidgetId)
- providerInfo?.provider?.className?.let { className ->
+ return providerInfo?.provider?.className?.let { className ->
val receiverClass = Class.forName(className)
- val glanceAppWidget =
- (receiverClass.getDeclaredConstructor()
- .newInstance() as GlanceAppWidgetReceiver).glanceAppWidget
- AppWidgetSession(glanceAppWidget, glanceId)
- .also { GlanceSessionManager.startSession(context, it) }
- .waitForReady()
- } ?: UnmanagedSessionReceiver.getSession(appWidgetId)?.waitForReady()
+ (receiverClass.getDeclaredConstructor()
+ .newInstance() as GlanceAppWidgetReceiver).glanceAppWidget
+ }
}
override fun onDestroy() {
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/AppWidgetSessionTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/AppWidgetSessionTest.kt
index 6eaa1a5..4c62efd 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/AppWidgetSessionTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/AppWidgetSessionTest.kt
@@ -219,6 +219,25 @@
assertThat(caught).isEqualTo(null)
}
+ @Test
+ fun waitForReadyResumesWhenEventIsReceived() = runTest {
+ launch {
+ session.waitForReady().join()
+ session.close()
+ }
+ session.receiveEvents(context) {}
+ }
+
+ @Test
+ fun waitForReadyResumesWhenSessionIsClosed() = runTest {
+ launch {
+ session.waitForReady().join()
+ }
+ // Advance until waitForReady suspends.
+ this.testScheduler.advanceUntilIdle()
+ session.close()
+ }
+
private class TestGlanceState : ConfigManager {
val getValueCalls = mutableListOf<String>()
diff --git a/glance/glance/src/androidTest/kotlin/androidx/glance/session/GlanceSessionManagerTest.kt b/glance/glance/src/androidTest/kotlin/androidx/glance/session/GlanceSessionManagerTest.kt
index 3f4fa05..474e759 100644
--- a/glance/glance/src/androidTest/kotlin/androidx/glance/session/GlanceSessionManagerTest.kt
+++ b/glance/glance/src/androidTest/kotlin/androidx/glance/session/GlanceSessionManagerTest.kt
@@ -127,7 +127,9 @@
val text = assertIs<EmittableText>(testSession.uiTree.receive().children.single())
assertThat(text.text).isEqualTo("Hello World")
- assertNotNull(GlanceSessionManager.getSession(testSession.key)).close()
+ GlanceSessionManager.runWithLock {
+ assertNotNull(getSession(testSession.key)).close()
+ }
waitForWorkerSuccess()
}
@@ -150,7 +152,9 @@
// The session is not subject to a timeout before the composition has been processed
// successfully for the first time.
delay(initialTimeout * 5)
- assertThat(GlanceSessionManager.isSessionRunning(context, testSession.key)).isTrue()
+ GlanceSessionManager.runWithLock {
+ assertThat(isSessionRunning(context, testSession.key)).isTrue()
+ }
testSession.uiTree.receive()
val timeout = testTimeSource.measureTime {
@@ -189,9 +193,11 @@
}
private suspend fun startSession() {
- GlanceSessionManager.startSession(context, testSession)
- waitForWorkerStart()
- assertThat(GlanceSessionManager.isSessionRunning(context, testSession.key)).isTrue()
+ GlanceSessionManager.runWithLock {
+ startSession(context, testSession)
+ waitForWorkerStart()
+ assertThat(isSessionRunning(context, testSession.key)).isTrue()
+ }
}
private suspend fun waitForWorkerState(vararg state: State) = workerState.first {
diff --git a/glance/glance/src/main/java/androidx/glance/session/Session.kt b/glance/glance/src/main/java/androidx/glance/session/Session.kt
index c7aec1e..614aff6 100644
--- a/glance/glance/src/main/java/androidx/glance/session/Session.kt
+++ b/glance/glance/src/main/java/androidx/glance/session/Session.kt
@@ -22,6 +22,7 @@
import androidx.compose.runtime.Composable
import androidx.glance.EmittableWithChildren
import androidx.glance.GlanceComposable
+import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ClosedReceiveChannelException
@@ -32,6 +33,12 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
abstract class Session(val key: String) {
+ // _isOpen/isOpen is used to check whether this Session's event channel is still open and
+ // accepting events (close has not been called). It may be checked or set from different
+ // threads, so we use an AtomicBoolean so that the value is updated atomically.
+ private val _isOpen = AtomicBoolean(true)
+ internal val isOpen: Boolean
+ get() = _isOpen.get()
private val eventChannel = Channel<Any>(Channel.UNLIMITED)
/**
@@ -85,11 +92,22 @@
}
}
+ /**
+ * Close the session. Any events sent before [close] will be processed unless the Worker for
+ * this session is cancelled.
+ */
fun close() {
eventChannel.close()
+ _isOpen.set(false)
+ onClosed()
}
/**
+ * Called after the session is closed. Can be used by implementers to clean up any resources.
+ */
+ open fun onClosed() {}
+
+ /**
* Called when there is an error in the composition. The session will be closed immediately
* after this.
*/
diff --git a/glance/glance/src/main/java/androidx/glance/session/SessionManager.kt b/glance/glance/src/main/java/androidx/glance/session/SessionManager.kt
index 5b83da2..4099d5e 100644
--- a/glance/glance/src/main/java/androidx/glance/session/SessionManager.kt
+++ b/glance/glance/src/main/java/androidx/glance/session/SessionManager.kt
@@ -16,6 +16,7 @@
package androidx.glance.session
+import android.annotation.SuppressLint
import android.content.Context
import android.util.Log
import androidx.annotation.RestrictTo
@@ -28,6 +29,8 @@
import androidx.work.await
import androidx.work.workDataOf
import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
@JvmDefaultWithCompatibility
/**
@@ -38,6 +41,27 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
interface SessionManager {
/**
+ * [runWithLock] provides a scope in which to run operations on SessionManager.
+ *
+ * The implementation must ensure that concurrent calls to [runWithLock] are mutually exclusive.
+ * Because this function holds a lock while running [block], clients should not run any
+ * long-running operations in [block]. The client should not maintain a reference to the
+ * [SessionManagerScope] after [block] returns.
+ */
+ suspend fun <T> runWithLock(block: suspend SessionManagerScope.() -> T): T
+
+ /**
+ * The name of the session key parameter, which is used to set the session key in the Worker's
+ * input data.
+ * TODO: consider using a typealias instead
+ */
+ val keyParam: String
+ get() = "KEY"
+}
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+interface SessionManagerScope {
+ /**
* Start a session for the Glance in [session].
*/
suspend fun startSession(context: Context, session: Session)
@@ -56,9 +80,6 @@
* Gets the session corresponding to [key] if it exists
*/
fun getSession(key: String): Session?
-
- val keyParam: String
- get() = "KEY"
}
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -67,49 +88,65 @@
internal class SessionManagerImpl(
private val workerClass: Class<out ListenableWorker>
) : SessionManager {
- private val sessions = mutableMapOf<String, Session>()
- companion object {
- private const val TAG = "GlanceSessionManager"
- private const val DEBUG = false
+ private companion object {
+ const val TAG = "GlanceSessionManager"
+ const val DEBUG = false
}
- override suspend fun startSession(context: Context, session: Session) {
- if (DEBUG) Log.d(TAG, "startSession(${session.key})")
- synchronized(sessions) {
- sessions.put(session.key, session)
- }?.close()
- val workRequest = OneTimeWorkRequest.Builder(workerClass)
- .setInputData(
- workDataOf(
- keyParam to session.key
+ // This mutex guards access to the SessionManagerScope, to prevent multiple clients from
+ // performing SessionManagerScope operations at the same time.
+ private val mutex = Mutex()
+
+ // All external access to this object is protected with a mutex, so there is no need for any
+ // internal synchronization.
+ private val scope = object : SessionManagerScope {
+ private val sessions = mutableMapOf<String, Session>()
+
+ override suspend fun startSession(context: Context, session: Session) {
+ if (DEBUG) Log.d(TAG, "startSession(${session.key})")
+ sessions.put(session.key, session)?.let { previousSession ->
+ previousSession.close()
+ }
+ val workRequest = OneTimeWorkRequest.Builder(workerClass)
+ .setInputData(
+ workDataOf(
+ keyParam to session.key
+ )
)
- )
- .build()
- WorkManager.getInstance(context)
- .enqueueUniqueWork(session.key, ExistingWorkPolicy.REPLACE, workRequest)
- .result.await()
- enqueueDelayedWorker(context)
- }
-
- override fun getSession(key: String): Session? = synchronized(sessions) {
- sessions[key]
- }
-
- override suspend fun isSessionRunning(context: Context, key: String) =
- (WorkManager.getInstance(context).getWorkInfosForUniqueWork(key).await()
- .any { it.state == WorkInfo.State.RUNNING } && synchronized(sessions) {
- sessions.containsKey(key)
- }).also {
- if (DEBUG) Log.d(TAG, "isSessionRunning($key) == $it")
+ .build()
+ WorkManager.getInstance(context)
+ .enqueueUniqueWork(session.key, ExistingWorkPolicy.REPLACE, workRequest)
+ .result.await()
+ enqueueDelayedWorker(context)
}
- override suspend fun closeSession(key: String) {
- if (DEBUG) Log.d(TAG, "closeSession($key)")
- synchronized(sessions) {
- sessions.remove(key)
- }?.close()
+ override fun getSession(key: String): Session? = sessions[key]
+
+ @SuppressLint("ListIterator")
+ override suspend fun isSessionRunning(
+ context: Context,
+ key: String
+ ): Boolean {
+ val workerIsRunningOrEnqueued = WorkManager.getInstance(context)
+ .getWorkInfosForUniqueWork(key)
+ .await()
+ .any { it.state in listOf(WorkInfo.State.RUNNING, WorkInfo.State.ENQUEUED) }
+ val hasOpenSession = sessions[key]?.isOpen ?: false
+ val isRunning = hasOpenSession && workerIsRunningOrEnqueued
+ if (DEBUG) Log.d(TAG, "isSessionRunning($key) == $isRunning")
+ return isRunning
+ }
+
+ override suspend fun closeSession(key: String) {
+ if (DEBUG) Log.d(TAG, "closeSession($key)")
+ sessions.remove(key)?.close()
+ }
}
+ override suspend fun <T> runWithLock(
+ block: suspend SessionManagerScope.() -> T
+ ): T = mutex.withLock { scope.block() }
+
/**
* Workaround worker to fix b/119920965
*/
diff --git a/glance/glance/src/main/java/androidx/glance/session/SessionWorker.kt b/glance/glance/src/main/java/androidx/glance/session/SessionWorker.kt
index 2cff47b..544dd2d 100644
--- a/glance/glance/src/main/java/androidx/glance/session/SessionWorker.kt
+++ b/glance/glance/src/main/java/androidx/glance/session/SessionWorker.kt
@@ -101,12 +101,14 @@
if (DEBUG) Log.d(TAG, "Received idle event, session timeout $timeLeft")
}
) {
- val session = sessionManager.getSession(key) ?: if (params.runAttemptCount == 0) {
+ val session = sessionManager.runWithLock {
+ getSession(key)
+ } ?: if (params.runAttemptCount == 0) {
error("No session available for key $key")
} else {
- // If this is a retry because the process was restarted (e.g. on app upgrade or
- // reinstall), the Session object won't be available because it's not persistable
- // at the moment.
+ // If this is a retry because the process was restarted (e.g. on app upgrade
+ // or reinstall), the Session object won't be available because it's not
+ // persistable.
Log.w(
TAG,
"SessionWorker attempted restart but Session is not available for $key"
@@ -114,14 +116,18 @@
return@observeIdleEvents Result.success()
}
- runSession(
- applicationContext,
- session,
- timeouts,
- effectJobFactory = {
- Job().also { effectJob = it }
- }
- )
+ try {
+ runSession(
+ applicationContext,
+ session,
+ timeouts,
+ effectJobFactory = {
+ Job().also { effectJob = it }
+ }
+ )
+ } finally {
+ session.close()
+ }
Result.success()
}
} ?: Result.success(Data.Builder().putBoolean(TimeoutExitReason, true).build())
diff --git a/glance/glance/src/test/kotlin/androidx/glance/session/SessionManagerImplTest.kt b/glance/glance/src/test/kotlin/androidx/glance/session/SessionManagerImplTest.kt
index 9a05800..b47fa4f 100644
--- a/glance/glance/src/test/kotlin/androidx/glance/session/SessionManagerImplTest.kt
+++ b/glance/glance/src/test/kotlin/androidx/glance/session/SessionManagerImplTest.kt
@@ -29,9 +29,12 @@
import androidx.work.impl.WorkManagerImpl
import androidx.work.testing.WorkManagerTestInitHelper
import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.yield
import org.junit.After
import org.junit.Before
import org.junit.Test
@@ -84,19 +87,55 @@
@Test
fun startSession() = runTest {
- assertThat(sessionManager.isSessionRunning(context, key)).isFalse()
- sessionManager.startSession(context, session)
- assertThat(sessionManager.isSessionRunning(context, key)).isTrue()
- assertThat(sessionManager.getSession(key)).isSameInstanceAs(session)
+ sessionManager.runWithLock {
+ assertThat(isSessionRunning(context, key)).isFalse()
+ startSession(context, session)
+ assertThat(isSessionRunning(context, key)).isTrue()
+ assertThat(getSession(key)).isSameInstanceAs(session)
+ }
}
@Test
fun closeSession() = runTest {
- sessionManager.startSession(context, session)
- assertThat(sessionManager.isSessionRunning(context, key)).isTrue()
- sessionManager.closeSession(key)
- assertThat(sessionManager.isSessionRunning(context, key)).isFalse()
- assertThat(sessionManager.getSession(key)).isNull()
+ sessionManager.runWithLock {
+ startSession(context, session)
+ assertThat(isSessionRunning(context, key)).isTrue()
+ closeSession(key)
+ assertThat(isSessionRunning(context, key)).isFalse()
+ assertThat(getSession(key)).isNull()
+ }
+ }
+
+ @Test
+ fun closedSessionIsNotRunning() = runTest {
+ sessionManager.runWithLock {
+ assertThat(isSessionRunning(context, key)).isFalse()
+ startSession(context, session)
+ assertThat(isSessionRunning(context, key)).isTrue()
+ session.close()
+ assertThat(isSessionRunning(context, key)).isFalse()
+ }
+ }
+
+ @Test
+ fun runWithLockIsMutuallyExclusive() = runTest {
+ val firstRan = AtomicBoolean(false)
+ launch {
+ sessionManager.runWithLock {
+ yield()
+ firstRan.set(true)
+ }
+ }
+ // Because test dispatchers are single threaded and do not run background `launch`es until
+ // we suspend, we yield here to allow the first transaction to run. This resumes after the
+ // yield above, while the first transaction still has the lock but is suspended.
+ yield()
+ // This call to runWithLock should suspend until the first transaction finishes, then run
+ // the block. If it is not mutually exclusive, it will run right away and firstRan will not
+ // be true.
+ sessionManager.runWithLock {
+ assertThat(firstRan.get()).isTrue()
+ }
}
}
diff --git a/glance/glance/src/test/kotlin/androidx/glance/session/SessionWorkerTest.kt b/glance/glance/src/test/kotlin/androidx/glance/session/SessionWorkerTest.kt
index c8275e6..693218f 100644
--- a/glance/glance/src/test/kotlin/androidx/glance/session/SessionWorkerTest.kt
+++ b/glance/glance/src/test/kotlin/androidx/glance/session/SessionWorkerTest.kt
@@ -75,8 +75,8 @@
val result = worker.doWork()
assertThat(result).isEqualTo(Result.success())
}
- sessionManager.startSession(context)
- sessionManager.closeSession()
+ sessionManager.scope.startSession(context)
+ sessionManager.scope.closeSession()
}
@Test
@@ -86,7 +86,7 @@
assertThat(result).isEqualTo(Result.success())
}
- val root = sessionManager.startSession(context) {
+ val root = sessionManager.scope.startSession(context) {
Box {
Text("Hello World")
}
@@ -94,7 +94,7 @@
val box = assertIs<EmittableBox>(root.children.single())
val text = assertIs<EmittableText>(box.children.single())
assertThat(text.text).isEqualTo("Hello World")
- sessionManager.closeSession()
+ sessionManager.scope.closeSession()
}
@Test
@@ -103,10 +103,10 @@
val result = worker.doWork()
assertThat(result).isEqualTo(Result.success())
}
- sessionManager.startSession(context).first()
- val session = assertIs<TestSession>(sessionManager.getSession(SESSION_KEY))
+ sessionManager.scope.startSession(context).first()
+ val session = assertIs<TestSession>(sessionManager.scope.getSession(SESSION_KEY))
assertThat(session.provideGlanceCalled).isEqualTo(1)
- sessionManager.closeSession()
+ sessionManager.scope.closeSession()
}
@Test
@@ -117,7 +117,7 @@
}
val state = mutableStateOf("Hello World")
- val uiFlow = sessionManager.startSession(context) {
+ val uiFlow = sessionManager.scope.startSession(context) {
Text(state.value)
}
uiFlow.first().getOrThrow().let { root ->
@@ -130,7 +130,7 @@
val text = assertIs<EmittableText>(root.children.single())
assertThat(text.text).isEqualTo("Hello Earth")
}
- sessionManager.closeSession()
+ sessionManager.scope.closeSession()
}
@Test
@@ -141,14 +141,14 @@
}
val state = mutableStateOf("Hello World")
- val uiFlow = sessionManager.startSession(context) {
+ val uiFlow = sessionManager.scope.startSession(context) {
Text(state.value)
}
uiFlow.first().getOrThrow().let { root ->
val text = assertIs<EmittableText>(root.children.single())
assertThat(text.text).isEqualTo("Hello World")
}
- val session = assertIs<TestSession>(sessionManager.getSession(SESSION_KEY))
+ val session = assertIs<TestSession>(sessionManager.scope.getSession(SESSION_KEY))
session.sendEvent {
state.value = "Hello Earth"
}
@@ -156,7 +156,7 @@
val text = assertIs<EmittableText>(root.children.single())
assertThat(text.text).isEqualTo("Hello Earth")
}
- sessionManager.closeSession()
+ sessionManager.scope.closeSession()
}
@Test
@@ -167,7 +167,7 @@
}
val state = mutableStateOf("Hello World")
- val uiFlow = sessionManager.startDelayedProcessingSession(context) {
+ val uiFlow = sessionManager.scope.startDelayedProcessingSession(context) {
Text(state.value)
}
uiFlow.first().getOrThrow().let { root ->
@@ -183,9 +183,9 @@
assertThat(text.text).isEqualTo("Hello Earth")
}
- val session = assertIs<TestSession>(sessionManager.getSession(SESSION_KEY))
+ val session = assertIs<TestSession>(sessionManager.scope.getSession(SESSION_KEY))
assertThat(session.processEmittableTreeCancelCount).isEqualTo(1)
- sessionManager.closeSession()
+ sessionManager.scope.closeSession()
}
@Test
@@ -197,7 +197,7 @@
val cause = Throwable()
val exception = Exception("message", cause)
- val result = sessionManager.startSession(context) {
+ val result = sessionManager.scope.startSession(context) {
throw exception
}.first().exceptionOrNull()
assertThat(result).hasCauseThat().isEqualTo(cause)
@@ -214,7 +214,7 @@
val runError = mutableStateOf(false)
val cause = Throwable()
val exception = Exception("message", cause)
- val resultFlow = sessionManager.startSession(context) {
+ val resultFlow = sessionManager.scope.startSession(context) {
if (runError.value) {
throw exception
} else {
@@ -245,7 +245,7 @@
val cause = Throwable()
val exception = Exception("message", cause)
- val result = sessionManager.startSession(context) {
+ val result = sessionManager.scope.startSession(context) {
SideEffect { throw exception }
}.first().exceptionOrNull()
assertThat(result).hasCauseThat().isEqualTo(cause)
@@ -261,7 +261,7 @@
val cause = Throwable()
val exception = Exception("message", cause)
- val result = sessionManager.startSession(context) {
+ val result = sessionManager.scope.startSession(context) {
LaunchedEffect(true) { throw exception }
}.first().exceptionOrNull()
assertThat(result).hasCauseThat().isEqualTo(cause)
@@ -282,7 +282,7 @@
}
}
- sessionManager.startSession(context).first()
+ sessionManager.scope.startSession(context).first()
workerJob.cancel()
}
@@ -294,54 +294,60 @@
assertThat(worker.effectJob?.isCancelled).isTrue()
}
- sessionManager.startSession(context).first()
- sessionManager.closeSession()
+ sessionManager.scope.startSession(context).first()
+ sessionManager.scope.closeSession()
}
}
private const val SESSION_KEY = "123"
class TestSessionManager : SessionManager {
- private val sessions = mutableMapOf<String, Session>()
+ val scope = TestSessionManagerScope()
+ // No locking needed, tests are run on single threaded environment and is only user of this
+ // SessionManager.
+ override suspend fun <T> runWithLock(block: suspend SessionManagerScope.() -> T): T =
+ scope.block()
- suspend fun startSession(
- context: Context,
- content: @GlanceComposable @Composable () -> Unit = {}
- ) = MutableSharedFlow<kotlin.Result<EmittableWithChildren>>().also { flow ->
- startSession(context, TestSession(resultFlow = flow, content = content))
- }
+ class TestSessionManagerScope : SessionManagerScope {
+ private val sessions = mutableMapOf<String, Session>()
+ suspend fun startSession(
+ context: Context,
+ content: @GlanceComposable @Composable () -> Unit = {}
+ ) = MutableSharedFlow<kotlin.Result<EmittableWithChildren>>().also { flow ->
+ startSession(context, TestSession(resultFlow = flow, content = content))
+ }
- suspend fun startDelayedProcessingSession(
- context: Context,
- content: @GlanceComposable @Composable () -> Unit = {}
- ) = MutableSharedFlow<kotlin.Result<EmittableWithChildren>>().also { flow ->
- startSession(
- context,
- TestSession(
- resultFlow = flow,
- content = content,
- processEmittableTreeHasInfiniteDelay = true,
+ suspend fun startDelayedProcessingSession(
+ context: Context,
+ content: @GlanceComposable @Composable () -> Unit = {}
+ ) = MutableSharedFlow<kotlin.Result<EmittableWithChildren>>().also { flow ->
+ startSession(
+ context,
+ TestSession(
+ resultFlow = flow,
+ content = content,
+ processEmittableTreeHasInfiniteDelay = true,
+ )
)
- )
- }
+ }
- suspend fun closeSession() {
- closeSession(SESSION_KEY)
- }
+ suspend fun closeSession() {
+ closeSession(SESSION_KEY)
+ }
- override suspend fun startSession(context: Context, session: Session) {
- sessions[session.key] = session
- }
+ override suspend fun startSession(context: Context, session: Session) {
+ sessions[session.key] = session
+ }
- override suspend fun closeSession(key: String) {
- sessions[key]?.close()
- }
+ override suspend fun closeSession(key: String) {
+ sessions[key]?.close()
+ }
- override suspend fun isSessionRunning(context: Context, key: String): Boolean {
- TODO("Not yet implemented")
+ override suspend fun isSessionRunning(context: Context, key: String): Boolean {
+ TODO("Not yet implemented")
+ }
+ override fun getSession(key: String): Session? = sessions[key]
}
-
- override fun getSession(key: String): Session? = sessions[key]
}
class TestSession(
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/CanvasBufferedRendererTests.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/CanvasBufferedRendererTests.kt
index c606168..3a91718 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/CanvasBufferedRendererTests.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/CanvasBufferedRendererTests.kt
@@ -138,12 +138,16 @@
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
@Test
fun testPreservationEnabledPreservesContents() =
- verifyPreservedBuffer(CanvasBufferedRenderer.DEFAULT_IMPL)
+ repeat(20) {
+ verifyPreservedBuffer(CanvasBufferedRenderer.DEFAULT_IMPL)
+ }
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
@Test
fun testPreservationEnabledPreservesContentsWithRedrawStrategy() =
- verifyPreservedBuffer(CanvasBufferedRenderer.USE_V29_IMPL_WITH_REDRAW)
+ repeat(20) {
+ verifyPreservedBuffer(CanvasBufferedRenderer.USE_V29_IMPL_WITH_REDRAW)
+ }
@RequiresApi(Build.VERSION_CODES.Q)
private fun verifyPreservedBuffer(
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/CanvasBufferedRendererV29.kt b/graphics/graphics-core/src/main/java/androidx/graphics/CanvasBufferedRendererV29.kt
index acb359c..a9ae731 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/CanvasBufferedRendererV29.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/CanvasBufferedRendererV29.kt
@@ -155,7 +155,7 @@
CanvasBufferedRenderer.RenderResult(
buffer,
fence,
- if (result != 0) ERROR_UNKNOWN else SUCCESS
+ if (isSuccess(result)) SUCCESS else ERROR_UNKNOWN
)
)
if (mMaxBuffers == 1) {
@@ -171,6 +171,14 @@
}
}
+ /**
+ * Helper method to determine if [HardwareRenderer.FrameRenderRequest.syncAndDraw] was
+ * successful. In this case we wait for the next buffer even if we miss the vsync.
+ */
+ private fun isSuccess(result: Int) =
+ result == HardwareRenderer.SYNC_OK ||
+ result == HardwareRenderer.SYNC_FRAME_DROPPED
+
private fun updateTransform(transform: Int): Matrix {
mBufferTransform = transform
return BufferTransformHintResolver.configureTransformMatrix(
diff --git a/leanback/leanback/src/main/java/androidx/leanback/transition/LeanbackTransitionHelper.java b/leanback/leanback/src/main/java/androidx/leanback/transition/LeanbackTransitionHelper.java
index f1389ba..5c5eda2 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/transition/LeanbackTransitionHelper.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/transition/LeanbackTransitionHelper.java
@@ -30,7 +30,7 @@
public class LeanbackTransitionHelper {
public static Object loadTitleInTransition(Context context) {
- if (Build.VERSION.SDK_INT < 19 || Build.VERSION.SDK_INT >= 21) {
+ if (Build.VERSION.SDK_INT >= 21) {
return TransitionHelper.loadTransition(context, R.transition.lb_title_in);
}
@@ -43,7 +43,7 @@
}
public static Object loadTitleOutTransition(Context context) {
- if (Build.VERSION.SDK_INT < 19 || Build.VERSION.SDK_INT >= 21) {
+ if (Build.VERSION.SDK_INT >= 21) {
return TransitionHelper.loadTransition(context, R.transition.lb_title_out);
}
diff --git a/libraryversions.toml b/libraryversions.toml
index ca8477b..6951452 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -139,7 +139,7 @@
TEST_UIAUTOMATOR = "2.3.0-alpha05"
TEXT = "1.0.0-alpha01"
TRACING = "1.3.0-alpha02"
-TRACING_PERFETTO = "1.0.0-beta03"
+TRACING_PERFETTO = "1.0.0"
TRANSITION = "1.5.0-alpha05"
TV = "1.0.0-alpha11"
TVPROVIDER = "1.1.0-alpha02"
diff --git a/media/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java b/media/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java
index a92d2198..cf9e20d 100644
--- a/media/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java
+++ b/media/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java
@@ -553,15 +553,9 @@
? Looper.myLooper() : Looper.getMainLooper());
setCallback(new Callback() {}, handler);
mImpl.setMediaButtonReceiver(mbrIntent);
- } else if (android.os.Build.VERSION.SDK_INT >= 19) {
+ } else {
mImpl = new MediaSessionImplApi19(context, tag, mbrComponent, mbrIntent,
session2Token, sessionInfo);
- } else if (android.os.Build.VERSION.SDK_INT >= 18) {
- mImpl = new MediaSessionImplApi18(context, tag, mbrComponent, mbrIntent,
- session2Token, sessionInfo);
- } else {
- mImpl = new MediaSessionImplBase(context, tag, mbrComponent, mbrIntent, session2Token,
- sessionInfo);
}
mController = new MediaControllerCompat(context, this);
@@ -3758,7 +3752,6 @@
}
}
- @RequiresApi(18)
static class MediaSessionImplApi18 extends MediaSessionImplBase {
private static boolean sIsMbrPendingIntentSupported = true;
@@ -3844,7 +3837,6 @@
}
}
- @RequiresApi(19)
static class MediaSessionImplApi19 extends MediaSessionImplApi18 {
MediaSessionImplApi19(Context context, String tag, ComponentName mbrComponent,
PendingIntent mbrIntent, VersionedParcelable session2Token, Bundle sessionInfo) {
diff --git a/media2/media2-common/src/main/java/androidx/media2/common/ClassVerificationHelper.java b/media2/media2-common/src/main/java/androidx/media2/common/ClassVerificationHelper.java
index f6e962ee..5fc397b 100644
--- a/media2/media2-common/src/main/java/androidx/media2/common/ClassVerificationHelper.java
+++ b/media2/media2-common/src/main/java/androidx/media2/common/ClassVerificationHelper.java
@@ -51,25 +51,6 @@
private AudioManager() {}
}
- /** Helper class for {@link android.os.HandlerThread}. */
- public static final class HandlerThread {
-
- /** Helper methods for {@link android.os.HandlerThread} APIs added in API level 18. */
- @RequiresApi(18)
- public static final class Api18 {
-
- /** Helper method to call {@link android.os.HandlerThread#quitSafely()}. */
- @DoNotInline
- public static boolean quitSafely(@NonNull android.os.HandlerThread handlerThread) {
- return handlerThread.quitSafely();
- }
-
- private Api18() {}
- }
-
- private HandlerThread() {}
- }
-
/** Helper class for {@link android.app.PendingIntent}. */
public static final class PendingIntent {
diff --git a/media2/media2-player/src/androidTest/java/androidx/media2/player/MediaPlayer_AudioFocusTest.java b/media2/media2-player/src/androidTest/java/androidx/media2/player/MediaPlayer_AudioFocusTest.java
index 20d1c73..6636bb4 100644
--- a/media2/media2-player/src/androidTest/java/androidx/media2/player/MediaPlayer_AudioFocusTest.java
+++ b/media2/media2-player/src/androidTest/java/androidx/media2/player/MediaPlayer_AudioFocusTest.java
@@ -43,7 +43,6 @@
import android.content.Intent;
import android.media.AudioManager;
import android.media.AudioManager.OnAudioFocusChangeListener;
-import android.os.Build;
import android.os.Build.VERSION;
import android.os.HandlerThread;
import android.os.Looper;
@@ -125,11 +124,7 @@
if (sHandler == null) {
return;
}
- if (Build.VERSION.SDK_INT >= 18) {
- sHandler.getLooper().quitSafely();
- } else {
- sHandler.getLooper().quit();
- }
+ sHandler.getLooper().quitSafely();
sHandler = null;
sHandlerExecutor = null;
}
diff --git a/media2/media2-session/src/androidTest/java/androidx/media2/session/MediaSessionTestBase.java b/media2/media2-session/src/androidTest/java/androidx/media2/session/MediaSessionTestBase.java
index d39d457..2b482e0 100644
--- a/media2/media2-session/src/androidTest/java/androidx/media2/session/MediaSessionTestBase.java
+++ b/media2/media2-session/src/androidTest/java/androidx/media2/session/MediaSessionTestBase.java
@@ -17,7 +17,6 @@
package androidx.media2.session;
import android.content.Context;
-import android.os.Build;
import android.os.Bundle;
import android.os.HandlerThread;
@@ -84,11 +83,7 @@
if (sHandler == null) {
return;
}
- if (Build.VERSION.SDK_INT >= 18) {
- sHandler.getLooper().quitSafely();
- } else {
- sHandler.getLooper().quit();
- }
+ sHandler.getLooper().quitSafely();
sHandler = null;
sHandlerExecutor = null;
}
diff --git a/media2/media2-session/src/main/java/androidx/media2/session/MediaControllerImplLegacy.java b/media2/media2-session/src/main/java/androidx/media2/session/MediaControllerImplLegacy.java
index dc21534..9a4987c 100644
--- a/media2/media2-session/src/main/java/androidx/media2/session/MediaControllerImplLegacy.java
+++ b/media2/media2-session/src/main/java/androidx/media2/session/MediaControllerImplLegacy.java
@@ -36,7 +36,6 @@
import android.app.PendingIntent;
import android.content.Context;
import android.net.Uri;
-import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
@@ -56,7 +55,6 @@
import androidx.annotation.Nullable;
import androidx.concurrent.futures.ResolvableFuture;
import androidx.core.util.ObjectsCompat;
-import androidx.media2.common.ClassVerificationHelper;
import androidx.media2.common.MediaItem;
import androidx.media2.common.MediaMetadata;
import androidx.media2.common.Rating;
@@ -205,11 +203,7 @@
}
mHandler.removeCallbacksAndMessages(null);
- if (Build.VERSION.SDK_INT >= 18) {
- ClassVerificationHelper.HandlerThread.Api18.quitSafely(mHandlerThread);
- } else {
- mHandlerThread.quit();
- }
+ mHandlerThread.quitSafely();
mClosed = true;
diff --git a/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java b/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
index 52ae967..e4ea230 100644
--- a/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
+++ b/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
@@ -322,11 +322,7 @@
});
mHandler.removeCallbacksAndMessages(null);
if (mHandlerThread.isAlive()) {
- if (Build.VERSION.SDK_INT >= 18) {
- ClassVerificationHelper.HandlerThread.Api18.quitSafely(mHandlerThread);
- } else {
- mHandlerThread.quit();
- }
+ mHandlerThread.quitSafely();
}
}
diff --git a/media2/media2-session/src/main/java/androidx/media2/session/SessionToken.java b/media2/media2-session/src/main/java/androidx/media2/session/SessionToken.java
index b6c7f26..a0997be 100644
--- a/media2/media2-session/src/main/java/androidx/media2/session/SessionToken.java
+++ b/media2/media2-session/src/main/java/androidx/media2/session/SessionToken.java
@@ -23,7 +23,6 @@
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
-import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
@@ -38,7 +37,6 @@
import androidx.annotation.RestrictTo;
import androidx.media.MediaBrowserServiceCompat;
import androidx.media.MediaSessionManager;
-import androidx.media2.common.ClassVerificationHelper;
import androidx.versionedparcelable.ParcelField;
import androidx.versionedparcelable.VersionedParcelable;
import androidx.versionedparcelable.VersionedParcelize;
@@ -345,11 +343,7 @@
@SuppressWarnings("WeakerAccess") /* synthetic access */
static void quitHandlerThread(HandlerThread thread) {
- if (Build.VERSION.SDK_INT >= 18) {
- ClassVerificationHelper.HandlerThread.Api18.quitSafely(thread);
- } else {
- thread.quit();
- }
+ thread.quitSafely();
}
@SuppressWarnings("deprecation")
diff --git a/media2/media2-session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/tests/MediaSessionTestBase.java b/media2/media2-session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/tests/MediaSessionTestBase.java
index 639fb5b..ea17c74 100644
--- a/media2/media2-session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/tests/MediaSessionTestBase.java
+++ b/media2/media2-session/version-compat-tests/current/client/src/androidTest/java/androidx/media2/test/client/tests/MediaSessionTestBase.java
@@ -17,7 +17,6 @@
package androidx.media2.test.client.tests;
import android.content.Context;
-import android.os.Build;
import android.os.Bundle;
import android.os.HandlerThread;
import android.support.v4.media.session.MediaSessionCompat;
@@ -88,11 +87,7 @@
if (sHandler == null) {
return;
}
- if (Build.VERSION.SDK_INT >= 18) {
- sHandler.getLooper().quitSafely();
- } else {
- sHandler.getLooper().quit();
- }
+ sHandler.getLooper().quitSafely();
sHandler = null;
sHandlerExecutor = null;
}
diff --git a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/MockMediaLibraryService.java b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/MockMediaLibraryService.java
index 78b5841..cb8b580 100644
--- a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/MockMediaLibraryService.java
+++ b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/MockMediaLibraryService.java
@@ -54,7 +54,6 @@
import android.app.Service;
import android.content.Context;
-import android.os.Build;
import android.os.Bundle;
import android.os.HandlerThread;
import android.util.Log;
@@ -124,11 +123,7 @@
sAssertLibraryParams = false;
sExpectedParams = null;
}
- if (Build.VERSION.SDK_INT >= 18) {
- mHandler.getLooper().quitSafely();
- } else {
- mHandler.getLooper().quit();
- }
+ mHandler.getLooper().quitSafely();
mHandler = null;
TestServiceRegistry.getInstance().cleanUp();
}
diff --git a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionTestBase.java b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionTestBase.java
index 6db0c22..40529f8 100644
--- a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionTestBase.java
+++ b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionTestBase.java
@@ -17,7 +17,6 @@
package androidx.media2.test.service.tests;
import android.content.Context;
-import android.os.Build;
import android.os.Bundle;
import android.os.HandlerThread;
@@ -79,11 +78,7 @@
if (sHandler == null) {
return;
}
- if (Build.VERSION.SDK_INT >= 18) {
- sHandler.getLooper().quitSafely();
- } else {
- sHandler.getLooper().quit();
- }
+ sHandler.getLooper().quitSafely();
sHandler = null;
sHandlerExecutor = null;
}
diff --git a/media2/media2-session/version-compat-tests/previous/client/src/androidTest/java/androidx/media2/test/client/tests/MediaSessionTestBase.java b/media2/media2-session/version-compat-tests/previous/client/src/androidTest/java/androidx/media2/test/client/tests/MediaSessionTestBase.java
index 639fb5b..ea17c74 100644
--- a/media2/media2-session/version-compat-tests/previous/client/src/androidTest/java/androidx/media2/test/client/tests/MediaSessionTestBase.java
+++ b/media2/media2-session/version-compat-tests/previous/client/src/androidTest/java/androidx/media2/test/client/tests/MediaSessionTestBase.java
@@ -17,7 +17,6 @@
package androidx.media2.test.client.tests;
import android.content.Context;
-import android.os.Build;
import android.os.Bundle;
import android.os.HandlerThread;
import android.support.v4.media.session.MediaSessionCompat;
@@ -88,11 +87,7 @@
if (sHandler == null) {
return;
}
- if (Build.VERSION.SDK_INT >= 18) {
- sHandler.getLooper().quitSafely();
- } else {
- sHandler.getLooper().quit();
- }
+ sHandler.getLooper().quitSafely();
sHandler = null;
sHandlerExecutor = null;
}
diff --git a/media2/media2-session/version-compat-tests/previous/service/src/androidTest/java/androidx/media2/test/service/MockMediaLibraryService.java b/media2/media2-session/version-compat-tests/previous/service/src/androidTest/java/androidx/media2/test/service/MockMediaLibraryService.java
index 78b5841..cb8b580 100644
--- a/media2/media2-session/version-compat-tests/previous/service/src/androidTest/java/androidx/media2/test/service/MockMediaLibraryService.java
+++ b/media2/media2-session/version-compat-tests/previous/service/src/androidTest/java/androidx/media2/test/service/MockMediaLibraryService.java
@@ -54,7 +54,6 @@
import android.app.Service;
import android.content.Context;
-import android.os.Build;
import android.os.Bundle;
import android.os.HandlerThread;
import android.util.Log;
@@ -124,11 +123,7 @@
sAssertLibraryParams = false;
sExpectedParams = null;
}
- if (Build.VERSION.SDK_INT >= 18) {
- mHandler.getLooper().quitSafely();
- } else {
- mHandler.getLooper().quit();
- }
+ mHandler.getLooper().quitSafely();
mHandler = null;
TestServiceRegistry.getInstance().cleanUp();
}
diff --git a/media2/media2-session/version-compat-tests/previous/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionTestBase.java b/media2/media2-session/version-compat-tests/previous/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionTestBase.java
index 6db0c22..40529f8 100644
--- a/media2/media2-session/version-compat-tests/previous/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionTestBase.java
+++ b/media2/media2-session/version-compat-tests/previous/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionTestBase.java
@@ -17,7 +17,6 @@
package androidx.media2.test.service.tests;
import android.content.Context;
-import android.os.Build;
import android.os.Bundle;
import android.os.HandlerThread;
@@ -79,11 +78,7 @@
if (sHandler == null) {
return;
}
- if (Build.VERSION.SDK_INT >= 18) {
- sHandler.getLooper().quitSafely();
- } else {
- sHandler.getLooper().quit();
- }
+ sHandler.getLooper().quitSafely();
sHandler = null;
sHandlerExecutor = null;
}
diff --git a/media2/media2-widget/src/main/java/androidx/media2/widget/ClosedCaptionWidget.java b/media2/media2-widget/src/main/java/androidx/media2/widget/ClosedCaptionWidget.java
index 70d7711..619a73b 100644
--- a/media2/media2-widget/src/main/java/androidx/media2/widget/ClosedCaptionWidget.java
+++ b/media2/media2-widget/src/main/java/androidx/media2/widget/ClosedCaptionWidget.java
@@ -156,9 +156,6 @@
* Manages whether this renderer is listening for caption style changes.
*/
private void manageChangeListener() {
- if (VERSION.SDK_INT < 19) {
- return;
- }
final boolean needsListener =
ViewCompat.isAttachedToWindow(this) && getVisibility() == View.VISIBLE;
if (mHasChangeListener != needsListener) {
diff --git a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphTest.kt b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphTest.kt
index c0b072a..937c4c8 100644
--- a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphTest.kt
+++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphTest.kt
@@ -97,6 +97,34 @@
.isFalse()
}
+ @Test
+ fun graphEqualsId() {
+ val graph = NavGraph(navGraphNavigator)
+ graph += navigator.createDestination().apply { id = DESTINATION_ID }
+ graph += navigator.createDestination().apply { id = SECOND_DESTINATION_ID }
+ val other = NavGraph(navGraphNavigator)
+ other += navigator.createDestination().apply { id = DESTINATION_ID }
+ other += navigator.createDestination().apply { id = SECOND_DESTINATION_ID }
+
+ assertWithMessage("Graphs should be equal")
+ .that(graph)
+ .isEqualTo(other)
+ }
+
+ @Test
+ fun graphNotEqualsId() {
+ val graph = NavGraph(navGraphNavigator)
+ graph += navigator.createDestination().apply { id = DESTINATION_ID }
+ graph += navigator.createDestination().apply { id = SECOND_DESTINATION_ID }
+ val other = NavGraph(navGraphNavigator)
+ other += navigator.createDestination().apply { id = DESTINATION_ID }
+ other += navigator.createDestination().apply { id = 3 }
+
+ assertWithMessage("Graphs should not be equal")
+ .that(graph)
+ .isNotEqualTo(other)
+ }
+
@Test(expected = IllegalArgumentException::class)
fun getIllegalArgumentException() {
val graph = NavGraph(navGraphNavigator)
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
index f4b21df..1e1020e 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
@@ -384,7 +384,7 @@
return super.equals(other) &&
nodes.size == other.nodes.size &&
startDestinationId == other.startDestinationId &&
- nodes.valueIterator().asSequence().all { it == nodes.get(it.id) }
+ nodes.valueIterator().asSequence().all { it == other.nodes.get(it.id) }
}
override fun hashCode(): Int {
diff --git a/percentlayout/percentlayout/src/androidTest/java/androidx/percentlayout/widget/PercentRelativeRtlTest.java b/percentlayout/percentlayout/src/androidTest/java/androidx/percentlayout/widget/PercentRelativeRtlTest.java
index 640be5b..68d01a3 100644
--- a/percentlayout/percentlayout/src/androidTest/java/androidx/percentlayout/widget/PercentRelativeRtlTest.java
+++ b/percentlayout/percentlayout/src/androidTest/java/androidx/percentlayout/widget/PercentRelativeRtlTest.java
@@ -161,22 +161,14 @@
@Test
public void testStartChild() {
- if (Build.VERSION.SDK_INT == 17) {
- return;
- }
+
final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_start);
- if (Build.VERSION.SDK_INT >= 17) {
- switchToRtl();
+ switchToRtl();
- final int childRight = childToTest.getRight();
- assertFuzzyEquals("Child start margin as 5% of the container",
- 0.05f * mContainerWidth, mContainerWidth - childRight);
- } else {
- final int childLeft = childToTest.getLeft();
- assertFuzzyEquals("Child start margin as 5% of the container",
- 0.05f * mContainerWidth, childLeft);
- }
+ final int childRight = childToTest.getRight();
+ assertFuzzyEquals("Child start margin as 5% of the container",
+ 0.05f * mContainerWidth, mContainerWidth - childRight);
final int childWidth = childToTest.getWidth();
final int childHeight = childToTest.getHeight();
@@ -194,23 +186,13 @@
@Test
public void testBottomChild() {
- if (Build.VERSION.SDK_INT == 17) {
- return;
- }
final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_bottom);
- if (Build.VERSION.SDK_INT >= 17) {
- switchToRtl();
+ switchToRtl();
- final int childLeft = childToTest.getLeft();
- assertFuzzyEquals("Child end margin as 20% of the container",
- 0.2f * mContainerWidth, childLeft);
- } else {
- final int childRight = childToTest.getRight();
- assertFuzzyEquals("Child end margin as 20% of the container",
- 0.2f * mContainerWidth, mContainerWidth - childRight);
- }
-
+ final int childLeft = childToTest.getLeft();
+ assertFuzzyEquals("Child end margin as 20% of the container",
+ 0.2f * mContainerWidth, childLeft);
final int childWidth = childToTest.getWidth();
final int childHeight = childToTest.getHeight();
@@ -228,22 +210,13 @@
@Test
public void testEndChild() {
- if (Build.VERSION.SDK_INT == 17) {
- return;
- }
final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_end);
- if (Build.VERSION.SDK_INT >= 17) {
- switchToRtl();
+ switchToRtl();
- final int childLeft = childToTest.getLeft();
- assertFuzzyEquals("Child end margin as 5% of the container",
- 0.05f * mContainerWidth, childLeft);
- } else {
- final int childRight = childToTest.getRight();
- assertFuzzyEquals("Child end margin as 5% of the container",
- 0.05f * mContainerWidth, mContainerWidth - childRight);
- }
+ final int childLeft = childToTest.getLeft();
+ assertFuzzyEquals("Child end margin as 5% of the container",
+ 0.05f * mContainerWidth, childLeft);
final int childWidth = childToTest.getWidth();
final int childHeight = childToTest.getHeight();
@@ -261,24 +234,15 @@
@Test
public void testCenterChild() {
- if (Build.VERSION.SDK_INT == 17) {
- return;
- }
final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_center);
-
- boolean supportsRtl = Build.VERSION.SDK_INT >= 17;
- if (supportsRtl) {
- switchToRtl();
- }
+ switchToRtl();
final int childLeft = childToTest.getLeft();
final int childTop = childToTest.getTop();
final int childRight = childToTest.getRight();
final int childBottom = childToTest.getBottom();
- final View leftChild = supportsRtl
- ? mPercentRelativeLayout.findViewById(R.id.child_end)
- : mPercentRelativeLayout.findViewById(R.id.child_start);
+ final View leftChild = mPercentRelativeLayout.findViewById(R.id.child_end);
assertFuzzyEquals("Child left margin as 10% of the container",
leftChild.getRight() + 0.1f * mContainerWidth, childLeft);
@@ -286,9 +250,8 @@
assertFuzzyEquals("Child top margin as 10% of the container",
topChild.getBottom() + 0.1f * mContainerHeight, childTop);
- final View rightChild = supportsRtl
- ? mPercentRelativeLayout.findViewById(R.id.child_start)
- : mPercentRelativeLayout.findViewById(R.id.child_end);
+ final View rightChild = mPercentRelativeLayout.findViewById(R.id.child_start);
+
assertFuzzyEquals("Child right margin as 10% of the container",
rightChild.getLeft() - 0.1f * mContainerWidth, childRight);
diff --git a/print/print/src/main/java/androidx/print/PrintHelper.java b/print/print/src/main/java/androidx/print/PrintHelper.java
index 01f01f7..7c1ed96 100644
--- a/print/print/src/main/java/androidx/print/PrintHelper.java
+++ b/print/print/src/main/java/androidx/print/PrintHelper.java
@@ -259,7 +259,7 @@
*/
public void printBitmap(@NonNull final String jobName, @NonNull final Bitmap bitmap,
@Nullable final OnPrintFinishCallback callback) {
- if (Build.VERSION.SDK_INT < 19 || bitmap == null) {
+ if (bitmap == null) {
return;
}
@@ -357,10 +357,6 @@
public void printBitmap(@NonNull final String jobName, @NonNull final Uri imageFile,
@Nullable final OnPrintFinishCallback callback)
throws FileNotFoundException {
- if (Build.VERSION.SDK_INT < 19) {
- return;
- }
-
PrintDocumentAdapter printDocumentAdapter = new PrintUriAdapter(jobName, imageFile,
callback, mScaleMode);
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewLayoutTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewLayoutTest.java
index c19f539..741bb69 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewLayoutTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewLayoutTest.java
@@ -53,7 +53,6 @@
import android.graphics.Color;
import android.graphics.PointF;
import android.graphics.Rect;
-import android.os.Build;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.view.Gravity;
@@ -841,8 +840,7 @@
public View onFocusSearchFailed(View focused, int direction,
RecyclerView.Recycler recycler,
RecyclerView.State state) {
- int expectedDir = Build.VERSION.SDK_INT <= 15 ? View.FOCUS_DOWN :
- View.FOCUS_FORWARD;
+ int expectedDir = View.FOCUS_FORWARD;
assertEquals(expectedDir, direction);
assertEquals(1, getChildCount());
View child0 = getChildAt(0);
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/Placeholder.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/Placeholder.kt
new file mode 100644
index 0000000..bad50f2
--- /dev/null
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/Placeholder.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room
+// empty file to trigger klib creation
+// see: https://youtrack.jetbrains.com/issue/KT-52344
diff --git a/slice/slice-core/src/main/java/androidx/slice/SliceProvider.java b/slice/slice-core/src/main/java/androidx/slice/SliceProvider.java
index 399ba9f..edb4a01 100644
--- a/slice/slice-core/src/main/java/androidx/slice/SliceProvider.java
+++ b/slice/slice-core/src/main/java/androidx/slice/SliceProvider.java
@@ -211,7 +211,6 @@
@Override
public final boolean onCreate() {
- if (Build.VERSION.SDK_INT < 19) return false;
return onCreateSliceProvider();
}
@@ -243,7 +242,6 @@
@Nullable
@Override
public final String getType(@NonNull Uri uri) {
- if (Build.VERSION.SDK_INT < 19) return null;
if (DEBUG) Log.d(TAG, "getFormat " + uri);
return SLICE_TYPE;
}
@@ -272,7 +270,7 @@
@Nullable
@Override
public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) {
- if (Build.VERSION.SDK_INT < 19 || Build.VERSION.SDK_INT >= 28) return null;
+ if (Build.VERSION.SDK_INT >= 28) return null;
if (extras == null) return null;
return getSliceProviderCompat().call(method, arg, extras);
}
diff --git a/testutils/testutils-runtime/src/main/java/androidx/fragment/app/StrictFragment.kt b/testutils/testutils-runtime/src/main/java/androidx/fragment/app/StrictFragment.kt
index fe1c603..f3fa6a7 100644
--- a/testutils/testutils-runtime/src/main/java/androidx/fragment/app/StrictFragment.kt
+++ b/testutils/testutils-runtime/src/main/java/androidx/fragment/app/StrictFragment.kt
@@ -17,7 +17,6 @@
package androidx.fragment.app
import android.content.Context
-import android.os.Build
import android.os.Bundle
import android.util.AttributeSet
import androidx.annotation.LayoutRes
@@ -53,9 +52,7 @@
}
fun checkActivityNotDestroyed() {
- if (Build.VERSION.SDK_INT >= 17) {
- check(!requireActivity().isDestroyed)
- }
+ check(!requireActivity().isDestroyed)
}
fun checkState(caller: String, vararg expected: State) {
diff --git a/text/text/lint-baseline.xml b/text/text/lint-baseline.xml
index 9d2be4d..2a6edab 100644
--- a/text/text/lint-baseline.xml
+++ b/text/text/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.2.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-beta01)" variant="all" version="8.2.0-beta01">
+<issues format="6" by="lint 8.3.0-alpha10" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-alpha10)" variant="all" version="8.3.0-alpha10">
<issue
id="PrimitiveInCollection"
@@ -7,7 +7,7 @@
errorLine1=" private val paragraphEnds: List<Int>"
errorLine2=" ~~~~~~~~~">
<location
- file="src/main/java/androidx/compose/ui/text/android/LayoutHelper.kt"/>
+ file="src/main/java/androidx/compose/ui/text/android/LayoutHelper.android.kt"/>
</issue>
<issue
@@ -16,7 +16,7 @@
errorLine1=" val lineFeeds = mutableListOf<Int>()"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
- file="src/main/java/androidx/compose/ui/text/android/LayoutHelper.kt"/>
+ file="src/main/java/androidx/compose/ui/text/android/LayoutHelper.android.kt"/>
</issue>
<issue
@@ -25,7 +25,7 @@
errorLine1=" private fun breakInWords(layoutHelper: LayoutHelper): List<Int> {"
errorLine2=" ~~~~~~~~~">
<location
- file="src/main/java/androidx/compose/ui/text/android/animation/SegmentBreaker.kt"/>
+ file="src/main/java/androidx/compose/ui/text/android/animation/SegmentBreaker.android.kt"/>
</issue>
<issue
@@ -34,7 +34,7 @@
errorLine1=" val words = breakWithBreakIterator(text, BreakIterator.getLineInstance(Locale.getDefault()))"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
- file="src/main/java/androidx/compose/ui/text/android/animation/SegmentBreaker.kt"/>
+ file="src/main/java/androidx/compose/ui/text/android/animation/SegmentBreaker.android.kt"/>
</issue>
<issue
@@ -43,7 +43,7 @@
errorLine1=" val set = TreeSet<Int>().apply {"
errorLine2=" ^">
<location
- file="src/main/java/androidx/compose/ui/text/android/animation/SegmentBreaker.kt"/>
+ file="src/main/java/androidx/compose/ui/text/android/animation/SegmentBreaker.android.kt"/>
</issue>
<issue
@@ -52,7 +52,7 @@
errorLine1=" private fun breakWithBreakIterator(text: CharSequence, breaker: BreakIterator): List<Int> {"
errorLine2=" ~~~~~~~~~">
<location
- file="src/main/java/androidx/compose/ui/text/android/animation/SegmentBreaker.kt"/>
+ file="src/main/java/androidx/compose/ui/text/android/animation/SegmentBreaker.android.kt"/>
</issue>
<issue
@@ -61,7 +61,7 @@
errorLine1=" val res = mutableListOf(0)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
- file="src/main/java/androidx/compose/ui/text/android/animation/SegmentBreaker.kt"/>
+ file="src/main/java/androidx/compose/ui/text/android/animation/SegmentBreaker.android.kt"/>
</issue>
<issue
@@ -70,7 +70,7 @@
errorLine1=" fun breakOffsets(layoutHelper: LayoutHelper, segmentType: SegmentType): List<Int> {"
errorLine2=" ~~~~~~~~~">
<location
- file="src/main/java/androidx/compose/ui/text/android/animation/SegmentBreaker.kt"/>
+ file="src/main/java/androidx/compose/ui/text/android/animation/SegmentBreaker.android.kt"/>
</issue>
</issues>
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/BoringLayoutFactory.kt b/text/text/src/main/java/androidx/compose/ui/text/android/BoringLayoutFactory.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/BoringLayoutFactory.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/BoringLayoutFactory.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/CharSequenceCharacterIterator.kt b/text/text/src/main/java/androidx/compose/ui/text/android/CharSequenceCharacterIterator.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/CharSequenceCharacterIterator.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/CharSequenceCharacterIterator.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/InlineClassUtils.kt b/text/text/src/main/java/androidx/compose/ui/text/android/InlineClassUtils.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/InlineClassUtils.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/InlineClassUtils.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/InternalPlatformTextApi.kt b/text/text/src/main/java/androidx/compose/ui/text/android/InternalPlatformTextApi.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/InternalPlatformTextApi.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/InternalPlatformTextApi.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/LayoutCompat.kt b/text/text/src/main/java/androidx/compose/ui/text/android/LayoutCompat.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/LayoutCompat.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/LayoutCompat.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/LayoutHelper.kt b/text/text/src/main/java/androidx/compose/ui/text/android/LayoutHelper.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/LayoutHelper.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/LayoutHelper.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/LayoutIntrinsics.kt b/text/text/src/main/java/androidx/compose/ui/text/android/LayoutIntrinsics.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/LayoutIntrinsics.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/LayoutIntrinsics.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/ListUtils.kt b/text/text/src/main/java/androidx/compose/ui/text/android/ListUtils.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/ListUtils.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/ListUtils.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/PaintExtensions.kt b/text/text/src/main/java/androidx/compose/ui/text/android/PaintExtensions.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/PaintExtensions.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/PaintExtensions.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/SpannedExtensions.kt b/text/text/src/main/java/androidx/compose/ui/text/android/SpannedExtensions.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/SpannedExtensions.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/SpannedExtensions.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/StaticLayoutFactory.kt b/text/text/src/main/java/androidx/compose/ui/text/android/StaticLayoutFactory.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/StaticLayoutFactory.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/StaticLayoutFactory.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/TextAndroidCanvas.kt b/text/text/src/main/java/androidx/compose/ui/text/android/TextAndroidCanvas.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/TextAndroidCanvas.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/TextAndroidCanvas.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt b/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/animation/SegmentBreaker.kt b/text/text/src/main/java/androidx/compose/ui/text/android/animation/SegmentBreaker.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/animation/SegmentBreaker.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/animation/SegmentBreaker.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/animation/SegmentType.kt b/text/text/src/main/java/androidx/compose/ui/text/android/animation/SegmentType.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/animation/SegmentType.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/animation/SegmentType.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/selection/WordBoundary.kt b/text/text/src/main/java/androidx/compose/ui/text/android/selection/WordBoundary.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/selection/WordBoundary.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/selection/WordBoundary.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/selection/WordIterator.kt b/text/text/src/main/java/androidx/compose/ui/text/android/selection/WordIterator.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/selection/WordIterator.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/selection/WordIterator.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/style/BaselineShiftSpan.kt b/text/text/src/main/java/androidx/compose/ui/text/android/style/BaselineShiftSpan.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/style/BaselineShiftSpan.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/style/BaselineShiftSpan.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/style/FontFeatureSpan.kt b/text/text/src/main/java/androidx/compose/ui/text/android/style/FontFeatureSpan.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/style/FontFeatureSpan.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/style/FontFeatureSpan.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/style/IndentationFixSpan.kt b/text/text/src/main/java/androidx/compose/ui/text/android/style/IndentationFixSpan.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/style/IndentationFixSpan.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/style/IndentationFixSpan.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/style/LetterSpacingSpanEm.kt b/text/text/src/main/java/androidx/compose/ui/text/android/style/LetterSpacingSpanEm.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/style/LetterSpacingSpanEm.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/style/LetterSpacingSpanEm.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/style/LetterSpacingSpanPx.kt b/text/text/src/main/java/androidx/compose/ui/text/android/style/LetterSpacingSpanPx.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/style/LetterSpacingSpanPx.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/style/LetterSpacingSpanPx.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/style/LineHeightSpan.kt b/text/text/src/main/java/androidx/compose/ui/text/android/style/LineHeightSpan.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/style/LineHeightSpan.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/style/LineHeightSpan.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/style/LineHeightStyleSpan.kt b/text/text/src/main/java/androidx/compose/ui/text/android/style/LineHeightStyleSpan.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/style/LineHeightStyleSpan.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/style/LineHeightStyleSpan.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/style/PlaceholderSpan.kt b/text/text/src/main/java/androidx/compose/ui/text/android/style/PlaceholderSpan.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/style/PlaceholderSpan.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/style/PlaceholderSpan.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/style/ShadowSpan.kt b/text/text/src/main/java/androidx/compose/ui/text/android/style/ShadowSpan.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/style/ShadowSpan.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/style/ShadowSpan.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/style/SkewXSpan.kt b/text/text/src/main/java/androidx/compose/ui/text/android/style/SkewXSpan.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/style/SkewXSpan.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/style/SkewXSpan.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/style/TextDecorationSpan.kt b/text/text/src/main/java/androidx/compose/ui/text/android/style/TextDecorationSpan.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/style/TextDecorationSpan.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/style/TextDecorationSpan.android.kt
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/style/TypefaceSpan.kt b/text/text/src/main/java/androidx/compose/ui/text/android/style/TypefaceSpan.android.kt
similarity index 100%
rename from text/text/src/main/java/androidx/compose/ui/text/android/style/TypefaceSpan.kt
rename to text/text/src/main/java/androidx/compose/ui/text/android/style/TypefaceSpan.android.kt
diff --git a/tracing/tracing-perfetto-binary/src/main/cpp/tracing_perfetto.cc b/tracing/tracing-perfetto-binary/src/main/cpp/tracing_perfetto.cc
index aa266f7..c7ad150 100644
--- a/tracing/tracing-perfetto-binary/src/main/cpp/tracing_perfetto.cc
+++ b/tracing/tracing-perfetto-binary/src/main/cpp/tracing_perfetto.cc
@@ -25,7 +25,7 @@
// Concept of version useful e.g. for human-readable error messages, and stable once released.
// Does not replace the need for a binary verification mechanism (e.g. checksum check).
// TODO: populate using CMake
-#define VERSION "1.0.0-beta03"
+#define VERSION "1.0.0"
namespace tracing_perfetto {
void RegisterWithPerfetto() {
diff --git a/tracing/tracing-perfetto/src/androidTest/java/androidx/tracing/perfetto/jni/test/PerfettoNativeTest.kt b/tracing/tracing-perfetto/src/androidTest/java/androidx/tracing/perfetto/jni/test/PerfettoNativeTest.kt
index ef3b214..d3ef52d 100644
--- a/tracing/tracing-perfetto/src/androidTest/java/androidx/tracing/perfetto/jni/test/PerfettoNativeTest.kt
+++ b/tracing/tracing-perfetto/src/androidTest/java/androidx/tracing/perfetto/jni/test/PerfettoNativeTest.kt
@@ -30,7 +30,7 @@
init {
PerfettoNative.loadLib()
}
- const val libraryVersion = "1.0.0-beta03" // TODO: get using reflection
+ const val libraryVersion = "1.0.0" // TODO: get using reflection
}
@Test
diff --git a/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt b/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt
index 3cc418e..47e3465 100644
--- a/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt
+++ b/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt
@@ -25,12 +25,12 @@
// TODO(224510255): load from a file produced at build time
object Metadata {
- const val version = "1.0.0-beta03"
+ const val version = "1.0.0"
val checksums = mapOf(
- "arm64-v8a" to "e11502d6fa0c949774a792c2406744a1fff112ba26e6af19a6722dd55a6061ca",
- "armeabi-v7a" to "cd286085893cc7760b658f48b436fd317493159dcbab680667c0bf01d25ffb04",
- "x86" to "2679c351d40e405e46dec58c8f63570eb29196cd9e67404cf66abe74bc933a88",
- "x86_64" to "2971a9135cd69feb2a4e3c7b42f6b52469421713331cdb7a841c1c8cc707b637",
+ "arm64-v8a" to "a152fbd7ebaa109a9c3cf6bbb6d585aa0df08f97ae022b2090b1096a8f5e2665",
+ "armeabi-v7a" to "b2821c9ddb77a3f070cce42be7cd3255d7ec92c868d7d518a99ed968d9018b9f",
+ "x86" to "4cefdc75fe41deeeb2306891c25ce4db33599698cc6fcb2e82caad5aece9aa09",
+ "x86_64" to "23daf0750238cf96bf9ea9fa1b13ae1d2eeb17644ea5439e18939ec6a8b9e5be",
)
}
diff --git a/transition/transition/src/androidTest/java/androidx/transition/ChangeBoundsTest.java b/transition/transition/src/androidTest/java/androidx/transition/ChangeBoundsTest.java
index 3c27a70..92b0821 100644
--- a/transition/transition/src/androidTest/java/androidx/transition/ChangeBoundsTest.java
+++ b/transition/transition/src/androidTest/java/androidx/transition/ChangeBoundsTest.java
@@ -26,7 +26,6 @@
import android.content.Context;
import android.graphics.Rect;
-import android.os.Build;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.LinearInterpolator;
@@ -92,11 +91,6 @@
@Test
public void testSuppressLayoutWhileAnimating() throws Throwable {
- if (Build.VERSION.SDK_INT < 18) {
- // prior Android 4.3 suppressLayout port has another implementation which is
- // harder to test
- return;
- }
final TestSuppressLayout suppressLayout = new TestSuppressLayout(rule.getActivity());
final View testView = new View(rule.getActivity());
rule.runOnUiThread(new Runnable() {
diff --git a/transition/transition/src/main/java/androidx/transition/ViewGroupOverlayApi18.java b/transition/transition/src/main/java/androidx/transition/ViewGroupOverlayApi18.java
index 20e4282..4c5f4e8 100644
--- a/transition/transition/src/main/java/androidx/transition/ViewGroupOverlayApi18.java
+++ b/transition/transition/src/main/java/androidx/transition/ViewGroupOverlayApi18.java
@@ -22,9 +22,7 @@
import android.view.ViewGroupOverlay;
import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-@RequiresApi(18)
class ViewGroupOverlayApi18 implements ViewGroupOverlayImpl {
private final ViewGroupOverlay mViewGroupOverlay;
diff --git a/transition/transition/src/main/java/androidx/transition/ViewGroupUtils.java b/transition/transition/src/main/java/androidx/transition/ViewGroupUtils.java
index 37126f5..52c2f97 100644
--- a/transition/transition/src/main/java/androidx/transition/ViewGroupUtils.java
+++ b/transition/transition/src/main/java/androidx/transition/ViewGroupUtils.java
@@ -44,10 +44,7 @@
* Backward-compatible {@link ViewGroup#getOverlay()}.
*/
static ViewGroupOverlayImpl getOverlay(@NonNull ViewGroup group) {
- if (Build.VERSION.SDK_INT >= 18) {
- return new ViewGroupOverlayApi18(group);
- }
- return ViewGroupOverlayApi14.createFrom(group);
+ return new ViewGroupOverlayApi18(group);
}
/**
@@ -56,14 +53,11 @@
static void suppressLayout(@NonNull ViewGroup group, boolean suppress) {
if (Build.VERSION.SDK_INT >= 29) {
Api29Impl.suppressLayout(group, suppress);
- } else if (Build.VERSION.SDK_INT >= 18) {
- hiddenSuppressLayout(group, suppress);
} else {
- ViewGroupUtilsApi14.suppressLayout(group, suppress);
+ hiddenSuppressLayout(group, suppress);
}
}
- @RequiresApi(18)
@SuppressLint("NewApi") // Lint doesn't know about the hidden method.
private static void hiddenSuppressLayout(@NonNull ViewGroup group, boolean suppress) {
if (sTryHiddenSuppressLayout) {
diff --git a/transition/transition/src/main/java/androidx/transition/ViewGroupUtilsApi14.java b/transition/transition/src/main/java/androidx/transition/ViewGroupUtilsApi14.java
deleted file mode 100644
index 2a83cc1..0000000
--- a/transition/transition/src/main/java/androidx/transition/ViewGroupUtilsApi14.java
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.transition;
-
-import android.animation.LayoutTransition;
-import android.annotation.SuppressLint;
-import android.util.Log;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-
-import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-
-class ViewGroupUtilsApi14 {
-
- private static final String TAG = "ViewGroupUtilsApi14";
-
- private static final int LAYOUT_TRANSITION_CHANGING = 4;
-
- private static LayoutTransition sEmptyLayoutTransition;
-
- private static Field sLayoutSuppressedField;
- private static boolean sLayoutSuppressedFieldFetched;
-
- private static Method sCancelMethod;
- private static boolean sCancelMethodFetched;
-
- static void suppressLayout(@NonNull ViewGroup group, boolean suppress) {
- // Prepare the empty LayoutTransition
- if (sEmptyLayoutTransition == null) {
- sEmptyLayoutTransition = new LayoutTransition() {
- @Override
- public boolean isChangingLayout() {
- return true;
- }
- };
- sEmptyLayoutTransition.setAnimator(LayoutTransition.APPEARING, null);
- sEmptyLayoutTransition.setAnimator(LayoutTransition.CHANGE_APPEARING, null);
- sEmptyLayoutTransition.setAnimator(LayoutTransition.CHANGE_DISAPPEARING, null);
- sEmptyLayoutTransition.setAnimator(LayoutTransition.DISAPPEARING, null);
- sEmptyLayoutTransition.setAnimator(LAYOUT_TRANSITION_CHANGING, null);
- }
- if (suppress) {
- // Save the current LayoutTransition
- final LayoutTransition layoutTransition = group.getLayoutTransition();
- if (layoutTransition != null) {
- if (layoutTransition.isRunning()) {
- cancelLayoutTransition(layoutTransition);
- }
- if (layoutTransition != sEmptyLayoutTransition) {
- group.setTag(R.id.transition_layout_save, layoutTransition);
- }
- }
- // Suppress the layout
- group.setLayoutTransition(sEmptyLayoutTransition);
- } else {
- // Thaw the layout suppression
- group.setLayoutTransition(null);
- // Request layout if necessary
- if (!sLayoutSuppressedFieldFetched) {
- try {
- sLayoutSuppressedField = ViewGroup.class.getDeclaredField("mLayoutSuppressed");
- sLayoutSuppressedField.setAccessible(true);
- } catch (NoSuchFieldException e) {
- Log.i(TAG, "Failed to access mLayoutSuppressed field by reflection");
- }
- sLayoutSuppressedFieldFetched = true;
- }
- boolean layoutSuppressed = false;
- if (sLayoutSuppressedField != null) {
- try {
- layoutSuppressed = sLayoutSuppressedField.getBoolean(group);
- if (layoutSuppressed) {
- sLayoutSuppressedField.setBoolean(group, false);
- }
- } catch (IllegalAccessException e) {
- Log.i(TAG, "Failed to get mLayoutSuppressed field by reflection");
- }
- }
- if (layoutSuppressed) {
- group.requestLayout();
- }
- // Restore the saved LayoutTransition
- final LayoutTransition layoutTransition =
- (LayoutTransition) group.getTag(R.id.transition_layout_save);
- if (layoutTransition != null) {
- group.setTag(R.id.transition_layout_save, null);
- group.setLayoutTransition(layoutTransition);
- }
- }
- }
-
- /**
- * Note, this is only called on API 17 and older.
- */
- @SuppressLint({"SoonBlockedPrivateApi", "BanUncheckedReflection"})
- private static void cancelLayoutTransition(LayoutTransition t) {
- if (!sCancelMethodFetched) {
- try {
- sCancelMethod = LayoutTransition.class.getDeclaredMethod("cancel");
- sCancelMethod.setAccessible(true);
- } catch (NoSuchMethodException e) {
- Log.i(TAG, "Failed to access cancel method by reflection");
- }
- sCancelMethodFetched = true;
- }
- if (sCancelMethod != null) {
- try {
- sCancelMethod.invoke(t);
- } catch (IllegalAccessException e) {
- Log.i(TAG, "Failed to access cancel method by reflection");
- } catch (InvocationTargetException e) {
- Log.i(TAG, "Failed to invoke cancel method by reflection");
- }
- }
- }
-
- private ViewGroupUtilsApi14() {
- }
-}
diff --git a/transition/transition/src/main/java/androidx/transition/ViewUtils.java b/transition/transition/src/main/java/androidx/transition/ViewUtils.java
index 388c11d..7d609b0 100644
--- a/transition/transition/src/main/java/androidx/transition/ViewUtils.java
+++ b/transition/transition/src/main/java/androidx/transition/ViewUtils.java
@@ -87,20 +87,14 @@
* Backward-compatible {@link View#getOverlay()}.
*/
static ViewOverlayImpl getOverlay(@NonNull View view) {
- if (Build.VERSION.SDK_INT >= 18) {
- return new ViewOverlayApi18(view);
- }
- return ViewOverlayApi14.createFrom(view);
+ return new ViewOverlayApi18(view);
}
/**
* Backward-compatible {@link View#getWindowId()}.
*/
static @NonNull WindowIdImpl getWindowId(@NonNull View view) {
- if (Build.VERSION.SDK_INT >= 18) {
- return new WindowIdApi18(view);
- }
- return new WindowIdApi14(view.getWindowToken());
+ return new WindowIdApi18(view);
}
static void setTransitionAlpha(@NonNull View view, float alpha) {
diff --git a/transition/transition/src/main/java/androidx/transition/WindowIdApi14.java b/transition/transition/src/main/java/androidx/transition/WindowIdApi14.java
deleted file mode 100644
index 6a9231e..0000000
--- a/transition/transition/src/main/java/androidx/transition/WindowIdApi14.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.transition;
-
-import android.os.IBinder;
-
-class WindowIdApi14 implements WindowIdImpl {
-
- private final IBinder mToken;
-
- WindowIdApi14(IBinder token) {
- mToken = token;
- }
-
- @Override
- public boolean equals(Object o) {
- return o instanceof WindowIdApi14 && ((WindowIdApi14) o).mToken.equals(this.mToken);
- }
-
- @Override
- public int hashCode() {
- return mToken.hashCode();
- }
-}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridTest.kt
index 13b3fde..5a30ac2 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridTest.kt
@@ -59,7 +59,6 @@
import androidx.tv.foundation.lazy.list.setContentWithTestViewConfiguration
import com.google.common.collect.Range
import com.google.common.truth.IntegerSubject
-import com.google.common.truth.Truth
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
import org.junit.Test
@@ -71,6 +70,8 @@
class LazyGridTest(
private val orientation: Orientation
) : BaseLazyGridTestWithOrientation(orientation) {
+
+ @Suppress("PrivatePropertyName")
private val LazyGridTag = "LazyGridTag"
companion object {
@@ -148,7 +149,7 @@
}
}
- rule.keyPress(3)
+ rule.keyPress(2)
rule.onNodeWithTag("4")
.assertIsDisplayed()
@@ -670,9 +671,9 @@
state.scrollToItem(50)
}
composedIndexes.forEach {
- Truth.assertThat(it).isLessThan(count)
+ assertThat(it).isLessThan(count)
}
- Truth.assertThat(state.firstVisibleItemIndex).isEqualTo(9)
+ assertThat(state.firstVisibleItemIndex).isEqualTo(9)
}
}
@@ -723,7 +724,7 @@
}
}
- rule.keyPress(3)
+ rule.keyPress(2)
rule.onNodeWithTag("1")
.assertMainAxisStartPositionInRootIsEqualTo(0.dp)
@@ -891,7 +892,7 @@
}
rule.runOnIdle {
- Truth.assertThat(exception).isInstanceOf(IllegalArgumentException::class.java)
+ assertThat(exception).isInstanceOf(IllegalArgumentException::class.java)
}
}
@@ -922,17 +923,17 @@
}
rule.runOnIdle {
- Truth.assertThat(remeasureCount).isEqualTo(1)
+ assertThat(remeasureCount).isEqualTo(1)
counter.value++
}
rule.runOnIdle {
- Truth.assertThat(remeasureCount).isEqualTo(1)
+ assertThat(remeasureCount).isEqualTo(1)
}
}
@Test
- fun scrollingALotDoesntCauseLazyLayoutRecomposition() {
+ fun scrollingALotDoesNotCauseLazyLayoutRecomposition() {
var recomposeCount = 0
lateinit var state: TvLazyGridState
@@ -953,7 +954,7 @@
}
rule.runOnIdle {
- Truth.assertThat(recomposeCount).isEqualTo(1)
+ assertThat(recomposeCount).isEqualTo(1)
runBlocking {
state.scrollToItem(100)
@@ -961,7 +962,7 @@
}
rule.runOnIdle {
- Truth.assertThat(recomposeCount).isEqualTo(1)
+ assertThat(recomposeCount).isEqualTo(1)
}
}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridsReverseLayoutTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridsReverseLayoutTest.kt
index 4d8ad86..196d022 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridsReverseLayoutTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridsReverseLayoutTest.kt
@@ -34,16 +34,20 @@
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
import androidx.tv.foundation.lazy.list.setContentWithTestViewConfiguration
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
+import org.junit.runner.RunWith
+private const val ContainerTag = "ContainerTag"
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
class LazyGridsReverseLayoutTest {
-
- private val ContainerTag = "ContainerTag"
-
@get:Rule
val rule = createComposeRule()
@@ -164,7 +168,7 @@
}
// we scroll down and as the scrolling is reversed it shouldn't affect anything
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 2)
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 2)
rule.runOnIdle {
assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
@@ -193,7 +197,7 @@
}
}
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 2)
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 1)
val scrolled = rule.runOnIdle {
assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListTest.kt
index baf2ba0..4fd6a16 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListTest.kt
@@ -93,8 +93,10 @@
@LargeTest
@RunWith(Parameterized::class)
class LazyListTest(orientation: Orientation) : BaseLazyListTestWithOrientation(orientation) {
+ @Suppress("PrivatePropertyName")
private val LazyListTag = "LazyListTag"
- private val firstItemTag = "firstItemTag"
+ @Suppress("PrivatePropertyName")
+ private val FirstItemTag = "firstItemTag"
@Test
fun lazyListShowsCombinedItems() {
@@ -243,7 +245,7 @@
}
}
- rule.keyPress(3)
+ rule.keyPress(2)
rule.onNodeWithTag("1")
.assertIsDisplayed()
@@ -274,7 +276,7 @@
}
}
- rule.keyPress(3)
+ rule.keyPress(2)
rule.onNodeWithTag("1")
.assertIsNotDisplayed()
@@ -306,7 +308,7 @@
}
}
- rule.keyPress(4)
+ rule.keyPress(3)
rule.onNodeWithTag("1")
.assertIsNotDisplayed()
@@ -463,7 +465,8 @@
}
}
- rule.keyPress(3)
+ rule.waitForIdle()
+ rule.keyPress(2)
rule.onNodeWithTag(thirdTag)
.assertExists()
@@ -487,13 +490,13 @@
LazyColumnOrRow(Modifier.requiredSize(width = 100.dp, height = 150.dp)) {
items(listOf(0)) {
Spacer(
- Modifier.fillParentMaxWidth().requiredHeight(50.dp).testTag(firstItemTag)
+ Modifier.fillParentMaxWidth().requiredHeight(50.dp).testTag(FirstItemTag)
)
}
}
}
- rule.onNodeWithTag(firstItemTag)
+ rule.onNodeWithTag(FirstItemTag)
.assertWidthIsEqualTo(100.dp)
.assertHeightIsEqualTo(50.dp)
}
@@ -504,13 +507,13 @@
LazyColumnOrRow(Modifier.requiredSize(width = 100.dp, height = 150.dp)) {
items(listOf(0)) {
Spacer(
- Modifier.requiredWidth(50.dp).fillParentMaxHeight().testTag(firstItemTag)
+ Modifier.requiredWidth(50.dp).fillParentMaxHeight().testTag(FirstItemTag)
)
}
}
}
- rule.onNodeWithTag(firstItemTag)
+ rule.onNodeWithTag(FirstItemTag)
.assertWidthIsEqualTo(50.dp)
.assertHeightIsEqualTo(150.dp)
}
@@ -520,12 +523,12 @@
rule.setContentWithTestViewConfiguration {
LazyColumnOrRow(Modifier.requiredSize(width = 100.dp, height = 150.dp)) {
items(listOf(0)) {
- Spacer(Modifier.fillParentMaxSize().testTag(firstItemTag))
+ Spacer(Modifier.fillParentMaxSize().testTag(FirstItemTag))
}
}
}
- rule.onNodeWithTag(firstItemTag)
+ rule.onNodeWithTag(FirstItemTag)
.assertWidthIsEqualTo(100.dp)
.assertHeightIsEqualTo(150.dp)
}
@@ -538,13 +541,13 @@
Spacer(
Modifier.fillParentMaxWidth(0.7f)
.requiredHeight(50.dp)
- .testTag(firstItemTag)
+ .testTag(FirstItemTag)
)
}
}
}
- rule.onNodeWithTag(firstItemTag)
+ rule.onNodeWithTag(FirstItemTag)
.assertWidthIsEqualTo(70.dp)
.assertHeightIsEqualTo(50.dp)
}
@@ -557,13 +560,13 @@
Spacer(
Modifier.requiredWidth(50.dp)
.fillParentMaxHeight(0.3f)
- .testTag(firstItemTag)
+ .testTag(FirstItemTag)
)
}
}
}
- rule.onNodeWithTag(firstItemTag)
+ rule.onNodeWithTag(FirstItemTag)
.assertWidthIsEqualTo(50.dp)
.assertHeightIsEqualTo(45.dp)
}
@@ -573,12 +576,12 @@
rule.setContentWithTestViewConfiguration {
LazyColumnOrRow(Modifier.requiredSize(width = 100.dp, height = 150.dp)) {
items(listOf(0)) {
- Spacer(Modifier.fillParentMaxSize(0.5f).testTag(firstItemTag))
+ Spacer(Modifier.fillParentMaxSize(0.5f).testTag(FirstItemTag))
}
}
}
- rule.onNodeWithTag(firstItemTag)
+ rule.onNodeWithTag(FirstItemTag)
.assertWidthIsEqualTo(50.dp)
.assertHeightIsEqualTo(75.dp)
}
@@ -589,7 +592,7 @@
rule.setContentWithTestViewConfiguration {
LazyColumnOrRow(Modifier.requiredSize(parentSize)) {
items(listOf(0)) {
- Spacer(Modifier.fillParentMaxSize().testTag(firstItemTag))
+ Spacer(Modifier.fillParentMaxSize().testTag(FirstItemTag))
}
}
}
@@ -598,7 +601,7 @@
parentSize = 150.dp
}
- rule.onNodeWithTag(firstItemTag)
+ rule.onNodeWithTag(FirstItemTag)
.assertWidthIsEqualTo(150.dp)
.assertHeightIsEqualTo(150.dp)
}
@@ -752,6 +755,8 @@
}
}
+ rule.waitForIdle()
+
// getting focus to the first element
rule.keyPress(2)
// we already displaying the first item, so this should do nothing
@@ -1066,12 +1071,10 @@
) {
items(items) {
Spacer(
- if (it == 0) {
- Modifier.crossAxisSize(30.dp).mainAxisSize(itemSize / 2)
- } else if (it == 1) {
- Modifier.crossAxisSize(20.dp).mainAxisSize(itemSize / 2)
- } else {
- Modifier.crossAxisSize(20.dp).mainAxisSize(itemSize)
+ when (it) {
+ 0 -> Modifier.crossAxisSize(30.dp).mainAxisSize(itemSize / 2)
+ 1 -> Modifier.crossAxisSize(20.dp).mainAxisSize(itemSize / 2)
+ else -> Modifier.crossAxisSize(20.dp).mainAxisSize(itemSize)
}
)
}
@@ -1161,7 +1164,7 @@
}
@Test
- fun overscrollingBackwardFromNotTheFirstPosition() {
+ fun overScrollingBackwardFromNotTheFirstPosition() {
val containerTag = "container"
val itemSizePx = 10
val itemSizeDp = with(rule.density) { itemSizePx.toDp() }
@@ -1513,7 +1516,7 @@
}
}
- rule.keyPress(3)
+ rule.keyPress(2)
rule.onNodeWithTag("1")
.assertStartPositionInRootIsEqualTo(0.dp)
@@ -1687,7 +1690,7 @@
}
@Test
- fun scrollingALotDoesntCauseLazyLayoutRecomposition() {
+ fun scrollingALotDoesNotCauseLazyLayoutRecomposition() {
var recomposeCount = 0
lateinit var state: TvLazyListState
@@ -1759,8 +1762,8 @@
Box(Modifier.fillParentMaxSize())
}
}
- }) { measurables, _ ->
- val placeable = measurables.first().measure(constraints)
+ }) { measurableList, _ ->
+ val placeable = measurableList.first().measure(constraints)
layout(constraints.maxWidth, constraints.maxHeight) {
placeable.place(0, 0)
}
@@ -1789,13 +1792,13 @@
.testTag("item"))
}
}
- }) { measurables, _ ->
+ }) { measurableList, _ ->
val crossInfinityConstraints = if (vertical) {
Constraints(maxWidth = Constraints.Infinity, maxHeight = 100)
} else {
Constraints(maxWidth = 100, maxHeight = Constraints.Infinity)
}
- val placeable = measurables.first().measure(crossInfinityConstraints)
+ val placeable = measurableList.first().measure(crossInfinityConstraints)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListsReverseLayoutTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListsReverseLayoutTest.kt
index 4ec7ab7..32ee3a2 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListsReverseLayoutTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListsReverseLayoutTest.kt
@@ -34,15 +34,22 @@
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
+import androidx.test.filters.MediumTest
import androidx.tv.foundation.PivotOffsets
import androidx.tv.foundation.lazy.grid.keyPress
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
+import org.junit.runner.RunWith
+@MediumTest
+@RunWith(AndroidJUnit4::class)
class LazyListsReverseLayoutTest {
+ @Suppress("PrivatePropertyName")
private val ContainerTag = "ContainerTag"
@get:Rule
@@ -151,6 +158,7 @@
.assertTopPositionInRootIsEqualTo(itemSize)
}
+ @FlakyTest(bugId = 313465577)
@Test
fun column_scrollForwardHalfWay() {
lateinit var state: TvLazyListState
@@ -167,7 +175,7 @@
}
}
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 3)
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 1)
val scrolled = rule.runOnIdle {
assertThat(state.firstVisibleItemIndex).isEqualTo(0)
@@ -310,6 +318,7 @@
.assertLeftPositionInRootIsEqualTo(itemSize)
}
+ @FlakyTest(bugId = 313465577)
@Test
fun row_scrollForwardHalfWay() {
lateinit var state: TvLazyListState
@@ -326,7 +335,7 @@
}
}
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT, 3)
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT, 1)
val scrolled = rule.runOnIdle {
assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
@@ -438,7 +447,7 @@
}
}
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT, 3)
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT, 2)
val scrolled = rule.runOnIdle {
assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyRowTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyRowTest.kt
index 0817a21..8ae3f7d 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyRowTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyRowTest.kt
@@ -46,6 +46,7 @@
@MediumTest
@RunWith(AndroidJUnit4::class)
class LazyRowTest {
+ @Suppress("PrivatePropertyName")
private val LazyListTag = "LazyListTag"
@get:Rule
@@ -144,7 +145,7 @@
}
}
- rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT, 3)
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT, 2)
rule.runOnIdle {
assertThat(state.firstVisibleItemIndex).isEqualTo(1)
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/SurfaceTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/SurfaceTest.kt
index a39c580..e8223a2 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/SurfaceTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/SurfaceTest.kt
@@ -21,6 +21,7 @@
import android.view.KeyEvent
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
+import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.FocusInteraction
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -28,13 +29,16 @@
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -516,7 +520,7 @@
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun clickableSurface_onFocus_changesGlowColor() {
- rule.setContent {
+ rule.setFocusableContent {
Surface(
modifier = Modifier
.testTag("surface")
@@ -926,8 +930,8 @@
@Test
fun toggleableSurface_onCheckedChange_changesGlowColor() {
var isChecked by mutableStateOf(false)
- var focusManager: FocusManager? = null
- rule.setContent {
+ lateinit var focusManager: FocusManager
+ rule.setFocusableContent {
focusManager = LocalFocusManager.current
Surface(
checked = isChecked,
@@ -960,7 +964,7 @@
.performKeyInput { pressKey(Key.DirectionCenter) }
// Remove focused state to reveal selected state
- focusManager?.clearFocus()
+ rule.runOnUiThread { focusManager.clearFocus() }
rule.onNodeWithTag("surface")
.captureToImage()
@@ -971,8 +975,8 @@
@Test
fun toggleableSurface_onCheckedChange_changesScaleFactor() {
var isChecked by mutableStateOf(false)
- var focusManager: FocusManager? = null
- rule.setContent {
+ lateinit var focusManager: FocusManager
+ rule.setFocusableContent {
focusManager = LocalFocusManager.current
Box(
modifier = Modifier
@@ -997,7 +1001,7 @@
.performKeyInput { pressKey(Key.DirectionCenter) }
// Remove focused state to reveal selected state
- focusManager?.clearFocus()
+ rule.runOnUiThread { focusManager.clearFocus() }
rule.onRoot().captureToImage().assertDoesNotContainColor(Color.Blue)
}
@@ -1006,8 +1010,8 @@
@Test
fun toggleableSurface_onCheckedChange_showsOutline() {
var isChecked by mutableStateOf(false)
- var focusManager: FocusManager? = null
- rule.setContent {
+ lateinit var focusManager: FocusManager
+ rule.setFocusableContent {
focusManager = LocalFocusManager.current
Surface(
checked = isChecked,
@@ -1036,7 +1040,9 @@
.performKeyInput { pressKey(Key.DirectionCenter) }
// Remove focused state to reveal selected state
- focusManager?.clearFocus()
+ rule.runOnUiThread { focusManager.clearFocus() }
+
+ rule.waitForIdle()
surface.captureToImage().assertContainsColor(Color.Magenta)
}
@@ -1073,7 +1079,7 @@
val clickableItemTag = "clickable-item"
val toggleableItemTag = "toggleable-item"
val rootElementTag = "root"
- var focusManager: FocusManager? = null
+ lateinit var focusManager: FocusManager
rule.setContent {
// arrange
@@ -1147,7 +1153,7 @@
// blue border shouldn't be visible
rootEl.captureToImage().assertDoesNotContainColor(Color.Blue)
- focusManager?.moveFocus(FocusDirection.Down)
+ focusManager.moveFocus(FocusDirection.Down)
rule.waitForIdle()
// blue border should be visible
@@ -1158,7 +1164,7 @@
.performSemanticsAction(SemanticsActions.OnClick)
rule.waitForIdle()
- focusManager?.moveFocus(FocusDirection.Up)
+ focusManager.moveFocus(FocusDirection.Up)
rule.waitForIdle()
// blue border shouldn't be visible
@@ -1207,8 +1213,7 @@
rule
.onNodeWithTag(containerTag)
.captureToImage()
- .toPixelMap(0, 0, 1, 1)
- .get(0, 0) == Color.White
+ .toPixelMap(0, 0, 1, 1)[0, 0] == Color.White
)
rule.onNodeWithTag(surfaceTag).requestFocus()
@@ -1219,8 +1224,7 @@
rule
.onNodeWithTag(containerTag)
.captureToImage()
- .toPixelMap(0, 0, 1, 1)
- .get(0, 0) == Color.Red
+ .toPixelMap(0, 0, 1, 1)[0, 0] == Color.Red
)
}
}
@@ -1258,3 +1262,29 @@
}
return this
}
+
+/**
+ * This function adds a parent composable which has size.
+ * [View.requestFocus()][android.view.View.requestFocus] will not take focus if the view has no
+ * size.
+ *
+ * @param extraItemForInitialFocus Includes an extra item that takes focus initially. This is
+ * useful in cases where we need tests that could be affected by initial focus. Eg. When there is
+ * only one focusable item and we clear focus, that item could end up being focused on again by the
+ * initial focus logic.
+ */
+private fun ComposeContentTestRule.setFocusableContent(
+ extraItemForInitialFocus: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ setContent {
+ if (extraItemForInitialFocus) {
+ Row {
+ Box(modifier = Modifier.requiredSize(10.dp, 10.dp).focusable())
+ Box(modifier = Modifier.requiredSize(100.dp, 100.dp)) { content() }
+ }
+ } else {
+ Box(modifier = Modifier.requiredSize(100.dp, 100.dp)) { content() }
+ }
+ }
+}
diff --git a/vectordrawable/vectordrawable/src/main/java/androidx/vectordrawable/graphics/drawable/VectorDrawableCompat.java b/vectordrawable/vectordrawable/src/main/java/androidx/vectordrawable/graphics/drawable/VectorDrawableCompat.java
index 2b8df02..2ae01e1 100644
--- a/vectordrawable/vectordrawable/src/main/java/androidx/vectordrawable/graphics/drawable/VectorDrawableCompat.java
+++ b/vectordrawable/vectordrawable/src/main/java/androidx/vectordrawable/graphics/drawable/VectorDrawableCompat.java
@@ -918,12 +918,8 @@
// We don't support RTL auto mirroring since the getLayoutDirection() is for API 17+.
private boolean needMirroring() {
- if (Build.VERSION.SDK_INT >= 17) {
- return isAutoMirrored()
- && DrawableCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL;
- } else {
- return false;
- }
+ return isAutoMirrored()
+ && DrawableCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL;
}
// Extra override functions for delegation for SDK >= 7.
diff --git a/viewpager2/integration-tests/testapp/src/main/java/androidx/viewpager2/integration/testapp/MutableCollectionBaseActivity.kt b/viewpager2/integration-tests/testapp/src/main/java/androidx/viewpager2/integration/testapp/MutableCollectionBaseActivity.kt
index 2f3ca9d..46f34c1 100644
--- a/viewpager2/integration-tests/testapp/src/main/java/androidx/viewpager2/integration/testapp/MutableCollectionBaseActivity.kt
+++ b/viewpager2/integration-tests/testapp/src/main/java/androidx/viewpager2/integration/testapp/MutableCollectionBaseActivity.kt
@@ -16,7 +16,6 @@
package androidx.viewpager2.integration.testapp
-import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
@@ -61,9 +60,7 @@
itemSpinner.adapter = object : BaseAdapter() {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View =
((convertView as TextView?) ?: TextView(parent.context)).apply {
- if (Build.VERSION.SDK_INT >= 17) {
- textDirection = View.TEXT_DIRECTION_LOCALE
- }
+ textDirection = View.TEXT_DIRECTION_LOCALE
text = getItem(position)
}
diff --git a/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/AccessibilityTest.kt b/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/AccessibilityTest.kt
index 2abc406..9e64aee 100644
--- a/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/AccessibilityTest.kt
+++ b/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/AccessibilityTest.kt
@@ -57,10 +57,8 @@
localeUtil.resetLocale()
localeUtil.setLocale(LocaleTestUtils.RTL_LANGUAGE)
}
- if (Build.VERSION.SDK_INT >= 18) {
- // Make sure accessibility is enabled (side effect of creating a UI Automator instance)
- uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
- }
+ // Make sure accessibility is enabled (side effect of creating a UI Automator instance)
+ uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
}
override fun tearDown() {
diff --git a/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/BaseTest.kt b/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/BaseTest.kt
index aaa96c9..b340bdf 100644
--- a/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/BaseTest.kt
+++ b/viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/BaseTest.kt
@@ -20,7 +20,6 @@
import android.os.Build
import android.util.Log
import android.view.View
-import android.view.View.OVER_SCROLL_NEVER
import android.view.ViewConfiguration
import android.view.accessibility.AccessibilityNodeInfo
import androidx.core.view.ViewCompat
@@ -125,11 +124,6 @@
}
onView(withId(R.id.view_pager)).check(matches(isDisplayed()))
- // animations getting in the way on API < 16
- if (Build.VERSION.SDK_INT < 16) {
- viewPager.recyclerView.overScrollMode = OVER_SCROLL_NEVER
- }
-
return Context(activityTestRule)
}
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/HierarchicalFocusCoordinatorTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/HierarchicalFocusCoordinatorTest.kt
index 53be515..3d55748 100644
--- a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/HierarchicalFocusCoordinatorTest.kt
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/HierarchicalFocusCoordinatorTest.kt
@@ -170,7 +170,7 @@
val selected = mutableStateOf(0)
var focused = false
rule.setContent {
- Box {
+ Box(Modifier.focusable()) {
HierarchicalFocusCoordinator({ selected.value == 0 }) {
FocusableTestItem { focused = it }
}
diff --git a/wear/compose/integration-tests/demos/src/androidTest/java/androidx/wear/compose/integration/demos/test/DemoTest.kt b/wear/compose/integration-tests/demos/src/androidTest/java/androidx/wear/compose/integration/demos/test/DemoTest.kt
index 79fd99f..8b9b028 100644
--- a/wear/compose/integration-tests/demos/src/androidTest/java/androidx/wear/compose/integration/demos/test/DemoTest.kt
+++ b/wear/compose/integration-tests/demos/src/androidTest/java/androidx/wear/compose/integration/demos/test/DemoTest.kt
@@ -17,7 +17,6 @@
package androidx.wear.compose.integration.demos.test
import android.util.Log
-import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.SemanticsNodeInteractionCollection
import androidx.compose.ui.test.hasClickAction
import androidx.compose.ui.test.hasScrollToNodeAction
@@ -47,7 +46,6 @@
// given the use of ScalingLAZYColumn.
@LargeTest
@RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalTestApi::class)
class DemoTest {
// We need to provide the recompose factory first to use new clock.
@get:Rule
@@ -152,16 +150,18 @@
private fun clearFocusFromDemo() {
with(rule.activity) {
- if (hostView.hasFocus()) {
- if (hostView.isFocused) {
- // One of the Compose components has focus.
- focusManager.clearFocus(force = true)
- } else {
- // A child view has focus. (View interop scenario).
- // We could also use hostViewGroup.focusedChild?.clearFocus(), but the
- // interop views might end up being focused if one of them is marked as
- // focusedByDefault. So we clear focus by requesting focus on the owner.
- rule.runOnUiThread { hostView.requestFocus() }
+ rule.runOnUiThread {
+ if (hostView.hasFocus()) {
+ if (hostView.isFocused) {
+ // One of the Compose components has focus.
+ focusManager.clearFocus(force = true)
+ } else {
+ // A child view has focus. (View interop scenario).
+ // We could also use hostViewGroup.focusedChild?.clearFocus(), but the
+ // interop views might end up being focused if one of them is marked as
+ // focusedByDefault. So we clear focus by requesting focus on the owner.
+ hostView.requestFocus()
+ }
}
}
}
@@ -191,13 +191,13 @@
val newPath = path + this
return DemoCategory(
title,
- demos.mapNotNull {
- when (it) {
+ demos.mapNotNull { demo ->
+ when (demo) {
is DemoCategory -> {
- it.filter(newPath, predicate).let { if (it.demos.isEmpty()) null else it }
+ demo.filter(newPath, predicate).let { if (it.demos.isEmpty()) null else it }
}
else -> {
- if (predicate(newPath, it)) it else null
+ if (predicate(newPath, demo)) demo else null
}
}
}
diff --git a/work/work-gcm/src/androidTest/java/androidx/work/impl/background/gcm/GcmTaskConverterTest.kt b/work/work-gcm/src/androidTest/java/androidx/work/impl/background/gcm/GcmTaskConverterTest.kt
index da05ab5..08e70ec 100644
--- a/work/work-gcm/src/androidTest/java/androidx/work/impl/background/gcm/GcmTaskConverterTest.kt
+++ b/work/work-gcm/src/androidTest/java/androidx/work/impl/background/gcm/GcmTaskConverterTest.kt
@@ -29,7 +29,6 @@
import com.google.android.gms.gcm.Task
import java.util.concurrent.TimeUnit
import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.greaterThan
import org.hamcrest.Matchers.lessThanOrEqualTo
import org.junit.Assert.assertEquals
import org.junit.Before
@@ -210,13 +209,15 @@
@Test
@SdkSuppress(
- minSdkVersion = 22, // b/269194015 for minSdkVersion = 22
maxSdkVersion = WorkManagerImpl.MAX_PRE_JOB_SCHEDULER_API_LEVEL
)
fun testPeriodicWorkRequest_withFlex_firstRun() {
val request = PeriodicWorkRequestBuilder<TestWorker>(
15L, TimeUnit.MINUTES, 5, TimeUnit.MINUTES
).build()
+ val now = System.currentTimeMillis()
+ `when`(mTaskConverter.now()).thenReturn(now)
+ request.workSpec.lastEnqueueTime = now
val task = mTaskConverter.convert(request.workSpec)
assertEquals(task.serviceName, WorkManagerGcmService::class.java.name)
@@ -224,12 +225,12 @@
assertEquals(task.isUpdateCurrent, true)
assertEquals(task.requiredNetwork, Task.NETWORK_STATE_ANY)
assertEquals(task.requiresCharging, false)
- assertThat(task.windowStart, greaterThan(0L)) // should be in the future
+ // should be period - flex
+ assertEquals(task.windowStart, TimeUnit.MINUTES.toSeconds(10))
}
@Test
@SdkSuppress(
- minSdkVersion = 22, // b/269194015 for minSdkVersion = 22
maxSdkVersion = WorkManagerImpl.MAX_PRE_JOB_SCHEDULER_API_LEVEL
)
fun testPeriodicWorkRequest_withFlex_nextRun() {
@@ -241,6 +242,7 @@
).build()
request.workSpec.lastEnqueueTime = now
+ request.workSpec.periodCount++
val expected = TimeUnit.MINUTES.toSeconds(15L)
val task = mTaskConverter.convert(request.workSpec)
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java
index 72a068d..72d4ff3 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/workers/ConstraintTrackingWorkerTest.java
@@ -28,7 +28,6 @@
import static org.mockito.Mockito.when;
import android.content.Context;
-import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
@@ -155,9 +154,7 @@
@After
public void tearDown() {
- if (Build.VERSION.SDK_INT >= 18) {
- mHandlerThread.quitSafely();
- }
+ mHandlerThread.quitSafely();
}
@Test