Merge "Add sample of links styling to material text" into androidx-main
diff --git a/compose/material3/material3/api/1.3.0-beta01.txt b/compose/material3/material3/api/1.3.0-beta01.txt
index c4a1b23..f92b5a9 100644
--- a/compose/material3/material3/api/1.3.0-beta01.txt
+++ b/compose/material3/material3/api/1.3.0-beta01.txt
@@ -17,12 +17,15 @@
field public static final androidx.compose.material3.AlertDialogDefaults INSTANCE;
}
- public final class AndroidAlertDialog_androidKt {
+ public final class AlertDialogKt {
method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void AlertDialog(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.window.DialogProperties properties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void AlertDialog(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function0<kotlin.Unit> confirmButton, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dismissButton, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long iconContentColor, optional long titleContentColor, optional long textContentColor, optional float tonalElevation, optional androidx.compose.ui.window.DialogProperties properties);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void BasicAlertDialog(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.window.DialogProperties properties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
+ public final class AndroidAlertDialog_androidKt {
+ method @androidx.compose.runtime.Composable public static void AlertDialog(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function0<kotlin.Unit> confirmButton, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dismissButton, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long iconContentColor, optional long titleContentColor, optional long textContentColor, optional float tonalElevation, optional androidx.compose.ui.window.DialogProperties properties);
+ }
+
public final class AndroidMenu_androidKt {
method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.foundation.ScrollState scrollState, optional androidx.compose.ui.window.PopupProperties properties, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.BorderStroke? border, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @Deprecated @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.foundation.ScrollState scrollState, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
@@ -2009,6 +2012,8 @@
}
public final class TooltipKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void PlainTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional long caretSize, optional androidx.compose.ui.graphics.Shape shape, optional long contentColor, optional long containerColor, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RichTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? action, optional long caretSize, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.RichTooltipColors colors, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> text);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TooltipBox(androidx.compose.ui.window.PopupPositionProvider positionProvider, kotlin.jvm.functions.Function1<? super androidx.compose.material3.TooltipScope,kotlin.Unit> tooltip, androidx.compose.material3.TooltipState state, optional androidx.compose.ui.Modifier modifier, optional boolean focusable, optional boolean enableUserInput, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.material3.TooltipState TooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.TooltipState rememberTooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
@@ -2030,11 +2035,6 @@
property public abstract androidx.compose.animation.core.MutableTransitionState<java.lang.Boolean> transition;
}
- public final class Tooltip_androidKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void PlainTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional long caretSize, optional androidx.compose.ui.graphics.Shape shape, optional long contentColor, optional long containerColor, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RichTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? action, optional long caretSize, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.RichTooltipColors colors, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> text);
- }
-
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class TopAppBarColors {
ctor public TopAppBarColors(long containerColor, long scrolledContainerColor, long navigationIconContentColor, long titleContentColor, long actionIconContentColor);
method public androidx.compose.material3.TopAppBarColors copy(optional long containerColor, optional long scrolledContainerColor, optional long navigationIconContentColor, optional long titleContentColor, optional long actionIconContentColor);
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index c4a1b23..f92b5a9 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -17,12 +17,15 @@
field public static final androidx.compose.material3.AlertDialogDefaults INSTANCE;
}
- public final class AndroidAlertDialog_androidKt {
+ public final class AlertDialogKt {
method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void AlertDialog(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.window.DialogProperties properties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void AlertDialog(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function0<kotlin.Unit> confirmButton, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dismissButton, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long iconContentColor, optional long titleContentColor, optional long textContentColor, optional float tonalElevation, optional androidx.compose.ui.window.DialogProperties properties);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void BasicAlertDialog(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.window.DialogProperties properties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
+ public final class AndroidAlertDialog_androidKt {
+ method @androidx.compose.runtime.Composable public static void AlertDialog(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function0<kotlin.Unit> confirmButton, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dismissButton, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long iconContentColor, optional long titleContentColor, optional long textContentColor, optional float tonalElevation, optional androidx.compose.ui.window.DialogProperties properties);
+ }
+
public final class AndroidMenu_androidKt {
method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.foundation.ScrollState scrollState, optional androidx.compose.ui.window.PopupProperties properties, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.BorderStroke? border, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @Deprecated @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.foundation.ScrollState scrollState, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
@@ -2009,6 +2012,8 @@
}
public final class TooltipKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void PlainTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional long caretSize, optional androidx.compose.ui.graphics.Shape shape, optional long contentColor, optional long containerColor, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RichTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? action, optional long caretSize, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.RichTooltipColors colors, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> text);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TooltipBox(androidx.compose.ui.window.PopupPositionProvider positionProvider, kotlin.jvm.functions.Function1<? super androidx.compose.material3.TooltipScope,kotlin.Unit> tooltip, androidx.compose.material3.TooltipState state, optional androidx.compose.ui.Modifier modifier, optional boolean focusable, optional boolean enableUserInput, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.material3.TooltipState TooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.TooltipState rememberTooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
@@ -2030,11 +2035,6 @@
property public abstract androidx.compose.animation.core.MutableTransitionState<java.lang.Boolean> transition;
}
- public final class Tooltip_androidKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void PlainTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional long caretSize, optional androidx.compose.ui.graphics.Shape shape, optional long contentColor, optional long containerColor, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RichTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? action, optional long caretSize, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.RichTooltipColors colors, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> text);
- }
-
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class TopAppBarColors {
ctor public TopAppBarColors(long containerColor, long scrolledContainerColor, long navigationIconContentColor, long titleContentColor, long actionIconContentColor);
method public androidx.compose.material3.TopAppBarColors copy(optional long containerColor, optional long scrolledContainerColor, optional long navigationIconContentColor, optional long titleContentColor, optional long actionIconContentColor);
diff --git a/compose/material3/material3/api/restricted_1.3.0-beta01.txt b/compose/material3/material3/api/restricted_1.3.0-beta01.txt
index c4a1b23..f92b5a9 100644
--- a/compose/material3/material3/api/restricted_1.3.0-beta01.txt
+++ b/compose/material3/material3/api/restricted_1.3.0-beta01.txt
@@ -17,12 +17,15 @@
field public static final androidx.compose.material3.AlertDialogDefaults INSTANCE;
}
- public final class AndroidAlertDialog_androidKt {
+ public final class AlertDialogKt {
method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void AlertDialog(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.window.DialogProperties properties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void AlertDialog(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function0<kotlin.Unit> confirmButton, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dismissButton, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long iconContentColor, optional long titleContentColor, optional long textContentColor, optional float tonalElevation, optional androidx.compose.ui.window.DialogProperties properties);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void BasicAlertDialog(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.window.DialogProperties properties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
+ public final class AndroidAlertDialog_androidKt {
+ method @androidx.compose.runtime.Composable public static void AlertDialog(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function0<kotlin.Unit> confirmButton, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dismissButton, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long iconContentColor, optional long titleContentColor, optional long textContentColor, optional float tonalElevation, optional androidx.compose.ui.window.DialogProperties properties);
+ }
+
public final class AndroidMenu_androidKt {
method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.foundation.ScrollState scrollState, optional androidx.compose.ui.window.PopupProperties properties, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.BorderStroke? border, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @Deprecated @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.foundation.ScrollState scrollState, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
@@ -2009,6 +2012,8 @@
}
public final class TooltipKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void PlainTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional long caretSize, optional androidx.compose.ui.graphics.Shape shape, optional long contentColor, optional long containerColor, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RichTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? action, optional long caretSize, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.RichTooltipColors colors, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> text);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TooltipBox(androidx.compose.ui.window.PopupPositionProvider positionProvider, kotlin.jvm.functions.Function1<? super androidx.compose.material3.TooltipScope,kotlin.Unit> tooltip, androidx.compose.material3.TooltipState state, optional androidx.compose.ui.Modifier modifier, optional boolean focusable, optional boolean enableUserInput, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.material3.TooltipState TooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.TooltipState rememberTooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
@@ -2030,11 +2035,6 @@
property public abstract androidx.compose.animation.core.MutableTransitionState<java.lang.Boolean> transition;
}
- public final class Tooltip_androidKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void PlainTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional long caretSize, optional androidx.compose.ui.graphics.Shape shape, optional long contentColor, optional long containerColor, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RichTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? action, optional long caretSize, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.RichTooltipColors colors, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> text);
- }
-
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class TopAppBarColors {
ctor public TopAppBarColors(long containerColor, long scrolledContainerColor, long navigationIconContentColor, long titleContentColor, long actionIconContentColor);
method public androidx.compose.material3.TopAppBarColors copy(optional long containerColor, optional long scrolledContainerColor, optional long navigationIconContentColor, optional long titleContentColor, optional long actionIconContentColor);
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index c4a1b23..f92b5a9 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -17,12 +17,15 @@
field public static final androidx.compose.material3.AlertDialogDefaults INSTANCE;
}
- public final class AndroidAlertDialog_androidKt {
+ public final class AlertDialogKt {
method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void AlertDialog(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.window.DialogProperties properties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void AlertDialog(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function0<kotlin.Unit> confirmButton, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dismissButton, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long iconContentColor, optional long titleContentColor, optional long textContentColor, optional float tonalElevation, optional androidx.compose.ui.window.DialogProperties properties);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void BasicAlertDialog(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.window.DialogProperties properties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
+ public final class AndroidAlertDialog_androidKt {
+ method @androidx.compose.runtime.Composable public static void AlertDialog(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function0<kotlin.Unit> confirmButton, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dismissButton, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long iconContentColor, optional long titleContentColor, optional long textContentColor, optional float tonalElevation, optional androidx.compose.ui.window.DialogProperties properties);
+ }
+
public final class AndroidMenu_androidKt {
method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.foundation.ScrollState scrollState, optional androidx.compose.ui.window.PopupProperties properties, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.BorderStroke? border, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @Deprecated @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.foundation.ScrollState scrollState, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
@@ -2009,6 +2012,8 @@
}
public final class TooltipKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void PlainTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional long caretSize, optional androidx.compose.ui.graphics.Shape shape, optional long contentColor, optional long containerColor, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RichTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? action, optional long caretSize, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.RichTooltipColors colors, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> text);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TooltipBox(androidx.compose.ui.window.PopupPositionProvider positionProvider, kotlin.jvm.functions.Function1<? super androidx.compose.material3.TooltipScope,kotlin.Unit> tooltip, androidx.compose.material3.TooltipState state, optional androidx.compose.ui.Modifier modifier, optional boolean focusable, optional boolean enableUserInput, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.material3.TooltipState TooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.TooltipState rememberTooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
@@ -2030,11 +2035,6 @@
property public abstract androidx.compose.animation.core.MutableTransitionState<java.lang.Boolean> transition;
}
- public final class Tooltip_androidKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void PlainTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional long caretSize, optional androidx.compose.ui.graphics.Shape shape, optional long contentColor, optional long containerColor, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RichTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? action, optional long caretSize, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.RichTooltipColors colors, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> text);
- }
-
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class TopAppBarColors {
ctor public TopAppBarColors(long containerColor, long scrolledContainerColor, long navigationIconContentColor, long titleContentColor, long actionIconContentColor);
method public androidx.compose.material3.TopAppBarColors copy(optional long containerColor, optional long scrolledContainerColor, optional long navigationIconContentColor, optional long titleContentColor, optional long actionIconContentColor);
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidAlertDialog.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidAlertDialog.android.kt
index 0aede79..060ab2b 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidAlertDialog.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidAlertDialog.android.kt
@@ -16,223 +16,46 @@
package androidx.compose.material3
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.sizeIn
-import androidx.compose.material3.internal.Strings
-import androidx.compose.material3.internal.getString
-import androidx.compose.material3.tokens.DialogTokens
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.semantics.paneTitle
-import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
-/**
- * <a href="https://m3.material.io/components/dialogs/overview" class="external" target="_blank">Material Design basic dialog</a>.
- *
- * Dialogs provide important prompts in a user flow. They can require an action, communicate
- * information, or help users accomplish a task.
- *
- * ![Basic dialog image](https://developer.android.com/images/reference/androidx/compose/material3/basic-dialog.png)
- *
- * The dialog will position its buttons, typically [TextButton]s, based on the available space.
- * By default it will try to place them horizontally next to each other and fallback to horizontal
- * placement if not enough space is available.
- *
- * Simple usage:
- * @sample androidx.compose.material3.samples.AlertDialogSample
- *
- * Usage with a "Hero" icon:
- * @sample androidx.compose.material3.samples.AlertDialogWithIconSample
- *
- * @param onDismissRequest called when the user tries to dismiss the Dialog by clicking outside
- * or pressing the back button. This is not called when the dismiss button is clicked.
- * @param confirmButton button which is meant to confirm a proposed action, thus resolving what
- * triggered the dialog. The dialog does not set up any events for this button so they need to be
- * set up by the caller.
- * @param modifier the [Modifier] to be applied to this dialog
- * @param dismissButton button which is meant to dismiss the dialog. The dialog does not set up any
- * events for this button so they need to be set up by the caller.
- * @param icon optional icon that will appear above the [title] or above the [text], in case a
- * title was not provided.
- * @param title title which should specify the purpose of the dialog. The title is not mandatory,
- * because there may be sufficient information inside the [text].
- * @param text text which presents the details regarding the dialog's purpose.
- * @param shape defines the shape of this dialog's container
- * @param containerColor the color used for the background of this dialog. Use [Color.Transparent]
- * to have no color.
- * @param iconContentColor the content color used for the icon.
- * @param titleContentColor the content color used for the title.
- * @param textContentColor the content color used for the text.
- * @param tonalElevation when [containerColor] is [ColorScheme.surface], a translucent primary color
- * overlay is applied on top of the container. A higher tonal elevation value will result in a
- * darker color in light theme and lighter color in dark theme. See also: [Surface].
- * @param properties typically platform specific properties to further configure the dialog.
- * @see BasicAlertDialog
- */
-@OptIn(ExperimentalMaterial3Api::class)
+// Keep expect/actual for maintain binary compatibility.
+// `@file:JvmName` doesn't work here because Android and Desktop were published with different names
+// Please note that binary compatibility for Desktop is tracked only in JetBrains fork
+
@Composable
-fun AlertDialog(
+actual fun AlertDialog(
onDismissRequest: () -> Unit,
confirmButton: @Composable () -> Unit,
- modifier: Modifier = Modifier,
- dismissButton: @Composable (() -> Unit)? = null,
- icon: @Composable (() -> Unit)? = null,
- title: @Composable (() -> Unit)? = null,
- text: @Composable (() -> Unit)? = null,
- shape: Shape = AlertDialogDefaults.shape,
- containerColor: Color = AlertDialogDefaults.containerColor,
- iconContentColor: Color = AlertDialogDefaults.iconContentColor,
- titleContentColor: Color = AlertDialogDefaults.titleContentColor,
- textContentColor: Color = AlertDialogDefaults.textContentColor,
- tonalElevation: Dp = AlertDialogDefaults.TonalElevation,
- properties: DialogProperties = DialogProperties()
-) = BasicAlertDialog(
+ modifier: Modifier,
+ dismissButton: @Composable (() -> Unit)?,
+ icon: @Composable (() -> Unit)?,
+ title: @Composable (() -> Unit)?,
+ text: @Composable (() -> Unit)?,
+ shape: Shape,
+ containerColor: Color,
+ iconContentColor: Color,
+ titleContentColor: Color,
+ textContentColor: Color,
+ tonalElevation: Dp,
+ properties: DialogProperties
+): Unit = AlertDialogImpl(
onDismissRequest = onDismissRequest,
+ confirmButton = confirmButton,
modifier = modifier,
+ dismissButton = dismissButton,
+ icon = icon,
+ title = title,
+ text = text,
+ shape = shape,
+ containerColor = containerColor,
+ iconContentColor = iconContentColor,
+ titleContentColor = titleContentColor,
+ textContentColor = textContentColor,
+ tonalElevation = tonalElevation,
properties = properties
-) {
- AlertDialogContent(
- buttons = {
- AlertDialogFlowRow(
- mainAxisSpacing = ButtonsMainAxisSpacing,
- crossAxisSpacing = ButtonsCrossAxisSpacing
- ) {
- dismissButton?.invoke()
- confirmButton()
- }
- },
- icon = icon,
- title = title,
- text = text,
- shape = shape,
- containerColor = containerColor,
- tonalElevation = tonalElevation,
- // Note that a button content color is provided here from the dialog's token, but in
- // most cases, TextButtons should be used for dismiss and confirm buttons.
- // TextButtons will not consume this provided content color value, and will used their
- // own defined or default colors.
- buttonContentColor = DialogTokens.ActionLabelTextColor.value,
- iconContentColor = iconContentColor,
- titleContentColor = titleContentColor,
- textContentColor = textContentColor,
- )
-}
-
-/**
- * <a href="https://m3.material.io/components/dialogs/overview" class="external" target="_blank">Basic alert dialog dialog</a>.
- *
- * Dialogs provide important prompts in a user flow. They can require an action, communicate
- * information, or help users accomplish a task.
- *
- * ![Basic dialog image](https://developer.android.com/images/reference/androidx/compose/material3/basic-dialog.png)
- *
- * This basic alert dialog expects an arbitrary content that is defined by the caller. Note that
- * your content will need to define its own styling.
- *
- * By default, the displayed dialog has the minimum height and width that the Material Design spec
- * defines. If required, these constraints can be overwritten by providing a `width` or `height`
- * [Modifier]s.
- *
- * Basic alert dialog usage with custom content:
- * @sample androidx.compose.material3.samples.BasicAlertDialogSample
- *
- * @param onDismissRequest called when the user tries to dismiss the Dialog by clicking outside
- * or pressing the back button. This is not called when the dismiss button is clicked.
- * @param modifier the [Modifier] to be applied to this dialog's content.
- * @param properties typically platform specific properties to further configure the dialog.
- * @param content the content of the dialog
- */
-@ExperimentalMaterial3Api
-@Composable
-fun BasicAlertDialog(
- onDismissRequest: () -> Unit,
- modifier: Modifier = Modifier,
- properties: DialogProperties = DialogProperties(),
- content: @Composable () -> Unit
-) {
- Dialog(
- onDismissRequest = onDismissRequest,
- properties = properties,
- ) {
- val dialogPaneDescription = getString(Strings.Dialog)
- Box(
- modifier = modifier
- .sizeIn(minWidth = DialogMinWidth, maxWidth = DialogMaxWidth)
- .then(Modifier.semantics { paneTitle = dialogPaneDescription }),
- propagateMinConstraints = true
- ) {
- content()
- }
- }
-}
-
-/**
- * <a href="https://m3.material.io/components/dialogs/overview" class="external" target="_blank">Basic alert dialog dialog</a>.
- *
- * Dialogs provide important prompts in a user flow. They can require an action, communicate
- * information, or help users accomplish a task.
- *
- * ![Basic dialog image](https://developer.android.com/images/reference/androidx/compose/material3/basic-dialog.png)
- *
- * This basic alert dialog expects an arbitrary content that is defined by the caller. Note that
- * your content will need to define its own styling.
- *
- * By default, the displayed dialog has the minimum height and width that the Material Design spec
- * defines. If required, these constraints can be overwritten by providing a `width` or `height`
- * [Modifier]s.
- *
- * Basic alert dialog usage with custom content:
- * @sample androidx.compose.material3.samples.BasicAlertDialogSample
- *
- * @param onDismissRequest called when the user tries to dismiss the Dialog by clicking outside
- * or pressing the back button. This is not called when the dismiss button is clicked.
- * @param modifier the [Modifier] to be applied to this dialog's content.
- * @param properties typically platform specific properties to further configure the dialog.
- * @param content the content of the dialog
- */
-@Deprecated(
- "Use BasicAlertDialog instead",
- replaceWith = ReplaceWith(
- "BasicAlertDialog(onDismissRequest, modifier, properties, content)"
- )
)
-@ExperimentalMaterial3Api
-@Composable
-fun AlertDialog(
- onDismissRequest: () -> Unit,
- modifier: Modifier = Modifier,
- properties: DialogProperties = DialogProperties(),
- content: @Composable () -> Unit
-) = BasicAlertDialog(onDismissRequest, modifier, properties, content)
-
-/**
- * Contains default values used for [AlertDialog] and [BasicAlertDialog].
- */
-object AlertDialogDefaults {
- /** The default shape for alert dialogs */
- val shape: Shape @Composable get() = DialogTokens.ContainerShape.value
-
- /** The default container color for alert dialogs */
- val containerColor: Color @Composable get() = DialogTokens.ContainerColor.value
-
- /** The default icon color for alert dialogs */
- val iconContentColor: Color @Composable get() = DialogTokens.IconColor.value
-
- /** The default title color for alert dialogs */
- val titleContentColor: Color @Composable get() = DialogTokens.HeadlineColor.value
-
- /** The default text color for alert dialogs */
- val textContentColor: Color @Composable get() = DialogTokens.SupportingTextColor.value
-
- /** The default tonal elevation for alert dialogs */
- val TonalElevation: Dp = 0.dp
-}
-
-private val ButtonsMainAxisSpacing = 8.dp
-private val ButtonsCrossAxisSpacing = 12.dp
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Tooltip.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Tooltip.android.kt
index 8777756..b42e087 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Tooltip.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Tooltip.android.kt
@@ -16,320 +16,12 @@
package androidx.compose.material3
-import android.content.res.Configuration
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.paddingFromBaseline
-import androidx.compose.foundation.layout.requiredHeightIn
-import androidx.compose.foundation.layout.sizeIn
-import androidx.compose.material3.tokens.PlainTooltipTokens
-import androidx.compose.material3.tokens.RichTooltipTokens
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.CacheDrawScope
-import androidx.compose.ui.draw.DrawResult
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Path
-import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.isSpecified
-/**
- * Plain tooltip that provides a descriptive message.
- *
- * Usually used with [TooltipBox].
- *
- * @param modifier the [Modifier] to be applied to the tooltip.
- * @param caretSize [DpSize] for the caret of the tooltip, if a default
- * caret is desired with a specific dimension. Please see [TooltipDefaults.caretSize] to
- * see the default dimensions. Pass in Dp.Unspecified for this parameter if no caret is desired.
- * @param shape the [Shape] that should be applied to the tooltip container.
- * @param contentColor [Color] that will be applied to the tooltip's content.
- * @param containerColor [Color] that will be applied to the tooltip's container.
- * @param tonalElevation the tonal elevation of the tooltip.
- * @param shadowElevation the shadow elevation of the tooltip.
- * @param content the composable that will be used to populate the tooltip's content.
- */
@Composable
-@ExperimentalMaterial3Api
-actual fun TooltipScope.PlainTooltip(
- modifier: Modifier,
- caretSize: DpSize,
- shape: Shape,
- contentColor: Color,
- containerColor: Color,
- tonalElevation: Dp,
- shadowElevation: Dp,
- content: @Composable () -> Unit
-) {
- val drawCaretModifier =
- if (caretSize.isSpecified) {
- val density = LocalDensity.current
- val configuration = LocalConfiguration.current
- Modifier.drawCaret { anchorLayoutCoordinates ->
- drawCaretWithPath(
- CaretType.Plain,
- density,
- configuration,
- containerColor,
- caretSize,
- anchorLayoutCoordinates
- )
- }.then(modifier)
- } else modifier
- Surface(
- modifier = drawCaretModifier,
- shape = shape,
- color = containerColor,
- tonalElevation = tonalElevation,
- shadowElevation = shadowElevation
- ) {
- Box(modifier = Modifier
- .sizeIn(
- minWidth = TooltipMinWidth,
- maxWidth = PlainTooltipMaxWidth,
- minHeight = TooltipMinHeight
- )
- .padding(PlainTooltipContentPadding)
- ) {
- val textStyle =
- PlainTooltipTokens.SupportingTextFont.value
-
- CompositionLocalProvider(
- LocalContentColor provides contentColor,
- LocalTextStyle provides textStyle,
- content = content
- )
- }
- }
-}
-
-/**
- * Rich text tooltip that allows the user to pass in a title, text, and action.
- * Tooltips are used to provide a descriptive message.
- *
- * Usually used with [TooltipBox]
- *
- * @param modifier the [Modifier] to be applied to the tooltip.
- * @param title An optional title for the tooltip.
- * @param action An optional action for the tooltip.
- * @param caretSize [DpSize] for the caret of the tooltip, if a default
- * caret is desired with a specific dimension. Please see [TooltipDefaults.caretSize] to
- * see the default dimensions. Pass in Dp.Unspecified for this parameter if no caret is desired.
- * @param shape the [Shape] that should be applied to the tooltip container.
- * @param colors [RichTooltipColors] that will be applied to the tooltip's container and content.
- * @param tonalElevation the tonal elevation of the tooltip.
- * @param shadowElevation the shadow elevation of the tooltip.
- * @param text the composable that will be used to populate the rich tooltip's text.
- */
-@Composable
-@ExperimentalMaterial3Api
-actual fun TooltipScope.RichTooltip(
- modifier: Modifier,
- title: (@Composable () -> Unit)?,
- action: (@Composable () -> Unit)?,
- caretSize: DpSize,
- shape: Shape,
- colors: RichTooltipColors,
- tonalElevation: Dp,
- shadowElevation: Dp,
- text: @Composable () -> Unit
-) {
- val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation
- val elevatedColor =
- MaterialTheme.colorScheme.applyTonalElevation(
- colors.containerColor,
- absoluteElevation
- )
- val drawCaretModifier =
- if (caretSize.isSpecified) {
- val density = LocalDensity.current
- val configuration = LocalConfiguration.current
- Modifier.drawCaret { anchorLayoutCoordinates ->
- drawCaretWithPath(
- CaretType.Rich,
- density,
- configuration,
- elevatedColor,
- caretSize,
- anchorLayoutCoordinates
- )
- }.then(modifier)
- } else modifier
- Surface(
- modifier = drawCaretModifier
- .sizeIn(
- minWidth = TooltipMinWidth,
- maxWidth = RichTooltipMaxWidth,
- minHeight = TooltipMinHeight
- ),
- shape = shape,
- color = colors.containerColor,
- tonalElevation = tonalElevation,
- shadowElevation = shadowElevation
- ) {
- val actionLabelTextStyle = RichTooltipTokens.ActionLabelTextFont.value
- val subheadTextStyle = RichTooltipTokens.SubheadFont.value
- val supportingTextStyle = RichTooltipTokens.SupportingTextFont.value
-
- Column(
- modifier = Modifier.padding(horizontal = RichTooltipHorizontalPadding)
- ) {
- title?.let {
- Box(
- modifier = Modifier.paddingFromBaseline(top = HeightToSubheadFirstLine)
- ) {
- CompositionLocalProvider(
- LocalContentColor provides colors.titleContentColor,
- LocalTextStyle provides subheadTextStyle,
- content = it
- )
- }
- }
- Box(
- modifier = Modifier.textVerticalPadding(
- title != null,
- action != null
- )
- ) {
- CompositionLocalProvider(
- LocalContentColor provides colors.contentColor,
- LocalTextStyle provides supportingTextStyle,
- content = text
- )
- }
- action?.let {
- Box(
- modifier = Modifier
- .requiredHeightIn(min = ActionLabelMinHeight)
- .padding(bottom = ActionLabelBottomPadding)
- ) {
- CompositionLocalProvider(
- LocalContentColor provides colors.actionContentColor,
- LocalTextStyle provides actionLabelTextStyle,
- content = it
- )
- }
- }
- }
- }
-}
-
-@ExperimentalMaterial3Api
-private fun CacheDrawScope.drawCaretWithPath(
- caretType: CaretType,
- density: Density,
- configuration: Configuration,
- containerColor: Color,
- caretSize: DpSize,
- anchorLayoutCoordinates: LayoutCoordinates?
-): DrawResult {
- val path = Path()
-
- if (anchorLayoutCoordinates != null) {
- val caretHeightPx: Int
- val caretWidthPx: Int
- val screenWidthPx: Int
- val tooltipAnchorSpacing: Int
- with(density) {
- caretHeightPx = caretSize.height.roundToPx()
- caretWidthPx = caretSize.width.roundToPx()
- screenWidthPx = configuration.screenWidthDp.dp.roundToPx()
- tooltipAnchorSpacing = SpacingBetweenTooltipAndAnchor.roundToPx()
- }
- val anchorBounds = anchorLayoutCoordinates.boundsInWindow()
- val anchorLeft = anchorBounds.left
- val anchorRight = anchorBounds.right
- val anchorTop = anchorBounds.top
- val anchorMid = (anchorRight + anchorLeft) / 2
- val anchorWidth = anchorRight - anchorLeft
- val tooltipWidth = this.size.width
- val tooltipHeight = this.size.height
- val isCaretTop = anchorTop - tooltipHeight - tooltipAnchorSpacing < 0
- val caretY = if (isCaretTop) { 0f } else { tooltipHeight }
-
- val position: Offset
- if (caretType == CaretType.Plain) {
- position =
- if (anchorMid + tooltipWidth / 2 > screenWidthPx) {
- // Caret needs to be near the right
- val anchorMidFromRightScreenEdge =
- screenWidthPx - anchorMid
- val caretX = tooltipWidth - anchorMidFromRightScreenEdge
- Offset(caretX, caretY)
- } else {
- // Caret needs to be near the left
- val tooltipLeft =
- anchorLeft - (this.size.width / 2 - anchorWidth / 2)
- val caretX = anchorMid - maxOf(tooltipLeft, 0f)
- Offset(caretX, caretY)
- }
- } else {
- // Default the caret to the left
- var preferredPosition = Offset(anchorMid - anchorLeft, caretY)
- if (anchorLeft + tooltipWidth > screenWidthPx) {
- // Need to move the caret to the right
- preferredPosition = Offset(anchorMid - (anchorRight - tooltipWidth), caretY)
- if (anchorRight - tooltipWidth < 0) {
- // Need to center the caret
- // Caret might need to be offset depending on where
- // the tooltip is placed relative to the anchor
- if (anchorLeft - tooltipWidth / 2 + anchorWidth / 2 <= 0) {
- preferredPosition = Offset(anchorMid, caretY)
- } else if (anchorRight + tooltipWidth / 2 - anchorWidth / 2 >= screenWidthPx) {
- val anchorMidFromRightScreenEdge =
- screenWidthPx - anchorMid
- val caretX = tooltipWidth - anchorMidFromRightScreenEdge
- preferredPosition = Offset(caretX, caretY)
- } else {
- preferredPosition = Offset(tooltipWidth / 2, caretY)
- }
- }
- }
- position = preferredPosition
- }
-
- if (isCaretTop) {
- path.apply {
- moveTo(x = position.x, y = position.y)
- lineTo(x = position.x + caretWidthPx / 2, y = position.y)
- lineTo(x = position.x, y = position.y - caretHeightPx)
- lineTo(x = position.x - caretWidthPx / 2, y = position.y)
- close()
- }
- } else {
- path.apply {
- moveTo(x = position.x, y = position.y)
- lineTo(x = position.x + caretWidthPx / 2, y = position.y)
- lineTo(x = position.x, y = position.y + caretHeightPx.toFloat())
- lineTo(x = position.x - caretWidthPx / 2, y = position.y)
- close()
- }
- }
- }
-
- return onDrawWithContent {
- if (anchorLayoutCoordinates != null) {
- drawContent()
- drawPath(
- path = path,
- color = containerColor
- )
- }
- }
-}
-
-@ExperimentalMaterial3Api
-private enum class CaretType {
- Plain, Rich
+internal actual fun windowContainerWidthInPx(): Int = with(LocalDensity.current) {
+ LocalConfiguration.current.screenWidthDp.dp.roundToPx()
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AlertDialog.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AlertDialog.kt
index cf167cd..ee43c24 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AlertDialog.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AlertDialog.kt
@@ -21,7 +21,10 @@
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material3.internal.ProvideContentColorTextStyle
+import androidx.compose.material3.internal.Strings
+import androidx.compose.material3.internal.getString
import androidx.compose.material3.tokens.DialogTokens
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -31,12 +34,236 @@
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.semantics.paneTitle
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
import kotlin.math.max
+/**
+ * <a href="https://m3.material.io/components/dialogs/overview" class="external" target="_blank">Material Design basic dialog</a>.
+ *
+ * Dialogs provide important prompts in a user flow. They can require an action, communicate
+ * information, or help users accomplish a task.
+ *
+ * ![Basic dialog image](https://developer.android.com/images/reference/androidx/compose/material3/basic-dialog.png)
+ *
+ * The dialog will position its buttons, typically [TextButton]s, based on the available space.
+ * By default it will try to place them horizontally next to each other and fallback to horizontal
+ * placement if not enough space is available.
+ *
+ * Simple usage:
+ * @sample androidx.compose.material3.samples.AlertDialogSample
+ *
+ * Usage with a "Hero" icon:
+ * @sample androidx.compose.material3.samples.AlertDialogWithIconSample
+ *
+ * @param onDismissRequest called when the user tries to dismiss the Dialog by clicking outside
+ * or pressing the back button. This is not called when the dismiss button is clicked.
+ * @param confirmButton button which is meant to confirm a proposed action, thus resolving what
+ * triggered the dialog. The dialog does not set up any events for this button so they need to be
+ * set up by the caller.
+ * @param modifier the [Modifier] to be applied to this dialog
+ * @param dismissButton button which is meant to dismiss the dialog. The dialog does not set up any
+ * events for this button so they need to be set up by the caller.
+ * @param icon optional icon that will appear above the [title] or above the [text], in case a
+ * title was not provided.
+ * @param title title which should specify the purpose of the dialog. The title is not mandatory,
+ * because there may be sufficient information inside the [text].
+ * @param text text which presents the details regarding the dialog's purpose.
+ * @param shape defines the shape of this dialog's container
+ * @param containerColor the color used for the background of this dialog. Use [Color.Transparent]
+ * to have no color.
+ * @param iconContentColor the content color used for the icon.
+ * @param titleContentColor the content color used for the title.
+ * @param textContentColor the content color used for the text.
+ * @param tonalElevation when [containerColor] is [ColorScheme.surface], a translucent primary color
+ * overlay is applied on top of the container. A higher tonal elevation value will result in a
+ * darker color in light theme and lighter color in dark theme. See also: [Surface].
+ * @param properties typically platform specific properties to further configure the dialog.
+ * @see BasicAlertDialog
+ */
+@Composable
+expect fun AlertDialog(
+ onDismissRequest: () -> Unit,
+ confirmButton: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+ dismissButton: @Composable (() -> Unit)? = null,
+ icon: @Composable (() -> Unit)? = null,
+ title: @Composable (() -> Unit)? = null,
+ text: @Composable (() -> Unit)? = null,
+ shape: Shape = AlertDialogDefaults.shape,
+ containerColor: Color = AlertDialogDefaults.containerColor,
+ iconContentColor: Color = AlertDialogDefaults.iconContentColor,
+ titleContentColor: Color = AlertDialogDefaults.titleContentColor,
+ textContentColor: Color = AlertDialogDefaults.textContentColor,
+ tonalElevation: Dp = AlertDialogDefaults.TonalElevation,
+ properties: DialogProperties = DialogProperties()
+)
+
+/**
+ * <a href="https://m3.material.io/components/dialogs/overview" class="external" target="_blank">Basic alert dialog dialog</a>.
+ *
+ * Dialogs provide important prompts in a user flow. They can require an action, communicate
+ * information, or help users accomplish a task.
+ *
+ * ![Basic dialog image](https://developer.android.com/images/reference/androidx/compose/material3/basic-dialog.png)
+ *
+ * This basic alert dialog expects an arbitrary content that is defined by the caller. Note that
+ * your content will need to define its own styling.
+ *
+ * By default, the displayed dialog has the minimum height and width that the Material Design spec
+ * defines. If required, these constraints can be overwritten by providing a `width` or `height`
+ * [Modifier]s.
+ *
+ * Basic alert dialog usage with custom content:
+ * @sample androidx.compose.material3.samples.BasicAlertDialogSample
+ *
+ * @param onDismissRequest called when the user tries to dismiss the Dialog by clicking outside
+ * or pressing the back button. This is not called when the dismiss button is clicked.
+ * @param modifier the [Modifier] to be applied to this dialog's content.
+ * @param properties typically platform specific properties to further configure the dialog.
+ * @param content the content of the dialog
+ */
+@ExperimentalMaterial3Api
+@Composable
+fun BasicAlertDialog(
+ onDismissRequest: () -> Unit,
+ modifier: Modifier = Modifier,
+ properties: DialogProperties = DialogProperties(),
+ content: @Composable () -> Unit
+) {
+ Dialog(
+ onDismissRequest = onDismissRequest,
+ properties = properties,
+ ) {
+ val dialogPaneDescription = getString(Strings.Dialog)
+ Box(
+ modifier = modifier
+ .sizeIn(minWidth = DialogMinWidth, maxWidth = DialogMaxWidth)
+ .then(Modifier.semantics { paneTitle = dialogPaneDescription }),
+ propagateMinConstraints = true
+ ) {
+ content()
+ }
+ }
+}
+
+/**
+ * <a href="https://m3.material.io/components/dialogs/overview" class="external" target="_blank">Basic alert dialog dialog</a>.
+ *
+ * Dialogs provide important prompts in a user flow. They can require an action, communicate
+ * information, or help users accomplish a task.
+ *
+ * ![Basic dialog image](https://developer.android.com/images/reference/androidx/compose/material3/basic-dialog.png)
+ *
+ * This basic alert dialog expects an arbitrary content that is defined by the caller. Note that
+ * your content will need to define its own styling.
+ *
+ * By default, the displayed dialog has the minimum height and width that the Material Design spec
+ * defines. If required, these constraints can be overwritten by providing a `width` or `height`
+ * [Modifier]s.
+ *
+ * Basic alert dialog usage with custom content:
+ * @sample androidx.compose.material3.samples.BasicAlertDialogSample
+ *
+ * @param onDismissRequest called when the user tries to dismiss the Dialog by clicking outside
+ * or pressing the back button. This is not called when the dismiss button is clicked.
+ * @param modifier the [Modifier] to be applied to this dialog's content.
+ * @param properties typically platform specific properties to further configure the dialog.
+ * @param content the content of the dialog
+ */
+@Deprecated(
+ "Use BasicAlertDialog instead",
+ replaceWith = ReplaceWith(
+ "BasicAlertDialog(onDismissRequest, modifier, properties, content)"
+ )
+)
+@ExperimentalMaterial3Api
+@Composable
+fun AlertDialog(
+ onDismissRequest: () -> Unit,
+ modifier: Modifier = Modifier,
+ properties: DialogProperties = DialogProperties(),
+ content: @Composable () -> Unit
+) = BasicAlertDialog(onDismissRequest, modifier, properties, content)
+
+/**
+ * Contains default values used for [AlertDialog] and [BasicAlertDialog].
+ */
+object AlertDialogDefaults {
+ /** The default shape for alert dialogs */
+ val shape: Shape @Composable get() = DialogTokens.ContainerShape.value
+
+ /** The default container color for alert dialogs */
+ val containerColor: Color @Composable get() = DialogTokens.ContainerColor.value
+
+ /** The default icon color for alert dialogs */
+ val iconContentColor: Color @Composable get() = DialogTokens.IconColor.value
+
+ /** The default title color for alert dialogs */
+ val titleContentColor: Color @Composable get() = DialogTokens.HeadlineColor.value
+
+ /** The default text color for alert dialogs */
+ val textContentColor: Color @Composable get() = DialogTokens.SupportingTextColor.value
+
+ /** The default tonal elevation for alert dialogs */
+ val TonalElevation: Dp = 0.dp
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun AlertDialogImpl(
+ onDismissRequest: () -> Unit,
+ confirmButton: @Composable () -> Unit,
+ modifier: Modifier,
+ dismissButton: @Composable (() -> Unit)?,
+ icon: @Composable (() -> Unit)?,
+ title: @Composable (() -> Unit)?,
+ text: @Composable (() -> Unit)?,
+ shape: Shape,
+ containerColor: Color,
+ iconContentColor: Color,
+ titleContentColor: Color,
+ textContentColor: Color,
+ tonalElevation: Dp,
+ properties: DialogProperties
+): Unit = BasicAlertDialog(
+ onDismissRequest = onDismissRequest,
+ modifier = modifier,
+ properties = properties
+) {
+ AlertDialogContent(
+ buttons = {
+ AlertDialogFlowRow(
+ mainAxisSpacing = ButtonsMainAxisSpacing,
+ crossAxisSpacing = ButtonsCrossAxisSpacing
+ ) {
+ dismissButton?.invoke()
+ confirmButton()
+ }
+ },
+ icon = icon,
+ title = title,
+ text = text,
+ shape = shape,
+ containerColor = containerColor,
+ tonalElevation = tonalElevation,
+ // Note that a button content color is provided here from the dialog's token, but in
+ // most cases, TextButtons should be used for dismiss and confirm buttons.
+ // TextButtons will not consume this provided content color value, and will used their
+ // own defined or default colors.
+ buttonContentColor = DialogTokens.ActionLabelTextColor.value,
+ iconContentColor = iconContentColor,
+ titleContentColor = titleContentColor,
+ textContentColor = textContentColor,
+ )
+}
+
@Composable
internal fun AlertDialogContent(
buttons: @Composable () -> Unit,
@@ -217,6 +444,9 @@
internal val DialogMinWidth = 280.dp
internal val DialogMaxWidth = 560.dp
+private val ButtonsMainAxisSpacing = 8.dp
+private val ButtonsCrossAxisSpacing = 12.dp
+
// Paddings for each of the dialog's parts.
private val DialogPadding = PaddingValues(all = 24.dp)
private val IconPadding = PaddingValues(bottom = 16.dp)
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt
index 366618d..18f443c 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt
@@ -26,15 +26,19 @@
import androidx.compose.foundation.MutatePriority
import androidx.compose.foundation.MutatorMutex
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.paddingFromBaseline
+import androidx.compose.foundation.layout.requiredHeightIn
+import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material3.internal.BasicTooltipBox
import androidx.compose.material3.internal.BasicTooltipDefaults
import androidx.compose.material3.tokens.ElevationTokens
import androidx.compose.material3.tokens.PlainTooltipTokens
import androidx.compose.material3.tokens.RichTooltipTokens
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
@@ -46,14 +50,18 @@
import androidx.compose.ui.draw.CacheDrawScope
import androidx.compose.ui.draw.DrawResult
import androidx.compose.ui.draw.drawWithCache
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntOffset
@@ -61,6 +69,7 @@
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.isSpecified
import androidx.compose.ui.window.PopupPositionProvider
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.suspendCancellableCoroutine
@@ -199,7 +208,7 @@
*/
@Composable
@ExperimentalMaterial3Api
-expect fun TooltipScope.PlainTooltip(
+fun TooltipScope.PlainTooltip(
modifier: Modifier = Modifier,
caretSize: DpSize = DpSize.Unspecified,
shape: Shape = TooltipDefaults.plainTooltipContainerShape,
@@ -208,7 +217,48 @@
tonalElevation: Dp = 0.dp,
shadowElevation: Dp = 0.dp,
content: @Composable () -> Unit
-)
+) {
+ val drawCaretModifier =
+ if (caretSize.isSpecified) {
+ val density = LocalDensity.current
+ val windowContainerWidthInPx = windowContainerWidthInPx()
+ Modifier.drawCaret { anchorLayoutCoordinates ->
+ drawCaretWithPath(
+ CaretType.Plain,
+ density,
+ windowContainerWidthInPx,
+ containerColor,
+ caretSize,
+ anchorLayoutCoordinates
+ )
+ }.then(modifier)
+ } else modifier
+ Surface(
+ modifier = drawCaretModifier,
+ shape = shape,
+ color = containerColor,
+ tonalElevation = tonalElevation,
+ shadowElevation = shadowElevation
+ ) {
+ Box(modifier = Modifier
+ .sizeIn(
+ minWidth = TooltipMinWidth,
+ maxWidth = PlainTooltipMaxWidth,
+ minHeight = TooltipMinHeight
+ )
+ .padding(PlainTooltipContentPadding)
+ ) {
+ val textStyle =
+ PlainTooltipTokens.SupportingTextFont.value
+
+ CompositionLocalProvider(
+ LocalContentColor provides contentColor,
+ LocalTextStyle provides textStyle,
+ content = content
+ )
+ }
+ }
+}
/**
* Rich text tooltip that allows the user to pass in a title, text, and action.
@@ -230,7 +280,7 @@
*/
@Composable
@ExperimentalMaterial3Api
-expect fun TooltipScope.RichTooltip(
+fun TooltipScope.RichTooltip(
modifier: Modifier = Modifier,
title: (@Composable () -> Unit)? = null,
action: (@Composable () -> Unit)? = null,
@@ -240,7 +290,86 @@
tonalElevation: Dp = ElevationTokens.Level0,
shadowElevation: Dp = RichTooltipTokens.ContainerElevation,
text: @Composable () -> Unit
-)
+) {
+ val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation
+ val elevatedColor =
+ MaterialTheme.colorScheme.applyTonalElevation(
+ colors.containerColor,
+ absoluteElevation
+ )
+ val drawCaretModifier =
+ if (caretSize.isSpecified) {
+ val density = LocalDensity.current
+ val windowContainerWidthInPx = windowContainerWidthInPx()
+ Modifier.drawCaret { anchorLayoutCoordinates ->
+ drawCaretWithPath(
+ CaretType.Rich,
+ density,
+ windowContainerWidthInPx,
+ elevatedColor,
+ caretSize,
+ anchorLayoutCoordinates
+ )
+ }.then(modifier)
+ } else modifier
+ Surface(
+ modifier = drawCaretModifier
+ .sizeIn(
+ minWidth = TooltipMinWidth,
+ maxWidth = RichTooltipMaxWidth,
+ minHeight = TooltipMinHeight
+ ),
+ shape = shape,
+ color = colors.containerColor,
+ tonalElevation = tonalElevation,
+ shadowElevation = shadowElevation
+ ) {
+ val actionLabelTextStyle = RichTooltipTokens.ActionLabelTextFont.value
+ val subheadTextStyle = RichTooltipTokens.SubheadFont.value
+ val supportingTextStyle = RichTooltipTokens.SupportingTextFont.value
+
+ Column(
+ modifier = Modifier.padding(horizontal = RichTooltipHorizontalPadding)
+ ) {
+ title?.let {
+ Box(
+ modifier = Modifier.paddingFromBaseline(top = HeightToSubheadFirstLine)
+ ) {
+ CompositionLocalProvider(
+ LocalContentColor provides colors.titleContentColor,
+ LocalTextStyle provides subheadTextStyle,
+ content = it
+ )
+ }
+ }
+ Box(
+ modifier = Modifier.textVerticalPadding(
+ title != null,
+ action != null
+ )
+ ) {
+ CompositionLocalProvider(
+ LocalContentColor provides colors.contentColor,
+ LocalTextStyle provides supportingTextStyle,
+ content = text
+ )
+ }
+ action?.let {
+ Box(
+ modifier = Modifier
+ .requiredHeightIn(min = ActionLabelMinHeight)
+ .padding(bottom = ActionLabelBottomPadding)
+ ) {
+ CompositionLocalProvider(
+ LocalContentColor provides colors.actionContentColor,
+ LocalTextStyle provides actionLabelTextStyle,
+ content = it
+ )
+ }
+ }
+ }
+ }
+}
/**
* Tooltip defaults that contain default values for both [PlainTooltip] and [RichTooltip]
@@ -683,6 +812,118 @@
)
}
+@ExperimentalMaterial3Api
+private fun CacheDrawScope.drawCaretWithPath(
+ caretType: CaretType,
+ density: Density,
+ windowContainerWidthInPx: Int,
+ containerColor: Color,
+ caretSize: DpSize,
+ anchorLayoutCoordinates: LayoutCoordinates?
+): DrawResult {
+ val path = Path()
+
+ if (anchorLayoutCoordinates != null) {
+ val caretHeightPx: Int
+ val caretWidthPx: Int
+ val tooltipAnchorSpacing: Int
+ with(density) {
+ caretHeightPx = caretSize.height.roundToPx()
+ caretWidthPx = caretSize.width.roundToPx()
+ tooltipAnchorSpacing = SpacingBetweenTooltipAndAnchor.roundToPx()
+ }
+ val anchorBounds = anchorLayoutCoordinates.boundsInWindow()
+ val anchorLeft = anchorBounds.left
+ val anchorRight = anchorBounds.right
+ val anchorTop = anchorBounds.top
+ val anchorMid = (anchorRight + anchorLeft) / 2
+ val anchorWidth = anchorRight - anchorLeft
+ val tooltipWidth = this.size.width
+ val tooltipHeight = this.size.height
+ val isCaretTop = anchorTop - tooltipHeight - tooltipAnchorSpacing < 0
+ val caretY = if (isCaretTop) { 0f } else { tooltipHeight }
+
+ val position: Offset
+ if (caretType == CaretType.Plain) {
+ position =
+ if (anchorMid + tooltipWidth / 2 > windowContainerWidthInPx) {
+ // Caret needs to be near the right
+ val anchorMidFromRightScreenEdge =
+ windowContainerWidthInPx - anchorMid
+ val caretX = tooltipWidth - anchorMidFromRightScreenEdge
+ Offset(caretX, caretY)
+ } else {
+ // Caret needs to be near the left
+ val tooltipLeft =
+ anchorLeft - (this.size.width / 2 - anchorWidth / 2)
+ val caretX = anchorMid - maxOf(tooltipLeft, 0f)
+ Offset(caretX, caretY)
+ }
+ } else {
+ // Default the caret to the left
+ var preferredPosition = Offset(anchorMid - anchorLeft, caretY)
+ if (anchorLeft + tooltipWidth > windowContainerWidthInPx) {
+ // Need to move the caret to the right
+ preferredPosition = Offset(anchorMid - (anchorRight - tooltipWidth), caretY)
+ if (anchorRight - tooltipWidth < 0) {
+ // Need to center the caret
+ // Caret might need to be offset depending on where
+ // the tooltip is placed relative to the anchor
+ if (anchorLeft - tooltipWidth / 2 + anchorWidth / 2 <= 0) {
+ preferredPosition = Offset(anchorMid, caretY)
+ } else if (
+ anchorRight + tooltipWidth / 2 - anchorWidth / 2 >= windowContainerWidthInPx
+ ) {
+ val anchorMidFromRightScreenEdge =
+ windowContainerWidthInPx - anchorMid
+ val caretX = tooltipWidth - anchorMidFromRightScreenEdge
+ preferredPosition = Offset(caretX, caretY)
+ } else {
+ preferredPosition = Offset(tooltipWidth / 2, caretY)
+ }
+ }
+ }
+ position = preferredPosition
+ }
+
+ if (isCaretTop) {
+ path.apply {
+ moveTo(x = position.x, y = position.y)
+ lineTo(x = position.x + caretWidthPx / 2, y = position.y)
+ lineTo(x = position.x, y = position.y - caretHeightPx)
+ lineTo(x = position.x - caretWidthPx / 2, y = position.y)
+ close()
+ }
+ } else {
+ path.apply {
+ moveTo(x = position.x, y = position.y)
+ lineTo(x = position.x + caretWidthPx / 2, y = position.y)
+ lineTo(x = position.x, y = position.y + caretHeightPx.toFloat())
+ lineTo(x = position.x - caretWidthPx / 2, y = position.y)
+ close()
+ }
+ }
+ }
+
+ return onDrawWithContent {
+ if (anchorLayoutCoordinates != null) {
+ drawContent()
+ drawPath(
+ path = path,
+ color = containerColor
+ )
+ }
+ }
+}
+
+@ExperimentalMaterial3Api
+private enum class CaretType {
+ Plain, Rich
+}
+
+@Composable
+internal expect fun windowContainerWidthInPx(): Int
+
internal val SpacingBetweenTooltipAndAnchor = 4.dp
internal val TooltipMinHeight = 24.dp
internal val TooltipMinWidth = 40.dp
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/Tooltip.desktop.kt b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/Tooltip.desktop.kt
index 71f431b..c3cbf05 100644
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/Tooltip.desktop.kt
+++ b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/Tooltip.desktop.kt
@@ -16,158 +16,12 @@
package androidx.compose.material3
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.paddingFromBaseline
-import androidx.compose.foundation.layout.requiredHeightIn
-import androidx.compose.foundation.layout.sizeIn
-import androidx.compose.material3.tokens.PlainTooltipTokens
-import androidx.compose.material3.tokens.RichTooltipTokens
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.LocalComposeScene
-/**
- * Plain tooltip that provides a descriptive message.
- *
- * Usually used with [TooltipBox].
- *
- * @param modifier the [Modifier] to be applied to the tooltip.
- * @param caretSize [DpSize] for the caret of the tooltip, if a default
- * caret is desired with a specific dimension. Please see [TooltipDefaults.caretSize] to
- * see the default dimensions. Pass in Dp.Unspecified for this parameter if no caret is desired.
- * @param shape the [Shape] that should be applied to the tooltip container.
- * @param contentColor [Color] that will be applied to the tooltip's content.
- * @param containerColor [Color] that will be applied to the tooltip's container.
- * @param tonalElevation the tonal elevation of the tooltip.
- * @param shadowElevation the shadow elevation of the tooltip.
- * @param content the composable that will be used to populate the tooltip's content.
- */
@Composable
-@ExperimentalMaterial3Api
-actual fun TooltipScope.PlainTooltip(
- modifier: Modifier,
- caretSize: DpSize,
- shape: Shape,
- contentColor: Color,
- containerColor: Color,
- tonalElevation: Dp,
- shadowElevation: Dp,
- content: @Composable () -> Unit
-) {
- Surface(
- modifier = modifier,
- shape = shape,
- color = containerColor,
- tonalElevation = tonalElevation,
- shadowElevation = shadowElevation
- ) {
- Box(modifier = Modifier
- .sizeIn(
- minWidth = TooltipMinWidth,
- maxWidth = PlainTooltipMaxWidth,
- minHeight = TooltipMinHeight
- )
- .padding(PlainTooltipContentPadding)
- ) {
- val textStyle = PlainTooltipTokens.SupportingTextFont.value
- CompositionLocalProvider(
- LocalContentColor provides contentColor,
- LocalTextStyle provides textStyle,
- content = content
- )
- }
- }
-}
-
-/**
- * Rich text tooltip that allows the user to pass in a title, text, and action.
- * Tooltips are used to provide a descriptive message.
- *
- * Usually used with [TooltipBox]
- *
- * @param modifier the [Modifier] to be applied to the tooltip.
- * @param title An optional title for the tooltip.
- * @param action An optional action for the tooltip.
- * @param caretSize [DpSize] for the caret of the tooltip, if a default
- * caret is desired with a specific dimension. Please see [TooltipDefaults.caretSize] to
- * see the default dimensions. Pass in Dp.Unspecified for this parameter if no caret is desired.
- * @param shape the [Shape] that should be applied to the tooltip container.
- * @param colors [RichTooltipColors] that will be applied to the tooltip's container and content.
- * @param tonalElevation the tonal elevation of the tooltip.
- * @param shadowElevation the shadow elevation of the tooltip.
- * @param text the composable that will be used to populate the rich tooltip's text.
- */
-@Composable
-@ExperimentalMaterial3Api
-actual fun TooltipScope.RichTooltip(
- modifier: Modifier,
- title: (@Composable () -> Unit)?,
- action: (@Composable () -> Unit)?,
- caretSize: DpSize,
- shape: Shape,
- colors: RichTooltipColors,
- tonalElevation: Dp,
- shadowElevation: Dp,
- text: @Composable () -> Unit
-) {
- Surface(
- modifier = modifier
- .sizeIn(
- minWidth = TooltipMinWidth,
- maxWidth = RichTooltipMaxWidth,
- minHeight = TooltipMinHeight
- ),
- shape = shape,
- color = colors.containerColor,
- tonalElevation = tonalElevation,
- shadowElevation = shadowElevation
- ) {
- val actionLabelTextStyle = RichTooltipTokens.ActionLabelTextFont.value
- val subheadTextStyle = RichTooltipTokens.SubheadFont.value
- val supportingTextStyle = RichTooltipTokens.SupportingTextFont.value
-
- Column(
- modifier = Modifier.padding(horizontal = RichTooltipHorizontalPadding)
- ) {
- title?.let {
- Box(
- modifier = Modifier.paddingFromBaseline(top = HeightToSubheadFirstLine)
- ) {
- CompositionLocalProvider(
- LocalContentColor provides colors.titleContentColor,
- LocalTextStyle provides subheadTextStyle,
- content = it
- )
- }
- }
- Box(
- modifier = Modifier.textVerticalPadding(title != null, action != null)
- ) {
- CompositionLocalProvider(
- LocalContentColor provides colors.contentColor,
- LocalTextStyle provides supportingTextStyle,
- content = text
- )
- }
- action?.let {
- Box(
- modifier = Modifier
- .requiredHeightIn(min = ActionLabelMinHeight)
- .padding(bottom = ActionLabelBottomPadding)
- ) {
- CompositionLocalProvider(
- LocalContentColor provides colors.actionContentColor,
- LocalTextStyle provides actionLabelTextStyle,
- content = it
- )
- }
- }
- }
- }
+internal actual fun windowContainerWidthInPx(): Int {
+ // TODO: Upstream a proper way to get this from JetBrains fork
+ // LocalWindowInfo.current.containerSize.width
+ return LocalComposeScene.current.constraints.maxWidth
}
diff --git a/compose/material3/material3/src/skikoMain/kotlin/androidx/compose/material3/AlertDialog.skiko.kt b/compose/material3/material3/src/skikoMain/kotlin/androidx/compose/material3/AlertDialog.skiko.kt
new file mode 100644
index 0000000..e3b2ca3
--- /dev/null
+++ b/compose/material3/material3/src/skikoMain/kotlin/androidx/compose/material3/AlertDialog.skiko.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.material3
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.window.DialogProperties
+
+// Keep expect/actual for maintain binary compatibility.
+// `@file:JvmName` doesn't work here because Android and Desktop were published with different names
+// Please note that binary compatibility for Desktop is tracked only in JetBrains fork
+
+@Composable
+actual fun AlertDialog(
+ onDismissRequest: () -> Unit,
+ confirmButton: @Composable () -> Unit,
+ modifier: Modifier,
+ dismissButton: @Composable (() -> Unit)?,
+ icon: @Composable (() -> Unit)?,
+ title: @Composable (() -> Unit)?,
+ text: @Composable (() -> Unit)?,
+ shape: Shape,
+ containerColor: Color,
+ iconContentColor: Color,
+ titleContentColor: Color,
+ textContentColor: Color,
+ tonalElevation: Dp,
+ properties: DialogProperties
+): Unit = AlertDialogImpl(
+ onDismissRequest = onDismissRequest,
+ confirmButton = confirmButton,
+ modifier = modifier,
+ dismissButton = dismissButton,
+ icon = icon,
+ title = title,
+ text = text,
+ shape = shape,
+ containerColor = containerColor,
+ iconContentColor = iconContentColor,
+ titleContentColor = titleContentColor,
+ textContentColor = textContentColor,
+ tonalElevation = tonalElevation,
+ properties = properties
+)
diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ComposeScene.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ComposeScene.skiko.kt
index 2f16c79..d510fac 100644
--- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ComposeScene.skiko.kt
+++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ComposeScene.skiko.kt
@@ -56,7 +56,9 @@
import kotlinx.coroutines.launch
import org.jetbrains.skia.Canvas
-internal val LocalComposeScene = staticCompositionLocalOf<ComposeScene> {
+// TODO: This val should not be public!
+// Upstream current state of [ComposeScene] from JetBrains fork
+val LocalComposeScene = staticCompositionLocalOf<ComposeScene> {
error("CompositionLocal LocalComposeScene not provided")
}
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt
index aea9726..63bbd51 100644
--- a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt
@@ -26,6 +26,7 @@
import androidx.health.connect.client.changes.UpsertionChange
import androidx.health.connect.client.permission.HealthPermission.Companion.PERMISSION_PREFIX
import androidx.health.connect.client.readRecord
+import androidx.health.connect.client.records.BloodPressureRecord
import androidx.health.connect.client.records.HeartRateRecord
import androidx.health.connect.client.records.NutritionRecord
import androidx.health.connect.client.records.StepsRecord
@@ -40,6 +41,8 @@
import androidx.health.connect.client.time.TimeRangeFilter
import androidx.health.connect.client.units.Energy
import androidx.health.connect.client.units.Mass
+import androidx.health.connect.client.units.grams
+import androidx.health.connect.client.units.millimetersOfMercury
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -405,19 +408,47 @@
startZoneOffset = ZoneOffset.UTC,
endTime = START_TIME + 1.minutes,
endZoneOffset = ZoneOffset.UTC,
- transFat = Mass.grams(0.5)
+ transFat = 0.5.grams
+ ),
+ BloodPressureRecord(
+ time = START_TIME,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 120.millimetersOfMercury,
+ diastolic = 80.millimetersOfMercury
)
)
)
val aggregateResponse = healthConnectClient.aggregate(
AggregateRequest(
- setOf(NutritionRecord.TRANS_FAT_TOTAL),
+ setOf(
+ BloodPressureRecord.DIASTOLIC_AVG,
+ BloodPressureRecord.DIASTOLIC_MAX,
+ BloodPressureRecord.DIASTOLIC_MIN,
+ BloodPressureRecord.SYSTOLIC_AVG,
+ BloodPressureRecord.SYSTOLIC_MAX,
+ BloodPressureRecord.SYSTOLIC_MIN,
+ NutritionRecord.TRANS_FAT_TOTAL
+ ),
TimeRangeFilter.none()
)
)
- assertThat(aggregateResponse[NutritionRecord.TRANS_FAT_TOTAL]).isEqualTo(Mass.grams(0.5))
+ assertEquals(
+ aggregateResponse[NutritionRecord.TRANS_FAT_TOTAL] to 0.5.grams,
+ aggregateResponse[BloodPressureRecord.SYSTOLIC_AVG] to
+ 120.millimetersOfMercury,
+ aggregateResponse[BloodPressureRecord.SYSTOLIC_MAX] to
+ 120.millimetersOfMercury,
+ aggregateResponse[BloodPressureRecord.SYSTOLIC_MIN] to
+ 120.millimetersOfMercury,
+ aggregateResponse[BloodPressureRecord.DIASTOLIC_AVG] to
+ 80.millimetersOfMercury,
+ aggregateResponse[BloodPressureRecord.DIASTOLIC_MAX] to
+ 80.millimetersOfMercury,
+ aggregateResponse[BloodPressureRecord.DIASTOLIC_MIN] to
+ 80.millimetersOfMercury,
+ )
}
@Ignore("b/314092270")
@@ -736,6 +767,10 @@
.containsExactlyElementsIn(allHealthPermissions)
}
+ private fun <A, E> assertEquals(vararg assertions: Pair<A, E>) {
+ assertions.forEach { (actual, expected) -> assertThat(actual).isEqualTo(expected) }
+ }
+
private val Int.seconds: Duration
get() = Duration.ofSeconds(this.toLong())
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/BloodPressureAggregationExtensionsTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/BloodPressureAggregationExtensionsTest.kt
new file mode 100644
index 0000000..afb5722e
--- /dev/null
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/BloodPressureAggregationExtensionsTest.kt
@@ -0,0 +1,539 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.connect.client.impl.platform.aggregate
+
+import android.annotation.TargetApi
+import android.content.Context
+import android.os.Build
+import android.os.ext.SdkExtensions
+import androidx.health.connect.client.HealthConnectClient
+import androidx.health.connect.client.impl.HealthConnectClientUpsideDownImpl
+import androidx.health.connect.client.permission.HealthPermission
+import androidx.health.connect.client.records.BloodPressureRecord
+import androidx.health.connect.client.records.NutritionRecord
+import androidx.health.connect.client.records.metadata.DataOrigin
+import androidx.health.connect.client.time.TimeRangeFilter
+import androidx.health.connect.client.units.millimetersOfMercury
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.rule.GrantPermissionRule
+import com.google.common.truth.Truth.assertThat
+import java.time.Duration
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.ZoneOffset
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Assert.assertThrows
+import org.junit.Assume.assumeTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
+class BloodPressureAggregationExtensionsTest {
+
+ private val context: Context = ApplicationProvider.getApplicationContext()
+ private val healthConnectClient: HealthConnectClient =
+ HealthConnectClientUpsideDownImpl(context)
+
+ private companion object {
+ private val START_TIME =
+ LocalDate.now().minusDays(5).atStartOfDay().toInstant(ZoneOffset.UTC)
+ }
+
+ @get:Rule
+ val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
+ HealthPermission.getWritePermission(BloodPressureRecord::class),
+ HealthPermission.getReadPermission(BloodPressureRecord::class)
+ )
+
+ @After
+ fun tearDown() = runTest {
+ healthConnectClient.deleteRecords(BloodPressureRecord::class, TimeRangeFilter.none())
+ }
+
+ @Test
+ fun aggregateBloodPressure_invalidMetrics() = runTest {
+ val metrics = setOf(
+ BloodPressureRecord.DIASTOLIC_AVG,
+ BloodPressureRecord.DIASTOLIC_MAX,
+ BloodPressureRecord.DIASTOLIC_MIN,
+ BloodPressureRecord.SYSTOLIC_AVG,
+ BloodPressureRecord.SYSTOLIC_MAX,
+ BloodPressureRecord.SYSTOLIC_MIN,
+ NutritionRecord.TRANS_FAT_TOTAL
+ )
+
+ assertThrows(IllegalStateException::class.java) {
+ runBlocking {
+ healthConnectClient.aggregateBloodPressure(
+ metrics,
+ TimeRangeFilter.none(),
+ emptySet()
+ )
+ }
+ }
+ }
+
+ @Test
+ fun aggregateBloodPressure_noFiltersNoData() = runTest {
+ val metrics = setOf(
+ BloodPressureRecord.DIASTOLIC_AVG,
+ BloodPressureRecord.DIASTOLIC_MAX,
+ BloodPressureRecord.DIASTOLIC_MIN,
+ BloodPressureRecord.SYSTOLIC_AVG,
+ BloodPressureRecord.SYSTOLIC_MAX,
+ BloodPressureRecord.SYSTOLIC_MIN,
+ )
+
+ val aggregationResult =
+ healthConnectClient.aggregateBloodPressure(metrics, TimeRangeFilter.none(), emptySet())
+
+ metrics.forEach {
+ assertThat(it in aggregationResult).isFalse()
+ }
+ assertThat(aggregationResult.dataOrigins).isEmpty()
+ }
+
+ @Test
+ fun aggregateBloodPressure_noFilters() = runTest {
+ healthConnectClient.insertRecords(
+ listOf(
+ BloodPressureRecord(
+ time = START_TIME,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 105.millimetersOfMercury,
+ diastolic = 60.millimetersOfMercury
+ ),
+ BloodPressureRecord(
+ time = START_TIME + 2.minutes,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 90.millimetersOfMercury,
+ diastolic = 70.millimetersOfMercury
+ ),
+ BloodPressureRecord(
+ time = START_TIME + 4.minutes,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 120.millimetersOfMercury,
+ diastolic = 80.millimetersOfMercury
+ ),
+ BloodPressureRecord(
+ time = START_TIME + 6.minutes,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 110.millimetersOfMercury,
+ diastolic = 65.millimetersOfMercury
+ ),
+ BloodPressureRecord(
+ time = START_TIME + 8.minutes,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 100.millimetersOfMercury,
+ diastolic = 80.millimetersOfMercury
+ )
+ )
+ )
+
+ val aggregationResult =
+ healthConnectClient.aggregateBloodPressure(
+ setOf(BloodPressureRecord.SYSTOLIC_MAX),
+ TimeRangeFilter.none(), emptySet()
+ )
+
+ assertThat(aggregationResult[BloodPressureRecord.SYSTOLIC_MAX])
+ .isEqualTo(120.millimetersOfMercury)
+ assertThat(aggregationResult.dataOrigins)
+ .containsExactly(DataOrigin(context.packageName))
+ }
+
+ @Test
+ fun aggregateBloodPressure_noFilters_allMetrics() = runTest {
+ healthConnectClient.insertRecords(
+ listOf(
+ BloodPressureRecord(
+ time = START_TIME + 4.minutes,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 120.millimetersOfMercury,
+ diastolic = 80.millimetersOfMercury
+ ),
+ BloodPressureRecord(
+ time = START_TIME + 8.minutes,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 100.millimetersOfMercury,
+ diastolic = 60.millimetersOfMercury
+ )
+ )
+ )
+
+ val aggregationResult =
+ healthConnectClient.aggregateBloodPressure(
+ setOf(
+ BloodPressureRecord.DIASTOLIC_AVG,
+ BloodPressureRecord.DIASTOLIC_MAX,
+ BloodPressureRecord.DIASTOLIC_MIN,
+ BloodPressureRecord.SYSTOLIC_AVG,
+ BloodPressureRecord.SYSTOLIC_MAX,
+ BloodPressureRecord.SYSTOLIC_MIN,
+ ),
+ TimeRangeFilter.none(), emptySet()
+ )
+
+ assertEquals(
+ aggregationResult[BloodPressureRecord.DIASTOLIC_AVG] to 70.millimetersOfMercury,
+ aggregationResult[BloodPressureRecord.DIASTOLIC_MAX] to 80.millimetersOfMercury,
+ aggregationResult[BloodPressureRecord.DIASTOLIC_MIN] to 60.millimetersOfMercury,
+ aggregationResult[BloodPressureRecord.SYSTOLIC_AVG] to 110.millimetersOfMercury,
+ aggregationResult[BloodPressureRecord.SYSTOLIC_MAX] to 120.millimetersOfMercury,
+ aggregationResult[BloodPressureRecord.SYSTOLIC_MIN] to 100.millimetersOfMercury,
+ )
+ assertThat(aggregationResult.dataOrigins)
+ .containsExactly(DataOrigin(context.packageName))
+ }
+
+ @Test
+ fun aggregateBloodPressure_noFilters_someMetrics() = runTest {
+ healthConnectClient.insertRecords(
+ listOf(
+ BloodPressureRecord(
+ time = START_TIME + 4.minutes,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 120.millimetersOfMercury,
+ diastolic = 80.millimetersOfMercury
+ ),
+ BloodPressureRecord(
+ time = START_TIME + 8.minutes,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 100.millimetersOfMercury,
+ diastolic = 60.millimetersOfMercury
+ )
+ )
+ )
+
+ val aggregationResult =
+ healthConnectClient.aggregateBloodPressure(
+ setOf(
+ BloodPressureRecord.DIASTOLIC_AVG,
+ BloodPressureRecord.SYSTOLIC_MAX,
+ BloodPressureRecord.SYSTOLIC_MIN,
+ ),
+ TimeRangeFilter.none(), emptySet()
+ )
+
+ assertEquals(
+ aggregationResult[BloodPressureRecord.DIASTOLIC_AVG] to 70.millimetersOfMercury,
+ aggregationResult[BloodPressureRecord.SYSTOLIC_MAX] to 120.millimetersOfMercury,
+ aggregationResult[BloodPressureRecord.SYSTOLIC_MIN] to 100.millimetersOfMercury,
+ )
+ setOf(
+ BloodPressureRecord.DIASTOLIC_MAX,
+ BloodPressureRecord.DIASTOLIC_MIN,
+ BloodPressureRecord.SYSTOLIC_AVG
+ ).forEach { assertThat(it in aggregationResult).isFalse() }
+ assertThat(aggregationResult.dataOrigins)
+ .containsExactly(DataOrigin(context.packageName))
+ }
+
+ @Test
+ fun aggregateBloodPressure_noFilters_noMetrics() = runTest {
+ healthConnectClient.insertRecords(
+ listOf(
+ BloodPressureRecord(
+ time = START_TIME,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 105.millimetersOfMercury,
+ diastolic = 60.millimetersOfMercury
+ ),
+ BloodPressureRecord(
+ time = START_TIME + 2.minutes,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 90.millimetersOfMercury,
+ diastolic = 70.millimetersOfMercury
+ ),
+ BloodPressureRecord(
+ time = START_TIME + 4.minutes,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 120.millimetersOfMercury,
+ diastolic = 80.millimetersOfMercury
+ ),
+ BloodPressureRecord(
+ time = START_TIME + 6.minutes,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 110.millimetersOfMercury,
+ diastolic = 65.millimetersOfMercury
+ ),
+ BloodPressureRecord(
+ time = START_TIME + 8.minutes,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 100.millimetersOfMercury,
+ diastolic = 80.millimetersOfMercury
+ )
+ )
+ )
+
+ val aggregationResult =
+ healthConnectClient.aggregateBloodPressure(
+ emptySet(),
+ TimeRangeFilter.none(), emptySet()
+ )
+
+ setOf(
+ BloodPressureRecord.DIASTOLIC_AVG,
+ BloodPressureRecord.DIASTOLIC_MAX,
+ BloodPressureRecord.DIASTOLIC_MIN,
+ BloodPressureRecord.SYSTOLIC_AVG,
+ BloodPressureRecord.SYSTOLIC_MAX,
+ BloodPressureRecord.SYSTOLIC_MIN,
+ ).forEach {
+ assertThat(it in aggregationResult).isFalse()
+ }
+ assertThat(aggregationResult.dataOrigins).isEmpty()
+ }
+
+ @Test
+ fun aggregateBloodPressure_instantTimeRangeFilter() = runTest {
+ healthConnectClient.insertRecords(
+ listOf(
+ BloodPressureRecord(
+ time = START_TIME,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 105.millimetersOfMercury,
+ diastolic = 60.millimetersOfMercury
+ ),
+ BloodPressureRecord(
+ time = START_TIME + 2.minutes,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 90.millimetersOfMercury,
+ diastolic = 70.millimetersOfMercury
+ ),
+ BloodPressureRecord(
+ time = START_TIME + 4.minutes,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 120.millimetersOfMercury,
+ diastolic = 80.millimetersOfMercury
+ ),
+ BloodPressureRecord(
+ time = START_TIME + 6.minutes,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 110.millimetersOfMercury,
+ diastolic = 65.millimetersOfMercury
+ ),
+ BloodPressureRecord(
+ time = START_TIME + 8.minutes,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 100.millimetersOfMercury,
+ diastolic = 80.millimetersOfMercury
+ )
+ )
+ )
+
+ val aggregationResult = healthConnectClient.aggregateBloodPressure(
+ setOf(BloodPressureRecord.DIASTOLIC_MIN),
+ TimeRangeFilter.between(
+ START_TIME + 30.seconds,
+ START_TIME + 6.minutes + 45.seconds
+ ), emptySet()
+ )
+
+ assertThat(aggregationResult[BloodPressureRecord.DIASTOLIC_MIN])
+ .isEqualTo(65.millimetersOfMercury)
+ assertThat(aggregationResult.dataOrigins)
+ .containsExactly(DataOrigin(context.packageName))
+ }
+
+ @Test
+ fun aggregateBloodPressure_instantTimeRangeFilter_filterEndTime() =
+ runTest {
+ healthConnectClient.insertRecords(
+ listOf(
+ BloodPressureRecord(
+ time = START_TIME,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 110.millimetersOfMercury,
+ diastolic = 65.millimetersOfMercury
+ ),
+ BloodPressureRecord(
+ time = START_TIME + 2.minutes,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 100.millimetersOfMercury,
+ diastolic = 80.millimetersOfMercury
+ )
+ )
+ )
+
+ val aggregationResult = healthConnectClient.aggregateBloodPressure(
+ setOf(BloodPressureRecord.DIASTOLIC_AVG, BloodPressureRecord.SYSTOLIC_AVG),
+ TimeRangeFilter.between(
+ START_TIME + 1.minutes,
+ START_TIME + 1.minutes + 59.seconds
+ ), emptySet()
+ )
+
+ assertThat(BloodPressureRecord.DIASTOLIC_AVG in aggregationResult).isFalse()
+ assertThat(BloodPressureRecord.SYSTOLIC_AVG in aggregationResult).isFalse()
+ assertThat(aggregationResult.dataOrigins).isEmpty()
+ }
+
+ @Test
+ fun aggregateBloodPressure_localTimeRangeFilter() = runTest {
+ assumeTrue(SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 10)
+ healthConnectClient.insertRecords(
+ listOf(
+ BloodPressureRecord(
+ time = START_TIME,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 105.millimetersOfMercury,
+ diastolic = 60.millimetersOfMercury
+ ),
+ BloodPressureRecord(
+ time = START_TIME + 2.minutes,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 100.millimetersOfMercury,
+ diastolic = 70.millimetersOfMercury
+ ),
+ BloodPressureRecord(
+ time = START_TIME + 4.minutes,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 120.millimetersOfMercury,
+ diastolic = 80.millimetersOfMercury
+ ),
+ BloodPressureRecord(
+ time = START_TIME + 6.minutes,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 110.millimetersOfMercury,
+ diastolic = 65.millimetersOfMercury
+ ),
+ BloodPressureRecord(
+ time = START_TIME + 8.minutes,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 100.millimetersOfMercury,
+ diastolic = 80.millimetersOfMercury
+ )
+ )
+ )
+
+ val aggregationResult = healthConnectClient.aggregateBloodPressure(
+ setOf(BloodPressureRecord.DIASTOLIC_MIN, BloodPressureRecord.SYSTOLIC_AVG),
+ TimeRangeFilter.between(
+ LocalDateTime.ofInstant(
+ START_TIME + 2.hours + 30.seconds,
+ ZoneOffset.ofHours(-2)
+ ),
+ LocalDateTime.ofInstant(
+ START_TIME + 2.hours + 6.minutes + 45.seconds,
+ ZoneOffset.ofHours(-2)
+ )
+ ), emptySet()
+ )
+
+ assertThat(aggregationResult[BloodPressureRecord.DIASTOLIC_MIN])
+ .isEqualTo(65.millimetersOfMercury)
+ assertThat(aggregationResult[BloodPressureRecord.SYSTOLIC_AVG])
+ .isEqualTo(110.millimetersOfMercury)
+ assertThat(aggregationResult.dataOrigins)
+ .containsExactly(DataOrigin(context.packageName))
+ }
+
+ // TODO(b/337195270): Test with data origins from multiple apps
+ @Test
+ fun aggregateBloodPressure_insertedDataOriginFilter() = runTest {
+ healthConnectClient.insertRecords(
+ listOf(
+ BloodPressureRecord(
+ time = START_TIME,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 110.millimetersOfMercury,
+ diastolic = 65.millimetersOfMercury
+ ),
+ )
+ )
+
+ val aggregationResult = healthConnectClient.aggregateBloodPressure(
+ setOf(BloodPressureRecord.SYSTOLIC_AVG),
+ TimeRangeFilter.none(),
+ setOf(DataOrigin(context.packageName))
+ )
+
+ assertThat(aggregationResult[BloodPressureRecord.SYSTOLIC_AVG])
+ .isEqualTo(110.millimetersOfMercury)
+ assertThat(aggregationResult.dataOrigins)
+ .containsExactly(DataOrigin(context.packageName))
+ }
+
+ @Test
+ fun aggregateBloodPressure_timeRangeFilterOutOfBounds() = runTest {
+ healthConnectClient.insertRecords(
+ listOf(
+ BloodPressureRecord(
+ time = START_TIME,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 110.millimetersOfMercury,
+ diastolic = 65.millimetersOfMercury
+ ),
+ )
+ )
+
+ val aggregationResult = healthConnectClient.aggregateBloodPressure(
+ setOf(BloodPressureRecord.SYSTOLIC_AVG),
+ TimeRangeFilter.after(START_TIME + 2.minutes),
+ emptySet()
+ )
+
+ assertThat(BloodPressureRecord.SYSTOLIC_AVG in aggregationResult).isFalse()
+ assertThat(aggregationResult.dataOrigins).isEmpty()
+ }
+
+ @Test
+ fun aggregateBloodPressure_nonExistingDataOriginFilter() = runTest {
+ healthConnectClient.insertRecords(
+ listOf(
+ BloodPressureRecord(
+ time = START_TIME,
+ zoneOffset = ZoneOffset.UTC,
+ systolic = 110.millimetersOfMercury,
+ diastolic = 65.millimetersOfMercury
+ ),
+ )
+ )
+
+ val aggregationResult = healthConnectClient.aggregateBloodPressure(
+ setOf(BloodPressureRecord.SYSTOLIC_AVG),
+ TimeRangeFilter.none(),
+ setOf(DataOrigin("some random package name"))
+ )
+
+ assertThat(BloodPressureRecord.SYSTOLIC_AVG in aggregationResult).isFalse()
+ assertThat(aggregationResult.dataOrigins).isEmpty()
+ }
+
+ private fun <A, E> assertEquals(vararg assertions: Pair<A, E>) {
+ assertions.forEach { (actual, expected) -> assertThat(actual).isEqualTo(expected) }
+ }
+
+ private val Int.seconds: Duration
+ get() = Duration.ofSeconds(this.toLong())
+
+ private val Int.minutes: Duration
+ get() = Duration.ofMinutes(this.toLong())
+
+ private val Int.hours: Duration
+ get() = Duration.ofHours(this.toLong())
+}
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensionsTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensionsTest.kt
index 78225d6..76952af 100644
--- a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensionsTest.kt
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensionsTest.kt
@@ -23,12 +23,14 @@
import androidx.health.connect.client.HealthConnectClient
import androidx.health.connect.client.impl.HealthConnectClientUpsideDownImpl
import androidx.health.connect.client.permission.HealthPermission
+import androidx.health.connect.client.records.BloodPressureRecord
import androidx.health.connect.client.records.NutritionRecord
import androidx.health.connect.client.records.StepsRecord
import androidx.health.connect.client.records.metadata.DataOrigin
import androidx.health.connect.client.request.AggregateRequest
import androidx.health.connect.client.time.TimeRangeFilter
-import androidx.health.connect.client.units.Mass
+import androidx.health.connect.client.units.grams
+import androidx.health.connect.client.units.millimetersOfMercury
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -65,6 +67,8 @@
@get:Rule
val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
+ HealthPermission.getWritePermission(BloodPressureRecord::class),
+ HealthPermission.getReadPermission(BloodPressureRecord::class),
HealthPermission.getWritePermission(NutritionRecord::class),
HealthPermission.getReadPermission(NutritionRecord::class),
HealthPermission.getWritePermission(StepsRecord::class),
@@ -83,11 +87,17 @@
healthConnectClient.insertRecords(
listOf(
+ BloodPressureRecord(
+ time = START_TIME,
+ zoneOffset = ZoneOffset.UTC,
+ diastolic = 70.millimetersOfMercury,
+ systolic = 110.millimetersOfMercury
+ ),
NutritionRecord(
startTime = START_TIME,
endTime = START_TIME + 1.minutes,
- transFat = Mass.grams(0.3),
- calcium = Mass.grams(0.1),
+ transFat = 0.3.grams,
+ calcium = 0.1.grams,
startZoneOffset = ZoneOffset.UTC,
endZoneOffset = ZoneOffset.UTC
)
@@ -95,14 +105,30 @@
)
val aggregationResult =
- healthConnectClient.aggregateFallback(AggregateRequest(
- metrics = setOf(NutritionRecord.TRANS_FAT_TOTAL, NutritionRecord.CALCIUM_TOTAL),
- timeRangeFilter = TimeRangeFilter.none()
- ))
+ healthConnectClient.aggregateFallback(
+ AggregateRequest(
+ metrics = setOf(
+ BloodPressureRecord.DIASTOLIC_AVG,
+ BloodPressureRecord.DIASTOLIC_MAX,
+ BloodPressureRecord.DIASTOLIC_MIN,
+ BloodPressureRecord.SYSTOLIC_AVG,
+ BloodPressureRecord.SYSTOLIC_MAX,
+ BloodPressureRecord.SYSTOLIC_MIN,
+ NutritionRecord.TRANS_FAT_TOTAL,
+ NutritionRecord.CALCIUM_TOTAL
+ ),
+ timeRangeFilter = TimeRangeFilter.none()
+ )
+ )
+ assertThat(BloodPressureRecord.DIASTOLIC_AVG in aggregationResult).isFalse()
+ assertThat(BloodPressureRecord.DIASTOLIC_MAX in aggregationResult).isFalse()
+ assertThat(BloodPressureRecord.DIASTOLIC_MIN in aggregationResult).isFalse()
+ assertThat(BloodPressureRecord.SYSTOLIC_AVG in aggregationResult).isFalse()
+ assertThat(BloodPressureRecord.SYSTOLIC_MAX in aggregationResult).isFalse()
+ assertThat(BloodPressureRecord.SYSTOLIC_MIN in aggregationResult).isFalse()
assertThat(NutritionRecord.TRANS_FAT_TOTAL in aggregationResult).isFalse()
assertThat(NutritionRecord.CALCIUM_TOTAL in aggregationResult).isFalse()
-
assertThat(aggregationResult.dataOrigins).isEmpty()
}
@@ -112,11 +138,17 @@
healthConnectClient.insertRecords(
listOf(
+ BloodPressureRecord(
+ time = START_TIME,
+ zoneOffset = ZoneOffset.UTC,
+ diastolic = 70.millimetersOfMercury,
+ systolic = 110.millimetersOfMercury
+ ),
NutritionRecord(
startTime = START_TIME,
endTime = START_TIME + 1.minutes,
- transFat = Mass.grams(0.3),
- calcium = Mass.grams(0.1),
+ transFat = 0.3.grams,
+ calcium = 0.1.grams,
startZoneOffset = ZoneOffset.UTC,
endZoneOffset = ZoneOffset.UTC
)
@@ -124,19 +156,69 @@
)
val aggregationResult =
- healthConnectClient.aggregateFallback(AggregateRequest(
- metrics = setOf(NutritionRecord.TRANS_FAT_TOTAL, NutritionRecord.CALCIUM_TOTAL),
- timeRangeFilter = TimeRangeFilter.none()
- ))
+ healthConnectClient.aggregateFallback(
+ AggregateRequest(
+ metrics = setOf(
+ NutritionRecord.TRANS_FAT_TOTAL,
+ NutritionRecord.CALCIUM_TOTAL,
+ BloodPressureRecord.DIASTOLIC_AVG,
+ BloodPressureRecord.DIASTOLIC_MAX,
+ BloodPressureRecord.DIASTOLIC_MIN,
+ BloodPressureRecord.SYSTOLIC_AVG,
+ BloodPressureRecord.SYSTOLIC_MAX,
+ BloodPressureRecord.SYSTOLIC_MIN,
+ ),
+ timeRangeFilter = TimeRangeFilter.none()
+ )
+ )
- assertThat(aggregationResult[NutritionRecord.TRANS_FAT_TOTAL]).isEqualTo(Mass.grams(0.3))
- assertThat(NutritionRecord.CALCIUM_TOTAL in aggregationResult).isFalse()
-
+ assertEquals(
+ aggregationResult[BloodPressureRecord.DIASTOLIC_AVG] to 70.millimetersOfMercury,
+ aggregationResult[BloodPressureRecord.DIASTOLIC_MAX] to 70.millimetersOfMercury,
+ aggregationResult[BloodPressureRecord.DIASTOLIC_MIN] to 70.millimetersOfMercury,
+ aggregationResult[BloodPressureRecord.SYSTOLIC_AVG] to 110.millimetersOfMercury,
+ aggregationResult[BloodPressureRecord.SYSTOLIC_MAX] to 110.millimetersOfMercury,
+ aggregationResult[BloodPressureRecord.SYSTOLIC_MIN] to 110.millimetersOfMercury,
+ aggregationResult[NutritionRecord.TRANS_FAT_TOTAL] to 0.3.grams,
+ (NutritionRecord.CALCIUM_TOTAL in aggregationResult) to false,
+ )
assertThat(aggregationResult.dataOrigins)
.containsExactly(DataOrigin(context.packageName))
}
@Test
+ fun aggregateFallback_belowSdkExt10NoData() = runTest {
+ assumeFalse(SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 10)
+
+ val aggregationResult =
+ healthConnectClient.aggregateFallback(
+ AggregateRequest(
+ metrics = setOf(
+ BloodPressureRecord.DIASTOLIC_AVG,
+ BloodPressureRecord.DIASTOLIC_MAX,
+ BloodPressureRecord.DIASTOLIC_MIN,
+ BloodPressureRecord.SYSTOLIC_AVG,
+ BloodPressureRecord.SYSTOLIC_MAX,
+ BloodPressureRecord.SYSTOLIC_MIN,
+ NutritionRecord.TRANS_FAT_TOTAL,
+ NutritionRecord.CALCIUM_TOTAL
+ ),
+ timeRangeFilter = TimeRangeFilter.none()
+ )
+ )
+
+ assertThat(BloodPressureRecord.DIASTOLIC_AVG in aggregationResult).isFalse()
+ assertThat(BloodPressureRecord.DIASTOLIC_MAX in aggregationResult).isFalse()
+ assertThat(BloodPressureRecord.DIASTOLIC_MIN in aggregationResult).isFalse()
+ assertThat(BloodPressureRecord.SYSTOLIC_AVG in aggregationResult).isFalse()
+ assertThat(BloodPressureRecord.SYSTOLIC_MAX in aggregationResult).isFalse()
+ assertThat(BloodPressureRecord.SYSTOLIC_MIN in aggregationResult).isFalse()
+ assertThat(NutritionRecord.TRANS_FAT_TOTAL in aggregationResult).isFalse()
+ assertThat(NutritionRecord.CALCIUM_TOTAL in aggregationResult).isFalse()
+ assertThat(aggregationResult.dataOrigins).isEmpty()
+ }
+
+ @Test
fun readRecordsFlow_noFilters_readsAllInsertedRecords() = runTest {
insertManyStepsRecords()
@@ -214,6 +296,10 @@
}
}
+ private fun <A, E> assertEquals(vararg assertions: Pair<A, E>) {
+ assertions.forEach { (actual, expected) -> assertThat(actual).isEqualTo(expected) }
+ }
+
private val Int.seconds: Duration
get() = Duration.ofSeconds(this.toLong())
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/NutritionAggregationExtensionsTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/NutritionAggregationExtensionsTest.kt
index db17e4f..27e1d07 100644
--- a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/NutritionAggregationExtensionsTest.kt
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/NutritionAggregationExtensionsTest.kt
@@ -69,6 +69,15 @@
}
@Test
+ fun aggregateNutritionTransFatTotal_noData() = runTest {
+ val aggregationResult =
+ healthConnectClient.aggregateNutritionTransFatTotal(TimeRangeFilter.none(), emptySet())
+
+ assertThat(NutritionRecord.TRANS_FAT_TOTAL in aggregationResult).isFalse()
+ assertThat(aggregationResult.dataOrigins).isEmpty()
+ }
+
+ @Test
fun aggregateNutritionTransFatTotal_noFilters() = runTest {
healthConnectClient.insertRecords(
listOf(
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/aggregate/BloodPressureAggregationExtensions.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/aggregate/BloodPressureAggregationExtensions.kt
new file mode 100644
index 0000000..d8181f5
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/aggregate/BloodPressureAggregationExtensions.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:RequiresApi(api = 34)
+
+package androidx.health.connect.client.impl.platform.aggregate
+
+import androidx.annotation.RequiresApi
+import androidx.health.connect.client.HealthConnectClient
+import androidx.health.connect.client.aggregate.AggregateMetric
+import androidx.health.connect.client.aggregate.AggregationResult
+import androidx.health.connect.client.records.BloodPressureRecord
+import androidx.health.connect.client.records.metadata.DataOrigin
+import androidx.health.connect.client.time.TimeRangeFilter
+import androidx.health.connect.client.units.Pressure
+import kotlin.math.max
+import kotlin.math.min
+
+private val BLOOD_PRESSURE_METRICS = setOf(
+ BloodPressureRecord.DIASTOLIC_AVG,
+ BloodPressureRecord.DIASTOLIC_MAX,
+ BloodPressureRecord.DIASTOLIC_MIN,
+ BloodPressureRecord.SYSTOLIC_AVG,
+ BloodPressureRecord.SYSTOLIC_MAX,
+ BloodPressureRecord.SYSTOLIC_MIN,
+)
+
+internal suspend fun HealthConnectClient.aggregateBloodPressure(
+ bloodPressureMetrics: Set<AggregateMetric<*>>,
+ timeRangeFilter: TimeRangeFilter,
+ dataOriginFilter: Set<DataOrigin>
+): AggregationResult {
+ check(BLOOD_PRESSURE_METRICS.containsAll(bloodPressureMetrics)) {
+ "Invalid set of blood pressure metrics $bloodPressureMetrics"
+ }
+
+ if (bloodPressureMetrics.isEmpty()) {
+ return emptyAggregationResult()
+ }
+
+ val readRecordsFlow = readRecordsFlow(
+ BloodPressureRecord::class,
+ timeRangeFilter,
+ dataOriginFilter
+ )
+
+ val avgDataMap = mutableMapOf<AggregateMetric<Pressure>, AvgData>()
+ val minMaxMap = mutableMapOf<AggregateMetric<Pressure>, Double?>()
+
+ for (metric in bloodPressureMetrics) {
+ when (metric) {
+ BloodPressureRecord.DIASTOLIC_AVG, BloodPressureRecord.SYSTOLIC_AVG ->
+ avgDataMap[metric] = AvgData()
+
+ BloodPressureRecord.DIASTOLIC_MAX, BloodPressureRecord.DIASTOLIC_MIN,
+ BloodPressureRecord.SYSTOLIC_MAX, BloodPressureRecord.SYSTOLIC_MIN ->
+ minMaxMap[metric] = null
+
+ else -> error("Invalid blood pressure fallback aggregation type ${metric.metricKey}")
+ }
+ }
+
+ val dataOrigins = mutableSetOf<DataOrigin>()
+
+ readRecordsFlow.collect { records ->
+ records.forEach {
+ val diastolic = it.diastolic.inMillimetersOfMercury
+ val systolic = it.systolic.inMillimetersOfMercury
+
+ for (metric in bloodPressureMetrics) {
+ when (metric) {
+ BloodPressureRecord.DIASTOLIC_AVG -> avgDataMap[metric]!! += diastolic
+ BloodPressureRecord.DIASTOLIC_MAX -> minMaxMap[metric] =
+ max(minMaxMap[metric] ?: diastolic, diastolic)
+
+ BloodPressureRecord.DIASTOLIC_MIN -> minMaxMap[metric] =
+ min(minMaxMap[metric] ?: diastolic, diastolic)
+
+ BloodPressureRecord.SYSTOLIC_AVG -> avgDataMap[metric]!! += systolic
+ BloodPressureRecord.SYSTOLIC_MAX -> minMaxMap[metric] =
+ max(minMaxMap[metric] ?: systolic, systolic)
+
+ BloodPressureRecord.SYSTOLIC_MIN -> minMaxMap[metric] =
+ min(minMaxMap[metric] ?: systolic, systolic)
+ }
+ }
+
+ dataOrigins += it.metadata.dataOrigin
+ }
+ }
+
+ if (dataOrigins.isEmpty()) {
+ return emptyAggregationResult()
+ }
+
+ val doubleValues = buildMap {
+ for (metric in bloodPressureMetrics) {
+ val aggregatedValue = when (metric) {
+ BloodPressureRecord.DIASTOLIC_AVG, BloodPressureRecord.SYSTOLIC_AVG ->
+ avgDataMap[metric]!!.average()
+
+ BloodPressureRecord.DIASTOLIC_MAX, BloodPressureRecord.DIASTOLIC_MIN,
+ BloodPressureRecord.SYSTOLIC_MAX, BloodPressureRecord.SYSTOLIC_MIN ->
+ minMaxMap[metric]!!
+
+ else ->
+ error("Invalid blood pressure fallback aggregation type ${metric.metricKey}")
+ }
+
+ put(metric.metricKey, aggregatedValue)
+ }
+ }
+
+ return AggregationResult(
+ longValues = mapOf(),
+ doubleValues = doubleValues,
+ dataOrigins = dataOrigins
+ )
+}
+
+private data class AvgData(var count: Int = 0, var total: Double = 0.0) {
+ operator fun plusAssign(value: Double) {
+ count++
+ total += value
+ }
+
+ fun average() = total / count
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensions.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensions.kt
index 5a45b8b..a659c1b 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensions.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensions.kt
@@ -48,45 +48,55 @@
// Max buffer to account for overlapping records that have startTime < timeRangeFilter.startTime
val RECORD_START_TIME_BUFFER: Duration = Duration.ofDays(1)
+private val AGGREGATION_FALLBACK_RECORD_TYPES = setOf(
+ BloodPressureRecord::class,
+ CyclingPedalingCadenceRecord::class,
+ NutritionRecord::class,
+ SpeedRecord::class,
+ StepsCadenceRecord::class
+)
+
internal suspend fun HealthConnectClient.aggregateFallback(request: AggregateRequest):
AggregationResult {
- return request.fallbackMetrics
- .fold(emptyAggregationResult()) { currentAggregateResult, metric ->
- currentAggregateResult + aggregate(
- metric,
- request.timeRangeFilter,
- request.dataOriginFilter
- )
- }
+ var aggregationResult = emptyAggregationResult()
+
+ for (recordType in AGGREGATION_FALLBACK_RECORD_TYPES) {
+ aggregationResult += aggregate(
+ recordType,
+ request.fallbackMetrics,
+ request.timeRangeFilter,
+ request.dataOriginFilter
+ )
+ }
+
+ return aggregationResult
}
-private suspend fun <T : Any> HealthConnectClient.aggregate(
- metric: AggregateMetric<T>,
+private suspend fun <T : Record> HealthConnectClient.aggregate(
+ recordType: KClass<T>,
+ metrics: Set<AggregateMetric<*>>,
timeRangeFilter: TimeRangeFilter,
dataOriginFilter: Set<DataOrigin>
): AggregationResult {
- return when (metric) {
- NutritionRecord.TRANS_FAT_TOTAL -> aggregateNutritionTransFatTotal(
+ val dataTypeName = recordType.simpleName!!.replace("Record", "")
+ val recordTypeMetrics = metrics.filter { it.dataTypeName == dataTypeName }.toSet()
+
+ if (recordTypeMetrics.isEmpty()) {
+ return emptyAggregationResult()
+ }
+
+ return when (recordType) {
+ BloodPressureRecord::class -> aggregateBloodPressure(
+ recordTypeMetrics,
timeRangeFilter,
dataOriginFilter
)
- BloodPressureRecord.DIASTOLIC_AVG -> TODO(reason = "b/326414908")
- BloodPressureRecord.DIASTOLIC_MAX -> TODO(reason = "b/326414908")
- BloodPressureRecord.DIASTOLIC_MIN -> TODO(reason = "b/326414908")
- BloodPressureRecord.SYSTOLIC_AVG -> TODO(reason = "b/326414908")
- BloodPressureRecord.SYSTOLIC_MAX -> TODO(reason = "b/326414908")
- BloodPressureRecord.SYSTOLIC_MIN -> TODO(reason = "b/326414908")
- CyclingPedalingCadenceRecord.RPM_AVG -> TODO(reason = "b/326414908")
- CyclingPedalingCadenceRecord.RPM_MAX -> TODO(reason = "b/326414908")
- CyclingPedalingCadenceRecord.RPM_MIN -> TODO(reason = "b/326414908")
- SpeedRecord.SPEED_AVG -> TODO(reason = "b/326414908")
- SpeedRecord.SPEED_MAX -> TODO(reason = "b/326414908")
- SpeedRecord.SPEED_MIN -> TODO(reason = "b/326414908")
- StepsCadenceRecord.RATE_AVG -> TODO(reason = "b/326414908")
- StepsCadenceRecord.RATE_MAX -> TODO(reason = "b/326414908")
- StepsCadenceRecord.RATE_MIN -> TODO(reason = "b/326414908")
- else -> error("Invalid fallback aggregation type ${metric.metricKey}")
+ CyclingPedalingCadenceRecord::class -> TODO(reason = "b/326414908")
+ NutritionRecord::class -> aggregateNutritionTransFatTotal(timeRangeFilter, dataOriginFilter)
+ SpeedRecord::class -> TODO(reason = "b/326414908")
+ StepsCadenceRecord::class -> TODO(reason = "b/326414908")
+ else -> error("Invalid record type for aggregation fallback: $recordType")
}
}
@@ -165,7 +175,7 @@
internal fun emptyAggregationResult() =
AggregationResult(longValues = mapOf(), doubleValues = mapOf(), dataOrigins = setOf())
-internal data class AggregatedData<T>(
+class AggregatedData<T>(
var value: T,
var dataOrigins: MutableSet<DataOrigin> = mutableSetOf()
)