diff options
121 files changed, 8130 insertions, 7140 deletions
diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 080f7cf7d92c..f08c2fdc429a 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -8320,104 +8320,6 @@ package android.media.tv { method public void onSetMain(boolean); } - @FlaggedApi("android.media.tv.flags.tif_extension_standardization") public final class TvInputServiceExtensionManager { - method @RequiresPermission(android.Manifest.permission.TV_INPUT_HARDWARE) public int registerExtensionIBinder(@NonNull String, @NonNull android.os.IBinder); - field public static final String IANALOG_ATTRIBUTE_INTERFACE = "android.media.tv.extension.analog.IAnalogAttributeInterface"; - field public static final String IANALOG_AUDIO_INFO = "android.media.tv.extension.signal.IAnalogAudioInfo"; - field public static final String IAUDIO_SIGNAL_INFO = "android.media.tv.extension.signal.IAudioSignalInfo"; - field public static final String IAUDIO_SIGNAL_INFO_LISTENER = "android.media.tv.extension.signal.IAudioSignalInfoListener"; - field public static final String IBROADCAST_TIME = "android.media.tv.extension.time.IBroadcastTime"; - field public static final String ICAM_APP_INFO_LISTENER = "android.media.tv.extension.cam.ICamAppInfoListener"; - field public static final String ICAM_APP_INFO_SERVICE = "android.media.tv.extension.cam.ICamAppInfoService"; - field public static final String ICAM_DRM_INFO_LISTENER = "android.media.tv.extension.cam.ICamDrmInfoListener"; - field public static final String ICAM_HOST_CONTROL_ASK_RELEASE_REPLY_CALLBACK = "android.media.tv.extension.cam.ICamHostControlAskReleaseReplyCallback"; - field public static final String ICAM_HOST_CONTROL_INFO_LISTENER = "android.media.tv.extension.cam.ICamHostControlInfoListener"; - field public static final String ICAM_HOST_CONTROL_SERVICE = "android.media.tv.extension.cam.ICamHostControlService"; - field public static final String ICAM_HOST_CONTROL_TUNE_QUIETLY_FLAG = "android.media.tv.extension.cam.ICamHostControlTuneQuietlyFlag"; - field public static final String ICAM_HOST_CONTROL_TUNE_QUIETLY_FLAG_LISTENER = "android.media.tv.extension.cam.ICamHostControlTuneQuietlyFlagListener"; - field public static final String ICAM_INFO_LISTENER = "android.media.tv.extension.cam.ICamInfoListener"; - field public static final String ICAM_MONITORING_SERVICE = "android.media.tv.extension.cam.ICamMonitoringService"; - field public static final String ICAM_PIN_CAPABILITY_LISTENER = "android.media.tv.extension.cam.ICamPinCapabilityListener"; - field public static final String ICAM_PIN_SERVICE = "android.media.tv.extension.cam.ICamPinService"; - field public static final String ICAM_PIN_STATUS_LISTENER = "android.media.tv.extension.cam.ICamPinStatusListener"; - field public static final String ICAM_PROFILE_INTERFACE = "android.media.tv.extension.cam.ICamProfileInterface"; - field public static final String ICHANNEL_LIST_TRANSFER = "android.media.tv.extension.servicedb.IChannelListTransfer"; - field public static final String ICHANNEL_TUNED_INTERFACE = "android.media.tv.extension.tune.IChannelTunedInterface"; - field public static final String ICHANNEL_TUNED_LISTENER = "android.media.tv.extension.tune.IChannelTunedListener"; - field public static final String ICI_OPERATOR_INTERFACE = "android.media.tv.extension.cam.ICiOperatorInterface"; - field public static final String ICI_OPERATOR_LISTENER = "android.media.tv.extension.cam.ICiOperatorListener"; - field public static final String ICLIENT_TOKEN = "android.media.tv.extension.clienttoken.IClientToken"; - field public static final String ICONTENT_CONTROL_SERVICE = "android.media.tv.extension.cam.IContentControlService"; - field public static final String IDATA_SERVICE_SIGNAL_INFO = "android.media.tv.extension.teletext.IDataServiceSignalInfo"; - field public static final String IDATA_SERVICE_SIGNAL_INFO_LISTENER = "android.media.tv.extension.teletext.IDataServiceSignalInfoListener"; - field public static final String IDELETE_RECORDED_CONTENTS_CALLBACK = "android.media.tv.extension.pvr.IDeleteRecordedContentsCallback"; - field public static final String IDOWNLOADABLE_RATING_TABLE_MONITOR = "android.media.tv.extension.rating.IDownloadableRatingTableMonitor"; - field public static final String IENTER_MENU_ERROR_CALLBACK = "android.media.tv.extension.cam.IEnterMenuErrorCallback"; - field public static final String IEVENT_DOWNLOAD = "android.media.tv.extension.event.IEventDownload"; - field public static final String IEVENT_DOWNLOAD_LISTENER = "android.media.tv.extension.event.IEventDownloadListener"; - field public static final String IEVENT_DOWNLOAD_SESSION = "android.media.tv.extension.event.IEventDownloadSession"; - field public static final String IEVENT_MONITOR = "android.media.tv.extension.event.IEventMonitor"; - field public static final String IEVENT_MONITOR_LISTENER = "android.media.tv.extension.event.IEventMonitorListener"; - field public static final String IFAVORITE_NETWORK = "android.media.tv.extension.scan.IFavoriteNetwork"; - field public static final String IFAVORITE_NETWORK_LISTENER = "android.media.tv.extension.scan.IFavoriteNetworkListener"; - field public static final String IGET_INFO_RECORDED_CONTENTS_CALLBACK = "android.media.tv.extension.pvr.IGetInfoRecordedContentsCallback"; - field public static final String IHDMI_SIGNAL_INFO_LISTENER = "android.media.tv.extension.signal.IHdmiSignalInfoListener"; - field public static final String IHDMI_SIGNAL_INTERFACE = "android.media.tv.extension.signal.IHdmiSignalInterface"; - field public static final String IHDPLUS_INFO = "android.media.tv.extension.scan.IHDPlusInfo"; - field public static final String ILCNV2_CHANNEL_LIST = "android.media.tv.extension.scan.ILcnV2ChannelList"; - field public static final String ILCNV2_CHANNEL_LIST_LISTENER = "android.media.tv.extension.scan.ILcnV2ChannelListListener"; - field public static final String ILCN_CONFLICT = "android.media.tv.extension.scan.ILcnConflict"; - field public static final String ILCN_CONFLICT_LISTENER = "android.media.tv.extension.scan.ILcnConflictListener"; - field public static final String IMMI_INTERFACE = "android.media.tv.extension.cam.IMmiInterface"; - field public static final String IMMI_SESSION = "android.media.tv.extension.cam.IMmiSession"; - field public static final String IMMI_STATUS_CALLBACK = "android.media.tv.extension.cam.IMmiStatusCallback"; - field public static final String IMUX_TUNE = "android.media.tv.extension.tune.IMuxTune"; - field public static final String IMUX_TUNE_SESSION = "android.media.tv.extension.tune.IMuxTuneSession"; - field public static final String IOAD_UPDATE_INTERFACE = "android.media.tv.extension.oad.IOadUpdateInterface"; - field public static final String IOPERATOR_DETECTION = "android.media.tv.extension.scan.IOperatorDetection"; - field public static final String IOPERATOR_DETECTION_LISTENER = "android.media.tv.extension.scan.IOperatorDetectionListener"; - field public static final String IPMT_RATING_INTERFACE = "android.media.tv.extension.rating.IPmtRatingInterface"; - field public static final String IPMT_RATING_LISTENER = "android.media.tv.extension.rating.IPmtRatingListener"; - field public static final String IPROGRAM_INFO = "android.media.tv.extension.rating.IProgramInfo"; - field public static final String IPROGRAM_INFO_LISTENER = "android.media.tv.extension.rating.IProgramInfoListener"; - field public static final String IRATING_INTERFACE = "android.media.tv.extension.rating.IRatingInterface"; - field public static final String IRECORDED_CONTENTS = "android.media.tv.extension.pvr.IRecordedContents"; - field public static final String IREGION_CHANNEL_LIST = "android.media.tv.extension.scan.IRegionChannelList"; - field public static final String IREGION_CHANNEL_LIST_LISTENER = "android.media.tv.extension.scan.IRegionChannelListListener"; - field public static final String ISCAN_BACKGROUND_SERVICE_UPDATE = "android.media.tv.extension.scanbsu.IScanBackgroundServiceUpdate"; - field public static final String ISCAN_BACKGROUND_SERVICE_UPDATE_LISTENER = "android.media.tv.extension.scanbsu.IScanBackgroundServiceUpdateListener"; - field public static final String ISCAN_INTERFACE = "android.media.tv.extension.scan.IScanInterface"; - field public static final String ISCAN_LISTENER = "android.media.tv.extension.scan.IScanListener"; - field public static final String ISCAN_SAT_SEARCH = "android.media.tv.extension.scan.IScanSatSearch"; - field public static final String ISCAN_SESSION = "android.media.tv.extension.scan.IScanSession"; - field public static final String ISCREEN_MODE_SETTINGS = "android.media.tv.extension.screenmode.IScreenModeSettings"; - field public static final String ISERVICE_LIST = "android.media.tv.extension.servicedb.IServiceList"; - field public static final String ISERVICE_LIST_EDIT = "android.media.tv.extension.servicedb.IServiceListEdit"; - field public static final String ISERVICE_LIST_EDIT_LISTENER = "android.media.tv.extension.servicedb.IServiceListEditListener"; - field public static final String ISERVICE_LIST_EXPORT_LISTENER = "android.media.tv.extension.servicedb.IServiceListExportListener"; - field public static final String ISERVICE_LIST_EXPORT_SESSION = "android.media.tv.extension.servicedb.IServiceListExportSession"; - field public static final String ISERVICE_LIST_IMPORT_LISTENER = "android.media.tv.extension.servicedb.IServiceListImportListener"; - field public static final String ISERVICE_LIST_IMPORT_SESSION = "android.media.tv.extension.servicedb.IServiceListImportSession"; - field public static final String ISERVICE_LIST_SET_CHANNEL_LIST_LISTENER = "android.media.tv.extension.servicedb.IServiceListSetChannelListListener"; - field public static final String ISERVICE_LIST_SET_CHANNEL_LIST_SESSION = "android.media.tv.extension.servicedb.IServiceListSetChannelListSession"; - field public static final String ISERVICE_LIST_TRANSFER_INTERFACE = "android.media.tv.extension.servicedb.IServiceListTransferInterface"; - field public static final String ITARGET_REGION = "android.media.tv.extension.scan.ITargetRegion"; - field public static final String ITARGET_REGION_LISTENER = "android.media.tv.extension.scan.ITargetRegionListener"; - field public static final String ITELETEXT_PAGE_SUB_CODE = "android.media.tv.extension.teletext.ITeletextPageSubCode"; - field public static final String ITKGS_INFO = "android.media.tv.extension.scan.ITkgsInfo"; - field public static final String ITKGS_INFO_LISTENER = "android.media.tv.extension.scan.ITkgsInfoListener"; - field public static final String ITUNER_FRONTEND_SIGNAL_INFO_INTERFACE = "android.media.tv.extension.signal.ITunerFrontendSignalInfoInterface"; - field public static final String ITUNER_FRONTEND_SIGNAL_INFO_LISTENER = "android.media.tv.extension.signal.ITunerFrontendSignalInfoListener"; - field public static final String IVBI_RATING_INTERFACE = "android.media.tv.extension.rating.IVbiRatingInterface"; - field public static final String IVBI_RATING_LISTENER = "android.media.tv.extension.rating.IVbiRatingListener"; - field public static final String IVIDEO_SIGNAL_INFO = "android.media.tv.extension.signal.IVideoSignalInfo"; - field public static final String IVIDEO_SIGNAL_INFO_LISTENER = "android.media.tv.extension.signal.IVideoSignalInfoListener"; - field public static final int REGISTER_FAIL_IMPLEMENTATION_NOT_STANDARDIZED = 2; // 0x2 - field public static final int REGISTER_FAIL_NAME_NOT_STANDARDIZED = 1; // 0x1 - field public static final int REGISTER_FAIL_REMOTE_EXCEPTION = 3; // 0x3 - field public static final int REGISTER_SUCCESS = 0; // 0x0 - } - public abstract static class TvRecordingClient.RecordingCallback { method public void onEvent(String, String, android.os.Bundle); } @@ -15956,7 +15858,7 @@ package android.telephony { method @RequiresPermission(android.Manifest.permission.READ_PRECISE_PHONE_STATE) public default void onCallStatesChanged(@NonNull java.util.List<android.telephony.CallState>); } - @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static interface TelephonyCallback.CarrierRoamingNtnModeListener { + @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static interface TelephonyCallback.CarrierRoamingNtnListener { method public default void onCarrierRoamingNtnAvailableServicesChanged(@NonNull int[]); method public default void onCarrierRoamingNtnEligibleStateChanged(boolean); method public void onCarrierRoamingNtnModeChanged(boolean); @@ -18634,9 +18536,9 @@ package android.telephony.satellite { method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public void onSatelliteCapabilitiesChanged(@NonNull android.telephony.satellite.SatelliteCapabilities); } - @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public interface SatelliteCommunicationAllowedStateCallback { - method public default void onSatelliteAccessConfigurationChanged(@Nullable android.telephony.satellite.SatelliteAccessConfiguration); - method public void onSatelliteCommunicationAllowedStateChanged(boolean); + @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public interface SatelliteCommunicationAccessStateCallback { + method public void onAccessAllowedStateChanged(boolean); + method public default void onAccessConfigurationChanged(@Nullable android.telephony.satellite.SatelliteAccessConfiguration); } @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public final class SatelliteDatagram implements android.os.Parcelable { @@ -18675,7 +18577,7 @@ package android.telephony.satellite { method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void provisionSatellite(@NonNull java.util.List<android.telephony.satellite.SatelliteSubscriberInfo>, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void provisionService(@NonNull String, @NonNull byte[], @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForCapabilitiesChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteCapabilitiesCallback); - method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForCommunicationAllowedStateChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteCommunicationAllowedStateCallback); + method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForCommunicationAccessStateChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteCommunicationAccessStateCallback); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForIncomingDatagram(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteDatagramCallback); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForModemStateChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteModemStateCallback); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void registerForNtnSignalStrengthChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.NtnSignalStrengthCallback); @@ -18704,7 +18606,7 @@ package android.telephony.satellite { method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void startTransmissionUpdates(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>, @NonNull android.telephony.satellite.SatelliteTransmissionUpdateCallback); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void stopTransmissionUpdates(@NonNull android.telephony.satellite.SatelliteTransmissionUpdateCallback, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForCapabilitiesChanged(@NonNull android.telephony.satellite.SatelliteCapabilitiesCallback); - method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForCommunicationAllowedStateChanged(@NonNull android.telephony.satellite.SatelliteCommunicationAllowedStateCallback); + method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForCommunicationAccessStateChanged(@NonNull android.telephony.satellite.SatelliteCommunicationAccessStateCallback); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForIncomingDatagram(@NonNull android.telephony.satellite.SatelliteDatagramCallback); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForModemStateChanged(@NonNull android.telephony.satellite.SatelliteModemStateCallback); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForNtnSignalStrengthChanged(@NonNull android.telephony.satellite.NtnSignalStrengthCallback); @@ -18833,9 +18735,9 @@ package android.telephony.satellite { method public int describeContents(); method public int getCarrierId(); method @NonNull public String getNiddApn(); - method public int getSubId(); method @NonNull public String getSubscriberId(); method public int getSubscriberIdType(); + method public int getSubscriptionId(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.telephony.satellite.SatelliteSubscriberInfo> CREATOR; field public static final int SUBSCRIBER_ID_TYPE_ICCID = 0; // 0x0 @@ -18847,9 +18749,9 @@ package android.telephony.satellite { method @NonNull public android.telephony.satellite.SatelliteSubscriberInfo build(); method @NonNull public android.telephony.satellite.SatelliteSubscriberInfo.Builder setCarrierId(int); method @NonNull public android.telephony.satellite.SatelliteSubscriberInfo.Builder setNiddApn(@NonNull String); - method @NonNull public android.telephony.satellite.SatelliteSubscriberInfo.Builder setSubId(int); method @NonNull public android.telephony.satellite.SatelliteSubscriberInfo.Builder setSubscriberId(@NonNull String); method @NonNull public android.telephony.satellite.SatelliteSubscriberInfo.Builder setSubscriberIdType(int); + method @NonNull public android.telephony.satellite.SatelliteSubscriberInfo.Builder setSubscriptionId(int); } @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public final class SatelliteSubscriberProvisionStatus implements android.os.Parcelable { diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 84d67415a4b4..a2fddb045179 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -4443,7 +4443,8 @@ public class DevicePolicyManager { * disabled through this Config. */ private static final IpcDataCache.Config sDpmCaches = - new IpcDataCache.Config(8, IpcDataCache.MODULE_SYSTEM, "DevicePolicyManagerCaches"); + new IpcDataCache.Config(8, IpcDataCache.MODULE_SYSTEM, "DevicePolicyManagerCaches") + .cacheNulls(true); /** @hide */ public static void invalidateBinderCaches() { diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig index c4d11cd8aff7..64159336f275 100644 --- a/core/java/android/hardware/input/input_framework.aconfig +++ b/core/java/android/hardware/input/input_framework.aconfig @@ -67,17 +67,17 @@ flag { } flag { - name: "modifier_shortcut_dump" - namespace: "input" - description: "Dump keyboard shortcuts in dumpsys window" - bug: "351963350" + name: "modifier_shortcut_dump" + namespace: "input" + description: "Dump keyboard shortcuts in dumpsys window" + bug: "351963350" } flag { - name: "modifier_shortcut_manager_refactor" - namespace: "input" - description: "Refactor ModifierShortcutManager internal representation of shortcuts." - bug: "358603902" + name: "modifier_shortcut_manager_refactor" + namespace: "input" + description: "Refactor ModifierShortcutManager internal representation of shortcuts." + bug: "358603902" } flag { @@ -114,31 +114,31 @@ flag { } flag { - name: "keyboard_repeat_keys" - namespace: "input_native" - description: "Allow configurable timeout before key repeat and repeat delay rate for key repeats" - bug: "336585002" + name: "keyboard_repeat_keys" + namespace: "input_native" + description: "Allow configurable timeout before key repeat and repeat delay rate for key repeats" + bug: "336585002" } flag { - name: "mouse_reverse_vertical_scrolling" - namespace: "input" - description: "Controls whether external mouse vertical scrolling can be reversed" - bug: "352598211" + name: "mouse_reverse_vertical_scrolling" + namespace: "input" + description: "Controls whether external mouse vertical scrolling can be reversed" + bug: "352598211" } flag { - name: "mouse_swap_primary_button" - namespace: "input" - description: "Controls whether the connected mice's primary buttons, left and right, can be swapped." - bug: "352598211" + name: "mouse_swap_primary_button" + namespace: "input" + description: "Controls whether the connected mice's primary buttons, left and right, can be swapped." + bug: "352598211" } flag { - name: "keyboard_a11y_shortcut_control" - namespace: "input" - description: "Adds shortcuts to toggle and control a11y keyboard features" - bug: "373458181" + name: "keyboard_a11y_shortcut_control" + namespace: "input" + description: "Adds shortcuts to toggle and control a11y keyboard features" + bug: "373458181" } flag { @@ -164,32 +164,32 @@ flag { } flag { - name: "override_power_key_behavior_in_focused_window" - namespace: "wallet_integration" - description: "Allows privileged focused windows to override the power key double tap behavior." - bug: "357144512" + name: "override_power_key_behavior_in_focused_window" + namespace: "wallet_integration" + description: "Allows privileged focused windows to override the power key double tap behavior." + bug: "357144512" } flag { - name: "touchpad_three_finger_tap_shortcut" - namespace: "input" - description: "Turns three-finger touchpad taps into a customizable shortcut." - bug: "365063048" + name: "touchpad_three_finger_tap_shortcut" + namespace: "input" + description: "Turns three-finger touchpad taps into a customizable shortcut." + bug: "365063048" } flag { - name: "enable_talkback_and_magnifier_key_gestures" - namespace: "input" - description: "Adds key gestures for talkback and magnifier" - bug: "375277034" + name: "enable_talkback_and_magnifier_key_gestures" + namespace: "input" + description: "Adds key gestures for talkback and magnifier" + bug: "375277034" } flag { - name: "can_window_override_power_gesture_api" - namespace: "wallet_integration" - description: "Adds new API in WindowManager class to check if the window can override the power key double tap behavior." - bug: "378736024" - } + name: "can_window_override_power_gesture_api" + namespace: "wallet_integration" + description: "Adds new API in WindowManager class to check if the window can override the power key double tap behavior." + bug: "378736024" +} flag { name: "pointer_acceleration" @@ -206,8 +206,8 @@ flag { } flag { - name: "remove_fallback_modifiers" - namespace: "input" - description: "Removes modifiers from the original key event that activated the fallback, ensuring that only the intended fallback event is sent." - bug: "382545048" + name: "remove_fallback_modifiers" + namespace: "input" + description: "Removes modifiers from the original key event that activated the fallback, ensuring that only the intended fallback event is sent." + bug: "382545048" } diff --git a/core/java/android/telephony/TelephonyCallback.java b/core/java/android/telephony/TelephonyCallback.java index cc19e7cab537..d05fa9e43cd4 100644 --- a/core/java/android/telephony/TelephonyCallback.java +++ b/core/java/android/telephony/TelephonyCallback.java @@ -1819,7 +1819,7 @@ public class TelephonyCallback { */ @SystemApi @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) - public interface CarrierRoamingNtnModeListener { + public interface CarrierRoamingNtnListener { /** * Callback invoked when carrier roaming non-terrestrial network mode changes. * @@ -2332,8 +2332,8 @@ public class TelephonyCallback { public void onCarrierRoamingNtnModeChanged(boolean active) { if (!Flags.carrierEnabledSatelliteFlag()) return; - CarrierRoamingNtnModeListener listener = - (CarrierRoamingNtnModeListener) mTelephonyCallbackWeakRef.get(); + CarrierRoamingNtnListener listener = + (CarrierRoamingNtnListener) mTelephonyCallbackWeakRef.get(); if (listener == null) return; Binder.withCleanCallingIdentity( @@ -2343,8 +2343,8 @@ public class TelephonyCallback { public void onCarrierRoamingNtnEligibleStateChanged(boolean eligible) { if (!Flags.carrierRoamingNbIotNtn()) return; - CarrierRoamingNtnModeListener listener = - (CarrierRoamingNtnModeListener) mTelephonyCallbackWeakRef.get(); + CarrierRoamingNtnListener listener = + (CarrierRoamingNtnListener) mTelephonyCallbackWeakRef.get(); if (listener == null) return; Binder.withCleanCallingIdentity(() -> mExecutor.execute( @@ -2355,8 +2355,8 @@ public class TelephonyCallback { @NetworkRegistrationInfo.ServiceType int[] availableServices) { if (!Flags.carrierRoamingNbIotNtn()) return; - CarrierRoamingNtnModeListener listener = - (CarrierRoamingNtnModeListener) mTelephonyCallbackWeakRef.get(); + CarrierRoamingNtnListener listener = + (CarrierRoamingNtnListener) mTelephonyCallbackWeakRef.get(); if (listener == null) return; Binder.withCleanCallingIdentity(() -> mExecutor.execute( @@ -2367,8 +2367,8 @@ public class TelephonyCallback { @NonNull NtnSignalStrength ntnSignalStrength) { if (!Flags.carrierRoamingNbIotNtn()) return; - CarrierRoamingNtnModeListener listener = - (CarrierRoamingNtnModeListener) mTelephonyCallbackWeakRef.get(); + CarrierRoamingNtnListener listener = + (CarrierRoamingNtnListener) mTelephonyCallbackWeakRef.get(); if (listener == null) return; Binder.withCleanCallingIdentity(() -> mExecutor.execute( diff --git a/core/java/android/telephony/TelephonyRegistryManager.java b/core/java/android/telephony/TelephonyRegistryManager.java index 4ec429d0c4ad..fa44fcf0dffa 100644 --- a/core/java/android/telephony/TelephonyRegistryManager.java +++ b/core/java/android/telephony/TelephonyRegistryManager.java @@ -1341,7 +1341,7 @@ public class TelephonyRegistryManager { TelephonyCallback.EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED); } - if (telephonyCallback instanceof TelephonyCallback.CarrierRoamingNtnModeListener) { + if (telephonyCallback instanceof TelephonyCallback.CarrierRoamingNtnListener) { eventList.add(TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_MODE_CHANGED); eventList.add(TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_ELIGIBLE_STATE_CHANGED); eventList.add(TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_AVAILABLE_SERVICES_CHANGED); diff --git a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java index e9dfdd826572..5da2564e5b7f 100644 --- a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java +++ b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java @@ -194,38 +194,38 @@ public class PropertyInvalidatedCacheTests { new ServerQuery(tester)); // Caches are enabled upon creation. - assertEquals(false, cache1.getDisabledState()); - assertEquals(false, cache2.getDisabledState()); - assertEquals(false, cache3.getDisabledState()); + assertFalse(cache1.isDisabled()); + assertFalse(cache2.isDisabled()); + assertFalse(cache3.isDisabled()); // Disable the cache1 instance. Only cache1 is disabled cache1.disableInstance(); - assertEquals(true, cache1.getDisabledState()); - assertEquals(false, cache2.getDisabledState()); - assertEquals(false, cache3.getDisabledState()); + assertTrue(cache1.isDisabled()); + assertFalse(cache2.isDisabled()); + assertFalse(cache3.isDisabled()); // Disable cache1. This will disable cache1 and cache2 because they share the // same name. cache3 has a different name and will not be disabled. cache1.disableLocal(); - assertEquals(true, cache1.getDisabledState()); - assertEquals(true, cache2.getDisabledState()); - assertEquals(false, cache3.getDisabledState()); + assertTrue(cache1.isDisabled()); + assertTrue(cache2.isDisabled()); + assertFalse(cache3.isDisabled()); // Create a new cache1. Verify that the new instance is disabled. cache1 = new PropertyInvalidatedCache<>(4, MODULE, API, "cache1", new ServerQuery(tester)); - assertEquals(true, cache1.getDisabledState()); + assertTrue(cache1.isDisabled()); // Remove the record of caches being locally disabled. This is a clean-up step. cache1.forgetDisableLocal(); - assertEquals(true, cache1.getDisabledState()); - assertEquals(true, cache2.getDisabledState()); - assertEquals(false, cache3.getDisabledState()); + assertTrue(cache1.isDisabled()); + assertTrue(cache2.isDisabled()); + assertFalse(cache3.isDisabled()); // Create a new cache1. Verify that the new instance is not disabled. cache1 = new PropertyInvalidatedCache<>(4, MODULE, API, "cache1", new ServerQuery(tester)); - assertEquals(false, cache1.getDisabledState()); + assertFalse(cache1.isDisabled()); } private static class TestQuery @@ -280,7 +280,7 @@ public class PropertyInvalidatedCacheTests { public void testCacheRecompute() { TestCache cache = new TestCache(); cache.invalidateCache(); - assertEquals(cache.isDisabled(), false); + assertFalse(cache.isDisabled()); assertEquals("foo5", cache.query(5)); assertEquals(1, cache.getRecomputeCount()); assertEquals("foo5", cache.query(5)); @@ -383,15 +383,15 @@ public class PropertyInvalidatedCacheTests { @Test public void testLocalProcessDisable() { TestCache cache = new TestCache(); - assertEquals(cache.isDisabled(), false); + assertFalse(cache.isDisabled()); cache.invalidateCache(); assertEquals("foo5", cache.query(5)); assertEquals(1, cache.getRecomputeCount()); assertEquals("foo5", cache.query(5)); assertEquals(1, cache.getRecomputeCount()); - assertEquals(cache.isDisabled(), false); + assertFalse(cache.isDisabled()); cache.disableLocal(); - assertEquals(cache.isDisabled(), true); + assertTrue(cache.isDisabled()); assertEquals("foo5", cache.query(5)); assertEquals("foo5", cache.query(5)); assertEquals(3, cache.getRecomputeCount()); diff --git a/core/tests/coretests/src/android/os/IpcDataCacheTest.java b/core/tests/coretests/src/android/os/IpcDataCacheTest.java index fc04e6438ac6..74b32a17d964 100644 --- a/core/tests/coretests/src/android/os/IpcDataCacheTest.java +++ b/core/tests/coretests/src/android/os/IpcDataCacheTest.java @@ -19,6 +19,8 @@ package android.os; import static android.app.Flags.FLAG_PIC_ISOLATE_CACHE_BY_UID; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import android.app.PropertyInvalidatedCache; @@ -195,9 +197,9 @@ public class IpcDataCacheTest { try { testCache.query(9); - assertEquals(false, true); // The code should not reach this point. + fail(); // The code should not reach this point. } catch (RuntimeException e) { - assertEquals(e.getCause() instanceof RemoteException, true); + assertTrue(e.getCause() instanceof RemoteException); } tester.verify(4); } @@ -256,38 +258,38 @@ public class IpcDataCacheTest { new ServerQuery(tester)); // Caches are enabled upon creation. - assertEquals(false, cache1.getDisabledState()); - assertEquals(false, cache2.getDisabledState()); - assertEquals(false, cache3.getDisabledState()); + assertFalse(cache1.isDisabled()); + assertFalse(cache2.isDisabled()); + assertFalse(cache3.isDisabled()); // Disable the cache1 instance. Only cache1 is disabled cache1.disableInstance(); - assertEquals(true, cache1.getDisabledState()); - assertEquals(false, cache2.getDisabledState()); - assertEquals(false, cache3.getDisabledState()); + assertTrue(cache1.isDisabled()); + assertFalse(cache2.isDisabled()); + assertFalse(cache3.isDisabled()); // Disable cache1. This will disable cache1 and cache2 because they share the // same name. cache3 has a different name and will not be disabled. cache1.disableForCurrentProcess(); - assertEquals(true, cache1.getDisabledState()); - assertEquals(true, cache2.getDisabledState()); - assertEquals(false, cache3.getDisabledState()); + assertTrue(cache1.isDisabled()); + assertTrue(cache2.isDisabled()); + assertFalse(cache3.isDisabled()); // Create a new cache1. Verify that the new instance is disabled. cache1 = new IpcDataCache<>(4, MODULE, API, "cacheA", new ServerQuery(tester)); - assertEquals(true, cache1.getDisabledState()); + assertTrue(cache1.isDisabled()); // Remove the record of caches being locally disabled. This is a clean-up step. cache1.forgetDisableLocal(); - assertEquals(true, cache1.getDisabledState()); - assertEquals(true, cache2.getDisabledState()); - assertEquals(false, cache3.getDisabledState()); + assertTrue(cache1.isDisabled()); + assertTrue(cache2.isDisabled()); + assertFalse(cache3.isDisabled()); // Create a new cache1. Verify that the new instance is not disabled. cache1 = new IpcDataCache<>(4, MODULE, API, "cacheA", new ServerQuery(tester)); - assertEquals(false, cache1.getDisabledState()); + assertFalse(cache1.isDisabled()); } private static class TestQuery @@ -345,7 +347,7 @@ public class IpcDataCacheTest { public void testCacheRecompute() { TestCache cache = new TestCache(); cache.invalidateCache(); - assertEquals(cache.isDisabled(), false); + assertFalse(cache.isDisabled()); assertEquals("foo5", cache.query(5)); assertEquals(1, cache.getRecomputeCount()); assertEquals("foo5", cache.query(5)); @@ -407,15 +409,15 @@ public class IpcDataCacheTest { @Test public void testLocalProcessDisable() { TestCache cache = new TestCache(); - assertEquals(cache.isDisabled(), false); + assertFalse(cache.isDisabled()); cache.invalidateCache(); assertEquals("foo5", cache.query(5)); assertEquals(1, cache.getRecomputeCount()); assertEquals("foo5", cache.query(5)); assertEquals(1, cache.getRecomputeCount()); - assertEquals(cache.isDisabled(), false); + assertFalse(cache.isDisabled()); cache.disableForCurrentProcess(); - assertEquals(cache.isDisabled(), true); + assertTrue(cache.isDisabled()); assertEquals("foo5", cache.query(5)); assertEquals("foo5", cache.query(5)); assertEquals(3, cache.getRecomputeCount()); @@ -434,20 +436,20 @@ public class IpcDataCacheTest { TestCache dc = new TestCache(d); a.disableForCurrentProcess(); - assertEquals(ac.isDisabled(), true); - assertEquals(bc.isDisabled(), false); - assertEquals(cc.isDisabled(), false); - assertEquals(dc.isDisabled(), false); + assertTrue(ac.isDisabled()); + assertFalse(bc.isDisabled()); + assertFalse(cc.isDisabled()); + assertFalse(dc.isDisabled()); a.disableAllForCurrentProcess(); - assertEquals(ac.isDisabled(), true); - assertEquals(bc.isDisabled(), false); - assertEquals(cc.isDisabled(), false); - assertEquals(dc.isDisabled(), true); + assertTrue(ac.isDisabled()); + assertFalse(bc.isDisabled()); + assertFalse(cc.isDisabled()); + assertTrue(dc.isDisabled()); IpcDataCache.Config e = a.child("nameE"); TestCache ec = new TestCache(e); - assertEquals(ec.isDisabled(), true); + assertTrue(ec.isDisabled()); } diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt index 957f0ca502a1..c45f6903c2e1 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt @@ -232,11 +232,16 @@ class BubbleBarAnimationHelperTest { val taskController = mock<TaskViewTaskController>() val bubble = createBubble("key", taskController).initialize(container) + val semaphore = Semaphore(0) + val after = Runnable { semaphore.release() } + activityScenario.onActivity { - animationHelper.animateExpansion(bubble) {} + animationHelper.animateExpansion(bubble, after) animatorTestRule.advanceTimeBy(1000) } getInstrumentation().waitForIdleSync() + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + getInstrumentation().runOnMainSync { animationHelper.animateToRestPosition() animatorTestRule.advanceTimeBy(100) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java index a73d43c54064..28da8bf3dd4d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java @@ -33,14 +33,12 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.app.ActivityManager; import android.content.Context; -import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Color; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Binder; -import android.os.Trace; import android.view.IWindow; import android.view.LayoutInflater; import android.view.SurfaceControl; @@ -52,12 +50,10 @@ import android.widget.FrameLayout; import android.widget.ImageView; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.R; import com.android.wm.shell.common.ScreenshotUtils; -import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SurfaceUtils; import java.util.function.Consumer; @@ -83,19 +79,9 @@ public class SplitDecorManager extends WindowlessWindowManager { private static final String RESIZING_BACKGROUND_SURFACE_NAME = "ResizingBackground"; private static final String GAP_BACKGROUND_SURFACE_NAME = "GapBackground"; - // Indicates the loading state of mIcon - enum IconLoadState { - NOT_LOADED, - LOADING, - LOADED - } - private final IconProvider mIconProvider; - private final ShellExecutor mMainExecutor; - private final ShellExecutor mBgExecutor; private Drawable mIcon; - private IconLoadState mIconLoadState = IconLoadState.NOT_LOADED; private ImageView mVeilIconView; private SurfaceControlViewHost mViewHost; /** The parent surface that this is attached to. Should be the stage root. */ @@ -123,14 +109,9 @@ public class SplitDecorManager extends WindowlessWindowManager { private int mOffsetY; private int mRunningAnimationCount = 0; - public SplitDecorManager(Configuration configuration, - IconProvider iconProvider, - ShellExecutor mainExecutor, - ShellExecutor bgExecutor) { + public SplitDecorManager(Configuration configuration, IconProvider iconProvider) { super(configuration, null /* rootSurface */, null /* hostInputToken */); mIconProvider = iconProvider; - mMainExecutor = mainExecutor; - mBgExecutor = bgExecutor; } @Override @@ -218,7 +199,6 @@ public class SplitDecorManager extends WindowlessWindowManager { } mHostLeash = null; mIcon = null; - mIconLoadState = IconLoadState.NOT_LOADED; mVeilIconView = null; mIsCurrentlyChanging = false; mShown = false; @@ -310,11 +290,10 @@ public class SplitDecorManager extends WindowlessWindowManager { .setWindowCrop(mGapBackgroundLeash, sideBounds.width(), sideBounds.height()); } - if (mIconLoadState == IconLoadState.NOT_LOADED && resizingTask.topActivityInfo != null) { - loadIconInBackground(resizingTask.topActivityInfo, () -> { - mVeilIconView.setImageDrawable(mIcon); - mVeilIconView.setVisibility(View.VISIBLE); - }); + if (mIcon == null && resizingTask.topActivityInfo != null) { + mIcon = mIconProvider.getIcon(resizingTask.topActivityInfo); + mVeilIconView.setImageDrawable(mIcon); + mVeilIconView.setVisibility(View.VISIBLE); WindowManager.LayoutParams lp = (WindowManager.LayoutParams) mViewHost.getView().getLayoutParams(); @@ -468,10 +447,10 @@ public class SplitDecorManager extends WindowlessWindowManager { } if (mIcon == null && resizingTask.topActivityInfo != null) { - loadIconInBackground(resizingTask.topActivityInfo, () -> { - mVeilIconView.setImageDrawable(mIcon); - mVeilIconView.setVisibility(View.VISIBLE); - }); + // Initialize icon + mIcon = mIconProvider.getIcon(resizingTask.topActivityInfo); + mVeilIconView.setImageDrawable(mIcon); + mVeilIconView.setVisibility(View.VISIBLE); WindowManager.LayoutParams lp = (WindowManager.LayoutParams) mViewHost.getView().getLayoutParams(); @@ -504,7 +483,7 @@ public class SplitDecorManager extends WindowlessWindowManager { return; } - // Re-center icon + // Recenter icon t.setPosition(mIconLeash, mInstantaneousBounds.width() / 2f - mIconSize / 2f, mInstantaneousBounds.height() / 2f - mIconSize / 2f); @@ -647,38 +626,9 @@ public class SplitDecorManager extends WindowlessWindowManager { mVeilIconView.setImageDrawable(null); t.hide(mIconLeash); mIcon = null; - mIconLoadState = IconLoadState.NOT_LOADED; } } - /** - * Loads the icon for the given {@param info}, calling {@param postLoadCb} on the main thread - * if provided. - */ - private void loadIconInBackground(@NonNull ActivityInfo info, @Nullable Runnable postLoadCb) { - mIconLoadState = IconLoadState.LOADING; - mBgExecutor.setBoost(); - mBgExecutor.execute(() -> { - Trace.beginSection("SplitDecorManager.loadIconInBackground(" - + info.applicationInfo.packageName + ")"); - final Drawable icon = mIconProvider.getIcon(info); - Trace.endSection(); - mMainExecutor.execute(() -> { - if (mIconLoadState != IconLoadState.LOADING) { - // The request was canceled while loading in the background, just drop the - // result - return; - } - mIcon = icon; - mIconLoadState = IconLoadState.LOADED; - if (postLoadCb != null) { - postLoadCb.run(); - } - }); - mBgExecutor.resetBoost(); - }); - } - private static float[] getResizingBackgroundColor(ActivityManager.RunningTaskInfo taskInfo) { final int taskBgColor = taskInfo.taskDescription.getBackgroundColor(); return Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).getComponents(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index fc274d6e7174..cd5c135691d7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -799,10 +799,6 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } void onStartDragging() { - // This triggers initialization of things like the resize veil in preparation for - // showing it when the user moves the divider past the slop - updateDividerBounds(getDividerPosition(), false /* shouldUseParallaxEffect */); - mInteractionJankMonitor.begin(getDividerLeash(), mContext, mHandler, CUJ_SPLIT_SCREEN_RESIZE); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java index 5b6b897e55d9..aebd94fc173a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java @@ -34,7 +34,6 @@ import com.android.wm.shell.common.split.SplitState; import com.android.wm.shell.dagger.pip.TvPipModule; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.shared.TransactionPool; -import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.splitscreen.tv.TvSplitScreenController; @@ -94,12 +93,11 @@ public class TvWMShellModule { SplitState splitState, @ShellMainThread ShellExecutor mainExecutor, Handler mainHandler, - @ShellBackgroundThread ShellExecutor bgExecutor, SystemWindows systemWindows) { return new TvSplitScreenController(context, shellInit, shellCommandHandler, shellController, shellTaskOrganizer, syncQueue, rootTDAOrganizer, displayController, displayImeController, displayInsetsController, transitions, transactionPool, iconProvider, recentTasks, launchAdjacentController, multiInstanceHelper, - splitState, mainExecutor, mainHandler, bgExecutor, systemWindows); + splitState, mainExecutor, mainHandler, systemWindows); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 48b0a6cb364b..1916215dea74 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -520,8 +520,7 @@ public abstract class WMShellModule { MultiInstanceHelper multiInstanceHelper, SplitState splitState, @ShellMainThread ShellExecutor mainExecutor, - @ShellMainThread Handler mainHandler, - @ShellBackgroundThread ShellExecutor bgExecutor) { + @ShellMainThread Handler mainHandler) { return new SplitScreenController( context, shellInit, @@ -545,8 +544,7 @@ public abstract class WMShellModule { multiInstanceHelper, splitState, mainExecutor, - mainHandler, - bgExecutor); + mainHandler); } // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index 39ed2061c675..c724135aeced 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -185,7 +185,6 @@ public class SplitScreenController implements SplitDragPolicy.Starter, private final LauncherApps mLauncherApps; private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer; private final ShellExecutor mMainExecutor; - private final ShellExecutor mBgExecutor; private final Handler mMainHandler; private final SplitScreenImpl mImpl = new SplitScreenImpl(); private final DisplayController mDisplayController; @@ -232,8 +231,7 @@ public class SplitScreenController implements SplitDragPolicy.Starter, MultiInstanceHelper multiInstanceHelper, SplitState splitState, ShellExecutor mainExecutor, - Handler mainHandler, - ShellExecutor bgExecutor) { + Handler mainHandler) { mShellCommandHandler = shellCommandHandler; mShellController = shellController; mTaskOrganizer = shellTaskOrganizer; @@ -243,7 +241,6 @@ public class SplitScreenController implements SplitDragPolicy.Starter, mRootTDAOrganizer = rootTDAOrganizer; mMainExecutor = mainExecutor; mMainHandler = mainHandler; - mBgExecutor = bgExecutor; mDisplayController = displayController; mDisplayImeController = displayImeController; mDisplayInsetsController = displayInsetsController; @@ -301,9 +298,8 @@ public class SplitScreenController implements SplitDragPolicy.Starter, return new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, mTaskOrganizer, mDisplayController, mDisplayImeController, mDisplayInsetsController, mTransitions, mTransactionPool, mIconProvider, - mMainExecutor, mMainHandler, mBgExecutor, mRecentTasksOptional, - mLaunchAdjacentController, mWindowDecorViewModel, mSplitState, - mDesktopTasksController); + mMainExecutor, mMainHandler, mRecentTasksOptional, mLaunchAdjacentController, + mWindowDecorViewModel, mSplitState, mDesktopTasksController); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 8ecf483a7e75..90c59176b991 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -217,7 +217,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private final SplitscreenEventLogger mLogger; private final ShellExecutor mMainExecutor; private final Handler mMainHandler; - private final ShellExecutor mBgExecutor; // Cache live tile tasks while entering recents, evict them from stages in finish transaction // if user is opening another task(s). private final ArrayList<Integer> mPausingTasks = new ArrayList<>(); @@ -346,20 +345,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } }; - protected StageCoordinator(Context context, - int displayId, - SyncTransactionQueue syncQueue, - ShellTaskOrganizer taskOrganizer, - DisplayController displayController, + protected StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, + ShellTaskOrganizer taskOrganizer, DisplayController displayController, DisplayImeController displayImeController, - DisplayInsetsController displayInsetsController, - Transitions transitions, - TransactionPool transactionPool, - IconProvider iconProvider, - ShellExecutor mainExecutor, - Handler mainHandler, - ShellExecutor bgExecutor, - Optional<RecentTasksController> recentTasks, + DisplayInsetsController displayInsetsController, Transitions transitions, + TransactionPool transactionPool, IconProvider iconProvider, ShellExecutor mainExecutor, + Handler mainHandler, Optional<RecentTasksController> recentTasks, LaunchAdjacentController launchAdjacentController, Optional<WindowDecorViewModel> windowDecorViewModel, SplitState splitState, Optional<DesktopTasksController> desktopTasksController) { @@ -370,7 +361,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mLogger = new SplitscreenEventLogger(); mMainExecutor = mainExecutor; mMainHandler = mainHandler; - mBgExecutor = bgExecutor; mRecentTasks = recentTasks; mLaunchAdjacentController = launchAdjacentController; mWindowDecorViewModel = windowDecorViewModel; @@ -387,8 +377,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, this /*stageListenerCallbacks*/, mSyncQueue, iconProvider, - mMainExecutor, - mBgExecutor, mWindowDecorViewModel); } else { mMainStage = new StageTaskListener( @@ -398,8 +386,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, this /*stageListenerCallbacks*/, mSyncQueue, iconProvider, - mMainExecutor, - mBgExecutor, mWindowDecorViewModel, STAGE_TYPE_MAIN); mSideStage = new StageTaskListener( mContext, @@ -408,8 +394,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, this /*stageListenerCallbacks*/, mSyncQueue, iconProvider, - mMainExecutor, - mBgExecutor, mWindowDecorViewModel, STAGE_TYPE_SIDE); } mDisplayController = displayController; @@ -431,22 +415,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } @VisibleForTesting - StageCoordinator(Context context, - int displayId, - SyncTransactionQueue syncQueue, - ShellTaskOrganizer taskOrganizer, - StageTaskListener mainStage, - StageTaskListener sideStage, - DisplayController displayController, + StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, + ShellTaskOrganizer taskOrganizer, StageTaskListener mainStage, + StageTaskListener sideStage, DisplayController displayController, DisplayImeController displayImeController, - DisplayInsetsController displayInsetsController, - SplitLayout splitLayout, - Transitions transitions, - TransactionPool transactionPool, - ShellExecutor mainExecutor, - Handler mainHandler, - ShellExecutor bgExecutor, - Optional<RecentTasksController> recentTasks, + DisplayInsetsController displayInsetsController, SplitLayout splitLayout, + Transitions transitions, TransactionPool transactionPool, ShellExecutor mainExecutor, + Handler mainHandler, Optional<RecentTasksController> recentTasks, LaunchAdjacentController launchAdjacentController, Optional<WindowDecorViewModel> windowDecorViewModel, SplitState splitState, Optional<DesktopTasksController> desktopTasksController) { @@ -466,7 +441,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mLogger = new SplitscreenEventLogger(); mMainExecutor = mainExecutor; mMainHandler = mainHandler; - mBgExecutor = bgExecutor; mRecentTasks = recentTasks; mLaunchAdjacentController = launchAdjacentController; mWindowDecorViewModel = windowDecorViewModel; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageOrderOperator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageOrderOperator.kt index 5256e7867499..a92100410d3d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageOrderOperator.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageOrderOperator.kt @@ -20,7 +20,6 @@ import android.content.Context import com.android.internal.protolog.ProtoLog import com.android.launcher3.icons.IconProvider import com.android.wm.shell.ShellTaskOrganizer -import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.protolog.ShellProtoLogGroup import com.android.wm.shell.shared.split.SplitScreenConstants @@ -53,8 +52,6 @@ class StageOrderOperator ( stageCallbacks: StageTaskListener.StageListenerCallbacks, syncQueue: SyncTransactionQueue, iconProvider: IconProvider, - mainExecutor: ShellExecutor, - bgExecutor: ShellExecutor, windowDecorViewModel: Optional<WindowDecorViewModel> ) { @@ -86,8 +83,6 @@ class StageOrderOperator ( stageCallbacks, syncQueue, iconProvider, - mainExecutor, - bgExecutor, windowDecorViewModel, stageIds[i]) ) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java index 29751986959b..bfe74122c5c2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java @@ -48,7 +48,6 @@ import com.android.internal.protolog.ProtoLog; import com.android.internal.util.ArrayUtils; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SurfaceUtils; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.split.SplitDecorManager; @@ -96,8 +95,6 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { private final StageListenerCallbacks mCallbacks; private final SyncTransactionQueue mSyncQueue; private final IconProvider mIconProvider; - private final ShellExecutor mMainExecutor; - private final ShellExecutor mBgExecutor; private final Optional<WindowDecorViewModel> mWindowDecorViewModel; /** Whether or not the root task has been created. */ @@ -114,21 +111,14 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { // TODO(b/204308910): Extracts SplitDecorManager related code to common package. private SplitDecorManager mSplitDecorManager; - StageTaskListener(Context context, - ShellTaskOrganizer taskOrganizer, - int displayId, - StageListenerCallbacks callbacks, - SyncTransactionQueue syncQueue, + StageTaskListener(Context context, ShellTaskOrganizer taskOrganizer, int displayId, + StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue, IconProvider iconProvider, - ShellExecutor mainExecutor, - ShellExecutor bgExecutor, Optional<WindowDecorViewModel> windowDecorViewModel, int id) { mContext = context; mCallbacks = callbacks; mSyncQueue = syncQueue; mIconProvider = iconProvider; - mMainExecutor = mainExecutor; - mBgExecutor = bgExecutor; mWindowDecorViewModel = windowDecorViewModel; taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this); mId = id; @@ -224,8 +214,9 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { if (mRootTaskInfo == null) { mRootLeash = leash; mRootTaskInfo = taskInfo; - mSplitDecorManager = new SplitDecorManager(mRootTaskInfo.configuration, mIconProvider, - mMainExecutor, mBgExecutor); + mSplitDecorManager = new SplitDecorManager( + mRootTaskInfo.configuration, + mIconProvider); mHasRootTask = true; mCallbacks.onRootTaskAppeared(); if (mVisible != mRootTaskInfo.isVisible) { @@ -353,6 +344,12 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { } } + void screenshotIfNeeded(SurfaceControl.Transaction t) { + if (mSplitDecorManager != null) { + mSplitDecorManager.screenshotIfNeeded(t); + } + } + void fadeOutDecor(Runnable finishedCallback) { if (mSplitDecorManager != null) { mSplitDecorManager.fadeOutDecor(finishedCallback, false /* addDelay */); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java index ea7553061137..c5e158c6b452 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java @@ -53,7 +53,6 @@ public class TvSplitScreenController extends SplitScreenController { private final SyncTransactionQueue mSyncQueue; private final Context mContext; private final ShellExecutor mMainExecutor; - private final ShellExecutor mBgExecutor; private final DisplayController mDisplayController; private final DisplayImeController mDisplayImeController; private final DisplayInsetsController mDisplayInsetsController; @@ -86,20 +85,18 @@ public class TvSplitScreenController extends SplitScreenController { SplitState splitState, ShellExecutor mainExecutor, Handler mainHandler, - ShellExecutor bgExecutor, SystemWindows systemWindows) { super(context, shellInit, shellCommandHandler, shellController, shellTaskOrganizer, syncQueue, rootTDAOrganizer, displayController, displayImeController, displayInsetsController, null, transitions, transactionPool, iconProvider, recentTasks, launchAdjacentController, Optional.empty(), Optional.empty(), null /* stageCoordinator */, multiInstanceHelper, splitState, - mainExecutor, mainHandler, bgExecutor); + mainExecutor, mainHandler); + mTaskOrganizer = shellTaskOrganizer; mSyncQueue = syncQueue; mContext = context; mMainExecutor = mainExecutor; - mMainHandler = mainHandler; - mBgExecutor = bgExecutor; mDisplayController = displayController; mDisplayImeController = displayImeController; mDisplayInsetsController = displayInsetsController; @@ -109,6 +106,8 @@ public class TvSplitScreenController extends SplitScreenController { mRecentTasksOptional = recentTasks; mLaunchAdjacentController = launchAdjacentController; mSplitState = splitState; + + mMainHandler = mainHandler; mSystemWindows = systemWindows; } @@ -121,7 +120,7 @@ public class TvSplitScreenController extends SplitScreenController { return new TvStageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, mTaskOrganizer, mDisplayController, mDisplayImeController, mDisplayInsetsController, mTransitions, mTransactionPool, - mIconProvider, mMainExecutor, mMainHandler, mBgExecutor, + mIconProvider, mMainExecutor, mMainHandler, mRecentTasksOptional, mLaunchAdjacentController, mSplitState, mSystemWindows); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java index 9d85bea421ae..e1bf12fc6082 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java @@ -51,14 +51,14 @@ public class TvStageCoordinator extends StageCoordinator DisplayInsetsController displayInsetsController, Transitions transitions, TransactionPool transactionPool, IconProvider iconProvider, ShellExecutor mainExecutor, - Handler mainHandler, ShellExecutor bgExecutor, + Handler mainHandler, Optional<RecentTasksController> recentTasks, LaunchAdjacentController launchAdjacentController, SplitState splitState, SystemWindows systemWindows) { super(context, displayId, syncQueue, taskOrganizer, displayController, displayImeController, displayInsetsController, transitions, transactionPool, iconProvider, - mainExecutor, mainHandler, bgExecutor, recentTasks, launchAdjacentController, + mainExecutor, mainHandler, recentTasks, launchAdjacentController, Optional.empty(), splitState, Optional.empty()); mTvSplitMenuController = new TvSplitMenuController(context, this, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java index bca9c3fdda39..bb9703fce2e3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java @@ -106,7 +106,6 @@ public class SplitScreenControllerTests extends ShellTestCase { @Mock RootTaskDisplayAreaOrganizer mRootTDAOrganizer; @Mock ShellExecutor mMainExecutor; @Mock Handler mMainHandler; - @Mock ShellExecutor mBgExecutor; @Mock DisplayController mDisplayController; @Mock DisplayImeController mDisplayImeController; @Mock DisplayInsetsController mDisplayInsetsController; @@ -138,8 +137,7 @@ public class SplitScreenControllerTests extends ShellTestCase { mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool, mIconProvider, Optional.of(mRecentTasks), mLaunchAdjacentController, Optional.of(mWindowDecorViewModel), Optional.of(mDesktopTasksController), - mStageCoordinator, mMultiInstanceHelper, mSplitState, mMainExecutor, mMainHandler, - mBgExecutor)); + mStageCoordinator, mMultiInstanceHelper, mSplitState, mMainExecutor, mMainHandler)); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java index ada7b4aff37d..ae0c9d6cbf7c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java @@ -79,15 +79,15 @@ public class SplitTestUtils { StageTaskListener sideStage, DisplayController displayController, DisplayImeController imeController, DisplayInsetsController insetsController, SplitLayout splitLayout, Transitions transitions, TransactionPool transactionPool, - ShellExecutor mainExecutor, Handler mainHandler, ShellExecutor bgExecutor, + ShellExecutor mainExecutor, Handler mainHandler, Optional<RecentTasksController> recentTasks, LaunchAdjacentController launchAdjacentController, Optional<WindowDecorViewModel> windowDecorViewModel, SplitState splitState, Optional<DesktopTasksController> desktopTasksController) { super(context, displayId, syncQueue, taskOrganizer, mainStage, sideStage, displayController, imeController, insetsController, splitLayout, - transitions, transactionPool, mainExecutor, mainHandler, bgExecutor, - recentTasks, launchAdjacentController, windowDecorViewModel, splitState, + transitions, transactionPool, mainExecutor, mainHandler, recentTasks, + launchAdjacentController, windowDecorViewModel, splitState, desktopTasksController); // Prepare root task for testing. diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java index ffa8b6089660..b0fdfacd7d83 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java @@ -111,7 +111,6 @@ public class SplitTransitionTests extends ShellTestCase { @Mock private WindowDecorViewModel mWindowDecorViewModel; @Mock private SplitState mSplitState; @Mock private ShellExecutor mMainExecutor; - @Mock private ShellExecutor mBgExecutor; @Mock private Handler mMainHandler; @Mock private LaunchAdjacentController mLaunchAdjacentController; @Mock private DefaultMixedHandler mMixedHandler; @@ -137,19 +136,18 @@ public class SplitTransitionTests extends ShellTestCase { mSplitLayout = SplitTestUtils.createMockSplitLayout(); mMainStage = spy(new StageTaskListener(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock( StageTaskListener.StageListenerCallbacks.class), mSyncQueue, - mIconProvider, mMainExecutor, mBgExecutor, Optional.of(mWindowDecorViewModel), - STAGE_TYPE_MAIN)); + mIconProvider, Optional.of(mWindowDecorViewModel), STAGE_TYPE_MAIN)); mMainStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface()); mSideStage = spy(new StageTaskListener(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock( StageTaskListener.StageListenerCallbacks.class), mSyncQueue, - mIconProvider, mMainExecutor, mBgExecutor, Optional.of(mWindowDecorViewModel), - STAGE_TYPE_SIDE)); + mIconProvider, Optional.of(mWindowDecorViewModel), STAGE_TYPE_SIDE)); mSideStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface()); mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController, mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions, - mTransactionPool, mMainExecutor, mMainHandler, mBgExecutor, Optional.empty(), + mTransactionPool, mMainExecutor, mMainHandler, Optional.empty(), mLaunchAdjacentController, Optional.empty(), mSplitState, Optional.empty()); + mStageCoordinator.setMixedHandler(mMixedHandler); mSplitScreenTransitions = mStageCoordinator.getSplitTransitions(); doAnswer((Answer<IBinder>) invocation -> mock(IBinder.class)) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java index 9d1df864764f..2d0ea5fdc884 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -119,8 +119,6 @@ public class StageCoordinatorTests extends ShellTestCase { private DefaultMixedHandler mDefaultMixedHandler; @Mock private SplitState mSplitState; - @Mock - private ShellExecutor mBgExecutor; private final Rect mBounds1 = new Rect(10, 20, 30, 40); private final Rect mBounds2 = new Rect(5, 10, 15, 20); @@ -143,9 +141,9 @@ public class StageCoordinatorTests extends ShellTestCase { mStageCoordinator = spy(new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController, mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool, - mMainExecutor, mMainHandler, mBgExecutor, Optional.empty(), - mLaunchAdjacentController, Optional.empty(), mSplitState, - Optional.empty())); + mMainExecutor, mMainHandler, Optional.empty(), mLaunchAdjacentController, + Optional.empty(), mSplitState, Optional.empty())); + mDividerLeash = new SurfaceControl.Builder().setName("fakeDivider").build(); when(mSplitLayout.getTopLeftBounds()).thenReturn(mBounds1); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageOrderOperatorTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageOrderOperatorTests.kt index 62b830dfd691..3b4a86a71d90 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageOrderOperatorTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageOrderOperatorTests.kt @@ -23,7 +23,6 @@ import com.android.launcher3.icons.IconProvider import com.android.wm.shell.Flags.enableFlexibleSplit import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase -import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_33_66 import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50 @@ -42,10 +41,6 @@ import java.util.Optional class StageOrderOperatorTests : ShellTestCase() { @Mock - lateinit var mMainExecutor: ShellExecutor - @Mock - lateinit var mBgExecutor: ShellExecutor - @Mock lateinit var mTaskOrganizer: ShellTaskOrganizer @Mock lateinit var mSyncQueue: SyncTransactionQueue @@ -67,8 +62,6 @@ class StageOrderOperatorTests : ShellTestCase() { stageListenerCallbacks, mSyncQueue, iconProvider, - mMainExecutor, - mBgExecutor, windowDecorViewModel, ) assert(stageOrderOperator.activeStages.size == 0) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java index effc6a7daf62..fe91440b106f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java @@ -43,7 +43,6 @@ import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; -import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.windowdecor.WindowDecorViewModel; @@ -74,10 +73,6 @@ public final class StageTaskListenerTests extends ShellTestCase { @Mock private SyncTransactionQueue mSyncQueue; @Mock - private ShellExecutor mMainExecutor; - @Mock - private ShellExecutor mBgExecutor; - @Mock private IconProvider mIconProvider; @Mock private WindowDecorViewModel mWindowDecorViewModel; @@ -100,8 +95,6 @@ public final class StageTaskListenerTests extends ShellTestCase { mCallbacks, mSyncQueue, mIconProvider, - mMainExecutor, - mBgExecutor, Optional.of(mWindowDecorViewModel), STAGE_TYPE_UNDEFINED); mRootTask = new TestRunningTaskInfoBuilder().build(); diff --git a/media/java/android/media/tv/TvInputServiceExtensionManager.java b/media/java/android/media/tv/TvInputServiceExtensionManager.java index b876bcf8cd7e..d33ac9256a21 100644 --- a/media/java/android/media/tv/TvInputServiceExtensionManager.java +++ b/media/java/android/media/tv/TvInputServiceExtensionManager.java @@ -22,7 +22,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.StringDef; -import android.annotation.SystemApi; import android.media.tv.flags.Flags; import android.os.IBinder; import android.os.RemoteException; @@ -45,7 +44,6 @@ import java.util.Set; * * @hide */ -@SystemApi @FlaggedApi(Flags.FLAG_TIF_EXTENSION_STANDARDIZATION) public final class TvInputServiceExtensionManager { private static final String TAG = "TvInputServiceExtensionManager"; @@ -65,7 +63,6 @@ public final class TvInputServiceExtensionManager { private static final String ANALOG_PACKAGE = "android.media.tv.extension.analog."; private static final String TUNE_PACKAGE = "android.media.tv.extension.tune."; - /** @hide */ @IntDef(prefix = {"REGISTER_"}, value = { REGISTER_SUCCESS, REGISTER_FAIL_NAME_NOT_STANDARDIZED, @@ -93,7 +90,6 @@ public final class TvInputServiceExtensionManager { */ public static final int REGISTER_FAIL_REMOTE_EXCEPTION = 3; - /** @hide */ @StringDef({ ISCAN_INTERFACE, ISCAN_SESSION, @@ -685,8 +681,6 @@ public final class TvInputServiceExtensionManager { /** * Function to return available extension interface names - * - * @hide */ public static @NonNull List<String> getStandardExtensionInterfaceNames() { return new ArrayList<>(sTisExtensions); @@ -711,10 +705,7 @@ public final class TvInputServiceExtensionManager { * {@link #REGISTER_FAIL_IMPLEMENTATION_NOT_STANDARDIZED} on failure due to IBinder not * implementing standardized AIDL interface * {@link #REGISTER_FAIL_REMOTE_EXCEPTION} on failure due to remote exception - * - * @hide */ - @SystemApi @RequiresPermission(android.Manifest.permission.TV_INPUT_HARDWARE) @RegisterResult public int registerExtensionIBinder(@StandardizedExtensionName @NonNull String extensionName, diff --git a/packages/CompanionDeviceManager/res/anim/progress_indeterminate_horizontal_rect1.xml b/packages/CompanionDeviceManager/res/anim/progress_indeterminate_horizontal_rect1.xml new file mode 100644 index 000000000000..a9fd55f77a9c --- /dev/null +++ b/packages/CompanionDeviceManager/res/anim/progress_indeterminate_horizontal_rect1.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 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. + --> +<set xmlns:android="http://schemas.android.com/apk/res/android" > + <objectAnimator + android:duration="2000" + android:propertyXName="translateX" + android:pathData="M -522.59998,0 c 48.89972,0 166.02656,0 301.21729,0 c 197.58128,0 420.9827,0 420.9827,0 " + android:interpolator="@interpolator/progress_indeterminate_horizontal_rect1_translatex" + android:repeatCount="infinite" /> + <objectAnimator + android:duration="2000" + android:propertyYName="scaleX" + android:pathData="M 0 0.1 L 1 0.826849212646 L 2 0.1" + android:interpolator="@interpolator/progress_indeterminate_horizontal_rect1_scalex" + android:repeatCount="infinite" /> +</set>
\ No newline at end of file diff --git a/packages/CompanionDeviceManager/res/anim/progress_indeterminate_horizontal_rect2.xml b/packages/CompanionDeviceManager/res/anim/progress_indeterminate_horizontal_rect2.xml new file mode 100644 index 000000000000..7b5b6a93473d --- /dev/null +++ b/packages/CompanionDeviceManager/res/anim/progress_indeterminate_horizontal_rect2.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 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. + --> +<set xmlns:android="http://schemas.android.com/apk/res/android" > + <objectAnimator + android:duration="2000" + android:propertyXName="translateX" + android:pathData="M -197.60001,0 c 14.28182,0 85.07782,0 135.54689,0 c 54.26191,0 90.42461,0 168.24331,0 c 144.72154,0 316.40982,0 316.40982,0 " + android:interpolator="@interpolator/progress_indeterminate_horizontal_rect2_translatex" + android:repeatCount="infinite" /> + <objectAnimator + android:duration="2000" + android:propertyYName="scaleX" + android:pathData="M 0.0,0.1 L 1.0,0.571379510698 L 2.0,0.909950256348 L 3.0,0.1" + android:interpolator="@interpolator/progress_indeterminate_horizontal_rect2_scalex" + android:repeatCount="infinite" /> +</set>
\ No newline at end of file diff --git a/packages/CompanionDeviceManager/res/drawable/indeterminate_progress_drawable.xml b/packages/CompanionDeviceManager/res/drawable/indeterminate_progress_drawable.xml new file mode 100644 index 000000000000..1029590796c1 --- /dev/null +++ b/packages/CompanionDeviceManager/res/drawable/indeterminate_progress_drawable.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 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. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + android:drawable="@drawable/indeterminate_progress_vector" > + <target + android:name="rect2_grp" + android:animation="@anim/progress_indeterminate_horizontal_rect2" /> + <target + android:name="rect1_grp" + android:animation="@anim/progress_indeterminate_horizontal_rect1" /> +</animated-vector>
\ No newline at end of file diff --git a/packages/CompanionDeviceManager/res/drawable/indeterminate_progress_vector.xml b/packages/CompanionDeviceManager/res/drawable/indeterminate_progress_vector.xml new file mode 100644 index 000000000000..4db3356176e7 --- /dev/null +++ b/packages/CompanionDeviceManager/res/drawable/indeterminate_progress_vector.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="10dp" + android:width="360dp" + android:viewportHeight="10" + android:viewportWidth="360" > + <group + android:name="progress_group" + android:translateX="180" + android:translateY="5" > + <path + android:name="background_track" + android:pathData="M -180.0,-5.0 l 360.0,0 l 0,10.0 l -360.0,0 Z" + android:fillColor="@color/progress_bg" /> + <group + android:name="rect2_grp" + android:translateX="-197.60001" + android:scaleX="0.1" > + <path + android:name="rect2" + android:pathData="M -144.0,-5.0 l 288.0,0 l 0,10.0 l -288.0,0 Z" + android:fillColor="@color/progress_fg" /> + </group> + <group + android:name="rect1_grp" + android:translateX="-522.59998" + android:scaleX="0.1" > + <path + android:name="rect1" + android:pathData="M -144.0,-5.0 l 288.0,0 l 0,10.0 l -288.0,0 Z" + android:fillColor="@color/progress_fg" /> + </group> + </group> +</vector>
\ No newline at end of file diff --git a/packages/CompanionDeviceManager/res/interpolator/progress_indeterminate_horizontal_rect1_scalex.xml b/packages/CompanionDeviceManager/res/interpolator/progress_indeterminate_horizontal_rect1_scalex.xml new file mode 100644 index 000000000000..453e0928a2b6 --- /dev/null +++ b/packages/CompanionDeviceManager/res/interpolator/progress_indeterminate_horizontal_rect1_scalex.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 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. + --> +<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android" + android:pathData="M 0 0 L 0.3665 0 C 0.47252618112021,0.062409910275 0.61541608570164,0.5 0.68325,0.5 C 0.75475061236836,0.5 0.75725829093844,0.814510098964 1.0,1.0" /> diff --git a/packages/CompanionDeviceManager/res/interpolator/progress_indeterminate_horizontal_rect1_translatex.xml b/packages/CompanionDeviceManager/res/interpolator/progress_indeterminate_horizontal_rect1_translatex.xml new file mode 100644 index 000000000000..a6da0eb77fc5 --- /dev/null +++ b/packages/CompanionDeviceManager/res/interpolator/progress_indeterminate_horizontal_rect1_translatex.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 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. + --> +<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android" + android:pathData="M 0.0,0.0 L 0.2 0 C 0.3958333333336,0.0 0.474845090492,0.206797621729 0.5916666666664,0.417082932942 C 0.7151610251224,0.639379624869 0.81625,0.974556908664 1.0,1.0 " /> diff --git a/packages/CompanionDeviceManager/res/interpolator/progress_indeterminate_horizontal_rect2_scalex.xml b/packages/CompanionDeviceManager/res/interpolator/progress_indeterminate_horizontal_rect2_scalex.xml new file mode 100644 index 000000000000..785d7abfe51d --- /dev/null +++ b/packages/CompanionDeviceManager/res/interpolator/progress_indeterminate_horizontal_rect2_scalex.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 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. + --> +<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android" + android:pathData="M 0,0 C 0.06834272400867,0.01992566661414 0.19220331656133,0.15855429260523 0.33333333333333,0.34926160892842 C 0.38410433133433,0.41477913453861 0.54945792615267,0.68136029463551 0.66666666666667,0.68279962777002 C 0.752586273196,0.68179620963216 0.737253971954,0.878896194318 1,1" /> diff --git a/packages/CompanionDeviceManager/res/interpolator/progress_indeterminate_horizontal_rect2_translatex.xml b/packages/CompanionDeviceManager/res/interpolator/progress_indeterminate_horizontal_rect2_translatex.xml new file mode 100644 index 000000000000..931dff1c3236 --- /dev/null +++ b/packages/CompanionDeviceManager/res/interpolator/progress_indeterminate_horizontal_rect2_translatex.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 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. + --> +<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android" + android:pathData="M 0.0,0.0 C 0.0375,0.0 0.128764607715,0.0895380946618 0.25,0.218553507947 C 0.322410320025,0.295610602487 0.436666666667,0.417591408114 0.483333333333,0.489826169306 C 0.69,0.80972296795 0.793333333333,0.950016125212 1.0,1.0 " /> diff --git a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml index 5805332418d0..afece5fac0fb 100644 --- a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml +++ b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml @@ -71,6 +71,13 @@ android:layout_height="wrap_content" android:visibility="gone"> + <TextView + android:id="@+id/timeout_message" + android:layout_width="match_parent" + android:layout_height="100dp" + android:visibility="gone" + style="@style/TimeoutMessage" /> + <androidx.recyclerview.widget.RecyclerView android:id="@+id/device_list" android:layout_width="match_parent" @@ -87,9 +94,9 @@ app:layout_constraintHeight_max="220dp" android:visibility="gone" /> - <View - android:id="@+id/border_top" - style="@style/DeviceListBorder" /> + <ProgressBar + android:id="@+id/progress_bar" + style="@style/HorizontalProgressBar" /> <View android:id="@+id/border_bottom" @@ -98,10 +105,6 @@ </androidx.constraintlayout.widget.ConstraintLayout> - <ProgressBar - android:id="@+id/spinner_multiple_device" - android:visibility="gone" - style="@style/Spinner" /> </RelativeLayout> @@ -135,7 +138,8 @@ android:layout_marginEnd="16dp" android:layout_marginBottom="16dp"> - <!-- Do NOT change the IDs of the buttons: they are referenced in CTS tests. --> + <!-- Do NOT change the IDs of the buttons: they are referenced in CTS tests. + Legacy name before the change that added single-device dialog.--> <LinearLayout android:id="@+id/negative_multiple_devices_layout" android:layout_width="wrap_content" @@ -159,18 +163,6 @@ </LinearLayout> - <RelativeLayout - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_weight="1" - android:importantForAccessibility="noHideDescendants"> - - <ProgressBar - android:id="@+id/spinner_single_device" - android:visibility="gone" - style="@style/Spinner" /> - </RelativeLayout>> - </LinearLayout> </ScrollView>
\ No newline at end of file diff --git a/packages/CompanionDeviceManager/res/values/colors.xml b/packages/CompanionDeviceManager/res/values/colors.xml new file mode 100644 index 000000000000..8782250f3bba --- /dev/null +++ b/packages/CompanionDeviceManager/res/values/colors.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 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. + --> +<resources> + <color name="border">@android:color/system_neutral1_200</color> + <color name="progress_bg">@android:color/system_neutral1_600</color> + <color name="progress_fg">@android:color/system_neutral1_0</color> +</resources> diff --git a/packages/CompanionDeviceManager/res/values/strings.xml b/packages/CompanionDeviceManager/res/values/strings.xml index 2a6d68d1ee35..20ede5ff91d3 100644 --- a/packages/CompanionDeviceManager/res/values/strings.xml +++ b/packages/CompanionDeviceManager/res/values/strings.xml @@ -22,6 +22,21 @@ <!-- Title of the device association confirmation dialog. --> <string name="confirmation_title">Allow the app <strong><xliff:g id="app_name" example="Android Wear">%1$s</xliff:g></strong> to access <strong><xliff:g id="device_name" example="ASUS ZenWatch 2">%2$s</xliff:g></strong>?</string> + <!-- Description of soft discovery timeout. [CHAR LIMIT= NONE] --> + <string name="message_discovery_soft_timeout">Make sure this <xliff:g id="device_type" example="phone">%1$s</xliff:g> has <xliff:g id="discovery_method" example="Bluetooth">%2$s</xliff:g> turned on, and keep your <xliff:g id="profile_name" example="watch">%3$s</xliff:g> nearby.</string> + + <!-- Description of hard discovery timeout. [CHAR LIMIT= NONE] --> + <string name="message_discovery_hard_timeout">No devices found. Please try again later.</string> + + <!-- The discovery method name for Bluetooth [CHAR LIMIT=30] --> + <string name="discovery_bluetooth">Bluetooth</string> + + <!-- The discovery method name for Wi-Fi [CHAR LIMIT=30] --> + <string name="discovery_wifi">Wi-Fi</string> + + <!-- The discovery method name for both Bluetooth and Wi-Fi [CHAR LIMIT=50] --> + <string name="discovery_mixed">Bluetooth and Wi-Fi</string> + <!-- ================= DEVICE_PROFILE_WATCH and null profile ================= --> <!-- The name of the "watch" device type [CHAR LIMIT=30] --> @@ -30,9 +45,12 @@ <!-- Title of the device selection dialog. --> <string name="chooser_title_non_profile">Choose a device to be managed by <strong><xliff:g id="app_name" example="Android Wear">%1$s</xliff:g></strong></string> - <!-- Tile of the multiple devices' dialog. --> + <!-- Title of the multiple devices' dialog. --> <string name="chooser_title">Choose a <xliff:g id="profile_name" example="watch">%1$s</xliff:g> to set up</string> + <!-- Title of the single device scan dialog. --> + <string name="single_device_title">Looking for a <xliff:g id="profile_name" example="watch">%1$s</xliff:g></string> + <!-- Description of the privileges the application will get if associated with the companion device of WATCH profile [CHAR LIMIT=NONE] --> <string name="summary_watch">This app will be allowed to sync info, like the name of someone calling, and access these permissions on your <xliff:g id="device_type" example="phone">%1$s</xliff:g></string> diff --git a/packages/CompanionDeviceManager/res/values/styles.xml b/packages/CompanionDeviceManager/res/values/styles.xml index a161a505a0ac..30813baa6e4a 100644 --- a/packages/CompanionDeviceManager/res/values/styles.xml +++ b/packages/CompanionDeviceManager/res/values/styles.xml @@ -59,6 +59,43 @@ <item name="android:textColor">?android:attr/textColorSecondary</item> </style> + <style name="HorizontalProgressBar" + parent="@android:style/Widget.Material.ProgressBar.Horizontal"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">1dp</item> + <item name="android:layout_marginStart">32dp</item> + <item name="android:layout_marginEnd">32dp</item> + <item name="android:progress">100</item> + <item name="android:indeterminate">true</item> + <item name="android:indeterminateOnly">false</item> + <item name="android:progressTint">@color/border</item> + <item name="android:indeterminateDrawable">@drawable/indeterminate_progress_drawable</item> + </style> + + <style name="DeviceListBorder"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">1dp</item> + <item name="android:layout_marginStart">32dp</item> + <item name="android:layout_marginEnd">32dp</item> + <item name="android:background">@color/border</item> + </style> + + <style name="TimeoutMessage" + parent="@android:style/TextAppearance.DeviceDefault.Medium"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:layout_marginStart">32dp</item> + <item name="android:layout_marginEnd">32dp</item> + <item name="android:paddingEnd">8dp</item> + <item name="android:paddingStart">8dp</item> + <item name="android:paddingTop">18dp</item> + <item name="android:paddingBottom">18dp</item> + <item name="android:textDirection">locale</item> + <item name="android:textSize">14sp</item> + <item name="android:lineSpacingExtra">2dp</item> + <item name="android:textColor">?android:attr/textColorSecondary</item> + </style> + <style name="VendorHelperBackButton" parent="@android:style/Widget.Material.Button.Borderless.Colored"> <item name="android:layout_width">wrap_content</item> @@ -111,22 +148,6 @@ <item name="android:textAppearance">@android:style/TextAppearance.DeviceDefault.Medium</item> </style> - <style name="DeviceListBorder"> - <item name="android:layout_width">match_parent</item> - <item name="android:layout_height">1dp</item> - <item name="android:layout_marginStart">32dp</item> - <item name="android:layout_marginEnd">32dp</item> - <item name="android:background">@android:color/system_neutral1_200</item> - </style> - - <style name="Spinner" - parent="@android:style/Widget.Material.Light.ProgressBar.Large"> - <item name="android:layout_width">56dp</item> - <item name="android:layout_height">56dp</item> - <item name="android:indeterminate">true</item> - <item name="android:layout_centerInParent">true</item> - </style> - <style name="ScrollViewStyle"> <item name="android:scrollbars">none</item> <item name="android:fillViewport">true</item> diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java index 50419f7368be..ea40e13fdcc9 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java @@ -17,14 +17,12 @@ package com.android.companiondevicemanager; import static android.companion.CompanionDeviceManager.RESULT_CANCELED; -import static android.companion.CompanionDeviceManager.RESULT_DISCOVERY_TIMEOUT; import static android.companion.CompanionDeviceManager.RESULT_INTERNAL_ERROR; import static android.companion.CompanionDeviceManager.RESULT_SECURITY_ERROR; import static android.companion.CompanionDeviceManager.RESULT_USER_REJECTED; import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; import static com.android.companiondevicemanager.CompanionDeviceDiscoveryService.DiscoveryState; -import static com.android.companiondevicemanager.CompanionDeviceDiscoveryService.DiscoveryState.FINISHED_TIMEOUT; import static com.android.companiondevicemanager.CompanionDeviceDiscoveryService.LOCK; import static com.android.companiondevicemanager.CompanionDeviceDiscoveryService.sDiscoveryStarted; import static com.android.companiondevicemanager.CompanionDeviceResources.PROFILE_ICONS; @@ -48,11 +46,13 @@ import static java.util.Objects.requireNonNull; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.StringRes; import android.annotation.SuppressLint; import android.companion.AssociatedDevice; import android.companion.AssociationInfo; import android.companion.AssociationRequest; import android.companion.CompanionDeviceManager; +import android.companion.DeviceFilter; import android.companion.Flags; import android.companion.IAssociationRequestCallback; import android.content.Intent; @@ -139,22 +139,19 @@ public class CompanionAssociationActivity extends FragmentActivity implements private TextView mVendorHeaderName; private ImageButton mVendorHeaderButton; - // Progress indicator is only shown while we are looking for the first suitable device for a - // multiple device association. - private ProgressBar mMultipleDeviceSpinner; - // Progress indicator is only shown while we are looking for the first suitable device for a - // single device association. - private ProgressBar mSingleDeviceSpinner; + // Message to be displayed when device hasn't been discovered for a certain duration + private TextView mTimeoutMessage; + + // Horizontal progress indicator is always shown as long as the scanner is searching for devices + private ProgressBar mProgressBar; // Present for self-managed association requests and "single-device" regular association // regular. private Button mButtonAllow; private Button mButtonNotAllow; - // Present for multiple devices' association requests only. - private Button mButtonNotAllowMultipleDevices; + private Button mButtonCancelScan; - // Present for top and bottom borders for permissions list and device list. - private View mBorderTop; + // Bottom border for permissions list and device list. The progress bar acts as the top border. private View mBorderBottom; private LinearLayout mAssociationConfirmationDialog; @@ -162,9 +159,9 @@ public class CompanionAssociationActivity extends FragmentActivity implements private ConstraintLayout mConstraintList; // Only present for self-managed association requests. private RelativeLayout mVendorHeader; - // A linearLayout for mButtonNotAllowMultipleDevices, user will press this layout instead + // A linearLayout for mButtonCancelScan, user will press this layout instead // of the button for accessibility. - private LinearLayout mNotAllowMultipleDevicesLayout; + private LinearLayout mCancelScanLayout; // The recycler view is only shown for multiple-device regular association request, after // at least one matching device is found. @@ -297,7 +294,6 @@ public class CompanionAssociationActivity extends FragmentActivity implements mAssociationConfirmationDialog = findViewById(R.id.association_confirmation); mVendorHeader = findViewById(R.id.vendor_header); - mBorderTop = findViewById(R.id.border_top); mBorderBottom = findViewById(R.id.border_bottom); mTitle = findViewById(R.id.title); @@ -311,43 +307,90 @@ public class CompanionAssociationActivity extends FragmentActivity implements mDeviceIcon = findViewById(R.id.device_icon); + mTimeoutMessage = findViewById(R.id.timeout_message); mDeviceListRecyclerView = findViewById(R.id.device_list); - mMultipleDeviceSpinner = findViewById(R.id.spinner_multiple_device); - mSingleDeviceSpinner = findViewById(R.id.spinner_single_device); + mProgressBar = findViewById(R.id.progress_bar); + mProgressBar.getIndeterminateDrawable().clearColorFilter(); mPermissionListRecyclerView = findViewById(R.id.permission_list); mPermissionListAdapter = new PermissionListAdapter(this); mButtonAllow = findViewById(R.id.btn_positive); mButtonNotAllow = findViewById(R.id.btn_negative); - mButtonNotAllowMultipleDevices = findViewById(R.id.btn_negative_multiple_devices); - mNotAllowMultipleDevicesLayout = findViewById(R.id.negative_multiple_devices_layout); + mButtonCancelScan = findViewById(R.id.btn_negative_multiple_devices); + mCancelScanLayout = findViewById(R.id.negative_multiple_devices_layout); mButtonAllow.setOnClickListener(this::onPositiveButtonClick); mButtonNotAllow.setOnClickListener(this::onNegativeButtonClick); - mNotAllowMultipleDevicesLayout.setOnClickListener(this::onNegativeButtonClick); + mCancelScanLayout.setOnClickListener(this::onNegativeButtonClick); mVendorHeaderButton.setOnClickListener(this::onShowHelperDialog); if (mRequest.isSelfManaged()) { initUiForSelfManagedAssociation(); - } else if (mRequest.isSingleDevice()) { - initUiForSingleDevice(); } else { - initUiForMultipleDevices(); + initUiForDeviceDiscovery(); } } private void onDiscoveryStateChanged(DiscoveryState newState) { - if (newState == FINISHED_TIMEOUT - && CompanionDeviceDiscoveryService.getScanResult().getValue().isEmpty()) { - synchronized (LOCK) { - if (sDiscoveryStarted) { - cancel(RESULT_DISCOVERY_TIMEOUT, null); + switch (newState) { + case IN_PROGRESS: { + mTimeoutMessage.setText(null); + mProgressBar.setIndeterminate(true); + break; + } + case IN_PROGRESS_EXTENDED: { + final String deviceType = getString(R.string.device_type); + final String discoveryType = getString(getDiscoveryMethod()); + final String profile = getString(PROFILE_NAMES.get(mRequest.getDeviceProfile())); + final Spanned message = getHtmlFromResources(this, + R.string.message_discovery_soft_timeout, + deviceType, discoveryType, profile); + mTimeoutMessage.setText(message); + break; + } + case FINISHED_STOPPED: { + if (CompanionDeviceDiscoveryService.getScanResult().getValue().isEmpty()) { + // If the scan times out, do NOT close the activity automatically and let the + // user manually cancel the flow. + synchronized (LOCK) { + if (sDiscoveryStarted) { + stopDiscovery(); + } + } + mTimeoutMessage.setText(getString(R.string.message_discovery_hard_timeout)); } + mProgressBar.setIndeterminate(false); + break; + } + } + } + + @StringRes + private int getDiscoveryMethod() { + // If no filter was given or at least one bluetooth filter was provided, then + // display message for Bluetooth. + // If filter is _only_ for Wi-Fi devices, then display message for Wi-Fi. + // e.g. "Make sure Bluetooth is on" vs "Make sure Wi-Fi is on" + boolean hasBluetooth = false; + boolean hasWifi = false; + for (DeviceFilter<?> filter : mRequest.getDeviceFilters()) { + if (filter.getMediumType() == DeviceFilter.MEDIUM_TYPE_BLUETOOTH + || filter.getMediumType() == DeviceFilter.MEDIUM_TYPE_BLUETOOTH_LE) { + hasBluetooth = true; + } else if (filter.getMediumType() == DeviceFilter.MEDIUM_TYPE_WIFI) { + hasWifi = true; } } + if (hasBluetooth == hasWifi) { + return R.string.discovery_mixed; + } else if (hasBluetooth) { + return R.string.discovery_bluetooth; + } else { + return R.string.discovery_wifi; + } } private void onUserSelectedDevice(@NonNull DeviceFilterPair<?> selectedDevice) { @@ -392,9 +435,7 @@ public class CompanionAssociationActivity extends FragmentActivity implements mCancelled = true; // Stop discovery service if it was used. - if (!mRequest.isSelfManaged()) { - CompanionDeviceDiscoveryService.stop(this); - } + stopDiscovery(); // First send callback to the app directly... try { @@ -408,6 +449,12 @@ public class CompanionAssociationActivity extends FragmentActivity implements setResultAndFinish(null, errorCode); } + private void stopDiscovery() { + if (!mRequest.isSelfManaged()) { + CompanionDeviceDiscoveryService.stop(this); + } + } + private void setResultAndFinish(@Nullable AssociationInfo association, int resultCode) { Slog.i(TAG, "setResultAndFinish(), association=" + (association == null ? "null" : association) @@ -479,41 +526,14 @@ public class CompanionAssociationActivity extends FragmentActivity implements mVendorHeader.setVisibility(View.VISIBLE); mProfileIcon.setVisibility(View.GONE); mDeviceListRecyclerView.setVisibility(View.GONE); - // Top and bottom borders should be gone for selfManaged dialog. - mBorderTop.setVisibility(View.GONE); + mProgressBar.setVisibility(View.GONE); mBorderBottom.setVisibility(View.GONE); } - private void initUiForSingleDevice() { - Slog.d(TAG, "initUiForSingleDevice()"); - - final String deviceProfile = mRequest.getDeviceProfile(); - - if (!SUPPORTED_PROFILES.contains(deviceProfile)) { - throw new RuntimeException("Unsupported profile " + deviceProfile); - } - - final Drawable profileIcon = getIcon(this, PROFILE_ICONS.get(deviceProfile)); - mProfileIcon.setImageDrawable(profileIcon); - - CompanionDeviceDiscoveryService.getScanResult().observe(this, deviceFilterPairs -> { - if (deviceFilterPairs.isEmpty()) { - return; - } - mSelectedDevice = requireNonNull(deviceFilterPairs.get(0)); - updateSingleDeviceUi(); - }); - - mSingleDeviceSpinner.setVisibility(View.VISIBLE); - // Hide permission list and confirmation dialog first before the - // first matched device is found. - mPermissionListRecyclerView.setVisibility(View.GONE); - mDeviceListRecyclerView.setVisibility(View.GONE); - mAssociationConfirmationDialog.setVisibility(View.GONE); - } - - private void initUiForMultipleDevices() { - Slog.d(TAG, "initUiForMultipleDevices()"); + private void initUiForDeviceDiscovery() { + Slog.d(TAG, "initUiForDeviceDiscovery() " + + "single-device=" + mRequest.isSingleDevice() + + ", profile=" + mRequest.getDeviceProfile()); final Drawable profileIcon; final Spanned title; @@ -525,41 +545,59 @@ public class CompanionAssociationActivity extends FragmentActivity implements profileIcon = getIcon(this, PROFILE_ICONS.get(deviceProfile)); - if (deviceProfile == null) { + if (mRequest.isSingleDevice()) { + title = getHtmlFromResources(this, + R.string.single_device_title, getString(PROFILE_NAMES.get(deviceProfile))); + } else if (deviceProfile == null) { title = getHtmlFromResources(this, R.string.chooser_title_non_profile, mAppLabel); - mButtonNotAllowMultipleDevices.setText(R.string.consent_no); } else { title = getHtmlFromResources(this, R.string.chooser_title, getString(PROFILE_NAMES.get(deviceProfile))); } - mDeviceAdapter = new DeviceListAdapter(this, this::onDeviceClicked); - mTitle.setText(title); mProfileIcon.setImageDrawable(profileIcon); - mDeviceListRecyclerView.setAdapter(mDeviceAdapter); - mDeviceListRecyclerView.setLayoutManager(new LinearLayoutManager(this)); - - CompanionDeviceDiscoveryService.getScanResult().observe(this, - deviceFilterPairs -> { - // Dismiss the progress bar once there's one device found for multiple devices. - if (deviceFilterPairs.size() >= 1) { - mMultipleDeviceSpinner.setVisibility(View.GONE); + if (mRequest.isSingleDevice()) { + mBorderBottom.setVisibility(View.GONE); + CompanionDeviceDiscoveryService.getScanResult().observe(this, deviceFilterPairs -> { + if (deviceFilterPairs.isEmpty()) { + return; + } + mSelectedDevice = requireNonNull(deviceFilterPairs.get(0)); + updateUiForAssociationConsent(); + }); + } else { + mDeviceAdapter = new DeviceListAdapter(this, this::onDeviceClicked); + mDeviceListRecyclerView.setAdapter(mDeviceAdapter); + mDeviceListRecyclerView.setLayoutManager(new LinearLayoutManager(this)); + + CompanionDeviceDiscoveryService.getScanResult().observe(this, deviceFilterPairs -> { + if (deviceFilterPairs.size() >= 1) { + // Dismiss the timeout message once there's at least one device found. + mTimeoutMessage.setText(null); + + // Update profile-less cancel scan button to read "Don't allow" to indicate + // that selecting a device implies user consent. + if (deviceProfile == null) { + mButtonCancelScan.setText(R.string.consent_no); } + } - mDeviceAdapter.setDevices(deviceFilterPairs); - }); + mDeviceAdapter.setDevices(deviceFilterPairs); + }); + + mDeviceListRecyclerView.setVisibility(View.VISIBLE); + } mSummary.setVisibility(View.GONE); - // "Remove" consent button: users would need to click on the list item. mButtonAllow.setVisibility(View.GONE); mButtonNotAllow.setVisibility(View.GONE); - mDeviceListRecyclerView.setVisibility(View.VISIBLE); - mButtonNotAllowMultipleDevices.setVisibility(View.VISIBLE); - mNotAllowMultipleDevicesLayout.setVisibility(View.VISIBLE); + + mTimeoutMessage.setVisibility(View.VISIBLE); + mButtonCancelScan.setVisibility(View.VISIBLE); + mCancelScanLayout.setVisibility(View.VISIBLE); mConstraintList.setVisibility(View.VISIBLE); - mMultipleDeviceSpinner.setVisibility(View.VISIBLE); } private void onDeviceClicked(int position) { @@ -586,15 +624,10 @@ public class CompanionAssociationActivity extends FragmentActivity implements } // The permission consent dialog should be displayed for the multiple device // dialog if a device profile exists. - updateSingleDeviceUi(); - mSummary.setVisibility(View.VISIBLE); - mButtonAllow.setVisibility(View.VISIBLE); - mButtonNotAllow.setVisibility(View.VISIBLE); - mDeviceListRecyclerView.setVisibility(View.GONE); - mNotAllowMultipleDevicesLayout.setVisibility(View.GONE); + updateUiForAssociationConsent(); } - private void updateSingleDeviceUi() { + private void updateUiForAssociationConsent() { // No need to show permission consent dialog if it is a isSkipPrompt(true) // AssociationRequest. See AssociationRequestsProcessor#mayAssociateWithoutPrompt. if (mRequest.isSkipPrompt()) { @@ -603,9 +636,14 @@ public class CompanionAssociationActivity extends FragmentActivity implements return; } - mSingleDeviceSpinner.setVisibility(View.GONE); mAssociationConfirmationDialog.setVisibility(View.VISIBLE); + mProgressBar.setIndeterminate(false); // Keep as border but remove animation + mBorderBottom.setVisibility(View.VISIBLE); + mTimeoutMessage.setVisibility(View.GONE); + mDeviceListRecyclerView.setVisibility(View.GONE); + mCancelScanLayout.setVisibility(View.GONE); + final String deviceProfile = mRequest.getDeviceProfile(); final int summaryResourceId = PROFILE_SUMMARIES.get(deviceProfile); final String remoteDeviceName = mSelectedDevice.getDisplayName(); @@ -624,6 +662,10 @@ public class CompanionAssociationActivity extends FragmentActivity implements mTitle.setText(title); mSummary.setText(summary); + + mSummary.setVisibility(View.VISIBLE); + mButtonAllow.setVisibility(View.VISIBLE); + mButtonNotAllow.setVisibility(View.VISIBLE); } private void onPositiveButtonClick(View v) { diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java index f586e3dedf9a..50a01b3bc7c9 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java @@ -76,7 +76,8 @@ public class CompanionDeviceDiscoveryService extends Service { private static final String TAG = "CDM_CompanionDeviceDiscoveryService"; private static final String SYS_PROP_DEBUG_TIMEOUT = "debug.cdm.discovery_timeout"; - private static final long TIMEOUT_DEFAULT = 20_000L; // 20 seconds + private static final long TIMEOUT_SOFT = 20_000L; // 20 seconds + private static final long TIMEOUT_HARD = 180_000L; // 3 minutes private static final long TIMEOUT_MIN = 1_000L; // 1 sec private static final long TIMEOUT_MAX = 60_000L; // 1 min @@ -102,7 +103,8 @@ public class CompanionDeviceDiscoveryService extends Service { private final List<DeviceFilterPair<?>> mDevicesFound = new ArrayList<>(); - private final Runnable mTimeoutRunnable = this::timeout; + private final Runnable mSoftTimeoutRunnable = this::softTimeout; + private final Runnable mHardTimeoutRunnable = this::stopDiscoveryAndFinish; private boolean mStopAfterFirstMatch; @@ -116,8 +118,8 @@ public class CompanionDeviceDiscoveryService extends Service { enum DiscoveryState { NOT_STARTED, IN_PROGRESS, + IN_PROGRESS_EXTENDED, FINISHED_STOPPED, - FINISHED_TIMEOUT } static boolean startForRequest( @@ -175,7 +177,7 @@ public class CompanionDeviceDiscoveryService extends Service { break; case ACTION_STOP_DISCOVERY: - stopDiscoveryAndFinish(/* timeout */ false); + stopDiscoveryAndFinish(); break; } return START_NOT_STICKY; @@ -223,9 +225,17 @@ public class CompanionDeviceDiscoveryService extends Service { } @MainThread - private void stopDiscoveryAndFinish(boolean timeout) { - Slog.d(TAG, "stopDiscoveryAndFinish(" + timeout + ")"); + private void softTimeout() { + // If no device is found at this point, display a message and continue discovery + if (mDevicesFound.isEmpty()) { + sStateLiveData.setValue(DiscoveryState.IN_PROGRESS_EXTENDED); + } else { + stopDiscoveryAndFinish(); + } + } + @MainThread + private void stopDiscoveryAndFinish() { synchronized (LOCK) { if (!sDiscoveryStarted) { stopSelf(); @@ -260,13 +270,10 @@ public class CompanionDeviceDiscoveryService extends Service { mBleScanner.stopScan(mBleScanCallback); } - Handler.getMain().removeCallbacks(mTimeoutRunnable); + Handler.getMain().removeCallbacks(mSoftTimeoutRunnable); + Handler.getMain().removeCallbacks(mHardTimeoutRunnable); - if (timeout) { - sStateLiveData.setValue(DiscoveryState.FINISHED_TIMEOUT); - } else { - sStateLiveData.setValue(DiscoveryState.FINISHED_STOPPED); - } + sStateLiveData.setValue(DiscoveryState.FINISHED_STOPPED); synchronized (LOCK) { sDiscoveryStarted = false; @@ -379,7 +386,7 @@ public class CompanionDeviceDiscoveryService extends Service { sScanResultsLiveData.setValue(mDevicesFound); // Stop discovery when there's one device found for singleDevice. if (mStopAfterFirstMatch) { - stopDiscoveryAndFinish(/* timeout */ false); + stopDiscoveryAndFinish(); } }); } @@ -396,20 +403,17 @@ public class CompanionDeviceDiscoveryService extends Service { } private void scheduleTimeout() { - long timeout = SystemProperties.getLong(SYS_PROP_DEBUG_TIMEOUT, -1); - if (timeout <= 0) { + long softTimeout = SystemProperties.getLong(SYS_PROP_DEBUG_TIMEOUT, -1); + if (softTimeout <= 0) { // 0 or negative values indicate that the sysprop was never set or should be ignored. - timeout = TIMEOUT_DEFAULT; + softTimeout = TIMEOUT_SOFT; } else { - timeout = min(timeout, TIMEOUT_MAX); // should be <= 1 min (TIMEOUT_MAX) - timeout = max(timeout, TIMEOUT_MIN); // should be >= 1 sec (TIMEOUT_MIN) + softTimeout = min(softTimeout, TIMEOUT_MAX); // should be <= 1 min (TIMEOUT_MAX) + softTimeout = max(softTimeout, TIMEOUT_MIN); // should be >= 1 sec (TIMEOUT_MIN) } - Handler.getMain().postDelayed(mTimeoutRunnable, timeout); - } - - private void timeout() { - stopDiscoveryAndFinish(/* timeout */ true); + Handler.getMain().postDelayed(mSoftTimeoutRunnable, softTimeout); + Handler.getMain().postDelayed(mHardTimeoutRunnable, TIMEOUT_HARD); } @Override diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java index 7d27a562f536..91fad4fb6dc2 100644 --- a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java +++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java @@ -43,6 +43,7 @@ import com.android.wm.shell.shared.TransitionUtil; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; /** * An implementation of {@link IRemoteTransition} that accepts a {@link UIComponent} as the origin @@ -52,6 +53,7 @@ import java.util.List; */ public class OriginRemoteTransition extends IRemoteTransition.Stub { private static final String TAG = "OriginRemoteTransition"; + private static final long FINISH_ANIMATION_TIMEOUT_MS = 100; private final Context mContext; private final boolean mIsEntry; @@ -248,23 +250,20 @@ public class OriginRemoteTransition extends IRemoteTransition.Stub { if (mIsEntry) { if (!closingSurfaces.isEmpty()) { - tmpTransaction - .setRelativeLayer(mOriginLeash, closingSurfaces.get(0), 1); + tmpTransaction.setRelativeLayer(mOriginLeash, closingSurfaces.get(0), 1); } else { logW("Missing closing surface is entry transition"); } if (!openingSurfaces.isEmpty()) { - tmpTransaction - .setRelativeLayer( - openingSurfaces.get(openingSurfaces.size() - 1), mOriginLeash, 1); + tmpTransaction.setRelativeLayer( + openingSurfaces.get(openingSurfaces.size() - 1), mOriginLeash, 1); } else { logW("Missing opening surface is entry transition"); } } else { if (!openingSurfaces.isEmpty()) { - tmpTransaction - .setRelativeLayer(mOriginLeash, openingSurfaces.get(0), 1); + tmpTransaction.setRelativeLayer(mOriginLeash, openingSurfaces.get(0), 1); } else { logW("Missing opening surface is exit transition"); } @@ -293,12 +292,26 @@ public class OriginRemoteTransition extends IRemoteTransition.Stub { private void finishAnimation(boolean finished) { logD("finishAnimation: finished=" + finished); + OneShotRunnable finishInternalRunnable = new OneShotRunnable(this::finishInternal); + Runnable timeoutRunnable = + () -> { + Log.w(TAG, "Timeout waiting for surface transaction!"); + finishInternalRunnable.run(); + }; + Runnable committedRunnable = + () -> { + // Remove the timeout runnable. + mHandler.removeCallbacks(timeoutRunnable); + finishInternalRunnable.run(); + }; if (mAnimator == null) { // The transition didn't start. Ensure we apply the start transaction and report // finish afterwards. mStartTransaction - .addTransactionCommittedListener(mHandler::post, this::finishInternal) + .addTransactionCommittedListener(mHandler::post, committedRunnable::run) .apply(); + // Call finishInternal() anyway after the timeout. + mHandler.postDelayed(timeoutRunnable, FINISH_ANIMATION_TIMEOUT_MS); return; } mAnimator = null; @@ -306,8 +319,10 @@ public class OriginRemoteTransition extends IRemoteTransition.Stub { mPlayer.onEnd(finished); // Detach the origin from the transition leash and report finish after it's done. mOriginTransaction - .detachFromTransitionLeash(mOrigin, mHandler::post, this::finishInternal) + .detachFromTransitionLeash(mOrigin, mHandler::post, committedRunnable) .commit(); + // Call finishInternal() anyway after the timeout. + mHandler.postDelayed(timeoutRunnable, FINISH_ANIMATION_TIMEOUT_MS); } private void finishInternal() { @@ -423,6 +438,23 @@ public class OriginRemoteTransition extends IRemoteTransition.Stub { return out; } + /** A {@link Runnable} that will only run once. */ + private static class OneShotRunnable implements Runnable { + private final AtomicBoolean mDone = new AtomicBoolean(); + private final Runnable mRunnable; + + OneShotRunnable(Runnable runnable) { + this.mRunnable = runnable; + } + + @Override + public void run() { + if (!mDone.getAndSet(true)) { + mRunnable.run(); + } + } + } + /** * An interface that represents an origin transitions. * diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java index d0404ec02306..7c219c6ca921 100644 --- a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java +++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java @@ -29,6 +29,7 @@ import android.view.Surface; import android.view.SurfaceControl; import android.view.View; import android.view.ViewGroup; +import android.view.ViewRootImpl; import android.view.ViewTreeObserver.OnDrawListener; import java.util.ArrayList; @@ -131,7 +132,6 @@ public class ViewUIComponent implements UIComponent { mView.getViewTreeObserver().removeOnDrawListener(mOnDrawListener); // Restore view visibility mView.setVisibility(mVisibleOverride ? View.VISIBLE : View.INVISIBLE); - mView.invalidate(); // Clean up surfaces. SurfaceControl.Transaction t = new SurfaceControl.Transaction(); t.reparent(sc, null) @@ -142,8 +142,16 @@ public class ViewUIComponent implements UIComponent { sc.release(); executor.execute(onDone); }); - // Apply transaction AFTER the view is drawn. - mView.getRootSurfaceControl().applyTransactionOnDraw(t); + ViewRootImpl viewRoot = mView.getViewRootImpl(); + if (viewRoot == null) { + t.apply(); + } else { + // Apply transaction AFTER the view is drawn. + viewRoot.applyTransactionOnDraw(t); + // Request layout to force redrawing the entire view tree, so that the transaction is + // guaranteed to be applied. + viewRoot.requestLayout(); + } } @Override diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt index b815c6ce0c51..92318b9820b8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt @@ -560,7 +560,6 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { updateStateToKeyguard() controller.setDozing(true) - controller.updateViewState() Truth.assertThat(keyguardStatusBarView.visibility).isEqualTo(View.INVISIBLE) } @@ -573,7 +572,6 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { updateStateToKeyguard() controller.setDozing(false) - controller.updateViewState() Truth.assertThat(keyguardStatusBarView.visibility).isEqualTo(View.VISIBLE) } @@ -633,7 +631,6 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { Truth.assertThat(keyguardStatusBarView.visibility).isEqualTo(View.VISIBLE) controller.setDozing(true) - controller.updateViewState() // setDozing(true) should typically cause the view to hide. But since the flag is on, we // should ignore these set dozing calls and stay the same visibility. diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt index e686edec8514..07d088bbb3d6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt @@ -80,7 +80,7 @@ class ModesDialogViewModelTest : SysuiTestCase() { kosmos.mockModesDialogEventLogger, ) - timeScheduleInfo = ZenModeConfig.ScheduleInfo() + timeScheduleInfo = ScheduleInfo() timeScheduleInfo.days = intArrayOf(Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY) timeScheduleInfo.startHour = 11 timeScheduleInfo.endHour = 15 @@ -126,7 +126,7 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles?.size).isEqualTo(3) + assertThat(tiles).hasSize(3) with(tiles?.elementAt(0)!!) { assertThat(this.text).isEqualTo("Disabled by other") assertThat(this.subtext).isEqualTo("Not set") @@ -176,7 +176,7 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles?.size).isEqualTo(3) + assertThat(tiles).hasSize(3) with(tiles?.elementAt(0)!!) { assertThat(this.text).isEqualTo("Active without manual") assertThat(this.subtext).isEqualTo("On") @@ -226,7 +226,7 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles?.size).isEqualTo(3) + assertThat(tiles).hasSize(3) // Check that tile is initially present with(tiles?.elementAt(0)!!) { @@ -239,7 +239,7 @@ class ModesDialogViewModelTest : SysuiTestCase() { runCurrent() } // Check that tile is still present at the same location, but turned off - assertThat(tiles?.size).isEqualTo(3) + assertThat(tiles).hasSize(3) with(tiles?.elementAt(0)!!) { assertThat(this.text).isEqualTo("Active without manual") assertThat(this.subtext).isEqualTo("Manage in settings") @@ -252,7 +252,7 @@ class ModesDialogViewModelTest : SysuiTestCase() { runCurrent() // Check that tile is now gone - assertThat(tiles2?.size).isEqualTo(2) + assertThat(tiles2).hasSize(2) assertThat(tiles2?.elementAt(0)!!.text).isEqualTo("Active with manual") assertThat(tiles2?.elementAt(1)!!.text).isEqualTo("Inactive with manual") } @@ -287,22 +287,22 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles?.size).isEqualTo(3) + assertThat(tiles).hasSize(3) repository.removeMode("A") runCurrent() - assertThat(tiles?.size).isEqualTo(2) + assertThat(tiles).hasSize(2) repository.removeMode("B") runCurrent() - assertThat(tiles?.size).isEqualTo(1) + assertThat(tiles).hasSize(1) repository.removeMode("C") runCurrent() - assertThat(tiles?.size).isEqualTo(0) + assertThat(tiles).hasSize(0) } @Test @@ -439,8 +439,11 @@ class ModesDialogViewModelTest : SysuiTestCase() { with(tiles?.elementAt(6)!!) { assertThat(this.stateDescription).isEqualTo("Off") assertThat(this.subtextDescription) - .isEqualTo(SystemZenRules.getDaysOfWeekFull(context, timeScheduleInfo) - + ", " + SystemZenRules.getTimeSummary(context, timeScheduleInfo)) + .isEqualTo( + SystemZenRules.getDaysOfWeekFull(context, timeScheduleInfo) + + ", " + + SystemZenRules.getTimeSummary(context, timeScheduleInfo) + ) } // All tiles have the same long click info @@ -464,7 +467,7 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles?.size).isEqualTo(1) + assertThat(tiles).hasSize(1) assertThat(tiles?.elementAt(0)?.enabled).isFalse() // Trigger onClick @@ -497,13 +500,13 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles?.size).isEqualTo(1) + assertThat(tiles).hasSize(1) // Click tile to toggle it off tiles?.elementAt(0)!!.onClick() runCurrent() - assertThat(tiles?.size).isEqualTo(1) + assertThat(tiles).hasSize(1) with(tiles?.elementAt(0)!!) { assertThat(this.text).isEqualTo("Active without manual") assertThat(this.subtext).isEqualTo("Manage in settings") @@ -538,7 +541,7 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles?.size).isEqualTo(1) + assertThat(tiles).hasSize(1) with(tiles?.elementAt(0)!!) { assertThat(this.text).isEqualTo("Disabled by other") assertThat(this.subtext).isEqualTo("Not set") @@ -590,7 +593,7 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles?.size).isEqualTo(2) + assertThat(tiles).hasSize(2) // Trigger onLongClick for A tiles?.first()?.onLongClick?.let { it() } @@ -641,7 +644,7 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles?.size).isEqualTo(3) + assertThat(tiles).hasSize(3) // Trigger onClick for each tile in sequence tiles?.forEach { it.onClick.invoke() } @@ -682,7 +685,7 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles?.size).isEqualTo(2) + assertThat(tiles).hasSize(2) val modeCaptor = argumentCaptor<ZenMode>() // long click manual DND and then automatic mode diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java index d95a126fe4bd..a7a432497be6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -60,7 +60,6 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionBootInteractor; import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule; import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransitionModule; -import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransitionModule; import com.android.systemui.keyguard.ui.view.AlternateBouncerWindowViewBinder; import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModelModule; import com.android.systemui.log.SessionTracker; @@ -110,6 +109,7 @@ import java.util.concurrent.Executor; DeviceEntryIconTransitionModule.class, FalsingModule.class, PrimaryBouncerTransitionModule.class, + PrimaryBouncerTransitionImplModule.class, KeyguardDataQuickAffordanceModule.class, KeyguardQuickAffordancesCombinedViewModelModule.class, KeyguardRepositoryModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/PrimaryBouncerTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/PrimaryBouncerTransitionModule.kt new file mode 100644 index 000000000000..cc070b66917b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/PrimaryBouncerTransitionModule.kt @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.dagger + +import android.content.res.Resources +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.ui.transitions.BlurConfig +import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition +import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToPrimaryBouncerTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.AodToPrimaryBouncerTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.DozingToPrimaryBouncerTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToPrimaryBouncerTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.LockscreenToPrimaryBouncerTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.OccludedToPrimaryBouncerTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToAodTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToDozingTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGlanceableHubTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToLockscreenTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToOccludedTransitionViewModel +import com.android.systemui.res.R +import com.android.systemui.window.flag.WindowBlurFlag +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoSet +import dagger.multibindings.Multibinds +import kotlinx.coroutines.ExperimentalCoroutinesApi + +/** + * Base module that defines the [PrimaryBouncerTransition] multibinding. All variants of SystemUI + * can install this module to get the default empty version of the multibinding + */ +@Module +interface PrimaryBouncerTransitionModule { + @Multibinds fun primaryBouncerTransitions(): Set<PrimaryBouncerTransition> + + companion object { + @Provides + @SysUISingleton + fun provideBlurConfig(@Main resources: Resources): BlurConfig { + val minBlurRadius = resources.getDimensionPixelSize(R.dimen.min_window_blur_radius) + val maxBlurRadius = + if (WindowBlurFlag.isEnabled) { + resources.getDimensionPixelSize(R.dimen.max_shade_window_blur_radius) + } else { + resources.getDimensionPixelSize(R.dimen.max_window_blur_radius) + } + return BlurConfig(minBlurRadius.toFloat(), maxBlurRadius.toFloat()) + } + } +} + +/** + * Module that installs all the implementations of [PrimaryBouncerTransition] from different + * keyguard states to and away from the primary bouncer. + */ +@ExperimentalCoroutinesApi +@Module +interface PrimaryBouncerTransitionImplModule { + @Binds + @IntoSet + fun fromAod(impl: AodToPrimaryBouncerTransitionViewModel): PrimaryBouncerTransition + + @Binds + @IntoSet + fun fromAlternateBouncer( + impl: AlternateBouncerToPrimaryBouncerTransitionViewModel + ): PrimaryBouncerTransition + + @Binds + @IntoSet + fun fromDozing(impl: DozingToPrimaryBouncerTransitionViewModel): PrimaryBouncerTransition + + @Binds + @IntoSet + fun fromLockscreen( + impl: LockscreenToPrimaryBouncerTransitionViewModel + ): PrimaryBouncerTransition + + @Binds + @IntoSet + fun fromGlanceableHub( + impl: GlanceableHubToPrimaryBouncerTransitionViewModel + ): PrimaryBouncerTransition + + @Binds + @IntoSet + fun fromOccluded(impl: OccludedToPrimaryBouncerTransitionViewModel): PrimaryBouncerTransition + + @Binds + @IntoSet + fun toAod(impl: PrimaryBouncerToAodTransitionViewModel): PrimaryBouncerTransition + + @Binds + @IntoSet + fun toLockscreen(impl: PrimaryBouncerToLockscreenTransitionViewModel): PrimaryBouncerTransition + + @Binds + @IntoSet + fun toDozing(impl: PrimaryBouncerToDozingTransitionViewModel): PrimaryBouncerTransition + + @Binds + @IntoSet + fun toGlanceableHub( + impl: PrimaryBouncerToGlanceableHubTransitionViewModel + ): PrimaryBouncerTransition + + @Binds + @IntoSet + fun toGone(impl: PrimaryBouncerToGoneTransitionViewModel): PrimaryBouncerTransition + + @Binds + @IntoSet + fun toOccluded(impl: PrimaryBouncerToOccludedTransitionViewModel): PrimaryBouncerTransition +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt new file mode 100644 index 000000000000..542fb9b46bef --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.transitions + +import javax.inject.Inject + +/** Config that provides the max and min blur radius for the window blurs. */ +data class BlurConfig(val minBlurRadiusPx: Float, val maxBlurRadiusPx: Float) { + // No-op config that will be used by dagger of other SysUI variants which don't blur the + // background surface. + @Inject constructor() : this(0.0f, 0.0f) +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt index 4a0817b2bdf8..e77e9dd9e9ed 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt @@ -16,37 +16,15 @@ package com.android.systemui.keyguard.ui.transitions -import android.content.res.Resources -import android.util.MathUtils -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToPrimaryBouncerTransitionViewModel -import com.android.systemui.keyguard.ui.viewmodel.AodToPrimaryBouncerTransitionViewModel -import com.android.systemui.keyguard.ui.viewmodel.DozingToPrimaryBouncerTransitionViewModel -import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToPrimaryBouncerTransitionViewModel -import com.android.systemui.keyguard.ui.viewmodel.LockscreenToPrimaryBouncerTransitionViewModel -import com.android.systemui.keyguard.ui.viewmodel.OccludedToPrimaryBouncerTransitionViewModel -import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToAodTransitionViewModel -import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToDozingTransitionViewModel -import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGlanceableHubTransitionViewModel -import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel -import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToLockscreenTransitionViewModel -import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToOccludedTransitionViewModel -import com.android.systemui.res.R -import com.android.systemui.window.flag.WindowBlurFlag -import dagger.Binds -import dagger.Module -import dagger.Provides -import dagger.multibindings.IntoSet -import javax.inject.Inject -import kotlinx.coroutines.ExperimentalCoroutinesApi +import android.util.MathUtils.lerp import kotlinx.coroutines.flow.Flow /** * Each PrimaryBouncerTransition is responsible for updating various UI states based on the nature * of the transition. * - * MUST list implementing classes in dagger module [PrimaryBouncerTransitionModule]. + * MUST list implementing classes in dagger module + * [com.android.systemui.keyguard.dagger.PrimaryBouncerTransitionImplModule]. */ interface PrimaryBouncerTransition { /** Radius of blur applied to the window's root view. */ @@ -56,93 +34,5 @@ interface PrimaryBouncerTransition { starBlurRadius: Float, endBlurRadius: Float, transitionProgress: Float, - ): Float { - return MathUtils.lerp(starBlurRadius, endBlurRadius, transitionProgress) - } -} - -/** - * Module that installs all the transitions from different keyguard states to and away from the - * primary bouncer. - */ -@ExperimentalCoroutinesApi -@Module -interface PrimaryBouncerTransitionModule { - @Binds - @IntoSet - fun fromAod(impl: AodToPrimaryBouncerTransitionViewModel): PrimaryBouncerTransition - - @Binds - @IntoSet - fun fromAlternateBouncer( - impl: AlternateBouncerToPrimaryBouncerTransitionViewModel - ): PrimaryBouncerTransition - - @Binds - @IntoSet - fun fromDozing(impl: DozingToPrimaryBouncerTransitionViewModel): PrimaryBouncerTransition - - @Binds - @IntoSet - fun fromLockscreen( - impl: LockscreenToPrimaryBouncerTransitionViewModel - ): PrimaryBouncerTransition - - @Binds - @IntoSet - fun fromGlanceableHub( - impl: GlanceableHubToPrimaryBouncerTransitionViewModel - ): PrimaryBouncerTransition - - @Binds - @IntoSet - fun fromOccluded(impl: OccludedToPrimaryBouncerTransitionViewModel): PrimaryBouncerTransition - - @Binds - @IntoSet - fun toAod(impl: PrimaryBouncerToAodTransitionViewModel): PrimaryBouncerTransition - - @Binds - @IntoSet - fun toLockscreen(impl: PrimaryBouncerToLockscreenTransitionViewModel): PrimaryBouncerTransition - - @Binds - @IntoSet - fun toDozing(impl: PrimaryBouncerToDozingTransitionViewModel): PrimaryBouncerTransition - - @Binds - @IntoSet - fun toGlanceableHub( - impl: PrimaryBouncerToGlanceableHubTransitionViewModel - ): PrimaryBouncerTransition - - @Binds - @IntoSet - fun toGone(impl: PrimaryBouncerToGoneTransitionViewModel): PrimaryBouncerTransition - - @Binds - @IntoSet - fun toOccluded(impl: PrimaryBouncerToOccludedTransitionViewModel): PrimaryBouncerTransition - - companion object { - @Provides - @SysUISingleton - fun provideBlurConfig(@Main resources: Resources): BlurConfig { - val minBlurRadius = resources.getDimensionPixelSize(R.dimen.min_window_blur_radius) - val maxBlurRadius = - if (WindowBlurFlag.isEnabled) { - resources.getDimensionPixelSize(R.dimen.max_shade_window_blur_radius) - } else { - resources.getDimensionPixelSize(R.dimen.max_window_blur_radius) - } - return BlurConfig(minBlurRadius.toFloat(), maxBlurRadius.toFloat()) - } - } -} - -/** Config that provides the max and min blur radius for the window blurs. */ -data class BlurConfig(val minBlurRadiusPx: Float, val maxBlurRadiusPx: Float) { - // No-op config that will be used by dagger of other SysUI variants which don't blur the - // background surface. - @Inject constructor() : this(0.0f, 0.0f) + ): Float = lerp(starBlurRadius, endBlurRadius, transitionProgress) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java index 2433b78fc183..6028f1727f52 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java @@ -528,6 +528,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat return; } mDozing = dozing; + updateViewState(); } /** Animate the keyguard status bar in. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt index 99b4aa4b0cf6..d69dc1e06cd6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt @@ -143,7 +143,7 @@ class MobileConnectionRepositoryImpl( object : TelephonyCallback(), TelephonyCallback.CarrierNetworkListener, - TelephonyCallback.CarrierRoamingNtnModeListener, + TelephonyCallback.CarrierRoamingNtnListener, TelephonyCallback.DataActivityListener, TelephonyCallback.DataConnectionStateListener, TelephonyCallback.DataEnabledListener, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt index 7aab47a5d64a..3a6716ab4c7e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt @@ -21,7 +21,7 @@ import android.os.OutcomeReceiver import android.telephony.TelephonyCallback import android.telephony.TelephonyManager import android.telephony.satellite.NtnSignalStrengthCallback -import android.telephony.satellite.SatelliteCommunicationAllowedStateCallback +import android.telephony.satellite.SatelliteCommunicationAccessStateCallback import android.telephony.satellite.SatelliteManager import android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_SUCCESS import android.telephony.satellite.SatelliteModemStateCallback @@ -263,9 +263,9 @@ constructor( private fun isSatelliteAvailableFlow(sm: SupportedSatelliteManager): Flow<Boolean> = conflatedCallbackFlow { - val callback = SatelliteCommunicationAllowedStateCallback { allowed -> + val callback = SatelliteCommunicationAccessStateCallback { allowed -> logBuffer.i({ bool1 = allowed }) { - "onSatelliteCommunicationAllowedStateChanged: $bool1" + "onSatelliteCommunicationAccessAllowedStateChanged: $bool1" } trySend(allowed) @@ -273,20 +273,20 @@ constructor( var registered = false try { - logBuffer.i { "registerForCommunicationAllowedStateChanged" } - sm.registerForCommunicationAllowedStateChanged( + logBuffer.i { "registerForCommunicationAccessStateChanged" } + sm.registerForCommunicationAccessStateChanged( bgDispatcher.asExecutor(), callback, ) registered = true } catch (e: Exception) { - logBuffer.e("Error calling registerForCommunicationAllowedStateChanged", e) + logBuffer.e("Error calling registerForCommunicationAccessStateChanged", e) } awaitClose { if (registered) { - logBuffer.i { "unRegisterForCommunicationAllowedStateChanged" } - sm.unregisterForCommunicationAllowedStateChanged(callback) + logBuffer.i { "unRegisterForCommunicationAccessStateChanged" } + sm.unregisterForCommunicationAccessStateChanged(callback) } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt index 728f4183ccce..3a25ecb27404 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt @@ -37,7 +37,7 @@ import android.telephony.ServiceState.STATE_OUT_OF_SERVICE import android.telephony.SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET import android.telephony.TelephonyCallback -import android.telephony.TelephonyCallback.CarrierRoamingNtnModeListener +import android.telephony.TelephonyCallback.CarrierRoamingNtnListener import android.telephony.TelephonyCallback.DataActivityListener import android.telephony.TelephonyCallback.DisplayInfoListener import android.telephony.TelephonyCallback.ServiceStateListener @@ -1311,7 +1311,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { // Starts out false assertThat(latest).isFalse() - val callback = getTelephonyCallbackForType<CarrierRoamingNtnModeListener>() + val callback = getTelephonyCallbackForType<CarrierRoamingNtnListener>() callback.onCarrierRoamingNtnModeChanged(true) assertThat(latest).isTrue() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt index 89f2d3db7f40..599729d953d4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt @@ -22,7 +22,7 @@ import android.telephony.TelephonyCallback import android.telephony.TelephonyManager import android.telephony.satellite.NtnSignalStrength import android.telephony.satellite.NtnSignalStrengthCallback -import android.telephony.satellite.SatelliteCommunicationAllowedStateCallback +import android.telephony.satellite.SatelliteCommunicationAccessStateCallback import android.telephony.satellite.SatelliteManager import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_CONNECTED import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_DATAGRAM_RETRYING @@ -193,19 +193,19 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { runCurrent() val callback = - withArgCaptor<SatelliteCommunicationAllowedStateCallback> { + withArgCaptor<SatelliteCommunicationAccessStateCallback> { verify(satelliteManager) - .registerForCommunicationAllowedStateChanged(any(), capture()) + .registerForCommunicationAccessStateChanged(any(), capture()) } // WHEN satellite manager says it's not available - callback.onSatelliteCommunicationAllowedStateChanged(false) + callback.onAccessAllowedStateChanged(false) // THEN it's not! assertThat(latest).isFalse() // WHEN satellite manager says it's changed to available - callback.onSatelliteCommunicationAllowedStateChanged(true) + callback.onAccessAllowedStateChanged(true) // THEN it is! assertThat(latest).isTrue() @@ -219,7 +219,7 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { // GIVEN SatelliteManager gon' throw exceptions when we ask to register the callback doThrow(RuntimeException("Test exception")) .`when`(satelliteManager) - .registerForCommunicationAllowedStateChanged(any(), any()) + .registerForCommunicationAccessStateChanged(any(), any()) // WHEN the latest value is requested (and thus causes an exception to be thrown) val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation) @@ -236,9 +236,9 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { runCurrent() val callback = - withArgCaptor<SatelliteCommunicationAllowedStateCallback> { + withArgCaptor<SatelliteCommunicationAccessStateCallback> { verify(satelliteManager) - .registerForCommunicationAllowedStateChanged(any(), capture()) + .registerForCommunicationAccessStateChanged(any(), capture()) } val telephonyCallback = @@ -249,7 +249,7 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { ) // GIVEN satellite is currently provisioned - callback.onSatelliteCommunicationAllowedStateChanged(true) + callback.onAccessAllowedStateChanged(true) assertThat(latest).isTrue() @@ -261,7 +261,7 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { // THEN listener is re-registered verify(satelliteManager, times(2)) - .registerForCommunicationAllowedStateChanged(any(), any()) + .registerForCommunicationAccessStateChanged(any(), any()) } @Test diff --git a/packages/SystemUI/utils/kairos/Android.bp b/packages/SystemUI/utils/kairos/Android.bp index 1442591eab99..e10de9978252 100644 --- a/packages/SystemUI/utils/kairos/Android.bp +++ b/packages/SystemUI/utils/kairos/Android.bp @@ -22,7 +22,7 @@ package { java_library { name: "kairos", host_supported: true, - kotlincflags: ["-opt-in=com.android.systemui.kairos.ExperimentalFrpApi"], + kotlincflags: ["-opt-in=com.android.systemui.kairos.ExperimentalKairosApi"], srcs: ["src/**/*.kt"], static_libs: [ "kotlin-stdlib", @@ -32,6 +32,7 @@ java_library { java_test { name: "kairos-test", + kotlincflags: ["-opt-in=com.android.systemui.kairos.ExperimentalKairosApi"], optimize: { enabled: false, }, diff --git a/packages/SystemUI/utils/kairos/README.md b/packages/SystemUI/utils/kairos/README.md index 85f622ca05f3..5174c45a8d82 100644 --- a/packages/SystemUI/utils/kairos/README.md +++ b/packages/SystemUI/utils/kairos/README.md @@ -22,22 +22,21 @@ you can view the semantics for `Kairos` [here](docs/semantics.md). ## Usage -First, stand up a new `FrpNetwork`. All reactive events and state is kept +First, stand up a new `KairosNetwork`. All reactive events and state is kept consistent within a single network. ``` kotlin val coroutineScope: CoroutineScope = ... -val frpNetwork = coroutineScope.newFrpNetwork() +val network = coroutineScope.launchKairosNetwork() ``` -You can use the `FrpNetwork` to stand-up a network of reactive events and state. -Events are modeled with `TFlow` (short for "transactional flow"), and state -`TState` (short for "transactional state"). +You can use the `KairosNetwork` to stand-up a network of reactive events and +state. Events are modeled with `Events`, and states with `State`. ``` kotlin -suspend fun activate(network: FrpNetwork) { +suspend fun activate(network: KairosNetwork) { network.activateSpec { - val input = network.mutableTFlow<Unit>() + val input = network.mutableEvents<Unit>() // Launch a long-running side-effect that emits to the network // every second. launchEffect { @@ -47,7 +46,7 @@ suspend fun activate(network: FrpNetwork) { } } // Accumulate state - val count: TState<Int> = input.fold { _, i -> i + 1 } + val count: State<Int> = input.foldState { _, i -> i + 1 } // Observe events to perform side-effects in reaction to them input.observe { println("Got event ${count.sample()} at time: ${System.currentTimeMillis()}") @@ -56,7 +55,7 @@ suspend fun activate(network: FrpNetwork) { } ``` -`FrpNetwork.activateSpec` will suspend indefinitely; cancelling the invocation +`KairosNetwork.activateSpec` will suspend indefinitely; cancelling the invocation will tear-down all effects and obervers running within the lambda. ## Resources diff --git a/packages/SystemUI/utils/kairos/docs/flow-to-kairos-cheatsheet.md b/packages/SystemUI/utils/kairos/docs/flow-to-kairos-cheatsheet.md index 9f7fd022f019..afe64377676d 100644 --- a/packages/SystemUI/utils/kairos/docs/flow-to-kairos-cheatsheet.md +++ b/packages/SystemUI/utils/kairos/docs/flow-to-kairos-cheatsheet.md @@ -2,117 +2,116 @@ ## Key differences -* Kairos evaluates all events (`TFlow` emissions + observers) in a transaction. +* Kairos evaluates all events (`Events` emissions + observers) in a transaction. -* Kairos splits `Flow` APIs into two distinct types: `TFlow` and `TState` +* Kairos splits `Flow` APIs into two distinct types: `Events` and `State` - * `TFlow` is roughly equivalent to `SharedFlow` w/ a replay cache that + * `Events` is roughly equivalent to `SharedFlow` w/ a replay cache that exists for the duration of the current Kairos transaction and shared with `SharingStarted.WhileSubscribed()` - * `TState` is roughly equivalent to `StateFlow` shared with + * `State` is roughly equivalent to `StateFlow` shared with `SharingStarted.Eagerly`, but the current value can only be queried within a Kairos transaction, and the value is only updated at the end of the transaction * Kairos further divides `Flow` APIs based on how they internally use state: - * **FrpTransactionScope:** APIs that internally query some state need to be + * **TransactionScope:** APIs that internally query some state need to be performed within an Kairos transaction * this scope is available from the other scopes, and from most lambdas passed to other Kairos APIs - * **FrpStateScope:** APIs that internally accumulate state in reaction to - events need to be performed within an FRP State scope (akin to a - `CoroutineScope`) + * **StateScope:** APIs that internally accumulate state in reaction to events + need to be performed within a State scope (akin to a `CoroutineScope`) - * this scope is a side-effect-free subset of FrpBuildScope, and so can be - used wherever you have an FrpBuildScope + * this scope is a side-effect-free subset of BuildScope, and so can be + used wherever you have an BuildScope - * **FrpBuildScope:** APIs that perform external side-effects (`Flow.collect`) - need to be performed within an FRP Build scope (akin to a `CoroutineScope`) + * **BuildScope:** APIs that perform external side-effects (`Flow.collect`) + need to be performed within a Build scope (akin to a `CoroutineScope`) - * this scope is available from `FrpNetwork.activateSpec { … }` + * this scope is available from `Network.activateSpec { … }` * All other APIs can be used anywhere ## emptyFlow() -Use `emptyTFlow` +Use `emptyEvents` ``` kotlin -// this TFlow emits nothing -val noEvents: TFlow<Int> = emptyTFlow +// this Events emits nothing +val noEvents: Events<Int> = emptyEvents ``` ## map { … } -Use `TFlow.map` / `TState.map` +Use `Events.map` / `State.map` ``` kotlin -val anInt: TState<Int> = … -val squared: TState<Int> = anInt.map { it * it } -val messages: TFlow<String> = … -val messageLengths: TFlow<Int> = messages.map { it.size } +val anInt: State<Int> = … +val squared: State<Int> = anInt.map { it * it } +val messages: Events<String> = … +val messageLengths: Events<Int> = messages.map { it.size } ``` ## filter { … } / mapNotNull { … } -### I have a TFlow +### I have an Events -Use `TFlow.filter` / `TFlow.mapNotNull` +Use `Events.filter` / `Events.mapNotNull` ``` kotlin -val messages: TFlow<String> = … -val nonEmpty: TFlow<String> = messages.filter { it.isNotEmpty() } +val messages: Events<String> = … +val nonEmpty: Events<String> = messages.filter { it.isNotEmpty() } ``` -### I have a TState +### I have a State -Convert the `TState` to `TFlow` using `TState.stateChanges`, then use -`TFlow.filter` / `TFlow.mapNotNull` +Convert the `State` to `Events` using `State.stateChanges`, then use +`Events.filter` / `Events.mapNotNull` -If you need to convert back to `TState`, use `TFlow.hold(initialValue)` on the -result. +If you need to convert back to `State`, use `Events.holdState(initialValue)` on +the result. ``` kotlin -tState.stateChanges.filter { … }.hold(initialValue) +state.stateChanges.filter { … }.holdState(initialValue) ``` -Note that `TFlow.hold` is only available within an `FrpStateScope` in order to -track the lifetime of the state accumulation. +Note that `Events.holdState` is only available within an `StateScope` in order +to track the lifetime of the state accumulation. ## combine(...) { … } -### I have TStates +### I have States -Use `combine(TStates)` +Use `combine(States)` ``` kotlin -val someInt: TState<Int> = … -val someString: TState<String> = … -val model: TState<MyModel> = combine(someInt, someString) { i, s -> MyModel(i, s) } +val someInt: State<Int> = … +val someString: State<String> = … +val model: State<MyModel> = combine(someInt, someString) { i, s -> MyModel(i, s) } ``` -### I have TFlows +### I have Events -Convert the TFlows to TStates using `TFlow.hold(initialValue)`, then use -`combine(TStates)` +Convert the Events to States using `Events.holdState(initialValue)`, then use +`combine(States)` If you want the behavior of Flow.combine where nothing is emitted until each -TFlow has emitted at least once, you can use filter: +Events has emitted at least once, you can use filter: ``` kotlin // null used as an example, can use a different sentinel if needed -combine(tFlowA.hold(null), tFlowB.hold(null)) { a, b -> +combine(eventsA.holdState(null), eventsB.holdState(null)) { a, b -> a?.let { b?.let { … } } } .filterNotNull() ``` -Note that `TFlow.hold` is only available within an `FrpStateScope` in order to -track the lifetime of the state accumulation. +Note that `Events.holdState` is only available within an `StateScope` in order +to track the lifetime of the state accumulation. #### Explanation @@ -126,7 +125,7 @@ has emitted at least once. This often bites developers. As a workaround, developers generally append `.onStart { emit(initialValue) }` to the `Flows` that don't immediately emit. -Kairos avoids this gotcha by forcing usage of `TState` for `combine`, thus +Kairos avoids this gotcha by forcing usage of `State` for `combine`, thus ensuring that there is always a current value to be combined for each input. ## collect { … } @@ -134,197 +133,197 @@ ensuring that there is always a current value to be combined for each input. Use `observe { … }` ``` kotlin -val job: Job = tFlow.observe { println("observed: $it") } +val job: Job = events.observe { println("observed: $it") } ``` -Note that `observe` is only available within an `FrpBuildScope` in order to -track the lifetime of the observer. `FrpBuildScope` can only come from a -top-level `FrpNetwork.transaction { … }`, or a sub-scope created by using a -`-Latest` operator. +Note that `observe` is only available within a `BuildScope` in order to track +the lifetime of the observer. `BuildScope` can only come from a top-level +`Network.transaction { … }`, or a sub-scope created by using a `-Latest` +operator. ## sample(flow) { … } -### I want to sample a TState +### I want to sample a State -Use `TState.sample()` to get the current value of a `TState`. This can be -invoked anywhere you have access to an `FrpTransactionScope`. +Use `State.sample()` to get the current value of a `State`. This can be +invoked anywhere you have access to an `TransactionScope`. ``` kotlin -// the lambda passed to map receives an FrpTransactionScope, so it can invoke +// the lambda passed to map receives an TransactionScope, so it can invoke // sample -tFlow.map { tState.sample() } +events.map { state.sample() } ``` #### Explanation -To keep all state-reads consistent, the current value of a TState can only be -queried within a Kairos transaction, modeled with `FrpTransactionScope`. Note -that both `FrpStateScope` and `FrpBuildScope` extend `FrpTransactionScope`. +To keep all state-reads consistent, the current value of a State can only be +queried within a Kairos transaction, modeled with `TransactionScope`. Note that +both `StateScope` and `BuildScope` extend `TransactionScope`. -### I want to sample a TFlow +### I want to sample an Events -Convert to a `TState` by using `TFlow.hold(initialValue)`, then use `sample`. +Convert to a `State` by using `Events.holdState(initialValue)`, then use `sample`. -Note that `hold` is only available within an `FrpStateScope` in order to track +Note that `holdState` is only available within an `StateScope` in order to track the lifetime of the state accumulation. ## stateIn(scope, sharingStarted, initialValue) -Use `TFlow.hold(initialValue)`. There is no need to supply a sharingStarted -argument; all states are accumulated eagerly. +Use `Events.holdState(initialValue)`. There is no need to supply a +sharingStarted argument; all states are accumulated eagerly. ``` kotlin -val ints: TFlow<Int> = … -val lastSeenInt: TState<Int> = ints.hold(initialValue = 0) +val ints: Events<Int> = … +val lastSeenInt: State<Int> = ints.holdState(initialValue = 0) ``` -Note that `hold` is only available within an `FrpStateScope` in order to track +Note that `holdState` is only available within an `StateScope` in order to track the lifetime of the state accumulation (akin to the scope parameter of -`Flow.stateIn`). `FrpStateScope` can only come from a top-level -`FrpNetwork.transaction { … }`, or a sub-scope created by using a `-Latest` -operator. Also note that `FrpBuildScope` extends `FrpStateScope`. +`Flow.stateIn`). `StateScope` can only come from a top-level +`Network.transaction { … }`, or a sub-scope created by using a `-Latest` +operator. Also note that `BuildScope` extends `StateScope`. ## distinctUntilChanged() -Use `distinctUntilChanged` like normal. This is only available for `TFlow`; -`TStates` are already `distinctUntilChanged`. +Use `distinctUntilChanged` like normal. This is only available for `Events`; +`States` are already `distinctUntilChanged`. ## merge(...) -### I have TFlows +### I have Eventss -Use `merge(TFlows) { … }`. The lambda argument is used to disambiguate multiple +Use `merge(Events) { … }`. The lambda argument is used to disambiguate multiple simultaneous emissions within the same transaction. #### Explanation -Under Kairos's rules, a `TFlow` may only emit up to once per transaction. This -means that if we are merging two or more `TFlows` that are emitting at the same -time (within the same transaction), the resulting merged `TFlow` must emit a +Under Kairos's rules, an `Events` may only emit up to once per transaction. This +means that if we are merging two or more `Events` that are emitting at the same +time (within the same transaction), the resulting merged `Events` must emit a single value. The lambda argument allows the developer to decide what to do in this case. -### I have TStates +### I have States -If `combine` doesn't satisfy your needs, you can use `TState.stateChanges` to -convert to a `TFlow`, and then `merge`. +If `combine` doesn't satisfy your needs, you can use `State.changes` to +convert to a `Events`, and then `merge`. ## conflatedCallbackFlow { … } -Use `tFlow { … }`. +Use `events { … }`. As a shortcut, if you already have a `conflatedCallbackFlow { … }`, you can -convert it to a TFlow via `Flow.toTFlow()`. +convert it to an Events via `Flow.toEvents()`. -Note that `tFlow` is only available within an `FrpBuildScope` in order to track -the lifetime of the input registration. +Note that `events` is only available within a `BuildScope` in order to track the +lifetime of the input registration. ## first() -### I have a TState +### I have a State -Use `TState.sample`. +Use `State.sample`. -### I have a TFlow +### I have an Events -Use `TFlow.nextOnly`, which works exactly like `Flow.first` but instead of -suspending it returns a `TFlow` that emits once. +Use `Events.nextOnly`, which works exactly like `Flow.first` but instead of +suspending it returns a `Events` that emits once. The naming is intentionally different because `first` implies that it is the first-ever value emitted from the `Flow` (which makes sense for cold `Flows`), whereas `nextOnly` indicates that only the next value relative to the current transaction (the one `nextOnly` is being invoked in) will be emitted. -Note that `nextOnly` is only available within an `FrpStateScope` in order to -track the lifetime of the state accumulation. +Note that `nextOnly` is only available within an `StateScope` in order to track +the lifetime of the state accumulation. ## flatMapLatest { … } If you want to use -Latest to cancel old side-effects, similar to what the Flow -Latest operators offer for coroutines, see `mapLatest`. -### I have a TState… +### I have a State… -#### …and want to switch TStates +#### …and want to switch States -Use `TState.flatMap` +Use `State.flatMap` ``` kotlin -val flattened = tState.flatMap { a -> getTState(a) } +val flattened = state.flatMap { a -> gestate(a) } ``` -#### …and want to switch TFlows +#### …and want to switch Events -Use `TState<TFlow<T>>.switch()` +Use `State<Events<T>>.switchEvents()` ``` kotlin -val tFlow = tState.map { a -> getTFlow(a) }.switch() +val events = state.map { a -> getEvents(a) }.switchEvents() ``` -### I have a TFlow… +### I have an Events… -#### …and want to switch TFlows +#### …and want to switch Events -Use `hold` to convert to a `TState<TFlow<T>>`, then use `switch` to switch to -the latest `TFlow`. +Use `holdState` to convert to a `State<Events<T>>`, then use `switchEvents` to +switch to the latest `Events`. ``` kotlin -val tFlow = tFlowOfFlows.hold(emptyTFlow).switch() +val events = eventsOfFlows.holdState(emptyEvents).switchEvents() ``` -#### …and want to switch TStates +#### …and want to switch States -Use `hold` to convert to a `TState<TState<T>>`, then use `flatMap` to switch to -the latest `TState`. +Use `holdState` to convert to a `State<State<T>>`, then use `flatMap` to switch +to the latest `State`. ``` kotlin -val tState = tFlowOfStates.hold(tStateOf(initialValue)).flatMap { it } +val state = eventsOfStates.holdState(stateOf(initialValue)).flatMap { it } ``` ## mapLatest { … } / collectLatest { … } -`FrpStateScope` and `FrpBuildScope` both provide `-Latest` operators that +`StateScope` and `BuildScope` both provide `-Latest` operators that automatically cancel old work when new values are emitted. ``` kotlin -val currentModel: TState<SomeModel> = … -val mapped: TState<...> = currentModel.mapLatestBuild { model -> +val currentModel: State<SomeModel> = … +val mapped: State<...> = currentModel.mapLatestBuild { model -> effect { "new model in the house: $model" } model.someState.observe { "someState: $it" } - val someData: TState<SomeInfo> = + val someData: State<SomeInfo> = getBroadcasts(model.uri) .map { extractInfo(it) } - .hold(initialInfo) + .holdState(initialInfo) … } ``` ## flowOf(...) -### I want a TState +### I want a State -Use `tStateOf(initialValue)`. +Use `stateOf(initialValue)`. -### I want a TFlow +### I want an Events Use `now.map { initialValue }` -Note that `now` is only available within an `FrpTransactionScope`. +Note that `now` is only available within an `TransactionScope`. #### Explanation -`TFlows` are not cold, and so there isn't a notion of "emit this value once +`Events` are not cold, and so there isn't a notion of "emit this value once there is a collector" like there is for `Flow`. The closest analog would be -`TState`, since the initial value is retained indefinitely until there is an +`State`, since the initial value is retained indefinitely until there is an observer. However, it is often useful to immediately emit a value within the -current transaction, usually when using a `flatMap` or `switch`. In these cases, -using `now` explicitly models that the emission will occur within the current -transaction. +current transaction, usually when using a `flatMap` or `switchEvents`. In these +cases, using `now` explicitly models that the emission will occur within the +current transaction. ``` kotlin -fun <T> FrpTransactionScope.tFlowOf(value: T): TFlow<T> = now.map { value } +fun <T> TransactionScope.eventsOf(value: T): Events<T> = now.map { value } ``` ## MutableStateFlow / MutableSharedFlow -Use `MutableTState(frpNetwork, initialValue)` and `MutableTFlow(frpNetwork)`. +Use `MutableState(frpNetwork, initialValue)` and `MutableEvents(frpNetwork)`. diff --git a/packages/SystemUI/utils/kairos/docs/semantics.md b/packages/SystemUI/utils/kairos/docs/semantics.md index d43bb4447061..c8e468050037 100644 --- a/packages/SystemUI/utils/kairos/docs/semantics.md +++ b/packages/SystemUI/utils/kairos/docs/semantics.md @@ -33,39 +33,39 @@ sealed class Time : Comparable<Time> { typealias Transactional<T> = (Time) -> T -typealias TFlow<T> = SortedMap<Time, T> +typealias Events<T> = SortedMap<Time, T> private fun <T> SortedMap<Time, T>.pairwise(): List<Pair<Pair<Time, T>, Pair<Time<T>>>> = // NOTE: pretend evaluation is lazy, so that error() doesn't immediately throw (toList() + Pair(Time.Infinity, error("no value"))).zipWithNext() -class TState<T> internal constructor( +class State<T> internal constructor( internal val current: Transactional<T>, - val stateChanges: TFlow<T>, + val stateChanges: Events<T>, ) -val emptyTFlow: TFlow<Nothing> = emptyMap() +val emptyEvents: Events<Nothing> = emptyMap() -fun <A, B> TFlow<A>.map(f: FrpTransactionScope.(A) -> B): TFlow<B> = - mapValues { (t, a) -> FrpTransactionScope(t).f(a) } +fun <A, B> Events<A>.map(f: TransactionScope.(A) -> B): Events<B> = + mapValues { (t, a) -> TransactionScope(t).f(a) } -fun <A> TFlow<A>.filter(f: FrpTransactionScope.(A) -> Boolean): TFlow<A> = - filter { (t, a) -> FrpTransactionScope(t).f(a) } +fun <A> Events<A>.filter(f: TransactionScope.(A) -> Boolean): Events<A> = + filter { (t, a) -> TransactionScope(t).f(a) } fun <A> merge( - first: TFlow<A>, - second: TFlow<A>, + first: Events<A>, + second: Events<A>, onCoincidence: Time.(A, A) -> A, -): TFlow<A> = +): Events<A> = first.toMutableMap().also { result -> second.forEach { (t, a) -> result.merge(t, a) { f, s -> - FrpTranscationScope(t).onCoincidence(f, a) + TransactionScope(t).onCoincidence(f, a) } } }.toSortedMap() -fun <A> TState<TFlow<A>>.switch(): TFlow<A> { +fun <A> State<Events<A>>.switchEvents(): Events<A> { val truncated = listOf(Pair(Time.BigBang, current.invoke(Time.BigBang))) + stateChanges.dropWhile { (time, _) -> time < time0 } val events = @@ -77,7 +77,7 @@ fun <A> TState<TFlow<A>>.switch(): TFlow<A> { return events.toSortedMap() } -fun <A> TState<TFlow<A>>.switchPromptly(): TFlow<A> { +fun <A> State<Events<A>>.switchEventsPromptly(): Events<A> { val truncated = listOf(Pair(Time.BigBang, current.invoke(Time.BigBang))) + stateChanges.dropWhile { (time, _) -> time < time0 } val events = @@ -89,24 +89,24 @@ fun <A> TState<TFlow<A>>.switchPromptly(): TFlow<A> { return events.toSortedMap() } -typealias GroupedTFlow<K, V> = TFlow<Map<K, V>> +typealias GroupedEvents<K, V> = Events<Map<K, V>> -fun <K, V> TFlow<Map<K, V>>.groupByKey(): GroupedTFlow<K, V> = this +fun <K, V> Events<Map<K, V>>.groupByKey(): GroupedEvents<K, V> = this -fun <K, V> GroupedTFlow<K, V>.eventsForKey(key: K): TFlow<V> = +fun <K, V> GroupedEvents<K, V>.eventsForKey(key: K): Events<V> = map { m -> m[k] }.filter { it != null }.map { it!! } -fun <A, B> TState<A>.map(f: (A) -> B): TState<B> = - TState( +fun <A, B> State<A>.map(f: (A) -> B): State<B> = + State( current = { t -> f(current.invoke(t)) }, stateChanges = stateChanges.map { f(it) }, ) -fun <A, B, C> TState<A>.combineWith( - other: TState<B>, +fun <A, B, C> State<A>.combineWith( + other: State<B>, f: (A, B) -> C, -): TState<C> = - TState( +): State<C> = + State( current = { t -> f(current.invoke(t), other.current.invoke(t)) }, stateChanges = run { val aChanges = @@ -129,7 +129,7 @@ fun <A, B, C> TState<A>.combineWith( }, ) -fun <A> TState<TState<A>>.flatten(): TState<A> { +fun <A> State<State<A>>.flatten(): State<A> { val changes = stateChanges .pairwise() @@ -144,55 +144,55 @@ fun <A> TState<TState<A>>.flatten(): TState<A> { inWindow } } - return TState( + return State( current = { t -> current.invoke(t).current.invoke(t) }, stateChanges = changes.toSortedMap(), ) } -open class FrpTranscationScope internal constructor( +open class TransactionScope internal constructor( internal val currentTime: Time, ) { - val now: TFlow<Unit> = + val now: Events<Unit> = sortedMapOf(currentTime to Unit) fun <A> Transactional<A>.sample(): A = invoke(currentTime) - fun <A> TState<A>.sample(): A = + fun <A> State<A>.sample(): A = current.sample() } -class FrpStateScope internal constructor( +class StateScope internal constructor( time: Time, internal val stopTime: Time, -): FrpTransactionScope(time) { +): TransactionScope(time) { - fun <A, B> TFlow<A>.fold( + fun <A, B> Events<A>.foldState( initialValue: B, - f: FrpTransactionScope.(B, A) -> B, - ): TState<B> { + f: TransactionScope.(B, A) -> B, + ): State<B> { val truncated = dropWhile { (t, _) -> t < currentTime } .takeWhile { (t, _) -> t <= stopTime } - val folded = + val foldStateed = truncated .scan(Pair(currentTime, initialValue)) { (_, b) (t, a) -> - Pair(t, FrpTransactionScope(t).f(a, b)) + Pair(t, TransactionScope(t).f(a, b)) } val lookup = { t1 -> - folded.lastOrNull { (t0, _) -> t0 < t1 }?.value ?: initialValue + foldStateed.lastOrNull { (t0, _) -> t0 < t1 }?.value ?: initialValue } - return TState(lookup, folded.toSortedMap()) + return State(lookup, foldStateed.toSortedMap()) } - fun <A> TFlow<A>.hold(initialValue: A): TState<A> = - fold(initialValue) { _, a -> a } + fun <A> Events<A>.holdState(initialValue: A): State<A> = + foldState(initialValue) { _, a -> a } - fun <K, V> TFlow<Map<K, Maybe<V>>>.foldMapIncrementally( + fun <K, V> Events<Map<K, Maybe<V>>>.foldStateMapIncrementally( initialValues: Map<K, V> - ): TState<Map<K, V>> = - fold(initialValues) { patch, map -> + ): State<Map<K, V>> = + foldState(initialValues) { patch, map -> val eithers = patch.map { (k, v) -> if (v is Just) Left(k to v.value) else Right(k) } @@ -203,18 +203,18 @@ class FrpStateScope internal constructor( updated } - fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally( - initialTFlows: Map<K, TFlow<V>>, - ): TFlow<Map<K, V>> = - foldMapIncrementally(initialTFlows).map { it.merge() }.switch() + fun <K : Any, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally( + initialEventss: Map<K, Events<V>>, + ): Events<Map<K, V>> = + foldStateMapIncrementally(initialEventss).map { it.merge() }.switchEvents() - fun <K, A, B> TFlow<Map<K, Maybe<A>>.mapLatestStatefulForKey( - transform: suspend FrpStateScope.(A) -> B, - ): TFlow<Map<K, Maybe<B>>> = + fun <K, A, B> Events<Map<K, Maybe<A>>.mapLatestStatefulForKey( + transform: suspend StateScope.(A) -> B, + ): Events<Map<K, Maybe<B>>> = pairwise().map { ((t0, patch), (t1, _)) -> patch.map { (k, ma) -> ma.map { a -> - FrpStateScope(t0, t1).transform(a) + StateScope(t0, t1).transform(a) } } } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt new file mode 100644 index 000000000000..b6918703b404 --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt @@ -0,0 +1,870 @@ +/* + * Copyright (C) 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 com.android.systemui.kairos + +import com.android.systemui.kairos.util.Maybe +import com.android.systemui.kairos.util.just +import com.android.systemui.kairos.util.map +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.Job +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.dropWhile +import kotlinx.coroutines.flow.scan +import kotlinx.coroutines.launch + +/** A function that modifies the KairosNetwork. */ +typealias BuildSpec<A> = BuildScope.() -> A + +/** + * Constructs a [BuildSpec]. The passed [block] will be invoked with a [BuildScope] that can be used + * to perform network-building operations, including adding new inputs and outputs to the network, + * as well as all operations available in [TransactionScope]. + */ +@ExperimentalKairosApi +@Suppress("NOTHING_TO_INLINE") +inline fun <A> buildSpec(noinline block: BuildScope.() -> A): BuildSpec<A> = block + +/** Applies the [BuildSpec] within this [BuildScope]. */ +@ExperimentalKairosApi +inline operator fun <A> BuildScope.invoke(block: BuildScope.() -> A) = run(block) + +/** Operations that add inputs and outputs to a Kairos network. */ +@ExperimentalKairosApi +interface BuildScope : StateScope { + + /** + * A [KairosNetwork] handle that is bound to this [BuildScope]. + * + * It supports all of the standard functionality by which external code can interact with this + * Kairos network, but all [activated][KairosNetwork.activateSpec] [BuildSpec]s are bound as + * children to this [BuildScope], such that when this [BuildScope] is destroyed, all children + * are also destroyed. + */ + val kairosNetwork: KairosNetwork + + /** + * Defers invoking [block] until after the current [BuildScope] code-path completes, returning a + * [DeferredValue] that can be used to reference the result. + * + * Useful for recursive definitions. + * + * @see deferredBuildScopeAction + * @see DeferredValue + */ + fun <R> deferredBuildScope(block: BuildScope.() -> R): DeferredValue<R> + + /** + * Defers invoking [block] until after the current [BuildScope] code-path completes. + * + * Useful for recursive definitions. + * + * @see deferredBuildScope + */ + fun deferredBuildScopeAction(block: BuildScope.() -> Unit) + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events]. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * Unlike [mapLatestBuild], these modifications are not undone with each subsequent emission of + * the original [Events]. + * + * **NOTE:** This API does not [observe] the original [Events], meaning that unless the returned + * (or a downstream) [Events] is observed separately, [transform] will not be invoked, and no + * internal side-effects will occur. + */ + fun <A, B> Events<A>.mapBuild(transform: BuildScope.(A) -> B): Events<B> + + /** + * Invokes [block] whenever this [Events] emits a value, allowing side-effects to be safely + * performed in reaction to the emission. + * + * Specifically, [block] is deferred to the end of the transaction, and is only actually + * executed if this [BuildScope] is still active by that time. It can be deactivated due to a + * -Latest combinator, for example. + * + * Shorthand for: + * ```kotlin + * events.observe { effect { ... } } + * ``` + */ + fun <A> Events<A>.observe( + coroutineContext: CoroutineContext = EmptyCoroutineContext, + block: EffectScope.(A) -> Unit = {}, + ): DisposableHandle + + /** + * Returns an [Events] containing the results of applying each [BuildSpec] emitted from the + * original [Events], and a [DeferredValue] containing the result of applying [initialSpecs] + * immediately. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] with the + * same key are undone (any registered [observers][observe] are unregistered, and any pending + * [side-effects][effect] are cancelled). + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [BuildSpec] will be undone with no replacement. + */ + fun <K, A, B> Events<Map<K, Maybe<BuildSpec<A>>>>.applyLatestSpecForKey( + initialSpecs: DeferredValue<Map<K, BuildSpec<B>>>, + numKeys: Int? = null, + ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> + + /** + * Creates an instance of an [Events] with elements that are from [builder]. + * + * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the + * provided [MutableState]. + * + * By default, [builder] is only running while the returned [Events] is being + * [observed][observe]. If you want it to run at all times, simply add a no-op observer: + * ```kotlin + * events { ... }.apply { observe() } + * ``` + */ + fun <T> events( + name: String? = null, + builder: suspend EventProducerScope<T>.() -> Unit, + ): Events<T> + + /** + * Creates an instance of an [Events] with elements that are emitted from [builder]. + * + * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the + * provided [MutableState]. + * + * By default, [builder] is only running while the returned [Events] is being + * [observed][observe]. If you want it to run at all times, simply add a no-op observer: + * ```kotlin + * events { ... }.apply { observe() } + * ``` + * + * In the event of backpressure, emissions are *coalesced* into batches. When a value is + * [emitted][CoalescingEventProducerScope.emit] from [builder], it is merged into the batch via + * [coalesce]. Once the batch is consumed by the kairos network in the next transaction, the + * batch is reset back to [getInitialValue]. + */ + fun <In, Out> coalescingEvents( + getInitialValue: () -> Out, + coalesce: (old: Out, new: In) -> Out, + builder: suspend CoalescingEventProducerScope<In>.() -> Unit, + ): Events<Out> + + /** + * Creates a new [BuildScope] that is a child of this one. + * + * This new scope can be manually cancelled via the returned [Job], or will be cancelled + * automatically when its parent is cancelled. Cancellation will unregister all + * [observers][observe] and cancel all scheduled [effects][effect]. + * + * The return value from [block] can be accessed via the returned [DeferredValue]. + */ + fun <A> asyncScope(block: BuildSpec<A>): Pair<DeferredValue<A>, Job> + + // TODO: once we have context params, these can all become extensions: + + /** + * Returns an [Events] containing the results of applying the given [transform] function to each + * value of the original [Events]. + * + * Unlike [Events.map], [transform] can perform arbitrary asynchronous code. This code is run + * outside of the current Kairos transaction; when [transform] returns, the returned value is + * emitted from the result [Events] in a new transaction. + * + * Shorthand for: + * ```kotlin + * events.mapLatestBuild { a -> asyncEvent { transform(a) } }.flatten() + * ``` + */ + fun <A, B> Events<A>.mapAsyncLatest(transform: suspend (A) -> B): Events<B> = + mapLatestBuild { a -> asyncEvent { transform(a) } }.flatten() + + /** + * Invokes [block] whenever this [Events] emits a value. [block] receives an [BuildScope] that + * can be used to make further modifications to the Kairos network, and/or perform side-effects + * via [effect]. + * + * @see observe + */ + fun <A> Events<A>.observeBuild(block: BuildScope.(A) -> Unit = {}): DisposableHandle = + mapBuild(block).observe() + + /** + * Returns a [StateFlow] whose [value][StateFlow.value] tracks the current + * [value of this State][State.sample], and will emit at the same rate as [State.changes]. + * + * Note that the [value][StateFlow.value] is not available until the *end* of the current + * transaction. If you need the current value before this time, then use [State.sample]. + */ + fun <A> State<A>.toStateFlow(): StateFlow<A> { + val uninitialized = Any() + var initialValue: Any? = uninitialized + val innerStateFlow = MutableStateFlow<Any?>(uninitialized) + deferredBuildScope { + initialValue = sample() + changes.observe { + innerStateFlow.value = it + initialValue = null + } + } + + @Suppress("UNCHECKED_CAST") + fun getValue(innerValue: Any?): A = + when { + innerValue !== uninitialized -> innerValue as A + initialValue !== uninitialized -> initialValue as A + else -> + error( + "Attempted to access StateFlow.value before Kairos transaction has completed." + ) + } + + return object : StateFlow<A> { + override val replayCache: List<A> + get() = innerStateFlow.replayCache.map(::getValue) + + override val value: A + get() = getValue(innerStateFlow.value) + + override suspend fun collect(collector: FlowCollector<A>): Nothing { + innerStateFlow.collect { collector.emit(getValue(it)) } + } + } + } + + /** + * Returns a [SharedFlow] configured with a replay cache of size [replay] that emits the current + * [value][State.sample] of this [State] followed by all [changes]. + */ + fun <A> State<A>.toSharedFlow(replay: Int = 0): SharedFlow<A> { + val result = MutableSharedFlow<A>(replay, extraBufferCapacity = 1) + deferredBuildScope { + result.tryEmit(sample()) + changes.observe { a -> result.tryEmit(a) } + } + return result + } + + /** + * Returns a [SharedFlow] configured with a replay cache of size [replay] that emits values + * whenever this [Events] emits. + */ + fun <A> Events<A>.toSharedFlow(replay: Int = 0): SharedFlow<A> { + val result = MutableSharedFlow<A>(replay, extraBufferCapacity = 1) + observe { a -> result.tryEmit(a) } + return result + } + + /** + * Returns a [State] that holds onto the value returned by applying the most recently emitted + * [BuildSpec] from the original [Events], or the value returned by applying [initialSpec] if + * nothing has been emitted since it was constructed. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] are undone + * (any registered [observers][observe] are unregistered, and any pending [side-effects][effect] + * are cancelled). + */ + fun <A> Events<BuildSpec<A>>.holdLatestSpec(initialSpec: BuildSpec<A>): State<A> { + val (changes: Events<A>, initApplied: DeferredValue<A>) = applyLatestSpec(initialSpec) + return changes.holdStateDeferred(initApplied) + } + + /** + * Returns a [State] containing the value returned by applying the [BuildSpec] held by the + * original [State]. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] are undone + * (any registered [observers][observe] are unregistered, and any pending [side-effects][effect] + * are cancelled). + */ + fun <A> State<BuildSpec<A>>.applyLatestSpec(): State<A> { + val (appliedChanges: Events<A>, init: DeferredValue<A>) = + changes.applyLatestSpec(buildSpec { sample().applySpec() }) + return appliedChanges.holdStateDeferred(init) + } + + /** + * Returns an [Events] containing the results of applying each [BuildSpec] emitted from the + * original [Events]. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] are undone + * (any registered [observers][observe] are unregistered, and any pending [side-effects][effect] + * are cancelled). + */ + fun <A> Events<BuildSpec<A>>.applyLatestSpec(): Events<A> = applyLatestSpec(buildSpec {}).first + + /** + * Returns an [Events] that switches to a new [Events] produced by [transform] every time the + * original [Events] emits a value. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * When the original [Events] emits a new value, those changes are undone (any registered + * [observers][observe] are unregistered, and any pending [effects][effect] are cancelled). + */ + fun <A, B> Events<A>.flatMapLatestBuild(transform: BuildScope.(A) -> Events<B>): Events<B> = + mapCheap { buildSpec { transform(it) } }.applyLatestSpec().flatten() + + /** + * Returns a [State] by applying [transform] to the value held by the original [State]. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * When the value held by the original [State] changes, those changes are undone (any registered + * [observers][observe] are unregistered, and any pending [effects][effect] are cancelled). + */ + fun <A, B> State<A>.flatMapLatestBuild(transform: BuildScope.(A) -> State<B>): State<B> = + mapLatestBuild { transform(it) }.flatten() + + /** + * Returns a [State] that transforms the value held inside this [State] by applying it to the + * [transform]. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * When the value held by the original [State] changes, those changes are undone (any registered + * [observers][observe] are unregistered, and any pending [effects][effect] are cancelled). + */ + fun <A, B> State<A>.mapLatestBuild(transform: BuildScope.(A) -> B): State<B> = + mapCheapUnsafe { buildSpec { transform(it) } }.applyLatestSpec() + + /** + * Returns an [Events] containing the results of applying each [BuildSpec] emitted from the + * original [Events], and a [DeferredValue] containing the result of applying [initialSpec] + * immediately. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] are undone + * (any registered [observers][observe] are unregistered, and any pending [side-effects][effect] + * are cancelled). + */ + fun <A : Any?, B> Events<BuildSpec<B>>.applyLatestSpec( + initialSpec: BuildSpec<A> + ): Pair<Events<B>, DeferredValue<A>> { + val (events, result) = + mapCheap { spec -> mapOf(Unit to just(spec)) } + .applyLatestSpecForKey(initialSpecs = mapOf(Unit to initialSpec), numKeys = 1) + val outEvents: Events<B> = + events.mapMaybe { + checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" } + } + val outInit: DeferredValue<A> = deferredBuildScope { + val initResult: Map<Unit, A> = result.get() + check(Unit in initResult) { + "applyLatest: expected initial result, but none present in: $initResult" + } + @Suppress("UNCHECKED_CAST") + initResult.getOrDefault(Unit) { null } as A + } + return Pair(outEvents, outInit) + } + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events]. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * With each invocation of [transform], changes from the previous invocation are undone (any + * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are + * cancelled). + */ + fun <A, B> Events<A>.mapLatestBuild(transform: BuildScope.(A) -> B): Events<B> = + mapCheap { buildSpec { transform(it) } }.applyLatestSpec() + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events], and a [DeferredValue] containing the result of applying [transform] to + * [initialValue] immediately. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * With each invocation of [transform], changes from the previous invocation are undone (any + * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are + * cancelled). + */ + fun <A, B> Events<A>.mapLatestBuild( + initialValue: A, + transform: BuildScope.(A) -> B, + ): Pair<Events<B>, DeferredValue<B>> = + mapLatestBuildDeferred(deferredOf(initialValue), transform) + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events], and a [DeferredValue] containing the result of applying [transform] to + * [initialValue] immediately. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * With each invocation of [transform], changes from the previous invocation are undone (any + * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are + * cancelled). + */ + fun <A, B> Events<A>.mapLatestBuildDeferred( + initialValue: DeferredValue<A>, + transform: BuildScope.(A) -> B, + ): Pair<Events<B>, DeferredValue<B>> = + mapCheap { buildSpec { transform(it) } } + .applyLatestSpec(initialSpec = buildSpec { transform(initialValue.get()) }) + + /** + * Returns an [Events] containing the results of applying each [BuildSpec] emitted from the + * original [Events], and a [DeferredValue] containing the result of applying [initialSpecs] + * immediately. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] with the + * same key are undone (any registered [observers][observe] are unregistered, and any pending + * [side-effects][effect] are cancelled). + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [BuildSpec] will be undone with no replacement. + */ + fun <K, A, B> Events<Map<K, Maybe<BuildSpec<A>>>>.applyLatestSpecForKey( + initialSpecs: Map<K, BuildSpec<B>>, + numKeys: Int? = null, + ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> = + applyLatestSpecForKey(deferredOf(initialSpecs), numKeys) + + fun <K, V> Incremental<K, BuildSpec<V>>.applyLatestSpecForKey( + numKeys: Int? = null + ): Incremental<K, V> { + val (events, initial) = updates.applyLatestSpecForKey(sampleDeferred(), numKeys) + return events.foldStateMapIncrementally(initial) + } + + /** + * Returns an [Events] containing the results of applying each [BuildSpec] emitted from the + * original [Events]. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] with the + * same key are undone (any registered [observers][observe] are unregistered, and any pending + * [side-effects][effect] are cancelled). + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [BuildSpec] will be undone with no replacement. + */ + fun <K, V> Events<Map<K, Maybe<BuildSpec<V>>>>.applyLatestSpecForKey( + numKeys: Int? = null + ): Events<Map<K, Maybe<V>>> = + applyLatestSpecForKey<K, V, Nothing>(deferredOf(emptyMap()), numKeys).first + + /** + * Returns a [State] containing the latest results of applying each [BuildSpec] emitted from the + * original [Events]. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] with the + * same key are undone (any registered [observers][observe] are unregistered, and any pending + * [side-effects][effect] are cancelled). + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [BuildSpec] will be undone with no replacement. + */ + fun <K, V> Events<Map<K, Maybe<BuildSpec<V>>>>.holdLatestSpecForKey( + initialSpecs: DeferredValue<Map<K, BuildSpec<V>>>, + numKeys: Int? = null, + ): Incremental<K, V> { + val (changes, initialValues) = applyLatestSpecForKey(initialSpecs, numKeys) + return changes.foldStateMapIncrementally(initialValues) + } + + /** + * Returns a [State] containing the latest results of applying each [BuildSpec] emitted from the + * original [Events]. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] with the + * same key are undone (any registered [observers][observe] are unregistered, and any pending + * [side-effects][effect] are cancelled). + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [BuildSpec] will be undone with no replacement. + */ + fun <K, V> Events<Map<K, Maybe<BuildSpec<V>>>>.holdLatestSpecForKey( + initialSpecs: Map<K, BuildSpec<V>> = emptyMap(), + numKeys: Int? = null, + ): Incremental<K, V> = holdLatestSpecForKey(deferredOf(initialSpecs), numKeys) + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events], and a [DeferredValue] containing the result of applying [transform] to + * [initialValues] immediately. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * With each invocation of [transform], changes from the previous invocation are undone (any + * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are + * cancelled). + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [BuildScope] will be undone with no replacement. + */ + fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestBuildForKey( + initialValues: DeferredValue<Map<K, A>>, + numKeys: Int? = null, + transform: BuildScope.(K, A) -> B, + ): Pair<Events<Map<K, Maybe<B>>>, DeferredValue<Map<K, B>>> = + map { patch -> patch.mapValues { (k, v) -> v.map { buildSpec { transform(k, it) } } } } + .applyLatestSpecForKey( + deferredBuildScope { + initialValues.get().mapValues { (k, v) -> buildSpec { transform(k, v) } } + }, + numKeys = numKeys, + ) + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events], and a [DeferredValue] containing the result of applying [transform] to + * [initialValues] immediately. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * With each invocation of [transform], changes from the previous invocation are undone (any + * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are + * cancelled). + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [BuildScope] will be undone with no replacement. + */ + fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestBuildForKey( + initialValues: Map<K, A>, + numKeys: Int? = null, + transform: BuildScope.(K, A) -> B, + ): Pair<Events<Map<K, Maybe<B>>>, DeferredValue<Map<K, B>>> = + mapLatestBuildForKey(deferredOf(initialValues), numKeys, transform) + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events]. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * With each invocation of [transform], changes from the previous invocation are undone (any + * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are + * cancelled). + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [BuildScope] will be undone with no replacement. + */ + fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestBuildForKey( + numKeys: Int? = null, + transform: BuildScope.(K, A) -> B, + ): Events<Map<K, Maybe<B>>> = mapLatestBuildForKey(emptyMap(), numKeys, transform).first + + /** Returns a [Deferred] containing the next value to be emitted from this [Events]. */ + fun <R> Events<R>.nextDeferred(): Deferred<R> { + lateinit var next: CompletableDeferred<R> + val job = launchScope { nextOnly().observe { next.complete(it) } } + next = CompletableDeferred<R>(parent = job) + return next + } + + /** Returns a [State] that reflects the [StateFlow.value] of this [StateFlow]. */ + fun <A> StateFlow<A>.toState(): State<A> { + val initial = value + return events { dropWhile { it == initial }.collect { emit(it) } }.holdState(initial) + } + + /** Returns an [Events] that emits whenever this [Flow] emits. */ + fun <A> Flow<A>.toEvents(name: String? = null): Events<A> = + events(name) { collect { emit(it) } } + + /** + * Shorthand for: + * ```kotlin + * flow.toEvents().holdState(initialValue) + * ``` + */ + fun <A> Flow<A>.toState(initialValue: A): State<A> = toEvents().holdState(initialValue) + + /** + * Shorthand for: + * ```kotlin + * flow.scan(initialValue, operation).toEvents().holdState(initialValue) + * ``` + */ + fun <A, B> Flow<A>.scanToState(initialValue: B, operation: (B, A) -> B): State<B> = + scan(initialValue, operation).toEvents().holdState(initialValue) + + /** + * Shorthand for: + * ```kotlin + * flow.scan(initialValue) { a, f -> f(a) }.toEvents().holdState(initialValue) + * ``` + */ + fun <A> Flow<(A) -> A>.scanToState(initialValue: A): State<A> = + scanToState(initialValue) { a, f -> f(a) } + + /** + * Invokes [block] whenever this [Events] emits a value. [block] receives an [BuildScope] that + * can be used to make further modifications to the Kairos network, and/or perform side-effects + * via [effect]. + * + * With each invocation of [block], changes from the previous invocation are undone (any + * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are + * cancelled). + */ + fun <A> Events<A>.observeLatestBuild(block: BuildScope.(A) -> Unit = {}): DisposableHandle = + mapLatestBuild { block(it) }.observe() + + /** + * Invokes [block] whenever this [Events] emits a value, allowing side-effects to be safely + * performed in reaction to the emission. + * + * With each invocation of [block], running effects from the previous invocation are cancelled. + */ + fun <A> Events<A>.observeLatest(block: EffectScope.(A) -> Unit = {}): DisposableHandle { + var innerJob: Job? = null + return observeBuild { + innerJob?.cancel() + innerJob = effect { block(it) } + } + } + + /** + * Invokes [block] with the value held by this [State], allowing side-effects to be safely + * performed in reaction to the state changing. + * + * With each invocation of [block], running effects from the previous invocation are cancelled. + */ + fun <A> State<A>.observeLatest(block: EffectScope.(A) -> Unit = {}): Job = launchScope { + var innerJob = effect { block(sample()) } + changes.observeBuild { + innerJob.cancel() + innerJob = effect { block(it) } + } + } + + /** + * Applies [block] to the value held by this [State]. [block] receives an [BuildScope] that can + * be used to make further modifications to the Kairos network, and/or perform side-effects via + * [effect]. + * + * [block] can perform modifications to the Kairos network via its [BuildScope] receiver. With + * each invocation of [block], changes from the previous invocation are undone (any registered + * [observers][observe] are unregistered, and any pending [side-effects][effect] are cancelled). + */ + fun <A> State<A>.observeLatestBuild(block: BuildScope.(A) -> Unit = {}): Job = launchScope { + var innerJob: Job = launchScope { block(sample()) } + changes.observeBuild { + innerJob.cancel() + innerJob = launchScope { block(it) } + } + } + + /** Applies the [BuildSpec] within this [BuildScope]. */ + fun <A> BuildSpec<A>.applySpec(): A = this() + + /** + * Applies the [BuildSpec] within this [BuildScope], returning the result as an [DeferredValue]. + */ + fun <A> BuildSpec<A>.applySpecDeferred(): DeferredValue<A> = deferredBuildScope { applySpec() } + + /** + * Invokes [block] on the value held in this [State]. [block] receives an [BuildScope] that can + * be used to make further modifications to the Kairos network, and/or perform side-effects via + * [effect]. + */ + fun <A> State<A>.observeBuild(block: BuildScope.(A) -> Unit = {}): Job = launchScope { + block(sample()) + changes.observeBuild(block) + } + + /** + * Invokes [block] with the current value of this [State], re-invoking whenever it changes, + * allowing side-effects to be safely performed in reaction value changing. + * + * Specifically, [block] is deferred to the end of the transaction, and is only actually + * executed if this [BuildScope] is still active by that time. It can be deactivated due to a + * -Latest combinator, for example. + * + * If the [State] is changing within the *current* transaction (i.e. [changes] is presently + * emitting) then [block] will be invoked for the first time with the new value; otherwise, it + * will be invoked with the [current][sample] value. + */ + fun <A> State<A>.observe(block: EffectScope.(A) -> Unit = {}): DisposableHandle = + now.map { sample() }.mergeWith(changes) { _, new -> new }.observe { block(it) } +} + +/** + * Returns an [Events] that emits the result of [block] once it completes. [block] is evaluated + * outside of the current Kairos transaction; when it completes, the returned [Events] emits in a + * new transaction. + * + * Shorthand for: + * ``` + * events { emitter: MutableEvents<A> -> + * val a = block() + * emitter.emit(a) + * } + * ``` + */ +@ExperimentalKairosApi +fun <A> BuildScope.asyncEvent(block: suspend () -> A): Events<A> = + events { + // TODO: if block completes synchronously, it would be nice to emit within this + // transaction + emit(block()) + } + .apply { observe() } + +/** + * Performs a side-effect in a safe manner w/r/t the current Kairos transaction. + * + * Specifically, [block] is deferred to the end of the current transaction, and is only actually + * executed if this [BuildScope] is still active by that time. It can be deactivated due to a + * -Latest combinator, for example. + * + * Shorthand for: + * ```kotlin + * launchScope { now.observe { block() } } + * ``` + */ +@ExperimentalKairosApi +fun BuildScope.effect( + context: CoroutineContext = EmptyCoroutineContext, + block: EffectScope.() -> Unit, +): Job = launchScope { now.observe(context) { block() } } + +/** + * Launches [block] in a new coroutine, returning a [Job] bound to the coroutine. + * + * This coroutine is not actually started until the *end* of the current Kairos transaction. This is + * done because the current [BuildScope] might be deactivated within this transaction, perhaps due + * to a -Latest combinator. If this happens, then the coroutine will never actually be started. + * + * Shorthand for: + * ```kotlin + * effect { effectCoroutineScope.launch { block() } } + * ``` + */ +@ExperimentalKairosApi +fun BuildScope.launchEffect(block: suspend CoroutineScope.() -> Unit): Job = asyncEffect(block) + +/** + * Launches [block] in a new coroutine, returning the result as a [Deferred]. + * + * This coroutine is not actually started until the *end* of the current Kairos transaction. This is + * done because the current [BuildScope] might be deactivated within this transaction, perhaps due + * to a -Latest combinator. If this happens, then the coroutine will never actually be started. + * + * Shorthand for: + * ```kotlin + * CompletableDeferred<R>.apply { + * effect { effectCoroutineScope.launch { complete(coroutineScope { block() }) } } + * } + * .await() + * ``` + */ +@ExperimentalKairosApi +fun <R> BuildScope.asyncEffect(block: suspend CoroutineScope.() -> R): Deferred<R> { + val result = CompletableDeferred<R>() + val job = effect { effectCoroutineScope.launch { result.complete(coroutineScope(block)) } } + val handle = job.invokeOnCompletion { result.cancel() } + result.invokeOnCompletion { + handle.dispose() + job.cancel() + } + return result +} + +/** Like [BuildScope.asyncScope], but ignores the result of [block]. */ +@ExperimentalKairosApi +fun BuildScope.launchScope(block: BuildSpec<*>): Job = asyncScope(block).second + +/** + * Creates an instance of an [Events] with elements that are emitted from [builder]. + * + * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the provided + * [MutableState]. + * + * By default, [builder] is only running while the returned [Events] is being + * [observed][BuildScope.observe]. If you want it to run at all times, simply add a no-op observer: + * ```kotlin + * events { ... }.apply { observe() } + * ``` + * + * In the event of backpressure, emissions are *coalesced* into batches. When a value is + * [emitted][CoalescingEventProducerScope.emit] from [builder], it is merged into the batch via + * [coalesce]. Once the batch is consumed by the Kairos network in the next transaction, the batch + * is reset back to [initialValue]. + */ +@ExperimentalKairosApi +fun <In, Out> BuildScope.coalescingEvents( + initialValue: Out, + coalesce: (old: Out, new: In) -> Out, + builder: suspend CoalescingEventProducerScope<In>.() -> Unit, +): Events<Out> = coalescingEvents(getInitialValue = { initialValue }, coalesce, builder) + +/** + * Creates an instance of an [Events] with elements that are emitted from [builder]. + * + * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the provided + * [MutableState]. + * + * By default, [builder] is only running while the returned [Events] is being + * [observed][BuildScope.observe]. If you want it to run at all times, simply add a no-op observer: + * ```kotlin + * events { ... }.apply { observe() } + * ``` + * + * In the event of backpressure, emissions are *conflated*; any older emissions are dropped and only + * the most recent emission will be used when the Kairos network is ready. + */ +@ExperimentalKairosApi +fun <T> BuildScope.conflatedEvents( + builder: suspend CoalescingEventProducerScope<T>.() -> Unit +): Events<T> = + coalescingEvents<T, Any?>(initialValue = Any(), coalesce = { _, new -> new }, builder = builder) + .mapCheap { + @Suppress("UNCHECKED_CAST") + it as T + } + +/** Scope for emitting to a [BuildScope.coalescingEvents]. */ +interface CoalescingEventProducerScope<in T> { + /** + * Inserts [value] into the current batch, enqueueing it for emission from this [Events] if not + * already pending. + * + * Backpressure occurs when [emit] is called while the Kairos network is currently in a + * transaction; if called multiple times, then emissions will be coalesced into a single batch + * that is then processed when the network is ready. + */ + fun emit(value: T) +} + +/** Scope for emitting to a [BuildScope.events]. */ +interface EventProducerScope<in T> { + /** + * Emits a [value] to this [Events], suspending the caller until the Kairos transaction + * containing the emission has completed. + */ + suspend fun emit(value: T) +} + +/** + * Suspends forever. Upon cancellation, runs [block]. Useful for unregistering callbacks inside of + * [BuildScope.events] and [BuildScope.coalescingEvents]. + */ +suspend fun awaitClose(block: () -> Unit): Nothing = + try { + awaitCancellation() + } finally { + block() + } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt index ae9b8c85910f..c20864648f00 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt @@ -25,42 +25,42 @@ import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.conflate /** - * Returns a [TFlow] that emits the value sampled from the [Transactional] produced by each emission - * of the original [TFlow], within the same transaction of the original emission. + * Returns an [Events] that emits the value sampled from the [Transactional] produced by each + * emission of the original [Events], within the same transaction of the original emission. */ -@ExperimentalFrpApi -fun <A> TFlow<Transactional<A>>.sampleTransactionals(): TFlow<A> = map { it.sample() } +@ExperimentalKairosApi +fun <A> Events<Transactional<A>>.sampleTransactionals(): Events<A> = map { it.sample() } -/** @see FrpTransactionScope.sample */ -@ExperimentalFrpApi -fun <A, B, C> TFlow<A>.sample( - state: TState<B>, - transform: suspend FrpTransactionScope.(A, B) -> C, -): TFlow<C> = map { transform(it, state.sample()) } +/** @see TransactionScope.sample */ +@ExperimentalKairosApi +fun <A, B, C> Events<A>.sample( + state: State<B>, + transform: TransactionScope.(A, B) -> C, +): Events<C> = map { transform(it, state.sample()) } -/** @see FrpTransactionScope.sample */ -@ExperimentalFrpApi -fun <A, B, C> TFlow<A>.sample( - transactional: Transactional<B>, - transform: suspend FrpTransactionScope.(A, B) -> C, -): TFlow<C> = map { transform(it, transactional.sample()) } +/** @see TransactionScope.sample */ +@ExperimentalKairosApi +fun <A, B, C> Events<A>.sample( + sampleable: Transactional<B>, + transform: TransactionScope.(A, B) -> C, +): Events<C> = map { transform(it, sampleable.sample()) } /** - * Like [sample], but if [state] is changing at the time it is sampled ([stateChanges] is emitting), - * then the new value is passed to [transform]. + * Like [sample], but if [state] is changing at the time it is sampled ([changes] is emitting), then + * the new value is passed to [transform]. * * Note that [sample] is both more performant, and safer to use with recursive definitions. You will * generally want to use it rather than this. * * @see sample */ -@ExperimentalFrpApi -fun <A, B, C> TFlow<A>.samplePromptly( - state: TState<B>, - transform: suspend FrpTransactionScope.(A, B) -> C, -): TFlow<C> = - sample(state) { a, b -> These.thiz<Pair<A, B>, B>(a to b) } - .mergeWith(state.stateChanges.map { These.that(it) }) { thiz, that -> +@ExperimentalKairosApi +fun <A, B, C> Events<A>.samplePromptly( + state: State<B>, + transform: TransactionScope.(A, B) -> C, +): Events<C> = + sample(state) { a, b -> These.thiz(a to b) } + .mergeWith(state.changes.map { These.that(it) }) { thiz, that -> These.both((thiz as These.This).thiz, (that as These.That).that) } .mapMaybe { these -> @@ -75,199 +75,204 @@ fun <A, B, C> TFlow<A>.samplePromptly( } /** - * Returns a cold [Flow] that, when collected, emits from this [TFlow]. [network] is needed to - * transactionally connect to / disconnect from the [TFlow] when collection starts/stops. + * Returns a cold [Flow] that, when collected, emits from this [Events]. [network] is needed to + * transactionally connect to / disconnect from the [Events] when collection starts/stops. */ -@ExperimentalFrpApi -fun <A> TFlow<A>.toColdConflatedFlow(network: FrpNetwork): Flow<A> = +@ExperimentalKairosApi +fun <A> Events<A>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate() /** - * Returns a cold [Flow] that, when collected, emits from this [TState]. [network] is needed to - * transactionally connect to / disconnect from the [TState] when collection starts/stops. + * Returns a cold [Flow] that, when collected, emits from this [State]. [network] is needed to + * transactionally connect to / disconnect from the [State] when collection starts/stops. */ -@ExperimentalFrpApi -fun <A> TState<A>.toColdConflatedFlow(network: FrpNetwork): Flow<A> = +@ExperimentalKairosApi +fun <A> State<A>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate() /** - * Returns a cold [Flow] that, when collected, applies this [FrpSpec] in a new transaction in this - * [network], and then emits from the returned [TFlow]. + * Returns a cold [Flow] that, when collected, applies this [BuildSpec] in a new transaction in this + * [network], and then emits from the returned [Events]. * - * When collection is cancelled, so is the [FrpSpec]. This means all ongoing work is cleaned up. + * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up. */ -@ExperimentalFrpApi -@JvmName("flowSpecToColdConflatedFlow") -fun <A> FrpSpec<TFlow<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> = +@ExperimentalKairosApi +@JvmName("eventsSpecToColdConflatedFlow") +fun <A> BuildSpec<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = channelFlow { network.activateSpec { applySpec().observe { trySend(it) } } }.conflate() /** - * Returns a cold [Flow] that, when collected, applies this [FrpSpec] in a new transaction in this - * [network], and then emits from the returned [TState]. + * Returns a cold [Flow] that, when collected, applies this [BuildSpec] in a new transaction in this + * [network], and then emits from the returned [State]. * - * When collection is cancelled, so is the [FrpSpec]. This means all ongoing work is cleaned up. + * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up. */ -@ExperimentalFrpApi +@ExperimentalKairosApi @JvmName("stateSpecToColdConflatedFlow") -fun <A> FrpSpec<TState<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> = +fun <A> BuildSpec<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = channelFlow { network.activateSpec { applySpec().observe { trySend(it) } } }.conflate() /** * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in - * this [network], and then emits from the returned [TFlow]. + * this [network], and then emits from the returned [Events]. */ -@ExperimentalFrpApi +@ExperimentalKairosApi @JvmName("transactionalFlowToColdConflatedFlow") -fun <A> Transactional<TFlow<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> = +fun <A> Transactional<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = channelFlow { network.activateSpec { sample().observe { trySend(it) } } }.conflate() /** * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in - * this [network], and then emits from the returned [TState]. + * this [network], and then emits from the returned [State]. */ -@ExperimentalFrpApi +@ExperimentalKairosApi @JvmName("transactionalStateToColdConflatedFlow") -fun <A> Transactional<TState<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> = +fun <A> Transactional<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = channelFlow { network.activateSpec { sample().observe { trySend(it) } } }.conflate() /** - * Returns a cold [Flow] that, when collected, applies this [FrpStateful] in a new transaction in - * this [network], and then emits from the returned [TFlow]. + * Returns a cold [Flow] that, when collected, applies this [Stateful] in a new transaction in this + * [network], and then emits from the returned [Events]. * - * When collection is cancelled, so is the [FrpStateful]. This means all ongoing work is cleaned up. + * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up. */ -@ExperimentalFrpApi +@ExperimentalKairosApi @JvmName("statefulFlowToColdConflatedFlow") -fun <A> FrpStateful<TFlow<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> = +fun <A> Stateful<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate() /** * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in - * this [network], and then emits from the returned [TState]. + * this [network], and then emits from the returned [State]. * - * When collection is cancelled, so is the [FrpStateful]. This means all ongoing work is cleaned up. + * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up. */ -@ExperimentalFrpApi +@ExperimentalKairosApi @JvmName("statefulStateToColdConflatedFlow") -fun <A> FrpStateful<TState<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> = +fun <A> Stateful<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate() -/** Return a [TFlow] that emits from the original [TFlow] only when [state] is `true`. */ -@ExperimentalFrpApi -fun <A> TFlow<A>.filter(state: TState<Boolean>): TFlow<A> = filter { state.sample() } +/** Return an [Events] that emits from the original [Events] only when [state] is `true`. */ +@ExperimentalKairosApi +fun <A> Events<A>.filter(state: State<Boolean>): Events<A> = filter { state.sample() } private fun Iterable<Boolean>.allTrue() = all { it } private fun Iterable<Boolean>.anyTrue() = any { it } -/** Returns a [TState] that is `true` only when all of [states] are `true`. */ -@ExperimentalFrpApi -fun allOf(vararg states: TState<Boolean>): TState<Boolean> = combine(*states) { it.allTrue() } +/** Returns a [State] that is `true` only when all of [states] are `true`. */ +@ExperimentalKairosApi +fun allOf(vararg states: State<Boolean>): State<Boolean> = combine(*states) { it.allTrue() } -/** Returns a [TState] that is `true` when any of [states] are `true`. */ -@ExperimentalFrpApi -fun anyOf(vararg states: TState<Boolean>): TState<Boolean> = combine(*states) { it.anyTrue() } +/** Returns a [State] that is `true` when any of [states] are `true`. */ +@ExperimentalKairosApi +fun anyOf(vararg states: State<Boolean>): State<Boolean> = combine(*states) { it.anyTrue() } -/** Returns a [TState] containing the inverse of the Boolean held by the original [TState]. */ -@ExperimentalFrpApi fun not(state: TState<Boolean>): TState<Boolean> = state.mapCheapUnsafe { !it } +/** Returns a [State] containing the inverse of the Boolean held by the original [State]. */ +@ExperimentalKairosApi fun not(state: State<Boolean>): State<Boolean> = state.mapCheapUnsafe { !it } /** - * Represents a modal FRP sub-network. + * Represents a modal Kairos sub-network. * - * When [enabled][enableMode], all network modifications are applied immediately to the FRP network. - * When the returned [TFlow] emits a [FrpBuildMode], that mode is enabled and replaces this mode, - * undoing all modifications in the process (any registered [observers][FrpBuildScope.observe] are - * unregistered, and any pending [side-effects][FrpBuildScope.effect] are cancelled). + * When [enabled][enableMode], all network modifications are applied immediately to the Kairos + * network. When the returned [Events] emits a [BuildMode], that mode is enabled and replaces this + * mode, undoing all modifications in the process (any registered [observers][BuildScope.observe] + * are unregistered, and any pending [side-effects][BuildScope.effect] are cancelled). * - * Use [compiledFrpSpec] to compile and stand-up a mode graph. + * Use [compiledBuildSpec] to compile and stand-up a mode graph. * - * @see FrpStatefulMode + * @see StatefulMode */ -@ExperimentalFrpApi -fun interface FrpBuildMode<out A> { +@ExperimentalKairosApi +fun interface BuildMode<out A> { /** - * Invoked when this mode is enabled. Returns a value and a [TFlow] that signals a switch to a + * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a * new mode. */ - suspend fun FrpBuildScope.enableMode(): Pair<A, TFlow<FrpBuildMode<A>>> + fun BuildScope.enableMode(): Pair<A, Events<BuildMode<A>>> } /** - * Returns an [FrpSpec] that, when [applied][FrpBuildScope.applySpec], stands up a modal-transition - * graph starting with this [FrpBuildMode], automatically switching to new modes as they are - * produced. + * Returns an [BuildSpec] that, when [applied][BuildScope.applySpec], stands up a modal-transition + * graph starting with this [BuildMode], automatically switching to new modes as they are produced. * - * @see FrpBuildMode + * @see BuildMode */ -@ExperimentalFrpApi -val <A> FrpBuildMode<A>.compiledFrpSpec: FrpSpec<TState<A>> - get() = frpSpec { - var modeChangeEvents by TFlowLoop<FrpBuildMode<A>>() - val activeMode: TState<Pair<A, TFlow<FrpBuildMode<A>>>> = +@ExperimentalKairosApi +val <A> BuildMode<A>.compiledBuildSpec: BuildSpec<State<A>> + get() = buildSpec { + var modeChangeEvents by EventsLoop<BuildMode<A>>() + val activeMode: State<Pair<A, Events<BuildMode<A>>>> = modeChangeEvents - .map { it.run { frpSpec { enableMode() } } } - .holdLatestSpec(frpSpec { enableMode() }) + .map { it.run { buildSpec { enableMode() } } } + .holdLatestSpec(buildSpec { enableMode() }) modeChangeEvents = - activeMode.map { statefully { it.second.nextOnly() } }.applyLatestStateful().switch() + activeMode + .map { statefully { it.second.nextOnly() } } + .applyLatestStateful() + .switchEvents() activeMode.map { it.first } } /** - * Represents a modal FRP sub-network. + * Represents a modal Kairos sub-network. * * When [enabled][enableMode], all state accumulation is immediately started. When the returned - * [TFlow] emits a [FrpBuildMode], that mode is enabled and replaces this mode, stopping all state + * [Events] emits a [BuildMode], that mode is enabled and replaces this mode, stopping all state * accumulation in the process. * * Use [compiledStateful] to compile and stand-up a mode graph. * - * @see FrpBuildMode + * @see BuildMode */ -@ExperimentalFrpApi -fun interface FrpStatefulMode<out A> { +@ExperimentalKairosApi +fun interface StatefulMode<out A> { /** - * Invoked when this mode is enabled. Returns a value and a [TFlow] that signals a switch to a + * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a * new mode. */ - suspend fun FrpStateScope.enableMode(): Pair<A, TFlow<FrpStatefulMode<A>>> + fun StateScope.enableMode(): Pair<A, Events<StatefulMode<A>>> } /** - * Returns an [FrpStateful] that, when [applied][FrpStateScope.applyStateful], stands up a - * modal-transition graph starting with this [FrpStatefulMode], automatically switching to new modes - * as they are produced. + * Returns an [Stateful] that, when [applied][StateScope.applyStateful], stands up a + * modal-transition graph starting with this [StatefulMode], automatically switching to new modes as + * they are produced. * - * @see FrpBuildMode + * @see BuildMode */ -@ExperimentalFrpApi -val <A> FrpStatefulMode<A>.compiledStateful: FrpStateful<TState<A>> +@ExperimentalKairosApi +val <A> StatefulMode<A>.compiledStateful: Stateful<State<A>> get() = statefully { - var modeChangeEvents by TFlowLoop<FrpStatefulMode<A>>() - val activeMode: TState<Pair<A, TFlow<FrpStatefulMode<A>>>> = + var modeChangeEvents by EventsLoop<StatefulMode<A>>() + val activeMode: State<Pair<A, Events<StatefulMode<A>>>> = modeChangeEvents .map { it.run { statefully { enableMode() } } } .holdLatestStateful(statefully { enableMode() }) modeChangeEvents = - activeMode.map { statefully { it.second.nextOnly() } }.applyLatestStateful().switch() + activeMode + .map { statefully { it.second.nextOnly() } } + .applyLatestStateful() + .switchEvents() activeMode.map { it.first } } /** - * Runs [spec] in this [FrpBuildScope], and then re-runs it whenever [rebuildSignal] emits. Returns - * a [TState] that holds the result of the currently-active [FrpSpec]. + * Runs [spec] in this [BuildScope], and then re-runs it whenever [rebuildSignal] emits. Returns a + * [State] that holds the result of the currently-active [BuildSpec]. */ -@ExperimentalFrpApi -fun <A> FrpBuildScope.rebuildOn(rebuildSignal: TFlow<*>, spec: FrpSpec<A>): TState<A> = +@ExperimentalKairosApi +fun <A> BuildScope.rebuildOn(rebuildSignal: Events<*>, spec: BuildSpec<A>): State<A> = rebuildSignal.map { spec }.holdLatestSpec(spec) /** - * Like [stateChanges] but also includes the old value of this [TState]. + * Like [changes] but also includes the old value of this [State]. * * Shorthand for: * ``` kotlin * stateChanges.map { WithPrev(previousValue = sample(), newValue = it) } * ``` */ -@ExperimentalFrpApi -val <A> TState<A>.transitions: TFlow<WithPrev<A, A>> - get() = stateChanges.map { WithPrev(previousValue = sample(), newValue = it) } +@ExperimentalKairosApi +val <A> State<A>.transitions: Events<WithPrev<A, A>> + get() = changes.map { WithPrev(previousValue = sample(), newValue = it) } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt new file mode 100644 index 000000000000..7e257f2831af --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 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 com.android.systemui.kairos + +import kotlinx.coroutines.CoroutineScope + +/** + * Scope for external side-effects triggered by the Kairos network. This still occurs within the + * context of a transaction, so general suspending calls are disallowed to prevent blocking the + * transaction. You can use [effectCoroutineScope] to [launch][kotlinx.coroutines.launch] new + * coroutines to perform long-running asynchronous work. This scope is alive for the duration of the + * containing [BuildScope] that this side-effect scope is running in. + */ +@ExperimentalKairosApi +interface EffectScope : TransactionScope { + /** + * A [CoroutineScope] whose lifecycle lives for as long as this [EffectScope] is alive. This is + * generally until the [Job][kotlinx.coroutines.Job] returned by [BuildScope.effect] is + * cancelled. + */ + @ExperimentalKairosApi val effectCoroutineScope: CoroutineScope + + /** + * A [KairosNetwork] instance that can be used to transactionally query / modify the Kairos + * network. + * + * The lambda passed to [KairosNetwork.transact] on this instance will receive an [BuildScope] + * that is lifetime-bound to this [EffectScope]. Once this [EffectScope] is no longer alive, any + * modifications to the Kairos network performed via this [KairosNetwork] instance will be + * undone (any registered [observers][BuildScope.observe] are unregistered, and any pending + * [side-effects][BuildScope.effect] are cancelled). + */ + @ExperimentalKairosApi val kairosNetwork: KairosNetwork +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt new file mode 100644 index 000000000000..e7d0096f2189 --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt @@ -0,0 +1,575 @@ +/* + * Copyright (C) 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 com.android.systemui.kairos + +import com.android.systemui.kairos.internal.CompletableLazy +import com.android.systemui.kairos.internal.DemuxImpl +import com.android.systemui.kairos.internal.EventsImpl +import com.android.systemui.kairos.internal.Init +import com.android.systemui.kairos.internal.InitScope +import com.android.systemui.kairos.internal.InputNode +import com.android.systemui.kairos.internal.Network +import com.android.systemui.kairos.internal.NoScope +import com.android.systemui.kairos.internal.activated +import com.android.systemui.kairos.internal.cached +import com.android.systemui.kairos.internal.constInit +import com.android.systemui.kairos.internal.demuxMap +import com.android.systemui.kairos.internal.filterImpl +import com.android.systemui.kairos.internal.filterJustImpl +import com.android.systemui.kairos.internal.init +import com.android.systemui.kairos.internal.mapImpl +import com.android.systemui.kairos.internal.mergeNodes +import com.android.systemui.kairos.internal.mergeNodesLeft +import com.android.systemui.kairos.internal.neverImpl +import com.android.systemui.kairos.internal.switchDeferredImplSingle +import com.android.systemui.kairos.internal.switchPromptImplSingle +import com.android.systemui.kairos.internal.util.hashString +import com.android.systemui.kairos.util.Either +import com.android.systemui.kairos.util.Either.Left +import com.android.systemui.kairos.util.Either.Right +import com.android.systemui.kairos.util.Maybe +import com.android.systemui.kairos.util.just +import com.android.systemui.kairos.util.toMaybe +import java.util.concurrent.atomic.AtomicReference +import kotlin.reflect.KProperty +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope + +/** A series of values of type [A] available at discrete points in time. */ +@ExperimentalKairosApi +sealed class Events<out A> { + companion object { + /** An [Events] with no values. */ + val empty: Events<Nothing> = EmptyEvents + } +} + +/** An [Events] with no values. */ +@ExperimentalKairosApi val emptyEvents: Events<Nothing> = Events.empty + +/** + * A forward-reference to an [Events]. Useful for recursive definitions. + * + * This reference can be used like a standard [Events], but will throw an error if its [loopback] is + * unset before the end of the first transaction which accesses it. + */ +@ExperimentalKairosApi +class EventsLoop<A> : Events<A>() { + private val deferred = CompletableLazy<Events<A>>() + + internal val init: Init<EventsImpl<A>> = + init(name = null) { deferred.value.init.connect(evalScope = this) } + + /** The [Events] this reference is referring to. */ + var loopback: Events<A>? = null + set(value) { + value?.let { + check(!deferred.isInitialized()) { "EventsLoop.loopback has already been set." } + deferred.setValue(value) + field = value + } + } + + operator fun getValue(thisRef: Any?, property: KProperty<*>): Events<A> = this + + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Events<A>) { + loopback = value + } + + override fun toString(): String = "${this::class.simpleName}@$hashString" +} + +/** + * Returns an [Events] that acts as a deferred-reference to the [Events] produced by this [Lazy]. + * + * When the returned [Events] is accessed by the Kairos network, the [Lazy]'s [value][Lazy.value] + * will be queried and used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi fun <A> Lazy<Events<A>>.defer(): Events<A> = deferInline { value } + +/** + * Returns an [Events] that acts as a deferred-reference to the [Events] produced by this + * [DeferredValue]. + * + * When the returned [Events] is accessed by the Kairos network, the [DeferredValue] will be queried + * and used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi +fun <A> DeferredValue<Events<A>>.defer(): Events<A> = deferInline { unwrapped.value } + +/** + * Returns an [Events] that acts as a deferred-reference to the [Events] produced by [block]. + * + * When the returned [Events] is accessed by the Kairos network, [block] will be invoked and the + * returned [Events] will be used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi +fun <A> deferredEvents(block: KairosScope.() -> Events<A>): Events<A> = deferInline { + NoScope.block() +} + +/** Returns an [Events] that emits the new value of this [State] when it changes. */ +@ExperimentalKairosApi +val <A> State<A>.changes: Events<A> + get() = EventsInit(init(name = null) { init.connect(evalScope = this).changes }) + +/** + * Returns an [Events] that contains only the [just] results of applying [transform] to each value + * of the original [Events]. + * + * @see mapNotNull + */ +@ExperimentalKairosApi +fun <A, B> Events<A>.mapMaybe(transform: TransactionScope.(A) -> Maybe<B>): Events<B> = + map(transform).filterJust() + +/** + * Returns an [Events] that contains only the non-null results of applying [transform] to each value + * of the original [Events]. + * + * @see mapMaybe + */ +@ExperimentalKairosApi +fun <A, B> Events<A>.mapNotNull(transform: TransactionScope.(A) -> B?): Events<B> = mapMaybe { + transform(it).toMaybe() +} + +/** Returns an [Events] containing only values of the original [Events] that are not null. */ +@ExperimentalKairosApi +fun <A> Events<A?>.filterNotNull(): Events<A> = mapCheap { it.toMaybe() }.filterJust() + +/** Shorthand for `mapNotNull { it as? A }`. */ +@ExperimentalKairosApi +inline fun <reified A> Events<*>.filterIsInstance(): Events<A> = + mapCheap { it as? A }.filterNotNull() + +/** Shorthand for `mapMaybe { it }`. */ +@ExperimentalKairosApi +fun <A> Events<Maybe<A>>.filterJust(): Events<A> = + EventsInit(constInit(name = null, filterJustImpl { init.connect(evalScope = this) })) + +/** + * Returns an [Events] containing the results of applying [transform] to each value of the original + * [Events]. + */ +@ExperimentalKairosApi +fun <A, B> Events<A>.map(transform: TransactionScope.(A) -> B): Events<B> { + val mapped: EventsImpl<B> = mapImpl({ init.connect(evalScope = this) }) { a, _ -> transform(a) } + return EventsInit(constInit(name = null, mapped.cached())) +} + +/** + * Like [map], but the emission is not cached during the transaction. Use only if [transform] is + * fast and pure. + * + * @see map + */ +@ExperimentalKairosApi +fun <A, B> Events<A>.mapCheap(transform: TransactionScope.(A) -> B): Events<B> = + EventsInit( + constInit(name = null, mapImpl({ init.connect(evalScope = this) }) { a, _ -> transform(a) }) + ) + +/** + * Returns an [Events] that invokes [action] before each value of the original [Events] is emitted. + * Useful for logging and debugging. + * + * ``` + * pulse.onEach { foo(it) } == pulse.map { foo(it); it } + * ``` + * + * Note that the side effects performed in [onEach] are only performed while the resulting [Events] + * is connected to an output of the Kairos network. If your goal is to reliably perform side effects + * in response to an [Events], use the output combinators available in [BuildScope], such as + * [BuildScope.toSharedFlow] or [BuildScope.observe]. + */ +@ExperimentalKairosApi +fun <A> Events<A>.onEach(action: TransactionScope.(A) -> Unit): Events<A> = map { + action(it) + it +} + +/** + * Returns an [Events] containing only values of the original [Events] that satisfy the given + * [predicate]. + */ +@ExperimentalKairosApi +fun <A> Events<A>.filter(predicate: TransactionScope.(A) -> Boolean): Events<A> { + val pulse = filterImpl({ init.connect(evalScope = this) }) { predicate(it) } + return EventsInit(constInit(name = null, pulse)) +} + +/** + * Splits an [Events] of pairs into a pair of [Events], where each returned [Events] emits half of + * the original. + * + * Shorthand for: + * ```kotlin + * val lefts = map { it.first } + * val rights = map { it.second } + * return Pair(lefts, rights) + * ``` + */ +@ExperimentalKairosApi +fun <A, B> Events<Pair<A, B>>.unzip(): Pair<Events<A>, Events<B>> { + val lefts = map { it.first } + val rights = map { it.second } + return lefts to rights +} + +/** + * Merges the given [Events] into a single [Events] that emits events from both. + * + * Because [Events] can only emit one value per transaction, the provided [transformCoincidence] + * function is used to combine coincident emissions to produce the result value to be emitted by the + * merged [Events]. + */ +@ExperimentalKairosApi +fun <A> Events<A>.mergeWith( + other: Events<A>, + name: String? = null, + transformCoincidence: TransactionScope.(A, A) -> A = { a, _ -> a }, +): Events<A> { + val node = + mergeNodes( + name = name, + getPulse = { init.connect(evalScope = this) }, + getOther = { other.init.connect(evalScope = this) }, + ) { a, b -> + transformCoincidence(a, b) + } + return EventsInit(constInit(name = null, node)) +} + +/** + * Merges the given [Events] into a single [Events] that emits events from all. All coincident + * emissions are collected into the emitted [List], preserving the input ordering. + * + * @see mergeWith + * @see mergeLeft + */ +@ExperimentalKairosApi +fun <A> merge(vararg events: Events<A>): Events<List<A>> = events.asIterable().merge() + +/** + * Merges the given [Events] into a single [Events] that emits events from all. In the case of + * coincident emissions, the emission from the left-most [Events] is emitted. + * + * @see merge + */ +@ExperimentalKairosApi +fun <A> mergeLeft(vararg events: Events<A>): Events<A> = events.asIterable().mergeLeft() + +/** + * Merges the given [Events] into a single [Events] that emits events from all. + * + * Because [Events] can only emit one value per transaction, the provided [transformCoincidence] + * function is used to combine coincident emissions to produce the result value to be emitted by the + * merged [Events]. + */ +// TODO: can be optimized to avoid creating the intermediate list +fun <A> merge(vararg events: Events<A>, transformCoincidence: (A, A) -> A): Events<A> = + merge(*events).map { l -> l.reduce(transformCoincidence) } + +/** + * Merges the given [Events] into a single [Events] that emits events from all. All coincident + * emissions are collected into the emitted [List], preserving the input ordering. + * + * @see mergeWith + * @see mergeLeft + */ +@ExperimentalKairosApi +fun <A> Iterable<Events<A>>.merge(): Events<List<A>> = + EventsInit(constInit(name = null, mergeNodes { map { it.init.connect(evalScope = this) } })) + +/** + * Merges the given [Events] into a single [Events] that emits events from all. In the case of + * coincident emissions, the emission from the left-most [Events] is emitted. + * + * @see merge + */ +@ExperimentalKairosApi +fun <A> Iterable<Events<A>>.mergeLeft(): Events<A> = + EventsInit(constInit(name = null, mergeNodesLeft { map { it.init.connect(evalScope = this) } })) + +/** + * Creates a new [Events] that emits events from all given [Events]. All simultaneous emissions are + * collected into the emitted [List], preserving the input ordering. + * + * @see mergeWith + */ +@ExperimentalKairosApi fun <A> Sequence<Events<A>>.merge(): Events<List<A>> = asIterable().merge() + +/** + * Creates a new [Events] that emits events from all given [Events]. All simultaneous emissions are + * collected into the emitted [Map], and are given the same key of the associated [Events] in the + * input [Map]. + * + * @see mergeWith + */ +@ExperimentalKairosApi +fun <K, A> Map<K, Events<A>>.merge(): Events<Map<K, A>> = + asSequence() + .map { (k, events) -> events.map { a -> k to a } } + .toList() + .merge() + .map { it.toMap() } + +/** + * Returns a [GroupedEvents] that can be used to efficiently split a single [Events] into multiple + * downstream [Events]. + * + * The input [Events] emits [Map] instances that specify which downstream [Events] the associated + * value will be emitted from. These downstream [Events] can be obtained via + * [GroupedEvents.eventsForKey]. + * + * An example: + * ``` + * val fooEvents: Events<Map<String, Foo>> = ... + * val fooById: GroupedEvents<String, Foo> = fooEvents.groupByKey() + * val fooBar: Events<Foo> = fooById["bar"] + * ``` + * + * This is semantically equivalent to `val fooBar = fooEvents.mapNotNull { map -> map["bar"] }` but + * is significantly more efficient; specifically, using [mapNotNull] in this way incurs a `O(n)` + * performance hit, where `n` is the number of different [mapNotNull] operations used to filter on a + * specific key's presence in the emitted [Map]. [groupByKey] internally uses a [HashMap] to lookup + * the appropriate downstream [Events], and so operates in `O(1)`. + * + * Note that the returned [GroupedEvents] should be cached and re-used to gain the performance + * benefit. + * + * @see selector + */ +@ExperimentalKairosApi +fun <K, A> Events<Map<K, A>>.groupByKey(numKeys: Int? = null): GroupedEvents<K, A> = + GroupedEvents(demuxMap({ init.connect(this) }, numKeys)) + +/** + * Shorthand for `map { mapOf(extractKey(it) to it) }.groupByKey()` + * + * @see groupByKey + */ +@ExperimentalKairosApi +fun <K, A> Events<A>.groupBy( + numKeys: Int? = null, + extractKey: TransactionScope.(A) -> K, +): GroupedEvents<K, A> = map { mapOf(extractKey(it) to it) }.groupByKey(numKeys) + +/** + * Returns two new [Events] that contain elements from this [Events] that satisfy or don't satisfy + * [predicate]. + * + * Using this is equivalent to `upstream.filter(predicate) to upstream.filter { !predicate(it) }` + * but is more efficient; specifically, [partition] will only invoke [predicate] once per element. + */ +@ExperimentalKairosApi +fun <A> Events<A>.partition( + predicate: TransactionScope.(A) -> Boolean +): Pair<Events<A>, Events<A>> { + val grouped: GroupedEvents<Boolean, A> = groupBy(numKeys = 2, extractKey = predicate) + return Pair(grouped.eventsForKey(true), grouped.eventsForKey(false)) +} + +/** + * Returns two new [Events] that contain elements from this [Events]; [Pair.first] will contain + * [Left] values, and [Pair.second] will contain [Right] values. + * + * Using this is equivalent to using [filterIsInstance] in conjunction with [map] twice, once for + * [Left]s and once for [Right]s, but is slightly more efficient; specifically, the + * [filterIsInstance] check is only performed once per element. + */ +@ExperimentalKairosApi +fun <A, B> Events<Either<A, B>>.partitionEither(): Pair<Events<A>, Events<B>> { + val (left, right) = partition { it is Left } + return Pair(left.mapCheap { (it as Left).value }, right.mapCheap { (it as Right).value }) +} + +/** + * A mapping from keys of type [K] to [Events] emitting values of type [A]. + * + * @see groupByKey + */ +@ExperimentalKairosApi +class GroupedEvents<in K, out A> internal constructor(internal val impl: DemuxImpl<K, A>) { + /** + * Returns an [Events] that emits values of type [A] that correspond to the given [key]. + * + * @see groupByKey + */ + fun eventsForKey(key: K): Events<A> = EventsInit(constInit(name = null, impl.eventsForKey(key))) + + /** + * Returns an [Events] that emits values of type [A] that correspond to the given [key]. + * + * @see groupByKey + */ + operator fun get(key: K): Events<A> = eventsForKey(key) +} + +/** + * Returns an [Events] that switches to the [Events] contained within this [State] whenever it + * changes. + * + * This switch does take effect until the *next* transaction after [State] changes. For a switch + * that takes effect immediately, see [switchEventsPromptly]. + */ +@ExperimentalKairosApi +fun <A> State<Events<A>>.switchEvents(name: String? = null): Events<A> { + val patches = + mapImpl({ init.connect(this).changes }) { newEvents, _ -> newEvents.init.connect(this) } + return EventsInit( + constInit( + name = null, + switchDeferredImplSingle( + name = name, + getStorage = { + init.connect(this).getCurrentWithEpoch(this).first.init.connect(this) + }, + getPatches = { patches }, + ), + ) + ) +} + +/** + * Returns an [Events] that switches to the [Events] contained within this [State] whenever it + * changes. + * + * This switch takes effect immediately within the same transaction that [State] changes. In + * general, you should prefer [switchEvents] over this method. It is both safer and more performant. + */ +// TODO: parameter to handle coincidental emission from both old and new +@ExperimentalKairosApi +fun <A> State<Events<A>>.switchEventsPromptly(): Events<A> { + val patches = + mapImpl({ init.connect(this).changes }) { newEvents, _ -> newEvents.init.connect(this) } + return EventsInit( + constInit( + name = null, + switchPromptImplSingle( + getStorage = { + init.connect(this).getCurrentWithEpoch(this).first.init.connect(this) + }, + getPatches = { patches }, + ), + ) + ) +} + +/** + * A mutable [Events] that provides the ability to [emit] values to the network, handling + * backpressure by coalescing all emissions into batches. + * + * @see KairosNetwork.coalescingMutableEvents + */ +@ExperimentalKairosApi +class CoalescingMutableEvents<in In, Out> +internal constructor( + internal val name: String?, + internal val coalesce: (old: Lazy<Out>, new: In) -> Out, + internal val network: Network, + private val getInitialValue: () -> Out, + internal val impl: InputNode<Out> = InputNode(), +) : Events<Out>() { + internal val storage = AtomicReference(false to lazy { getInitialValue() }) + + override fun toString(): String = "${this::class.simpleName}@$hashString" + + /** + * Inserts [value] into the current batch, enqueueing it for emission from this [Events] if not + * already pending. + * + * Backpressure occurs when [emit] is called while the Kairos network is currently in a + * transaction; if called multiple times, then emissions will be coalesced into a single batch + * that is then processed when the network is ready. + */ + fun emit(value: In) { + val (scheduled, _) = + storage.getAndUpdate { (_, batch) -> true to CompletableLazy(coalesce(batch, value)) } + if (!scheduled) { + @Suppress("DeferredResultUnused") + network.transaction( + "CoalescingMutableEvents${name?.let { "($name)" }.orEmpty()}.emit" + ) { + val (_, batch) = storage.getAndSet(false to lazy { getInitialValue() }) + impl.visit(this, batch.value) + } + } + } +} + +/** + * A mutable [Events] that provides the ability to [emit] values to the network, handling + * backpressure by suspending the emitter. + * + * @see KairosNetwork.coalescingMutableEvents + */ +@ExperimentalKairosApi +class MutableEvents<T> +internal constructor(internal val network: Network, internal val impl: InputNode<T> = InputNode()) : + Events<T>() { + internal val name: String? = null + + private val storage = AtomicReference<Job?>(null) + + override fun toString(): String = "${this::class.simpleName}@$hashString" + + /** + * Emits a [value] to this [Events], suspending the caller until the Kairos transaction + * containing the emission has completed. + */ + suspend fun emit(value: T) { + coroutineScope { + var jobOrNull: Job? = null + val newEmit = + async(start = CoroutineStart.LAZY) { + jobOrNull?.join() + network.transaction("MutableEvents.emit") { impl.visit(this, value) }.await() + } + jobOrNull = storage.getAndSet(newEmit) + newEmit.await() + } + } +} + +private data object EmptyEvents : Events<Nothing>() + +internal class EventsInit<out A>(val init: Init<EventsImpl<A>>) : Events<A>() { + override fun toString(): String = "${this::class.simpleName}@$hashString" +} + +internal val <A> Events<A>.init: Init<EventsImpl<A>> + get() = + when (this) { + is EmptyEvents -> constInit("EmptyEvents", neverImpl) + is EventsInit -> init + is EventsLoop -> init + is CoalescingMutableEvents<*, A> -> constInit(name, impl.activated()) + is MutableEvents -> constInit(name, impl.activated()) + } + +private inline fun <A> deferInline(crossinline block: InitScope.() -> Events<A>): Events<A> = + EventsInit(init(name = null) { block().init.connect(evalScope = this) }) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpBuildScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpBuildScope.kt deleted file mode 100644 index 209a402bd629..000000000000 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpBuildScope.kt +++ /dev/null @@ -1,885 +0,0 @@ -/* - * Copyright (C) 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:OptIn(ExperimentalCoroutinesApi::class) - -package com.android.systemui.kairos - -import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.just -import com.android.systemui.kairos.util.map -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.RestrictsSuspension -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.Job -import kotlinx.coroutines.awaitCancellation -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.dropWhile -import kotlinx.coroutines.flow.scan -import kotlinx.coroutines.launch - -/** A function that modifies the FrpNetwork. */ -typealias FrpSpec<A> = suspend FrpBuildScope.() -> A - -/** - * Constructs an [FrpSpec]. The passed [block] will be invoked with an [FrpBuildScope] that can be - * used to perform network-building operations, including adding new inputs and outputs to the - * network, as well as all operations available in [FrpTransactionScope]. - */ -@ExperimentalFrpApi -@Suppress("NOTHING_TO_INLINE") -inline fun <A> frpSpec(noinline block: suspend FrpBuildScope.() -> A): FrpSpec<A> = block - -/** Applies the [FrpSpec] within this [FrpBuildScope]. */ -@ExperimentalFrpApi -inline operator fun <A> FrpBuildScope.invoke(block: FrpBuildScope.() -> A) = run(block) - -/** Operations that add inputs and outputs to an FRP network. */ -@ExperimentalFrpApi -@RestrictsSuspension -interface FrpBuildScope : FrpStateScope { - - /** TODO: Javadoc */ - @ExperimentalFrpApi - fun <R> deferredBuildScope(block: suspend FrpBuildScope.() -> R): FrpDeferredValue<R> - - /** TODO: Javadoc */ - @ExperimentalFrpApi fun deferredBuildScopeAction(block: suspend FrpBuildScope.() -> Unit) - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow]. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * Unlike [mapLatestBuild], these modifications are not undone with each subsequent emission of - * the original [TFlow]. - * - * **NOTE:** This API does not [observe] the original [TFlow], meaning that unless the returned - * (or a downstream) [TFlow] is observed separately, [transform] will not be invoked, and no - * internal side-effects will occur. - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.mapBuild(transform: suspend FrpBuildScope.(A) -> B): TFlow<B> - - /** - * Invokes [block] whenever this [TFlow] emits a value, allowing side-effects to be safely - * performed in reaction to the emission. - * - * Specifically, [block] is deferred to the end of the transaction, and is only actually - * executed if this [FrpBuildScope] is still active by that time. It can be deactivated due to a - * -Latest combinator, for example. - * - * Shorthand for: - * ```kotlin - * tFlow.observe { effect { ... } } - * ``` - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.observe( - coroutineContext: CoroutineContext = EmptyCoroutineContext, - block: suspend FrpEffectScope.(A) -> Unit = {}, - ): Job - - /** - * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original - * [TFlow], and a [FrpDeferredValue] containing the result of applying [initialSpecs] - * immediately. - * - * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the same - * key are undone (any registered [observers][observe] are unregistered, and any pending - * [side-effects][effect] are cancelled). - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpSpec] will be undone with no replacement. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestSpecForKey( - initialSpecs: FrpDeferredValue<Map<K, FrpSpec<B>>>, - numKeys: Int? = null, - ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> - - /** - * Creates an instance of a [TFlow] with elements that are from [builder]. - * - * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the - * provided [MutableTFlow]. - * - * By default, [builder] is only running while the returned [TFlow] is being - * [observed][observe]. If you want it to run at all times, simply add a no-op observer: - * ```kotlin - * tFlow { ... }.apply { observe() } - * ``` - */ - @ExperimentalFrpApi fun <T> tFlow(builder: suspend FrpProducerScope<T>.() -> Unit): TFlow<T> - - /** - * Creates an instance of a [TFlow] with elements that are emitted from [builder]. - * - * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the - * provided [MutableTFlow]. - * - * By default, [builder] is only running while the returned [TFlow] is being - * [observed][observe]. If you want it to run at all times, simply add a no-op observer: - * ```kotlin - * tFlow { ... }.apply { observe() } - * ``` - * - * In the event of backpressure, emissions are *coalesced* into batches. When a value is - * [emitted][FrpCoalescingProducerScope.emit] from [builder], it is merged into the batch via - * [coalesce]. Once the batch is consumed by the frp network in the next transaction, the batch - * is reset back to [getInitialValue]. - */ - @ExperimentalFrpApi - fun <In, Out> coalescingTFlow( - getInitialValue: () -> Out, - coalesce: (old: Out, new: In) -> Out, - builder: suspend FrpCoalescingProducerScope<In>.() -> Unit, - ): TFlow<Out> - - /** - * Creates a new [FrpBuildScope] that is a child of this one. - * - * This new scope can be manually cancelled via the returned [Job], or will be cancelled - * automatically when its parent is cancelled. Cancellation will unregister all - * [observers][observe] and cancel all scheduled [effects][effect]. - * - * The return value from [block] can be accessed via the returned [FrpDeferredValue]. - */ - @ExperimentalFrpApi fun <A> asyncScope(block: FrpSpec<A>): Pair<FrpDeferredValue<A>, Job> - - // TODO: once we have context params, these can all become extensions: - - /** - * Returns a [TFlow] containing the results of applying the given [transform] function to each - * value of the original [TFlow]. - * - * Unlike [TFlow.map], [transform] can perform arbitrary asynchronous code. This code is run - * outside of the current FRP transaction; when [transform] returns, the returned value is - * emitted from the result [TFlow] in a new transaction. - * - * Shorthand for: - * ```kotlin - * tflow.mapLatestBuild { a -> asyncTFlow { transform(a) } }.flatten() - * ``` - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.mapAsyncLatest(transform: suspend (A) -> B): TFlow<B> = - mapLatestBuild { a -> asyncTFlow { transform(a) } }.flatten() - - /** - * Invokes [block] whenever this [TFlow] emits a value. [block] receives an [FrpBuildScope] that - * can be used to make further modifications to the FRP network, and/or perform side-effects via - * [effect]. - * - * @see observe - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.observeBuild(block: suspend FrpBuildScope.(A) -> Unit = {}): Job = - mapBuild(block).observe() - - /** - * Returns a [StateFlow] whose [value][StateFlow.value] tracks the current - * [value of this TState][TState.sample], and will emit at the same rate as - * [TState.stateChanges]. - * - * Note that the [value][StateFlow.value] is not available until the *end* of the current - * transaction. If you need the current value before this time, then use [TState.sample]. - */ - @ExperimentalFrpApi - fun <A> TState<A>.toStateFlow(): StateFlow<A> { - val uninitialized = Any() - var initialValue: Any? = uninitialized - val innerStateFlow = MutableStateFlow<Any?>(uninitialized) - deferredBuildScope { - initialValue = sample() - stateChanges.observe { - innerStateFlow.value = it - initialValue = null - } - } - - @Suppress("UNCHECKED_CAST") - fun getValue(innerValue: Any?): A = - when { - innerValue !== uninitialized -> innerValue as A - initialValue !== uninitialized -> initialValue as A - else -> - error( - "Attempted to access StateFlow.value before FRP transaction has completed." - ) - } - - return object : StateFlow<A> { - override val replayCache: List<A> - get() = innerStateFlow.replayCache.map(::getValue) - - override val value: A - get() = getValue(innerStateFlow.value) - - override suspend fun collect(collector: FlowCollector<A>): Nothing { - innerStateFlow.collect { collector.emit(getValue(it)) } - } - } - } - - /** - * Returns a [SharedFlow] configured with a replay cache of size [replay] that emits the current - * [value][TState.sample] of this [TState] followed by all [stateChanges]. - */ - @ExperimentalFrpApi - fun <A> TState<A>.toSharedFlow(replay: Int = 0): SharedFlow<A> { - val result = MutableSharedFlow<A>(replay, extraBufferCapacity = 1) - deferredBuildScope { - result.tryEmit(sample()) - stateChanges.observe { a -> result.tryEmit(a) } - } - return result - } - - /** - * Returns a [SharedFlow] configured with a replay cache of size [replay] that emits values - * whenever this [TFlow] emits. - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.toSharedFlow(replay: Int = 0): SharedFlow<A> { - val result = MutableSharedFlow<A>(replay, extraBufferCapacity = 1) - observe { a -> result.tryEmit(a) } - return result - } - - /** - * Returns a [TState] that holds onto the value returned by applying the most recently emitted - * [FrpSpec] from the original [TFlow], or the value returned by applying [initialSpec] if - * nothing has been emitted since it was constructed. - * - * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A> TFlow<FrpSpec<A>>.holdLatestSpec(initialSpec: FrpSpec<A>): TState<A> { - val (changes: TFlow<A>, initApplied: FrpDeferredValue<A>) = applyLatestSpec(initialSpec) - return changes.holdDeferred(initApplied) - } - - /** - * Returns a [TState] containing the value returned by applying the [FrpSpec] held by the - * original [TState]. - * - * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A> TState<FrpSpec<A>>.applyLatestSpec(): TState<A> { - val (appliedChanges: TFlow<A>, init: FrpDeferredValue<A>) = - stateChanges.applyLatestSpec(frpSpec { sample().applySpec() }) - return appliedChanges.holdDeferred(init) - } - - /** - * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original - * [TFlow]. - * - * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A> TFlow<FrpSpec<A>>.applyLatestSpec(): TFlow<A> = applyLatestSpec(frpSpec {}).first - - /** - * Returns a [TFlow] that switches to a new [TFlow] produced by [transform] every time the - * original [TFlow] emits a value. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * When the original [TFlow] emits a new value, those changes are undone (any registered - * [observers][observe] are unregistered, and any pending [effects][effect] are cancelled). - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.flatMapLatestBuild( - transform: suspend FrpBuildScope.(A) -> TFlow<B> - ): TFlow<B> = mapCheap { frpSpec { transform(it) } }.applyLatestSpec().flatten() - - /** - * Returns a [TState] by applying [transform] to the value held by the original [TState]. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * When the value held by the original [TState] changes, those changes are undone (any - * registered [observers][observe] are unregistered, and any pending [effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A, B> TState<A>.flatMapLatestBuild( - transform: suspend FrpBuildScope.(A) -> TState<B> - ): TState<B> = mapLatestBuild { transform(it) }.flatten() - - /** - * Returns a [TState] that transforms the value held inside this [TState] by applying it to the - * [transform]. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * When the value held by the original [TState] changes, those changes are undone (any - * registered [observers][observe] are unregistered, and any pending [effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A, B> TState<A>.mapLatestBuild(transform: suspend FrpBuildScope.(A) -> B): TState<B> = - mapCheapUnsafe { frpSpec { transform(it) } }.applyLatestSpec() - - /** - * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original - * [TFlow], and a [FrpDeferredValue] containing the result of applying [initialSpec] - * immediately. - * - * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A : Any?, B> TFlow<FrpSpec<B>>.applyLatestSpec( - initialSpec: FrpSpec<A> - ): Pair<TFlow<B>, FrpDeferredValue<A>> { - val (flow, result) = - mapCheap { spec -> mapOf(Unit to just(spec)) } - .applyLatestSpecForKey(initialSpecs = mapOf(Unit to initialSpec), numKeys = 1) - val outFlow: TFlow<B> = - flow.mapMaybe { - checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" } - } - val outInit: FrpDeferredValue<A> = deferredBuildScope { - val initResult: Map<Unit, A> = result.get() - check(Unit in initResult) { - "applyLatest: expected initial result, but none present in: $initResult" - } - @Suppress("UNCHECKED_CAST") - initResult.getOrDefault(Unit) { null } as A - } - return Pair(outFlow, outInit) - } - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow]. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * With each invocation of [transform], changes from the previous invocation are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.mapLatestBuild(transform: suspend FrpBuildScope.(A) -> B): TFlow<B> = - mapCheap { frpSpec { transform(it) } }.applyLatestSpec() - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to - * [initialValue] immediately. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * With each invocation of [transform], changes from the previous invocation are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.mapLatestBuild( - initialValue: A, - transform: suspend FrpBuildScope.(A) -> B, - ): Pair<TFlow<B>, FrpDeferredValue<B>> = - mapLatestBuildDeferred(deferredOf(initialValue), transform) - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to - * [initialValue] immediately. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * With each invocation of [transform], changes from the previous invocation are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.mapLatestBuildDeferred( - initialValue: FrpDeferredValue<A>, - transform: suspend FrpBuildScope.(A) -> B, - ): Pair<TFlow<B>, FrpDeferredValue<B>> = - mapCheap { frpSpec { transform(it) } } - .applyLatestSpec(initialSpec = frpSpec { transform(initialValue.get()) }) - - /** - * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original - * [TFlow], and a [FrpDeferredValue] containing the result of applying [initialSpecs] - * immediately. - * - * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the same - * key are undone (any registered [observers][observe] are unregistered, and any pending - * [side-effects][effect] are cancelled). - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpSpec] will be undone with no replacement. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestSpecForKey( - initialSpecs: Map<K, FrpSpec<B>>, - numKeys: Int? = null, - ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> = - applyLatestSpecForKey(deferredOf(initialSpecs), numKeys) - - /** - * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original - * [TFlow]. - * - * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the same - * key are undone (any registered [observers][observe] are unregistered, and any pending - * [side-effects][effect] are cancelled). - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpSpec] will be undone with no replacement. - */ - @ExperimentalFrpApi - fun <K, A> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestSpecForKey( - numKeys: Int? = null - ): TFlow<Map<K, Maybe<A>>> = - applyLatestSpecForKey<K, A, Nothing>(deferredOf(emptyMap()), numKeys).first - - /** - * Returns a [TState] containing the latest results of applying each [FrpSpec] emitted from the - * original [TFlow]. - * - * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the same - * key are undone (any registered [observers][observe] are unregistered, and any pending - * [side-effects][effect] are cancelled). - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpSpec] will be undone with no replacement. - */ - @ExperimentalFrpApi - fun <K, A> TFlow<Map<K, Maybe<FrpSpec<A>>>>.holdLatestSpecForKey( - initialSpecs: FrpDeferredValue<Map<K, FrpSpec<A>>>, - numKeys: Int? = null, - ): TState<Map<K, A>> { - val (changes, initialValues) = applyLatestSpecForKey(initialSpecs, numKeys) - return changes.foldMapIncrementally(initialValues) - } - - /** - * Returns a [TState] containing the latest results of applying each [FrpSpec] emitted from the - * original [TFlow]. - * - * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the same - * key are undone (any registered [observers][observe] are unregistered, and any pending - * [side-effects][effect] are cancelled). - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpSpec] will be undone with no replacement. - */ - @ExperimentalFrpApi - fun <K, A> TFlow<Map<K, Maybe<FrpSpec<A>>>>.holdLatestSpecForKey( - initialSpecs: Map<K, FrpSpec<A>> = emptyMap(), - numKeys: Int? = null, - ): TState<Map<K, A>> = holdLatestSpecForKey(deferredOf(initialSpecs), numKeys) - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to - * [initialValues] immediately. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * With each invocation of [transform], changes from the previous invocation are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpBuildScope] will be undone with no replacement. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestBuildForKey( - initialValues: FrpDeferredValue<Map<K, A>>, - numKeys: Int? = null, - transform: suspend FrpBuildScope.(A) -> B, - ): Pair<TFlow<Map<K, Maybe<B>>>, FrpDeferredValue<Map<K, B>>> = - map { patch -> patch.mapValues { (_, v) -> v.map { frpSpec { transform(it) } } } } - .applyLatestSpecForKey( - deferredBuildScope { - initialValues.get().mapValues { (_, v) -> frpSpec { transform(v) } } - }, - numKeys = numKeys, - ) - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to - * [initialValues] immediately. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * With each invocation of [transform], changes from the previous invocation are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpBuildScope] will be undone with no replacement. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestBuildForKey( - initialValues: Map<K, A>, - numKeys: Int? = null, - transform: suspend FrpBuildScope.(A) -> B, - ): Pair<TFlow<Map<K, Maybe<B>>>, FrpDeferredValue<Map<K, B>>> = - mapLatestBuildForKey(deferredOf(initialValues), numKeys, transform) - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow]. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * With each invocation of [transform], changes from the previous invocation are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpBuildScope] will be undone with no replacement. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestBuildForKey( - numKeys: Int? = null, - transform: suspend FrpBuildScope.(A) -> B, - ): TFlow<Map<K, Maybe<B>>> = mapLatestBuildForKey(emptyMap(), numKeys, transform).first - - /** Returns a [Deferred] containing the next value to be emitted from this [TFlow]. */ - @ExperimentalFrpApi - fun <R> TFlow<R>.nextDeferred(): Deferred<R> { - lateinit var next: CompletableDeferred<R> - val job = nextOnly().observe { next.complete(it) } - next = CompletableDeferred<R>(parent = job) - return next - } - - /** Returns a [TState] that reflects the [StateFlow.value] of this [StateFlow]. */ - @ExperimentalFrpApi - fun <A> StateFlow<A>.toTState(): TState<A> { - val initial = value - return tFlow { dropWhile { it == initial }.collect { emit(it) } }.hold(initial) - } - - /** Returns a [TFlow] that emits whenever this [Flow] emits. */ - @ExperimentalFrpApi fun <A> Flow<A>.toTFlow(): TFlow<A> = tFlow { collect { emit(it) } } - - /** - * Shorthand for: - * ```kotlin - * flow.toTFlow().hold(initialValue) - * ``` - */ - @ExperimentalFrpApi - fun <A> Flow<A>.toTState(initialValue: A): TState<A> = toTFlow().hold(initialValue) - - /** - * Shorthand for: - * ```kotlin - * flow.scan(initialValue, operation).toTFlow().hold(initialValue) - * ``` - */ - @ExperimentalFrpApi - fun <A, B> Flow<A>.scanToTState(initialValue: B, operation: (B, A) -> B): TState<B> = - scan(initialValue, operation).toTFlow().hold(initialValue) - - /** - * Shorthand for: - * ```kotlin - * flow.scan(initialValue) { a, f -> f(a) }.toTFlow().hold(initialValue) - * ``` - */ - @ExperimentalFrpApi - fun <A> Flow<(A) -> A>.scanToTState(initialValue: A): TState<A> = - scanToTState(initialValue) { a, f -> f(a) } - - /** - * Invokes [block] whenever this [TFlow] emits a value. [block] receives an [FrpBuildScope] that - * can be used to make further modifications to the FRP network, and/or perform side-effects via - * [effect]. - * - * With each invocation of [block], changes from the previous invocation are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.observeLatestBuild(block: suspend FrpBuildScope.(A) -> Unit = {}): Job = - mapLatestBuild { block(it) }.observe() - - /** - * Invokes [block] whenever this [TFlow] emits a value, allowing side-effects to be safely - * performed in reaction to the emission. - * - * With each invocation of [block], running effects from the previous invocation are cancelled. - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.observeLatest(block: suspend FrpEffectScope.(A) -> Unit = {}): Job { - var innerJob: Job? = null - return observeBuild { - innerJob?.cancel() - innerJob = effect { block(it) } - } - } - - /** - * Invokes [block] with the value held by this [TState], allowing side-effects to be safely - * performed in reaction to the state changing. - * - * With each invocation of [block], running effects from the previous invocation are cancelled. - */ - @ExperimentalFrpApi - fun <A> TState<A>.observeLatest(block: suspend FrpEffectScope.(A) -> Unit = {}): Job = - launchScope { - var innerJob = effect { block(sample()) } - stateChanges.observeBuild { - innerJob.cancel() - innerJob = effect { block(it) } - } - } - - /** - * Applies [block] to the value held by this [TState]. [block] receives an [FrpBuildScope] that - * can be used to make further modifications to the FRP network, and/or perform side-effects via - * [effect]. - * - * [block] can perform modifications to the FRP network via its [FrpBuildScope] receiver. With - * each invocation of [block], changes from the previous invocation are undone (any registered - * [observers][observe] are unregistered, and any pending [side-effects][effect] are cancelled). - */ - @ExperimentalFrpApi - fun <A> TState<A>.observeLatestBuild(block: suspend FrpBuildScope.(A) -> Unit = {}): Job = - launchScope { - var innerJob: Job = launchScope { block(sample()) } - stateChanges.observeBuild { - innerJob.cancel() - innerJob = launchScope { block(it) } - } - } - - /** Applies the [FrpSpec] within this [FrpBuildScope]. */ - @ExperimentalFrpApi suspend fun <A> FrpSpec<A>.applySpec(): A = this() - - /** - * Applies the [FrpSpec] within this [FrpBuildScope], returning the result as an - * [FrpDeferredValue]. - */ - @ExperimentalFrpApi - fun <A> FrpSpec<A>.applySpecDeferred(): FrpDeferredValue<A> = deferredBuildScope { applySpec() } - - /** - * Invokes [block] on the value held in this [TState]. [block] receives an [FrpBuildScope] that - * can be used to make further modifications to the FRP network, and/or perform side-effects via - * [effect]. - */ - @ExperimentalFrpApi - fun <A> TState<A>.observeBuild(block: suspend FrpBuildScope.(A) -> Unit = {}): Job = - launchScope { - block(sample()) - stateChanges.observeBuild(block) - } - - /** - * Invokes [block] with the current value of this [TState], re-invoking whenever it changes, - * allowing side-effects to be safely performed in reaction value changing. - * - * Specifically, [block] is deferred to the end of the transaction, and is only actually - * executed if this [FrpBuildScope] is still active by that time. It can be deactivated due to a - * -Latest combinator, for example. - * - * If the [TState] is changing within the *current* transaction (i.e. [stateChanges] is - * presently emitting) then [block] will be invoked for the first time with the new value; - * otherwise, it will be invoked with the [current][sample] value. - */ - @ExperimentalFrpApi - fun <A> TState<A>.observe(block: suspend FrpEffectScope.(A) -> Unit = {}): Job = - now.map { sample() }.mergeWith(stateChanges) { _, new -> new }.observe { block(it) } -} - -/** - * Returns a [TFlow] that emits the result of [block] once it completes. [block] is evaluated - * outside of the current FRP transaction; when it completes, the returned [TFlow] emits in a new - * transaction. - * - * Shorthand for: - * ``` - * tFlow { emitter: MutableTFlow<A> -> - * val a = block() - * emitter.emit(a) - * } - * ``` - */ -@ExperimentalFrpApi -fun <A> FrpBuildScope.asyncTFlow(block: suspend () -> A): TFlow<A> = - tFlow { - // TODO: if block completes synchronously, it would be nice to emit within this - // transaction - emit(block()) - } - .apply { observe() } - -/** - * Performs a side-effect in a safe manner w/r/t the current FRP transaction. - * - * Specifically, [block] is deferred to the end of the current transaction, and is only actually - * executed if this [FrpBuildScope] is still active by that time. It can be deactivated due to a - * -Latest combinator, for example. - * - * Shorthand for: - * ```kotlin - * now.observe { block() } - * ``` - */ -@ExperimentalFrpApi -fun FrpBuildScope.effect(block: suspend FrpEffectScope.() -> Unit): Job = now.observe { block() } - -/** - * Launches [block] in a new coroutine, returning a [Job] bound to the coroutine. - * - * This coroutine is not actually started until the *end* of the current FRP transaction. This is - * done because the current [FrpBuildScope] might be deactivated within this transaction, perhaps - * due to a -Latest combinator. If this happens, then the coroutine will never actually be started. - * - * Shorthand for: - * ```kotlin - * effect { frpCoroutineScope.launch { block() } } - * ``` - */ -@ExperimentalFrpApi -fun FrpBuildScope.launchEffect(block: suspend CoroutineScope.() -> Unit): Job = asyncEffect(block) - -/** - * Launches [block] in a new coroutine, returning the result as a [Deferred]. - * - * This coroutine is not actually started until the *end* of the current FRP transaction. This is - * done because the current [FrpBuildScope] might be deactivated within this transaction, perhaps - * due to a -Latest combinator. If this happens, then the coroutine will never actually be started. - * - * Shorthand for: - * ```kotlin - * CompletableDeferred<R>.apply { - * effect { frpCoroutineScope.launch { complete(coroutineScope { block() }) } } - * } - * .await() - * ``` - */ -@ExperimentalFrpApi -fun <R> FrpBuildScope.asyncEffect(block: suspend CoroutineScope.() -> R): Deferred<R> { - val result = CompletableDeferred<R>() - val job = now.observe { frpCoroutineScope.launch { result.complete(coroutineScope(block)) } } - val handle = job.invokeOnCompletion { result.cancel() } - result.invokeOnCompletion { - handle.dispose() - job.cancel() - } - return result -} - -/** Like [FrpBuildScope.asyncScope], but ignores the result of [block]. */ -@ExperimentalFrpApi fun FrpBuildScope.launchScope(block: FrpSpec<*>): Job = asyncScope(block).second - -/** - * Creates an instance of a [TFlow] with elements that are emitted from [builder]. - * - * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the provided - * [MutableTFlow]. - * - * By default, [builder] is only running while the returned [TFlow] is being - * [observed][FrpBuildScope.observe]. If you want it to run at all times, simply add a no-op - * observer: - * ```kotlin - * tFlow { ... }.apply { observe() } - * ``` - * - * In the event of backpressure, emissions are *coalesced* into batches. When a value is - * [emitted][FrpCoalescingProducerScope.emit] from [builder], it is merged into the batch via - * [coalesce]. Once the batch is consumed by the FRP network in the next transaction, the batch is - * reset back to [initialValue]. - */ -@ExperimentalFrpApi -fun <In, Out> FrpBuildScope.coalescingTFlow( - initialValue: Out, - coalesce: (old: Out, new: In) -> Out, - builder: suspend FrpCoalescingProducerScope<In>.() -> Unit, -): TFlow<Out> = coalescingTFlow(getInitialValue = { initialValue }, coalesce, builder) - -/** - * Creates an instance of a [TFlow] with elements that are emitted from [builder]. - * - * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the provided - * [MutableTFlow]. - * - * By default, [builder] is only running while the returned [TFlow] is being - * [observed][FrpBuildScope.observe]. If you want it to run at all times, simply add a no-op - * observer: - * ```kotlin - * tFlow { ... }.apply { observe() } - * ``` - * - * In the event of backpressure, emissions are *conflated*; any older emissions are dropped and only - * the most recent emission will be used when the FRP network is ready. - */ -@ExperimentalFrpApi -fun <T> FrpBuildScope.conflatedTFlow( - builder: suspend FrpCoalescingProducerScope<T>.() -> Unit -): TFlow<T> = - coalescingTFlow<T, Any?>(initialValue = Any(), coalesce = { _, new -> new }, builder = builder) - .mapCheap { - @Suppress("UNCHECKED_CAST") - it as T - } - -/** Scope for emitting to a [FrpBuildScope.coalescingTFlow]. */ -interface FrpCoalescingProducerScope<in T> { - /** - * Inserts [value] into the current batch, enqueueing it for emission from this [TFlow] if not - * already pending. - * - * Backpressure occurs when [emit] is called while the FRP network is currently in a - * transaction; if called multiple times, then emissions will be coalesced into a single batch - * that is then processed when the network is ready. - */ - fun emit(value: T) -} - -/** Scope for emitting to a [FrpBuildScope.tFlow]. */ -interface FrpProducerScope<in T> { - /** - * Emits a [value] to this [TFlow], suspending the caller until the FRP transaction containing - * the emission has completed. - */ - suspend fun emit(value: T) -} - -/** - * Suspends forever. Upon cancellation, runs [block]. Useful for unregistering callbacks inside of - * [FrpBuildScope.tFlow] and [FrpBuildScope.coalescingTFlow]. - */ -suspend fun awaitClose(block: () -> Unit): Nothing = - try { - awaitCancellation() - } finally { - block() - } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpEffectScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpEffectScope.kt deleted file mode 100644 index b39dcc131b1d..000000000000 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpEffectScope.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 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 com.android.systemui.kairos - -import kotlin.coroutines.RestrictsSuspension -import kotlinx.coroutines.CoroutineScope - -/** - * Scope for external side-effects triggered by the Frp network. This still occurs within the - * context of a transaction, so general suspending calls are disallowed to prevent blocking the - * transaction. You can use [frpCoroutineScope] to [launch][kotlinx.coroutines.launch] new - * coroutines to perform long-running asynchronous work. This scope is alive for the duration of the - * containing [FrpBuildScope] that this side-effect scope is running in. - */ -@RestrictsSuspension -@ExperimentalFrpApi -interface FrpEffectScope : FrpTransactionScope { - /** - * A [CoroutineScope] whose lifecycle lives for as long as this [FrpEffectScope] is alive. This - * is generally until the [Job][kotlinx.coroutines.Job] returned by [FrpBuildScope.effect] is - * cancelled. - */ - @ExperimentalFrpApi val frpCoroutineScope: CoroutineScope - - /** - * A [FrpNetwork] instance that can be used to transactionally query / modify the FRP network. - * - * The lambda passed to [FrpNetwork.transact] on this instance will receive an [FrpBuildScope] - * that is lifetime-bound to this [FrpEffectScope]. Once this [FrpEffectScope] is no longer - * alive, any modifications to the FRP network performed via this [FrpNetwork] instance will be - * undone (any registered [observers][FrpBuildScope.observe] are unregistered, and any pending - * [side-effects][FrpBuildScope.effect] are cancelled). - */ - @ExperimentalFrpApi val frpNetwork: FrpNetwork -} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpNetwork.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpNetwork.kt deleted file mode 100644 index 97252b4a199a..000000000000 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpNetwork.kt +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright (C) 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 com.android.systemui.kairos - -import com.android.systemui.kairos.internal.BuildScopeImpl -import com.android.systemui.kairos.internal.Network -import com.android.systemui.kairos.internal.StateScopeImpl -import com.android.systemui.kairos.internal.util.awaitCancellationAndThen -import com.android.systemui.kairos.internal.util.childScope -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.coroutineContext -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineName -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.job -import kotlinx.coroutines.launch - -/** - * Marks declarations that are still **experimental** and shouldn't be used in general production - * code. - */ -@RequiresOptIn( - message = "This API is experimental and should not be used in general production code." -) -@Retention(AnnotationRetention.BINARY) -annotation class ExperimentalFrpApi - -/** - * External interface to an FRP network. Can be used to make transactional queries and modifications - * to the network. - */ -@ExperimentalFrpApi -interface FrpNetwork { - /** - * Runs [block] inside of a transaction, suspending until the transaction is complete. - * - * The [FrpBuildScope] receiver exposes methods that can be used to query or modify the network. - * If the network is cancelled while the caller of [transact] is suspended, then the call will - * be cancelled. - */ - @ExperimentalFrpApi suspend fun <R> transact(block: suspend FrpTransactionScope.() -> R): R - - /** - * Activates [spec] in a transaction, suspending indefinitely. While suspended, all observers - * and long-running effects are kept alive. When cancelled, observers are unregistered and - * effects are cancelled. - */ - @ExperimentalFrpApi suspend fun activateSpec(spec: FrpSpec<*>) - - /** Returns a [CoalescingMutableTFlow] that can emit values into this [FrpNetwork]. */ - @ExperimentalFrpApi - fun <In, Out> coalescingMutableTFlow( - coalesce: (old: Out, new: In) -> Out, - getInitialValue: () -> Out, - ): CoalescingMutableTFlow<In, Out> - - /** Returns a [MutableTFlow] that can emit values into this [FrpNetwork]. */ - @ExperimentalFrpApi fun <T> mutableTFlow(): MutableTFlow<T> - - /** Returns a [MutableTState]. with initial state [initialValue]. */ - @ExperimentalFrpApi - fun <T> mutableTStateDeferred(initialValue: FrpDeferredValue<T>): MutableTState<T> -} - -/** Returns a [CoalescingMutableTFlow] that can emit values into this [FrpNetwork]. */ -@ExperimentalFrpApi -fun <In, Out> FrpNetwork.coalescingMutableTFlow( - coalesce: (old: Out, new: In) -> Out, - initialValue: Out, -): CoalescingMutableTFlow<In, Out> = - coalescingMutableTFlow(coalesce, getInitialValue = { initialValue }) - -/** Returns a [MutableTState]. with initial state [initialValue]. */ -@ExperimentalFrpApi -fun <T> FrpNetwork.mutableTState(initialValue: T): MutableTState<T> = - mutableTStateDeferred(deferredOf(initialValue)) - -/** Returns a [MutableTState]. with initial state [initialValue]. */ -@ExperimentalFrpApi -fun <T> MutableTState(network: FrpNetwork, initialValue: T): MutableTState<T> = - network.mutableTState(initialValue) - -/** Returns a [MutableTFlow] that can emit values into this [FrpNetwork]. */ -@ExperimentalFrpApi -fun <T> MutableTFlow(network: FrpNetwork): MutableTFlow<T> = network.mutableTFlow() - -/** Returns a [CoalescingMutableTFlow] that can emit values into this [FrpNetwork]. */ -@ExperimentalFrpApi -fun <In, Out> CoalescingMutableTFlow( - network: FrpNetwork, - coalesce: (old: Out, new: In) -> Out, - initialValue: Out, -): CoalescingMutableTFlow<In, Out> = network.coalescingMutableTFlow(coalesce) { initialValue } - -/** Returns a [CoalescingMutableTFlow] that can emit values into this [FrpNetwork]. */ -@ExperimentalFrpApi -fun <In, Out> CoalescingMutableTFlow( - network: FrpNetwork, - coalesce: (old: Out, new: In) -> Out, - getInitialValue: () -> Out, -): CoalescingMutableTFlow<In, Out> = network.coalescingMutableTFlow(coalesce, getInitialValue) - -/** - * Activates [spec] in a transaction and invokes [block] with the result, suspending indefinitely. - * While suspended, all observers and long-running effects are kept alive. When cancelled, observers - * are unregistered and effects are cancelled. - */ -@ExperimentalFrpApi -suspend fun <R> FrpNetwork.activateSpec(spec: FrpSpec<R>, block: suspend (R) -> Unit) { - activateSpec { - val result = spec.applySpec() - launchEffect { block(result) } - } -} - -internal class LocalFrpNetwork( - private val network: Network, - private val scope: CoroutineScope, - private val endSignal: TFlow<Any>, -) : FrpNetwork { - override suspend fun <R> transact(block: suspend FrpTransactionScope.() -> R): R { - val result = CompletableDeferred<R>(coroutineContext[Job]) - @Suppress("DeferredResultUnused") - network.transaction("FrpNetwork.transact") { - val buildScope = - BuildScopeImpl( - stateScope = StateScopeImpl(evalScope = this, endSignal = endSignal), - coroutineScope = scope, - ) - buildScope.runInBuildScope { effect { result.complete(block()) } } - } - return result.await() - } - - override suspend fun activateSpec(spec: FrpSpec<*>) { - val job = - network - .transaction("FrpNetwork.activateSpec") { - val buildScope = - BuildScopeImpl( - stateScope = StateScopeImpl(evalScope = this, endSignal = endSignal), - coroutineScope = scope, - ) - buildScope.runInBuildScope { launchScope(spec) } - } - .await() - awaitCancellationAndThen { job.cancel() } - } - - override fun <In, Out> coalescingMutableTFlow( - coalesce: (old: Out, new: In) -> Out, - getInitialValue: () -> Out, - ): CoalescingMutableTFlow<In, Out> = - CoalescingMutableTFlow(null, coalesce, network, getInitialValue) - - override fun <T> mutableTFlow(): MutableTFlow<T> = MutableTFlow(network) - - override fun <T> mutableTStateDeferred(initialValue: FrpDeferredValue<T>): MutableTState<T> = - MutableTState(network, initialValue.unwrapped) -} - -/** - * Combination of an [FrpNetwork] and a [Job] that, when cancelled, will cancel the entire FRP - * network. - */ -@ExperimentalFrpApi -class RootFrpNetwork -internal constructor(private val network: Network, private val scope: CoroutineScope, job: Job) : - Job by job, FrpNetwork by LocalFrpNetwork(network, scope, emptyTFlow) - -/** Constructs a new [RootFrpNetwork] in the given [CoroutineScope]. */ -@ExperimentalFrpApi -fun CoroutineScope.newFrpNetwork( - context: CoroutineContext = EmptyCoroutineContext -): RootFrpNetwork { - val scope = childScope(context) - val network = Network(scope) - scope.launch(CoroutineName("newFrpNetwork scheduler")) { network.runInputScheduler() } - return RootFrpNetwork(network, scope, scope.coroutineContext.job) -} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpScope.kt deleted file mode 100644 index ad6b2c8d04eb..000000000000 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpScope.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (C) 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 com.android.systemui.kairos - -import kotlin.coroutines.RestrictsSuspension -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.suspendCancellableCoroutine - -/** Denotes [FrpScope] interfaces as [DSL markers][DslMarker]. */ -@DslMarker annotation class FrpScopeMarker - -/** - * Base scope for all FRP scopes. Used to prevent implicitly capturing other scopes from in lambdas. - */ -@FrpScopeMarker -@RestrictsSuspension -@ExperimentalFrpApi -interface FrpScope { - /** - * Returns the value held by the [FrpDeferredValue], suspending until available if necessary. - */ - @ExperimentalFrpApi - @OptIn(ExperimentalCoroutinesApi::class) - suspend fun <A> FrpDeferredValue<A>.get(): A = suspendCancellableCoroutine { k -> - unwrapped.invokeOnCompletion { ex -> - ex?.let { k.resumeWithException(ex) } ?: k.resume(unwrapped.getCompleted()) - } - } -} - -/** - * A value that may not be immediately (synchronously) available, but is guaranteed to be available - * before this transaction is completed. - * - * @see FrpScope.get - */ -@ExperimentalFrpApi -class FrpDeferredValue<out A> internal constructor(internal val unwrapped: Deferred<A>) - -/** - * Returns the value held by this [FrpDeferredValue], or throws [IllegalStateException] if it is not - * yet available. - * - * This API is not meant for general usage within the FRP network. It is made available mainly for - * debugging and logging. You should always prefer [get][FrpScope.get] if possible. - * - * @see FrpScope.get - */ -@ExperimentalFrpApi -@OptIn(ExperimentalCoroutinesApi::class) -fun <A> FrpDeferredValue<A>.getUnsafe(): A = unwrapped.getCompleted() - -/** Returns an already-available [FrpDeferredValue] containing [value]. */ -@ExperimentalFrpApi -fun <A> deferredOf(value: A): FrpDeferredValue<A> = FrpDeferredValue(CompletableDeferred(value)) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpStateScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpStateScope.kt deleted file mode 100644 index c7ea6808a53e..000000000000 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpStateScope.kt +++ /dev/null @@ -1,780 +0,0 @@ -/* - * Copyright (C) 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 com.android.systemui.kairos - -import com.android.systemui.kairos.combine as combinePure -import com.android.systemui.kairos.map as mapPure -import com.android.systemui.kairos.util.Just -import com.android.systemui.kairos.util.Left -import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.Right -import com.android.systemui.kairos.util.WithPrev -import com.android.systemui.kairos.util.just -import com.android.systemui.kairos.util.map -import com.android.systemui.kairos.util.none -import com.android.systemui.kairos.util.partitionEithers -import com.android.systemui.kairos.util.zipWith -import kotlin.coroutines.RestrictsSuspension - -typealias FrpStateful<R> = suspend FrpStateScope.() -> R - -/** - * Returns a [FrpStateful] that, when [applied][FrpStateScope.applyStateful], invokes [block] with - * the applier's [FrpStateScope]. - */ -// TODO: caching story? should each Scope have a cache of applied FrpStateful instances? -@ExperimentalFrpApi -@Suppress("NOTHING_TO_INLINE") -inline fun <A> statefully(noinline block: suspend FrpStateScope.() -> A): FrpStateful<A> = block - -/** - * Operations that accumulate state within the FRP network. - * - * State accumulation is an ongoing process that has a lifetime. Use `-Latest` combinators, such as - * [mapLatestStateful], to create smaller, nested lifecycles so that accumulation isn't running - * longer than needed. - */ -@ExperimentalFrpApi -@RestrictsSuspension -interface FrpStateScope : FrpTransactionScope { - - /** TODO */ - @ExperimentalFrpApi - // TODO: wish this could just be `deferred` but alas - fun <A> deferredStateScope(block: suspend FrpStateScope.() -> A): FrpDeferredValue<A> - - /** - * Returns a [TState] that holds onto the most recently emitted value from this [TFlow], or - * [initialValue] if nothing has been emitted since it was constructed. - * - * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s - * have been processed; this keeps the value of the [TState] consistent during the entire FRP - * transaction. - */ - @ExperimentalFrpApi fun <A> TFlow<A>.holdDeferred(initialValue: FrpDeferredValue<A>): TState<A> - - /** - * Returns a [TFlow] that emits from a merged, incrementally-accumulated collection of [TFlow]s - * emitted from this, following the same "patch" rules as outlined in [foldMapIncrementally]. - * - * Conceptually this is equivalent to: - * ```kotlin - * fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally( - * initialTFlows: Map<K, TFlow<V>>, - * ): TFlow<Map<K, V>> = - * foldMapIncrementally(initialTFlows).map { it.merge() }.switch() - * ``` - * - * While the behavior is equivalent to the conceptual definition above, the implementation is - * significantly more efficient. - * - * @see merge - */ - @ExperimentalFrpApi - fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally( - initialTFlows: FrpDeferredValue<Map<K, TFlow<V>>> - ): TFlow<Map<K, V>> - - /** - * Returns a [TFlow] that emits from a merged, incrementally-accumulated collection of [TFlow]s - * emitted from this, following the same "patch" rules as outlined in [foldMapIncrementally]. - * - * Conceptually this is equivalent to: - * ```kotlin - * fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPrompt( - * initialTFlows: Map<K, TFlow<V>>, - * ): TFlow<Map<K, V>> = - * foldMapIncrementally(initialTFlows).map { it.merge() }.switchPromptly() - * ``` - * - * While the behavior is equivalent to the conceptual definition above, the implementation is - * significantly more efficient. - * - * @see merge - */ - @ExperimentalFrpApi - fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPromptly( - initialTFlows: FrpDeferredValue<Map<K, TFlow<V>>> - ): TFlow<Map<K, V>> - - // TODO: everything below this comment can be made into extensions once we have context params - - /** - * Returns a [TFlow] that emits from a merged, incrementally-accumulated collection of [TFlow]s - * emitted from this, following the same "patch" rules as outlined in [foldMapIncrementally]. - * - * Conceptually this is equivalent to: - * ```kotlin - * fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally( - * initialTFlows: Map<K, TFlow<V>>, - * ): TFlow<Map<K, V>> = - * foldMapIncrementally(initialTFlows).map { it.merge() }.switch() - * ``` - * - * While the behavior is equivalent to the conceptual definition above, the implementation is - * significantly more efficient. - * - * @see merge - */ - @ExperimentalFrpApi - fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally( - initialTFlows: Map<K, TFlow<V>> = emptyMap() - ): TFlow<Map<K, V>> = mergeIncrementally(deferredOf(initialTFlows)) - - /** - * Returns a [TFlow] that emits from a merged, incrementally-accumulated collection of [TFlow]s - * emitted from this, following the same "patch" rules as outlined in [foldMapIncrementally]. - * - * Conceptually this is equivalent to: - * ```kotlin - * fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPrompt( - * initialTFlows: Map<K, TFlow<V>>, - * ): TFlow<Map<K, V>> = - * foldMapIncrementally(initialTFlows).map { it.merge() }.switchPromptly() - * ``` - * - * While the behavior is equivalent to the conceptual definition above, the implementation is - * significantly more efficient. - * - * @see merge - */ - @ExperimentalFrpApi - fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPromptly( - initialTFlows: Map<K, TFlow<V>> = emptyMap() - ): TFlow<Map<K, V>> = mergeIncrementallyPromptly(deferredOf(initialTFlows)) - - /** Applies the [FrpStateful] within this [FrpStateScope]. */ - @ExperimentalFrpApi suspend fun <A> FrpStateful<A>.applyStateful(): A = this() - - /** - * Applies the [FrpStateful] within this [FrpStateScope], returning the result as an - * [FrpDeferredValue]. - */ - @ExperimentalFrpApi - fun <A> FrpStateful<A>.applyStatefulDeferred(): FrpDeferredValue<A> = deferredStateScope { - applyStateful() - } - - /** - * Returns a [TState] that holds onto the most recently emitted value from this [TFlow], or - * [initialValue] if nothing has been emitted since it was constructed. - * - * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s - * have been processed; this keeps the value of the [TState] consistent during the entire FRP - * transaction. - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.hold(initialValue: A): TState<A> = holdDeferred(deferredOf(initialValue)) - - /** - * Returns a [TFlow] the emits the result of applying [FrpStatefuls][FrpStateful] emitted from - * the original [TFlow]. - * - * Unlike [applyLatestStateful], state accumulation is not stopped with each subsequent emission - * of the original [TFlow]. - */ - @ExperimentalFrpApi fun <A> TFlow<FrpStateful<A>>.applyStatefuls(): TFlow<A> - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow]. - * - * [transform] can perform state accumulation via its [FrpStateScope] receiver. Unlike - * [mapLatestStateful], accumulation is not stopped with each subsequent emission of the - * original [TFlow]. - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.mapStateful(transform: suspend FrpStateScope.(A) -> B): TFlow<B> = - mapPure { statefully { transform(it) } }.applyStatefuls() - - /** - * Returns a [TState] the holds the result of applying the [FrpStateful] held by the original - * [TState]. - * - * Unlike [applyLatestStateful], state accumulation is not stopped with each state change. - */ - @ExperimentalFrpApi - fun <A> TState<FrpStateful<A>>.applyStatefuls(): TState<A> = - stateChanges - .applyStatefuls() - .holdDeferred(initialValue = deferredStateScope { sampleDeferred().get()() }) - - /** Returns a [TFlow] that switches to the [TFlow] emitted by the original [TFlow]. */ - @ExperimentalFrpApi fun <A> TFlow<TFlow<A>>.flatten() = hold(emptyTFlow).switch() - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow]. - * - * [transform] can perform state accumulation via its [FrpStateScope] receiver. With each - * invocation of [transform], state accumulation from previous invocation is stopped. - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.mapLatestStateful(transform: suspend FrpStateScope.(A) -> B): TFlow<B> = - mapPure { statefully { transform(it) } }.applyLatestStateful() - - /** - * Returns a [TFlow] that switches to a new [TFlow] produced by [transform] every time the - * original [TFlow] emits a value. - * - * [transform] can perform state accumulation via its [FrpStateScope] receiver. With each - * invocation of [transform], state accumulation from previous invocation is stopped. - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.flatMapLatestStateful( - transform: suspend FrpStateScope.(A) -> TFlow<B> - ): TFlow<B> = mapLatestStateful(transform).flatten() - - /** - * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the - * original [TFlow]. - * - * When each [FrpStateful] is applied, state accumulation from the previously-active - * [FrpStateful] is stopped. - */ - @ExperimentalFrpApi - fun <A> TFlow<FrpStateful<A>>.applyLatestStateful(): TFlow<A> = applyLatestStateful {}.first - - /** - * Returns a [TState] containing the value returned by applying the [FrpStateful] held by the - * original [TState]. - * - * When each [FrpStateful] is applied, state accumulation from the previously-active - * [FrpStateful] is stopped. - */ - @ExperimentalFrpApi - fun <A> TState<FrpStateful<A>>.applyLatestStateful(): TState<A> { - val (changes, init) = stateChanges.applyLatestStateful { sample()() } - return changes.holdDeferred(init) - } - - /** - * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [init] - * immediately. - * - * When each [FrpStateful] is applied, state accumulation from the previously-active - * [FrpStateful] is stopped. - */ - @ExperimentalFrpApi - fun <A, B> TFlow<FrpStateful<B>>.applyLatestStateful( - init: FrpStateful<A> - ): Pair<TFlow<B>, FrpDeferredValue<A>> { - val (flow, result) = - mapCheap { spec -> mapOf(Unit to just(spec)) } - .applyLatestStatefulForKey(init = mapOf(Unit to init), numKeys = 1) - val outFlow: TFlow<B> = - flow.mapMaybe { - checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" } - } - val outInit: FrpDeferredValue<A> = deferredTransactionScope { - val initResult: Map<Unit, A> = result.get() - check(Unit in initResult) { - "applyLatest: expected initial result, but none present in: $initResult" - } - @Suppress("UNCHECKED_CAST") - initResult.getOrDefault(Unit) { null } as A - } - return Pair(outFlow, outInit) - } - - /** - * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [init] - * immediately. - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpStateful] will be stopped with no replacement. - * - * When each [FrpStateful] is applied, state accumulation from the previously-active - * [FrpStateful] with the same key is stopped. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKey( - init: FrpDeferredValue<Map<K, FrpStateful<B>>>, - numKeys: Int? = null, - ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> - - /** - * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [init] - * immediately. - * - * When each [FrpStateful] is applied, state accumulation from the previously-active - * [FrpStateful] with the same key is stopped. - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpStateful] will be stopped with no replacement. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKey( - init: Map<K, FrpStateful<B>>, - numKeys: Int? = null, - ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> = - applyLatestStatefulForKey(deferredOf(init), numKeys) - - /** - * Returns a [TState] containing the latest results of applying each [FrpStateful] emitted from - * the original [TFlow]. - * - * When each [FrpStateful] is applied, state accumulation from the previously-active - * [FrpStateful] with the same key is stopped. - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpStateful] will be stopped with no replacement. - */ - @ExperimentalFrpApi - fun <K, A> TFlow<Map<K, Maybe<FrpStateful<A>>>>.holdLatestStatefulForKey( - init: FrpDeferredValue<Map<K, FrpStateful<A>>>, - numKeys: Int? = null, - ): TState<Map<K, A>> { - val (changes, initialValues) = applyLatestStatefulForKey(init, numKeys) - return changes.foldMapIncrementally(initialValues) - } - - /** - * Returns a [TState] containing the latest results of applying each [FrpStateful] emitted from - * the original [TFlow]. - * - * When each [FrpStateful] is applied, state accumulation from the previously-active - * [FrpStateful] with the same key is stopped. - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpStateful] will be stopped with no replacement. - */ - @ExperimentalFrpApi - fun <K, A> TFlow<Map<K, Maybe<FrpStateful<A>>>>.holdLatestStatefulForKey( - init: Map<K, FrpStateful<A>> = emptyMap(), - numKeys: Int? = null, - ): TState<Map<K, A>> = holdLatestStatefulForKey(deferredOf(init), numKeys) - - /** - * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [init] - * immediately. - * - * When each [FrpStateful] is applied, state accumulation from the previously-active - * [FrpStateful] with the same key is stopped. - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpStateful] will be stopped with no replacement. - */ - @ExperimentalFrpApi - fun <K, A> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKey( - numKeys: Int? = null - ): TFlow<Map<K, Maybe<A>>> = - applyLatestStatefulForKey(init = emptyMap<K, FrpStateful<*>>(), numKeys = numKeys).first - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to - * [initialValues] immediately. - * - * [transform] can perform state accumulation via its [FrpStateScope] receiver. With each - * invocation of [transform], state accumulation from previous invocation is stopped. - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpStateScope] will be stopped with no replacement. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestStatefulForKey( - initialValues: FrpDeferredValue<Map<K, A>>, - numKeys: Int? = null, - transform: suspend FrpStateScope.(A) -> B, - ): Pair<TFlow<Map<K, Maybe<B>>>, FrpDeferredValue<Map<K, B>>> = - mapPure { patch -> patch.mapValues { (_, v) -> v.map { statefully { transform(it) } } } } - .applyLatestStatefulForKey( - deferredStateScope { - initialValues.get().mapValues { (_, v) -> statefully { transform(v) } } - }, - numKeys = numKeys, - ) - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to - * [initialValues] immediately. - * - * [transform] can perform state accumulation via its [FrpStateScope] receiver. With each - * invocation of [transform], state accumulation from previous invocation is stopped. - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpStateScope] will be stopped with no replacement. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestStatefulForKey( - initialValues: Map<K, A>, - numKeys: Int? = null, - transform: suspend FrpStateScope.(A) -> B, - ): Pair<TFlow<Map<K, Maybe<B>>>, FrpDeferredValue<Map<K, B>>> = - mapLatestStatefulForKey(deferredOf(initialValues), numKeys, transform) - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow]. - * - * [transform] can perform state accumulation via its [FrpStateScope] receiver. With each - * invocation of [transform], state accumulation from previous invocation is stopped. - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpStateScope] will be stopped with no replacement. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestStatefulForKey( - numKeys: Int? = null, - transform: suspend FrpStateScope.(A) -> B, - ): TFlow<Map<K, Maybe<B>>> = mapLatestStatefulForKey(emptyMap(), numKeys, transform).first - - /** - * Returns a [TFlow] that will only emit the next event of the original [TFlow], and then will - * act as [emptyTFlow]. - * - * If the original [TFlow] is emitting an event at this exact time, then it will be the only - * even emitted from the result [TFlow]. - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.nextOnly(): TFlow<A> = - if (this === emptyTFlow) { - this - } else { - TFlowLoop<A>().also { - it.loopback = it.mapCheap { emptyTFlow }.hold(this@nextOnly).switch() - } - } - - /** Returns a [TFlow] that skips the next emission of the original [TFlow]. */ - @ExperimentalFrpApi - fun <A> TFlow<A>.skipNext(): TFlow<A> = - if (this === emptyTFlow) { - this - } else { - nextOnly().mapCheap { this@skipNext }.hold(emptyTFlow).switch() - } - - /** - * Returns a [TFlow] that emits values from the original [TFlow] up until [stop] emits a value. - * - * If the original [TFlow] emits at the same time as [stop], then the returned [TFlow] will emit - * that value. - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.takeUntil(stop: TFlow<*>): TFlow<A> = - if (stop === emptyTFlow) { - this - } else { - stop.mapCheap { emptyTFlow }.nextOnly().hold(this).switch() - } - - /** - * Invokes [stateful] in a new [FrpStateScope] that is a child of this one. - * - * This new scope is stopped when [stop] first emits a value, or when the parent scope is - * stopped. Stopping will end all state accumulation; any [TStates][TState] returned from this - * scope will no longer update. - */ - @ExperimentalFrpApi - fun <A> childStateScope(stop: TFlow<*>, stateful: FrpStateful<A>): FrpDeferredValue<A> { - val (_, init: FrpDeferredValue<Map<Unit, A>>) = - stop - .nextOnly() - .mapPure { mapOf(Unit to none<FrpStateful<A>>()) } - .applyLatestStatefulForKey(init = mapOf(Unit to stateful), numKeys = 1) - return deferredStateScope { init.get().getValue(Unit) } - } - - /** - * Returns a [TFlow] that emits values from the original [TFlow] up to and including a value is - * emitted that satisfies [predicate]. - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.takeUntil(predicate: suspend FrpTransactionScope.(A) -> Boolean): TFlow<A> = - takeUntil(filter(predicate)) - - /** - * Returns a [TState] that is incrementally updated when this [TFlow] emits a value, by applying - * [transform] to both the emitted value and the currently tracked state. - * - * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s - * have been processed; this keeps the value of the [TState] consistent during the entire FRP - * transaction. - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.fold( - initialValue: B, - transform: suspend FrpTransactionScope.(A, B) -> B, - ): TState<B> { - lateinit var state: TState<B> - return mapPure { a -> transform(a, state.sample()) }.hold(initialValue).also { state = it } - } - - /** - * Returns a [TState] that is incrementally updated when this [TFlow] emits a value, by applying - * [transform] to both the emitted value and the currently tracked state. - * - * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s - * have been processed; this keeps the value of the [TState] consistent during the entire FRP - * transaction. - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.foldDeferred( - initialValue: FrpDeferredValue<B>, - transform: suspend FrpTransactionScope.(A, B) -> B, - ): TState<B> { - lateinit var state: TState<B> - return mapPure { a -> transform(a, state.sample()) } - .holdDeferred(initialValue) - .also { state = it } - } - - /** - * Returns a [TState] that holds onto the result of applying the most recently emitted - * [FrpStateful] this [TFlow], or [init] if nothing has been emitted since it was constructed. - * - * When each [FrpStateful] is applied, state accumulation from the previously-active - * [FrpStateful] is stopped. - * - * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s - * have been processed; this keeps the value of the [TState] consistent during the entire FRP - * transaction. - * - * Shorthand for: - * ```kotlin - * val (changes, initApplied) = applyLatestStateful(init) - * return changes.toTStateDeferred(initApplied) - * ``` - */ - @ExperimentalFrpApi - fun <A> TFlow<FrpStateful<A>>.holdLatestStateful(init: FrpStateful<A>): TState<A> { - val (changes, initApplied) = applyLatestStateful(init) - return changes.holdDeferred(initApplied) - } - - /** - * Returns a [TFlow] that emits the two most recent emissions from the original [TFlow]. - * [initialValue] is used as the previous value for the first emission. - * - * Shorthand for `sample(hold(init)) { new, old -> Pair(old, new) }` - */ - @ExperimentalFrpApi - fun <S, T : S> TFlow<T>.pairwise(initialValue: S): TFlow<WithPrev<S, T>> { - val previous = hold(initialValue) - return mapCheap { new -> WithPrev(previousValue = previous.sample(), newValue = new) } - } - - /** - * Returns a [TFlow] that emits the two most recent emissions from the original [TFlow]. Note - * that the returned [TFlow] will not emit until the original [TFlow] has emitted twice. - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.pairwise(): TFlow<WithPrev<A, A>> = - mapCheap { just(it) } - .pairwise(none) - .mapMaybe { (prev, next) -> prev.zipWith(next, ::WithPrev) } - - /** - * Returns a [TState] that holds both the current and previous values of the original [TState]. - * [initialPreviousValue] is used as the first previous value. - * - * Shorthand for `sample(hold(init)) { new, old -> Pair(old, new) }` - */ - @ExperimentalFrpApi - fun <S, T : S> TState<T>.pairwise(initialPreviousValue: S): TState<WithPrev<S, T>> = - stateChanges - .pairwise(initialPreviousValue) - .holdDeferred(deferredTransactionScope { WithPrev(initialPreviousValue, sample()) }) - - /** - * Returns a [TState] holding a [Map] that is updated incrementally whenever this emits a value. - * - * The value emitted is used as a "patch" for the tracked [Map]; for each key [K] in the emitted - * map, an associated value of [Just] will insert or replace the value in the tracked [Map], and - * an associated value of [none] will remove the key from the tracked [Map]. - */ - @ExperimentalFrpApi - fun <K, V> TFlow<Map<K, Maybe<V>>>.foldMapIncrementally( - initialValues: FrpDeferredValue<Map<K, V>> - ): TState<Map<K, V>> = - foldDeferred(initialValues) { patch, map -> - val (adds: List<Pair<K, V>>, removes: List<K>) = - patch - .asSequence() - .map { (k, v) -> if (v is Just) Left(k to v.value) else Right(k) } - .partitionEithers() - val removed: Map<K, V> = map - removes.toSet() - val updated: Map<K, V> = removed + adds - updated - } - - /** - * Returns a [TState] holding a [Map] that is updated incrementally whenever this emits a value. - * - * The value emitted is used as a "patch" for the tracked [Map]; for each key [K] in the emitted - * map, an associated value of [Just] will insert or replace the value in the tracked [Map], and - * an associated value of [none] will remove the key from the tracked [Map]. - */ - @ExperimentalFrpApi - fun <K, V> TFlow<Map<K, Maybe<V>>>.foldMapIncrementally( - initialValues: Map<K, V> = emptyMap() - ): TState<Map<K, V>> = foldMapIncrementally(deferredOf(initialValues)) - - /** - * Returns a [TFlow] that wraps each emission of the original [TFlow] into an [IndexedValue], - * containing the emitted value and its index (starting from zero). - * - * Shorthand for: - * ``` - * val index = fold(0) { _, oldIdx -> oldIdx + 1 } - * sample(index) { a, idx -> IndexedValue(idx, a) } - * ``` - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.withIndex(): TFlow<IndexedValue<A>> { - val index = fold(0) { _, old -> old + 1 } - return sample(index) { a, idx -> IndexedValue(idx, a) } - } - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow] and its index (starting from zero). - * - * Shorthand for: - * ``` - * withIndex().map { (idx, a) -> transform(idx, a) } - * ``` - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.mapIndexed(transform: suspend FrpTransactionScope.(Int, A) -> B): TFlow<B> { - val index = fold(0) { _, i -> i + 1 } - return sample(index) { a, idx -> transform(idx, a) } - } - - /** Returns a [TFlow] where all subsequent repetitions of the same value are filtered out. */ - @ExperimentalFrpApi - fun <A> TFlow<A>.distinctUntilChanged(): TFlow<A> { - val state: TState<Any?> = hold(Any()) - return filter { it != state.sample() } - } - - /** - * Returns a new [TFlow] that emits at the same rate as the original [TFlow], but combines the - * emitted value with the most recent emission from [other] using [transform]. - * - * Note that the returned [TFlow] will not emit anything until [other] has emitted at least one - * value. - */ - @ExperimentalFrpApi - fun <A, B, C> TFlow<A>.sample( - other: TFlow<B>, - transform: suspend FrpTransactionScope.(A, B) -> C, - ): TFlow<C> { - val state = other.mapCheap { just(it) }.hold(none) - return sample(state) { a, b -> b.map { transform(a, it) } }.filterJust() - } - - /** - * Returns a [TState] that samples the [Transactional] held by the given [TState] within the - * same transaction that the state changes. - */ - @ExperimentalFrpApi - fun <A> TState<Transactional<A>>.sampleTransactionals(): TState<A> = - stateChanges - .sampleTransactionals() - .holdDeferred(deferredTransactionScope { sample().sample() }) - - /** - * Returns a [TState] that transforms the value held inside this [TState] by applying it to the - * given function [transform]. - */ - @ExperimentalFrpApi - fun <A, B> TState<A>.map(transform: suspend FrpTransactionScope.(A) -> B): TState<B> = - mapPure { transactionally { transform(it) } }.sampleTransactionals() - - /** - * Returns a [TState] whose value is generated with [transform] by combining the current values - * of each given [TState]. - * - * @see TState.combineWith - */ - @ExperimentalFrpApi - fun <A, B, Z> combine( - stateA: TState<A>, - stateB: TState<B>, - transform: suspend FrpTransactionScope.(A, B) -> Z, - ): TState<Z> = - com.android.systemui.kairos - .combine(stateA, stateB) { a, b -> transactionally { transform(a, b) } } - .sampleTransactionals() - - /** - * Returns a [TState] whose value is generated with [transform] by combining the current values - * of each given [TState]. - * - * @see TState.combineWith - */ - @ExperimentalFrpApi - fun <A, B, C, D, Z> combine( - stateA: TState<A>, - stateB: TState<B>, - stateC: TState<C>, - stateD: TState<D>, - transform: suspend FrpTransactionScope.(A, B, C, D) -> Z, - ): TState<Z> = - com.android.systemui.kairos - .combine(stateA, stateB, stateC, stateD) { a, b, c, d -> - transactionally { transform(a, b, c, d) } - } - .sampleTransactionals() - - /** Returns a [TState] by applying [transform] to the value held by the original [TState]. */ - @ExperimentalFrpApi - fun <A, B> TState<A>.flatMap( - transform: suspend FrpTransactionScope.(A) -> TState<B> - ): TState<B> = mapPure { transactionally { transform(it) } }.sampleTransactionals().flatten() - - /** - * Returns a [TState] whose value is generated with [transform] by combining the current values - * of each given [TState]. - * - * @see TState.combineWith - */ - @ExperimentalFrpApi - fun <A, Z> combine( - vararg states: TState<A>, - transform: suspend FrpTransactionScope.(List<A>) -> Z, - ): TState<Z> = combinePure(*states).map(transform) - - /** - * Returns a [TState] whose value is generated with [transform] by combining the current values - * of each given [TState]. - * - * @see TState.combineWith - */ - @ExperimentalFrpApi - fun <A, Z> Iterable<TState<A>>.combine( - transform: suspend FrpTransactionScope.(List<A>) -> Z - ): TState<Z> = combinePure().map(transform) - - /** - * Returns a [TState] by combining the values held inside the given [TState]s by applying them - * to the given function [transform]. - */ - @ExperimentalFrpApi - fun <A, B, C> TState<A>.combineWith( - other: TState<B>, - transform: suspend FrpTransactionScope.(A, B) -> C, - ): TState<C> = combine(this, other, transform) -} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpTransactionScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpTransactionScope.kt deleted file mode 100644 index a7ae1d9646b3..000000000000 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpTransactionScope.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 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 com.android.systemui.kairos - -import kotlin.coroutines.RestrictsSuspension - -/** - * FRP operations that are available while a transaction is active. - * - * These operations do not accumulate state, which makes [FrpTransactionScope] weaker than - * [FrpStateScope], but allows them to be used in more places. - */ -@ExperimentalFrpApi -@RestrictsSuspension -interface FrpTransactionScope : FrpScope { - - /** - * Returns the current value of this [Transactional] as a [FrpDeferredValue]. - * - * @see sample - */ - @ExperimentalFrpApi fun <A> Transactional<A>.sampleDeferred(): FrpDeferredValue<A> - - /** - * Returns the current value of this [TState] as a [FrpDeferredValue]. - * - * @see sample - */ - @ExperimentalFrpApi fun <A> TState<A>.sampleDeferred(): FrpDeferredValue<A> - - /** TODO */ - @ExperimentalFrpApi - fun <A> deferredTransactionScope( - block: suspend FrpTransactionScope.() -> A - ): FrpDeferredValue<A> - - /** A [TFlow] that emits once, within this transaction, and then never again. */ - @ExperimentalFrpApi val now: TFlow<Unit> - - /** - * Returns the current value held by this [TState]. Guaranteed to be consistent within the same - * transaction. - */ - @ExperimentalFrpApi suspend fun <A> TState<A>.sample(): A = sampleDeferred().get() - - /** - * Returns the current value held by this [Transactional]. Guaranteed to be consistent within - * the same transaction. - */ - @ExperimentalFrpApi suspend fun <A> Transactional<A>.sample(): A = sampleDeferred().get() -} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt new file mode 100644 index 000000000000..c95b9e83594f --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt @@ -0,0 +1,297 @@ +/* + * Copyright (C) 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 com.android.systemui.kairos + +import com.android.systemui.kairos.internal.CompletableLazy +import com.android.systemui.kairos.internal.IncrementalImpl +import com.android.systemui.kairos.internal.Init +import com.android.systemui.kairos.internal.InitScope +import com.android.systemui.kairos.internal.NoScope +import com.android.systemui.kairos.internal.awaitValues +import com.android.systemui.kairos.internal.constIncremental +import com.android.systemui.kairos.internal.constInit +import com.android.systemui.kairos.internal.init +import com.android.systemui.kairos.internal.mapImpl +import com.android.systemui.kairos.internal.mapValuesImpl +import com.android.systemui.kairos.internal.store.ConcurrentHashMapK +import com.android.systemui.kairos.internal.switchDeferredImpl +import com.android.systemui.kairos.internal.switchPromptImpl +import com.android.systemui.kairos.internal.util.hashString +import com.android.systemui.kairos.util.MapPatch +import com.android.systemui.kairos.util.map +import com.android.systemui.kairos.util.mapPatchFromFullDiff +import kotlin.reflect.KProperty + +/** A [State] tracking a [Map] that receives incremental updates. */ +sealed class Incremental<K, out V> : State<Map<K, V>>() { + abstract override val init: Init<IncrementalImpl<K, V>> +} + +/** An [Incremental] that never changes. */ +@ExperimentalKairosApi +fun <K, V> incrementalOf(value: Map<K, V>): Incremental<K, V> { + val operatorName = "stateOf" + val name = "$operatorName($value)" + return IncrementalInit(constInit(name, constIncremental(name, operatorName, value))) +} + +/** + * Returns an [Incremental] that acts as a deferred-reference to the [Incremental] produced by this + * [Lazy]. + * + * When the returned [Incremental] is accessed by the Kairos network, the [Lazy]'s + * [value][Lazy.value] will be queried and used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi +fun <K, V> Lazy<Incremental<K, V>>.defer(): Incremental<K, V> = deferInline { value } + +/** + * Returns an [Incremental] that acts as a deferred-reference to the [Incremental] produced by this + * [DeferredValue]. + * + * When the returned [Incremental] is accessed by the Kairos network, the [DeferredValue] will be + * queried and used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi +fun <K, V> DeferredValue<Incremental<K, V>>.defer(): Incremental<K, V> = deferInline { + unwrapped.value +} + +/** + * Returns an [Incremental] that acts as a deferred-reference to the [Incremental] produced by + * [block]. + * + * When the returned [Incremental] is accessed by the Kairos network, [block] will be invoked and + * the returned [Incremental] will be used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi +fun <K, V> deferredIncremental(block: KairosScope.() -> Incremental<K, V>): Incremental<K, V> = + deferInline { + NoScope.block() + } + +/** + * An [Events] that emits every time this [Incremental] changes, containing the subset of the map + * that has changed. + * + * @see MapPatch + */ +val <K, V> Incremental<K, V>.updates: Events<MapPatch<K, V>> + get() = EventsInit(init("patches") { init.connect(this).patches }) + +internal class IncrementalInit<K, V>(override val init: Init<IncrementalImpl<K, V>>) : + Incremental<K, V>() + +/** + * Returns an [Incremental] that tracks the entries of the original incremental, but values replaced + * with those obtained by applying [transform] to each original entry. + */ +fun <K, V, U> Incremental<K, V>.mapValues( + transform: KairosScope.(Map.Entry<K, V>) -> U +): Incremental<K, U> { + val operatorName = "mapValues" + val name = operatorName + return IncrementalInit( + init(name) { + mapValuesImpl({ init.connect(this) }, name, operatorName) { NoScope.transform(it) } + } + ) +} + +/** + * Returns an [Events] that emits from a merged, incrementally-accumulated collection of [Events] + * emitted from this, following the same "patch" rules as outlined in + * [StateScope.foldStateMapIncrementally]. + * + * Conceptually this is equivalent to: + * ```kotlin + * fun <K, V> State<Map<K, V>>.mergeEventsIncrementally(): Events<Map<K, V>> = + * map { it.merge() }.switchEvents() + * ``` + * + * While the behavior is equivalent to the conceptual definition above, the implementation is + * significantly more efficient. + * + * @see merge + */ +fun <K, V> Incremental<K, Events<V>>.mergeEventsIncrementally(): Events<Map<K, V>> { + val operatorName = "mergeEventsIncrementally" + val name = operatorName + val patches = + mapImpl({ init.connect(this).patches }) { patch, _ -> + patch.mapValues { (_, m) -> m.map { events -> events.init.connect(this) } }.asIterable() + } + return EventsInit( + constInit( + name, + switchDeferredImpl( + name = name, + getStorage = { + init + .connect(this) + .getCurrentWithEpoch(this) + .first + .mapValues { (_, events) -> events.init.connect(this) } + .asIterable() + }, + getPatches = { patches }, + storeFactory = ConcurrentHashMapK.Factory(), + ) + .awaitValues(), + ) + ) +} + +/** + * Returns an [Events] that emits from a merged, incrementally-accumulated collection of [Events] + * emitted from this, following the same "patch" rules as outlined in + * [StateScope.foldStateMapIncrementally]. + * + * Conceptually this is equivalent to: + * ```kotlin + * fun <K, V> State<Map<K, V>>.mergeEventsIncrementallyPromptly(): Events<Map<K, V>> = + * map { it.merge() }.switchEventsPromptly() + * ``` + * + * While the behavior is equivalent to the conceptual definition above, the implementation is + * significantly more efficient. + * + * @see merge + */ +fun <K, V> Incremental<K, Events<V>>.mergeEventsIncrementallyPromptly(): Events<Map<K, V>> { + val operatorName = "mergeEventsIncrementally" + val name = operatorName + val patches = + mapImpl({ init.connect(this).patches }) { patch, _ -> + patch.mapValues { (_, m) -> m.map { events -> events.init.connect(this) } }.asIterable() + } + return EventsInit( + constInit( + name, + switchPromptImpl( + name = name, + getStorage = { + init + .connect(this) + .getCurrentWithEpoch(this) + .first + .mapValues { (_, events) -> events.init.connect(this) } + .asIterable() + }, + getPatches = { patches }, + storeFactory = ConcurrentHashMapK.Factory(), + ) + .awaitValues(), + ) + ) +} + +/** A forward-reference to an [Incremental], allowing for recursive definitions. */ +@ExperimentalKairosApi +class IncrementalLoop<K, V>(private val name: String? = null) : Incremental<K, V>() { + + private val deferred = CompletableLazy<Incremental<K, V>>(name = name) + + override val init: Init<IncrementalImpl<K, V>> = + init(name) { deferred.value.init.connect(evalScope = this) } + + /** The [Incremental] this [IncrementalLoop] will forward to. */ + var loopback: Incremental<K, V>? = null + set(value) { + value?.let { + check(!deferred.isInitialized()) { + "IncrementalLoop($name).loopback has already been set." + } + deferred.setValue(value) + field = value + } + } + + operator fun getValue(thisRef: Any?, property: KProperty<*>): Incremental<K, V> = this + + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Incremental<K, V>) { + loopback = value + } + + override fun toString(): String = "${this::class.simpleName}($name)@$hashString" +} + +/** + * Returns an [Incremental] whose [updates] are calculated by diffing the given [State]'s + * [transitions]. + */ +fun <K, V> State<Map<K, V>>.asIncremental(): Incremental<K, V> { + if (this is Incremental<K, V>) return this + + val hashState = map { if (it is HashMap) it else HashMap(it) } + + val patches = + transitions.mapNotNull { (old, new) -> + mapPatchFromFullDiff(old, new).takeIf { it.isNotEmpty() } + } + + return IncrementalInit( + init("asIncremental") { + val upstream = hashState.init.connect(this) + IncrementalImpl( + upstream.name, + upstream.operatorName, + upstream.changes, + patches.init.connect(this), + upstream.store, + ) + } + ) +} + +/** Returns an [Incremental] that acts like the current value of the given [State]. */ +fun <K, V> State<Incremental<K, V>>.switchIncremental(): Incremental<K, V> { + val stateChangePatches = + transitions.mapNotNull { (old, new) -> + mapPatchFromFullDiff(old.sample(), new.sample()).takeIf { it.isNotEmpty() } + } + val innerChanges = + map { inner -> + merge(stateChangePatches, inner.updates) { switchPatch, upcomingPatch -> + switchPatch + upcomingPatch + } + } + .switchEventsPromptly() + val flattened = flatten() + return IncrementalInit( + init("switchIncremental") { + val upstream = flattened.init.connect(this) + IncrementalImpl( + "switchIncremental", + "switchIncremental", + upstream.changes, + innerChanges.init.connect(this), + upstream.store, + ) + } + ) +} + +private inline fun <K, V> deferInline( + crossinline block: InitScope.() -> Incremental<K, V> +): Incremental<K, V> = IncrementalInit(init(name = null) { block().init.connect(evalScope = this) }) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt new file mode 100644 index 000000000000..77598b30658a --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt @@ -0,0 +1,216 @@ +/* + * Copyright (C) 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 com.android.systemui.kairos + +import com.android.systemui.kairos.internal.BuildScopeImpl +import com.android.systemui.kairos.internal.Network +import com.android.systemui.kairos.internal.StateScopeImpl +import com.android.systemui.kairos.internal.util.awaitCancellationAndThen +import com.android.systemui.kairos.internal.util.childScope +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.job +import kotlinx.coroutines.launch + +/** + * Marks declarations that are still **experimental** and shouldn't be used in general production + * code. + */ +@RequiresOptIn( + message = "This API is experimental and should not be used in general production code." +) +@Retention(AnnotationRetention.BINARY) +annotation class ExperimentalKairosApi + +/** + * External interface to a Kairos network of reactive components. Can be used to make transactional + * queries and modifications to the network. + */ +@ExperimentalKairosApi +interface KairosNetwork { + /** + * Runs [block] inside of a transaction, suspending until the transaction is complete. + * + * The [BuildScope] receiver exposes methods that can be used to query or modify the network. If + * the network is cancelled while the caller of [transact] is suspended, then the call will be + * cancelled. + */ + suspend fun <R> transact(block: TransactionScope.() -> R): R + + /** + * Activates [spec] in a transaction, suspending indefinitely. While suspended, all observers + * and long-running effects are kept alive. When cancelled, observers are unregistered and + * effects are cancelled. + */ + suspend fun activateSpec(spec: BuildSpec<*>) + + /** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */ + fun <In, Out> coalescingMutableEvents( + coalesce: (old: Out, new: In) -> Out, + getInitialValue: () -> Out, + ): CoalescingMutableEvents<In, Out> + + /** Returns a [MutableState] that can emit values into this [KairosNetwork]. */ + fun <T> mutableEvents(): MutableEvents<T> + + /** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */ + fun <T> conflatedMutableEvents(): CoalescingMutableEvents<T, T> + + /** Returns a [MutableState]. with initial state [initialValue]. */ + fun <T> mutableStateDeferred(initialValue: DeferredValue<T>): MutableState<T> +} + +/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */ +@ExperimentalKairosApi +fun <In, Out> KairosNetwork.coalescingMutableEvents( + coalesce: (old: Out, new: In) -> Out, + initialValue: Out, +): CoalescingMutableEvents<In, Out> = + coalescingMutableEvents(coalesce, getInitialValue = { initialValue }) + +/** Returns a [MutableState] with initial state [initialValue]. */ +@ExperimentalKairosApi +fun <T> KairosNetwork.mutableState(initialValue: T): MutableState<T> = + mutableStateDeferred(deferredOf(initialValue)) + +/** Returns a [MutableState] with initial state [initialValue]. */ +@ExperimentalKairosApi +fun <T> MutableState(network: KairosNetwork, initialValue: T): MutableState<T> = + network.mutableState(initialValue) + +/** Returns a [MutableEvents] that can emit values into this [KairosNetwork]. */ +@ExperimentalKairosApi +fun <T> MutableEvents(network: KairosNetwork): MutableEvents<T> = network.mutableEvents() + +/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */ +@ExperimentalKairosApi +fun <In, Out> CoalescingMutableEvents( + network: KairosNetwork, + coalesce: (old: Out, new: In) -> Out, + initialValue: Out, +): CoalescingMutableEvents<In, Out> = network.coalescingMutableEvents(coalesce) { initialValue } + +/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */ +@ExperimentalKairosApi +fun <In, Out> CoalescingMutableEvents( + network: KairosNetwork, + coalesce: (old: Out, new: In) -> Out, + getInitialValue: () -> Out, +): CoalescingMutableEvents<In, Out> = network.coalescingMutableEvents(coalesce, getInitialValue) + +/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */ +@ExperimentalKairosApi +fun <T> ConflatedMutableEvents(network: KairosNetwork): CoalescingMutableEvents<T, T> = + network.conflatedMutableEvents() + +/** + * Activates [spec] in a transaction and invokes [block] with the result, suspending indefinitely. + * While suspended, all observers and long-running effects are kept alive. When cancelled, observers + * are unregistered and effects are cancelled. + */ +@ExperimentalKairosApi +suspend fun <R> KairosNetwork.activateSpec(spec: BuildSpec<R>, block: suspend (R) -> Unit) { + activateSpec { + val result = spec.applySpec() + launchEffect { block(result) } + } +} + +internal class LocalNetwork( + private val network: Network, + private val scope: CoroutineScope, + private val endSignal: Events<Any>, +) : KairosNetwork { + override suspend fun <R> transact(block: TransactionScope.() -> R): R = + network.transaction("KairosNetwork.transact") { block() }.await() + + override suspend fun activateSpec(spec: BuildSpec<*>) { + val stopEmitter = + CoalescingMutableEvents( + name = "activateSpec", + coalesce = { _, _: Unit -> }, + network = network, + getInitialValue = {}, + ) + val job = + network + .transaction("KairosNetwork.activateSpec") { + val buildScope = + BuildScopeImpl( + stateScope = + StateScopeImpl( + evalScope = this, + endSignal = mergeLeft(stopEmitter, endSignal), + ), + coroutineScope = scope, + ) + buildScope.launchScope(spec) + } + .await() + awaitCancellationAndThen { + stopEmitter.emit(Unit) + job.cancel() + } + } + + override fun <In, Out> coalescingMutableEvents( + coalesce: (old: Out, new: In) -> Out, + getInitialValue: () -> Out, + ): CoalescingMutableEvents<In, Out> = + CoalescingMutableEvents( + null, + coalesce = { old, new -> coalesce(old.value, new) }, + network, + getInitialValue, + ) + + override fun <T> conflatedMutableEvents(): CoalescingMutableEvents<T, T> = + CoalescingMutableEvents( + null, + coalesce = { _, new -> new }, + network, + { error("WTF: init value accessed for conflatedMutableEvents") }, + ) + + override fun <T> mutableEvents(): MutableEvents<T> = MutableEvents(network) + + override fun <T> mutableStateDeferred(initialValue: DeferredValue<T>): MutableState<T> = + MutableState(network, initialValue.unwrapped) +} + +/** + * Combination of an [KairosNetwork] and a [Job] that, when cancelled, will cancel the entire Kairos + * network. + */ +@ExperimentalKairosApi +class RootKairosNetwork +internal constructor(private val network: Network, private val scope: CoroutineScope, job: Job) : + Job by job, KairosNetwork by LocalNetwork(network, scope, emptyEvents) + +/** Constructs a new [RootKairosNetwork] in the given [CoroutineScope]. */ +@ExperimentalKairosApi +fun CoroutineScope.launchKairosNetwork( + context: CoroutineContext = EmptyCoroutineContext +): RootKairosNetwork { + val scope = childScope(context) + val network = Network(scope) + scope.launch(CoroutineName("launchKairosNetwork scheduler")) { network.runInputScheduler() } + return RootKairosNetwork(network, scope, scope.coroutineContext.job) +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt new file mode 100644 index 000000000000..ce3e9235efa8 --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 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 com.android.systemui.kairos + +import com.android.systemui.kairos.internal.CompletableLazy + +/** Denotes [KairosScope] interfaces as [DSL markers][DslMarker]. */ +@DslMarker annotation class KairosScopeMarker + +/** + * Base scope for all Kairos scopes. Used to prevent implicitly capturing other scopes from in + * lambdas. + */ +@KairosScopeMarker +@ExperimentalKairosApi +interface KairosScope { + /** Returns the value held by the [DeferredValue], suspending until available if necessary. */ + fun <A> DeferredValue<A>.get(): A = unwrapped.value +} + +/** + * A value that may not be immediately (synchronously) available, but is guaranteed to be available + * before this transaction is completed. + * + * @see KairosScope.get + */ +@ExperimentalKairosApi +class DeferredValue<out A> internal constructor(internal val unwrapped: Lazy<A>) + +/** + * Returns the value held by this [DeferredValue], or throws [IllegalStateException] if it is not + * yet available. + * + * This API is not meant for general usage within the Kairos network. It is made available mainly + * for debugging and logging. You should always prefer [get][KairosScope.get] if possible. + * + * @see KairosScope.get + */ +@ExperimentalKairosApi fun <A> DeferredValue<A>.getUnsafe(): A = unwrapped.value + +/** Returns an already-available [DeferredValue] containing [value]. */ +@ExperimentalKairosApi +fun <A> deferredOf(value: A): DeferredValue<A> = DeferredValue(CompletableLazy(value)) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt new file mode 100644 index 000000000000..1f0a19d5752b --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt @@ -0,0 +1,530 @@ +/* + * Copyright (C) 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 com.android.systemui.kairos + +import com.android.systemui.kairos.internal.CompletableLazy +import com.android.systemui.kairos.internal.DerivedMapCheap +import com.android.systemui.kairos.internal.EventsImpl +import com.android.systemui.kairos.internal.Init +import com.android.systemui.kairos.internal.InitScope +import com.android.systemui.kairos.internal.Network +import com.android.systemui.kairos.internal.NoScope +import com.android.systemui.kairos.internal.Schedulable +import com.android.systemui.kairos.internal.StateImpl +import com.android.systemui.kairos.internal.StateSource +import com.android.systemui.kairos.internal.activated +import com.android.systemui.kairos.internal.cached +import com.android.systemui.kairos.internal.constInit +import com.android.systemui.kairos.internal.constState +import com.android.systemui.kairos.internal.filterImpl +import com.android.systemui.kairos.internal.flatMapStateImpl +import com.android.systemui.kairos.internal.init +import com.android.systemui.kairos.internal.mapImpl +import com.android.systemui.kairos.internal.mapStateImpl +import com.android.systemui.kairos.internal.mapStateImplCheap +import com.android.systemui.kairos.internal.util.hashString +import com.android.systemui.kairos.internal.zipStateMap +import com.android.systemui.kairos.internal.zipStates +import kotlin.reflect.KProperty + +/** + * A time-varying value with discrete changes. Essentially, a combination of a [Transactional] that + * holds a value, and an [Events] that emits when the value changes. + */ +@ExperimentalKairosApi +sealed class State<out A> { + internal abstract val init: Init<StateImpl<A>> +} + +/** A [State] that never changes. */ +@ExperimentalKairosApi +fun <A> stateOf(value: A): State<A> { + val operatorName = "stateOf" + val name = "$operatorName($value)" + return StateInit(constInit(name, constState(name, operatorName, value))) +} + +/** + * Returns a [State] that acts as a deferred-reference to the [State] produced by this [Lazy]. + * + * When the returned [State] is accessed by the Kairos network, the [Lazy]'s [value][Lazy.value] + * will be queried and used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi fun <A> Lazy<State<A>>.defer(): State<A> = deferInline { value } + +/** + * Returns a [State] that acts as a deferred-reference to the [State] produced by this + * [DeferredValue]. + * + * When the returned [State] is accessed by the Kairos network, the [DeferredValue] will be queried + * and used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi +fun <A> DeferredValue<State<A>>.defer(): State<A> = deferInline { unwrapped.value } + +/** + * Returns a [State] that acts as a deferred-reference to the [State] produced by [block]. + * + * When the returned [State] is accessed by the Kairos network, [block] will be invoked and the + * returned [State] will be used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi +fun <A> deferredState(block: KairosScope.() -> State<A>): State<A> = deferInline { NoScope.block() } + +/** + * Returns a [State] containing the results of applying [transform] to the value held by the + * original [State]. + */ +@ExperimentalKairosApi +fun <A, B> State<A>.map(transform: KairosScope.(A) -> B): State<B> { + val operatorName = "map" + val name = operatorName + return StateInit( + init(name) { + mapStateImpl({ init.connect(this) }, name, operatorName) { NoScope.transform(it) } + } + ) +} + +/** + * Returns a [State] that transforms the value held inside this [State] by applying it to the + * [transform]. + * + * Note that unlike [map], the result is not cached. This means that not only should [transform] be + * fast and pure, it should be *monomorphic* (1-to-1). Failure to do this means that [changes] for + * the returned [State] will operate unexpectedly, emitting at rates that do not reflect an + * observable change to the returned [State]. + */ +@ExperimentalKairosApi +fun <A, B> State<A>.mapCheapUnsafe(transform: KairosScope.(A) -> B): State<B> { + val operatorName = "map" + val name = operatorName + return StateInit( + init(name) { mapStateImplCheap(init, name, operatorName) { NoScope.transform(it) } } + ) +} + +/** + * Returns a [State] by combining the values held inside the given [State]s by applying them to the + * given function [transform]. + */ +@ExperimentalKairosApi +fun <A, B, C> State<A>.combineWith(other: State<B>, transform: KairosScope.(A, B) -> C): State<C> = + combine(this, other, transform) + +/** + * Splits a [State] of pairs into a pair of [Events][State], where each returned [State] holds half + * of the original. + * + * Shorthand for: + * ```kotlin + * val lefts = map { it.first } + * val rights = map { it.second } + * return Pair(lefts, rights) + * ``` + */ +@ExperimentalKairosApi +fun <A, B> State<Pair<A, B>>.unzip(): Pair<State<A>, State<B>> { + val left = map { it.first } + val right = map { it.second } + return left to right +} + +/** + * Returns a [State] by combining the values held inside the given [States][State] into a [List]. + * + * @see State.combineWith + */ +@ExperimentalKairosApi +fun <A> Iterable<State<A>>.combine(): State<List<A>> { + val operatorName = "combine" + val name = operatorName + return StateInit( + init(name) { + val states = map { it.init } + zipStates( + name, + operatorName, + states.size, + states = init(null) { states.map { it.connect(this) } }, + ) + } + ) +} + +/** + * Returns a [State] by combining the values held inside the given [States][State] into a [Map]. + * + * @see State.combineWith + */ +@ExperimentalKairosApi +fun <K, A> Map<K, State<A>>.combine(): State<Map<K, A>> { + val operatorName = "combine" + val name = operatorName + return StateInit( + init(name) { + zipStateMap( + name, + operatorName, + size, + states = init(null) { mapValues { it.value.init.connect(evalScope = this) } }, + ) + } + ) +} + +/** + * Returns a [State] whose value is generated with [transform] by combining the current values of + * each given [State]. + * + * @see State.combineWith + */ +@ExperimentalKairosApi +fun <A, B> Iterable<State<A>>.combine(transform: KairosScope.(List<A>) -> B): State<B> = + combine().map(transform) + +/** + * Returns a [State] by combining the values held inside the given [State]s into a [List]. + * + * @see State.combineWith + */ +@ExperimentalKairosApi +fun <A> combine(vararg states: State<A>): State<List<A>> = states.asIterable().combine() + +/** + * Returns a [State] whose value is generated with [transform] by combining the current values of + * each given [State]. + * + * @see State.combineWith + */ +@ExperimentalKairosApi +fun <A, B> combine(vararg states: State<A>, transform: KairosScope.(List<A>) -> B): State<B> = + states.asIterable().combine(transform) + +/** + * Returns a [State] whose value is generated with [transform] by combining the current values of + * each given [State]. + * + * @see State.combineWith + */ +@ExperimentalKairosApi +fun <A, B, Z> combine( + stateA: State<A>, + stateB: State<B>, + transform: KairosScope.(A, B) -> Z, +): State<Z> { + val operatorName = "combine" + val name = operatorName + return StateInit( + init(name) { + zipStates(name, operatorName, stateA.init, stateB.init) { a, b -> + NoScope.transform(a, b) + } + } + ) +} + +/** + * Returns a [State] whose value is generated with [transform] by combining the current values of + * each given [State]. + * + * @see State.combineWith + */ +@ExperimentalKairosApi +fun <A, B, C, Z> combine( + stateA: State<A>, + stateB: State<B>, + stateC: State<C>, + transform: KairosScope.(A, B, C) -> Z, +): State<Z> { + val operatorName = "combine" + val name = operatorName + return StateInit( + init(name) { + zipStates(name, operatorName, stateA.init, stateB.init, stateC.init) { a, b, c -> + NoScope.transform(a, b, c) + } + } + ) +} + +/** + * Returns a [State] whose value is generated with [transform] by combining the current values of + * each given [State]. + * + * @see State.combineWith + */ +@ExperimentalKairosApi +fun <A, B, C, D, Z> combine( + stateA: State<A>, + stateB: State<B>, + stateC: State<C>, + stateD: State<D>, + transform: KairosScope.(A, B, C, D) -> Z, +): State<Z> { + val operatorName = "combine" + val name = operatorName + return StateInit( + init(name) { + zipStates(name, operatorName, stateA.init, stateB.init, stateC.init, stateD.init) { + a, + b, + c, + d -> + NoScope.transform(a, b, c, d) + } + } + ) +} + +/** + * Returns a [State] whose value is generated with [transform] by combining the current values of + * each given [State]. + * + * @see State.combineWith + */ +@ExperimentalKairosApi +fun <A, B, C, D, E, Z> combine( + stateA: State<A>, + stateB: State<B>, + stateC: State<C>, + stateD: State<D>, + stateE: State<E>, + transform: KairosScope.(A, B, C, D, E) -> Z, +): State<Z> { + val operatorName = "combine" + val name = operatorName + return StateInit( + init(name) { + zipStates( + name, + operatorName, + stateA.init, + stateB.init, + stateC.init, + stateD.init, + stateE.init, + ) { a, b, c, d, e -> + NoScope.transform(a, b, c, d, e) + } + } + ) +} + +/** Returns a [State] by applying [transform] to the value held by the original [State]. */ +@ExperimentalKairosApi +fun <A, B> State<A>.flatMap(transform: KairosScope.(A) -> State<B>): State<B> { + val operatorName = "flatMap" + val name = operatorName + return StateInit( + init(name) { + flatMapStateImpl({ init.connect(this) }, name, operatorName) { a -> + NoScope.transform(a).init.connect(this) + } + } + ) +} + +/** Shorthand for `flatMap { it }` */ +@ExperimentalKairosApi fun <A> State<State<A>>.flatten() = flatMap { it } + +/** + * Returns a [StateSelector] that can be used to efficiently check if the input [State] is currently + * holding a specific value. + * + * An example: + * ``` + * val intState: State<Int> = ... + * val intSelector: StateSelector<Int> = intState.selector() + * // Tracks if lInt is holding 1 + * val isOne: State<Boolean> = intSelector.whenSelected(1) + * ``` + * + * This is semantically equivalent to `val isOne = intState.map { i -> i == 1 }`, but is + * significantly more efficient; specifically, using [State.map] in this way incurs a `O(n)` + * performance hit, where `n` is the number of different [State.map] operations used to track a + * specific value. [selector] internally uses a [HashMap] to lookup the appropriate downstream + * [State] to update, and so operates in `O(1)`. + * + * Note that the returned [StateSelector] should be cached and re-used to gain the performance + * benefit. + * + * @see groupByKey + */ +@ExperimentalKairosApi +fun <A> State<A>.selector(numDistinctValues: Int? = null): StateSelector<A> = + StateSelector( + this, + changes + .map { new -> mapOf(new to true, sampleDeferred().get() to false) } + .groupByKey(numDistinctValues), + ) + +/** + * Tracks the currently selected value of type [A] from an upstream [State]. + * + * @see selector + */ +@ExperimentalKairosApi +class StateSelector<in A> +internal constructor( + private val upstream: State<A>, + private val groupedChanges: GroupedEvents<A, Boolean>, +) { + /** + * Returns a [State] that tracks whether the upstream [State] is currently holding the given + * [value]. + * + * @see selector + */ + fun whenSelected(value: A): State<Boolean> { + val operatorName = "StateSelector#whenSelected" + val name = "$operatorName[$value]" + return StateInit( + init(name) { + StateImpl( + name, + operatorName, + groupedChanges.impl.eventsForKey(value), + DerivedMapCheap(upstream.init) { it == value }, + ) + } + ) + } + + operator fun get(value: A): State<Boolean> = whenSelected(value) +} + +/** + * A mutable [State] that provides the ability to manually [set its value][setValue]. + * + * Multiple invocations of [setValue] that occur before a transaction are conflated; only the most + * recent value is used. + * + * Effectively equivalent to: + * ``` kotlin + * ConflatedMutableEvents(kairosNetwork).holdState(initialValue) + * ``` + */ +@ExperimentalKairosApi +class MutableState<T> internal constructor(internal val network: Network, initialValue: Lazy<T>) : + State<T>() { + + private val input: CoalescingMutableEvents<Lazy<T>, Lazy<T>?> = + CoalescingMutableEvents( + name = null, + coalesce = { _, new -> new }, + network = network, + getInitialValue = { null }, + ) + + override val init: Init<StateImpl<T>> + get() = state.init + + internal val state = run { + val changes = input.impl + val name = null + val operatorName = "MutableState" + val state: StateSource<T> = StateSource(initialValue) + val mapImpl = mapImpl(upstream = { changes.activated() }) { it, _ -> it!!.value } + val calm: EventsImpl<T> = + filterImpl({ mapImpl }) { new -> + new != state.getCurrentWithEpoch(evalScope = this).first + } + .cached() + @Suppress("DeferredResultUnused") + network.transaction("MutableState.init") { + calm.activate(evalScope = this, downstream = Schedulable.S(state))?.let { + (connection, needsEval) -> + state.upstreamConnection = connection + if (needsEval) { + schedule(state) + } + } + } + StateInit(constInit(name, StateImpl(name, operatorName, calm, state))) + } + + /** + * Sets the value held by this [State]. + * + * Invoking will cause a [state change event][State.changes] to emit with the new value, which + * will then be applied (and thus returned by [TransactionScope.sample]) after the transaction + * is complete. + * + * Multiple invocations of [setValue] that occur before a transaction are conflated; only the + * most recent value is used. + */ + fun setValue(value: T) = input.emit(CompletableLazy(value)) + + /** + * Sets the value held by this [State]. The [DeferredValue] will not be queried until this + * [State] is explicitly [sampled][TransactionScope.sample] or [observed][BuildScope.observe]. + * + * Invoking will cause a [state change event][State.changes] to emit with the new value, which + * will then be applied (and thus returned by [TransactionScope.sample]) after the transaction + * is complete. + * + * Multiple invocations of [setValue] that occur before a transaction are conflated; only the + * most recent value is used. + */ + fun setValueDeferred(value: DeferredValue<T>) = input.emit(value.unwrapped) +} + +/** A forward-reference to a [State], allowing for recursive definitions. */ +@ExperimentalKairosApi +class StateLoop<A> : State<A>() { + + private val name: String? = null + + private val deferred = CompletableLazy<State<A>>() + + override val init: Init<StateImpl<A>> = + init(name) { deferred.value.init.connect(evalScope = this) } + + /** The [State] this [StateLoop] will forward to. */ + var loopback: State<A>? = null + set(value) { + value?.let { + check(!deferred.isInitialized()) { "StateLoop.loopback has already been set." } + deferred.setValue(value) + field = value + } + } + + operator fun getValue(thisRef: Any?, property: KProperty<*>): State<A> = this + + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: State<A>) { + loopback = value + } + + override fun toString(): String = "${this::class.simpleName}@$hashString" +} + +internal class StateInit<A> internal constructor(override val init: Init<StateImpl<A>>) : + State<A>() { + override fun toString(): String = "${this::class.simpleName}@$hashString" +} + +private inline fun <A> deferInline(crossinline block: InitScope.() -> State<A>): State<A> = + StateInit(init(name = null) { block().init.connect(evalScope = this) }) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt new file mode 100644 index 000000000000..933ff1a75a02 --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt @@ -0,0 +1,806 @@ +/* + * Copyright (C) 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 com.android.systemui.kairos + +import com.android.systemui.kairos.util.Maybe +import com.android.systemui.kairos.util.Maybe.Just +import com.android.systemui.kairos.util.WithPrev +import com.android.systemui.kairos.util.just +import com.android.systemui.kairos.util.map +import com.android.systemui.kairos.util.mapMaybeValues +import com.android.systemui.kairos.util.none +import com.android.systemui.kairos.util.zipWith + +// TODO: caching story? should each Scope have a cache of applied Stateful instances? +/** A computation that can accumulate [Events] into [State]. */ +typealias Stateful<R> = StateScope.() -> R + +/** + * Returns a [Stateful] that, when [applied][StateScope.applyStateful], invokes [block] with the + * applier's [StateScope]. + */ +@ExperimentalKairosApi +@Suppress("NOTHING_TO_INLINE") +inline fun <A> statefully(noinline block: StateScope.() -> A): Stateful<A> = block + +/** + * Operations that accumulate state within the Kairos network. + * + * State accumulation is an ongoing process that has a lifetime. Use `-Latest` combinators, such as + * [mapLatestStateful], to create smaller, nested lifecycles so that accumulation isn't running + * longer than needed. + */ +@ExperimentalKairosApi +interface StateScope : TransactionScope { + + /** + * Defers invoking [block] until after the current [StateScope] code-path completes, returning a + * [DeferredValue] that can be used to reference the result. + * + * Useful for recursive definitions. + * + * @see DeferredValue + */ + fun <A> deferredStateScope(block: StateScope.() -> A): DeferredValue<A> + + /** + * Returns a [State] that holds onto the most recently emitted value from this [Events], or + * [initialValue] if nothing has been emitted since it was constructed. + * + * Note that the value contained within the [State] is not updated until *after* all [Events] + * have been processed; this keeps the value of the [State] consistent during the entire Kairos + * transaction. + */ + fun <A> Events<A>.holdStateDeferred(initialValue: DeferredValue<A>): State<A> + + /** + * Returns a [State] holding a [Map] that is updated incrementally whenever this emits a value. + * + * The value emitted is used as a "patch" for the tracked [Map]; for each key [K] in the emitted + * map, an associated value of [Just] will insert or replace the value in the tracked [Map], and + * an associated value of [none] will remove the key from the tracked [Map]. + */ + fun <K, V> Events<Map<K, Maybe<V>>>.foldStateMapIncrementally( + initialValues: DeferredValue<Map<K, V>> + ): Incremental<K, V> + + // TODO: everything below this comment can be made into extensions once we have context params + + /** + * Returns an [Events] that emits from a merged, incrementally-accumulated collection of + * [Events] emitted from this, following the same "patch" rules as outlined in + * [foldStateMapIncrementally]. + * + * Conceptually this is equivalent to: + * ```kotlin + * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally( + * initialEvents: Map<K, Events<V>>, + * ): Events<Map<K, V>> = + * foldMapIncrementally(initialEvents).map { it.merge() }.switchEvents() + * ``` + * + * While the behavior is equivalent to the conceptual definition above, the implementation is + * significantly more efficient. + * + * @see merge + */ + fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally( + name: String? = null, + initialEvents: DeferredValue<Map<K, Events<V>>>, + ): Events<Map<K, V>> = foldStateMapIncrementally(initialEvents).mergeEventsIncrementally() + + /** + * Returns an [Events] that emits from a merged, incrementally-accumulated collection of + * [Events] emitted from this, following the same "patch" rules as outlined in + * [foldStateMapIncrementally]. + * + * Conceptually this is equivalent to: + * ```kotlin + * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly( + * initialEvents: Map<K, Events<V>>, + * ): Events<Map<K, V>> = + * foldMapIncrementally(initialEvents).map { it.merge() }.switchEventsPromptly() + * ``` + * + * While the behavior is equivalent to the conceptual definition above, the implementation is + * significantly more efficient. + * + * @see merge + */ + fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly( + initialEvents: DeferredValue<Map<K, Events<V>>>, + name: String? = null, + ): Events<Map<K, V>> = + foldStateMapIncrementally(initialEvents).mergeEventsIncrementallyPromptly() + + /** + * Returns an [Events] that emits from a merged, incrementally-accumulated collection of + * [Events] emitted from this, following the same "patch" rules as outlined in + * [foldStateMapIncrementally]. + * + * Conceptually this is equivalent to: + * ```kotlin + * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally( + * initialEvents: Map<K, Events<V>>, + * ): Events<Map<K, V>> = + * foldMapIncrementally(initialEvents).map { it.merge() }.switchEvents() + * ``` + * + * While the behavior is equivalent to the conceptual definition above, the implementation is + * significantly more efficient. + * + * @see merge + */ + fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally( + name: String? = null, + initialEvents: Map<K, Events<V>> = emptyMap(), + ): Events<Map<K, V>> = mergeIncrementally(name, deferredOf(initialEvents)) + + /** + * Returns an [Events] that emits from a merged, incrementally-accumulated collection of + * [Events] emitted from this, following the same "patch" rules as outlined in + * [foldStateMapIncrementally]. + * + * Conceptually this is equivalent to: + * ```kotlin + * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly( + * initialEvents: Map<K, Events<V>>, + * ): Events<Map<K, V>> = + * foldMapIncrementally(initialEvents).map { it.merge() }.switchEventsPromptly() + * ``` + * + * While the behavior is equivalent to the conceptual definition above, the implementation is + * significantly more efficient. + * + * @see merge + */ + fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly( + initialEvents: Map<K, Events<V>> = emptyMap(), + name: String? = null, + ): Events<Map<K, V>> = mergeIncrementallyPromptly(deferredOf(initialEvents), name) + + /** Applies the [Stateful] within this [StateScope]. */ + fun <A> Stateful<A>.applyStateful(): A = this() + + /** + * Applies the [Stateful] within this [StateScope], returning the result as an [DeferredValue]. + */ + fun <A> Stateful<A>.applyStatefulDeferred(): DeferredValue<A> = deferredStateScope { + applyStateful() + } + + /** + * Returns a [State] that holds onto the most recently emitted value from this [Events], or + * [initialValue] if nothing has been emitted since it was constructed. + * + * Note that the value contained within the [State] is not updated until *after* all [Events] + * have been processed; this keeps the value of the [State] consistent during the entire Kairos + * transaction. + */ + fun <A> Events<A>.holdState(initialValue: A): State<A> = + holdStateDeferred(deferredOf(initialValue)) + + /** + * Returns an [Events] the emits the result of applying [Statefuls][Stateful] emitted from the + * original [Events]. + * + * Unlike [applyLatestStateful], state accumulation is not stopped with each subsequent emission + * of the original [Events]. + */ + fun <A> Events<Stateful<A>>.applyStatefuls(): Events<A> + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events]. + * + * [transform] can perform state accumulation via its [StateScope] receiver. Unlike + * [mapLatestStateful], accumulation is not stopped with each subsequent emission of the + * original [Events]. + */ + fun <A, B> Events<A>.mapStateful(transform: StateScope.(A) -> B): Events<B> = + map { statefully { transform(it) } }.applyStatefuls() + + /** + * Returns a [State] the holds the result of applying the [Stateful] held by the original + * [State]. + * + * Unlike [applyLatestStateful], state accumulation is not stopped with each state change. + */ + fun <A> State<Stateful<A>>.applyStatefuls(): State<A> = + changes + .applyStatefuls() + .holdStateDeferred(initialValue = deferredStateScope { sampleDeferred().get()() }) + + /** Returns an [Events] that switches to the [Events] emitted by the original [Events]. */ + fun <A> Events<Events<A>>.flatten() = holdState(emptyEvents).switchEvents() + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events]. + * + * [transform] can perform state accumulation via its [StateScope] receiver. With each + * invocation of [transform], state accumulation from previous invocation is stopped. + */ + fun <A, B> Events<A>.mapLatestStateful(transform: StateScope.(A) -> B): Events<B> = + map { statefully { transform(it) } }.applyLatestStateful() + + /** + * Returns an [Events] that switches to a new [Events] produced by [transform] every time the + * original [Events] emits a value. + * + * [transform] can perform state accumulation via its [StateScope] receiver. With each + * invocation of [transform], state accumulation from previous invocation is stopped. + */ + fun <A, B> Events<A>.flatMapLatestStateful(transform: StateScope.(A) -> Events<B>): Events<B> = + mapLatestStateful(transform).flatten() + + /** + * Returns an [Events] containing the results of applying each [Stateful] emitted from the + * original [Events]. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] is + * stopped. + */ + fun <A> Events<Stateful<A>>.applyLatestStateful(): Events<A> = applyLatestStateful {}.first + + /** + * Returns a [State] containing the value returned by applying the [Stateful] held by the + * original [State]. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] is + * stopped. + */ + fun <A> State<Stateful<A>>.applyLatestStateful(): State<A> { + val (changes, init) = changes.applyLatestStateful { sample()() } + return changes.holdStateDeferred(init) + } + + /** + * Returns an [Events] containing the results of applying each [Stateful] emitted from the + * original [Events], and a [DeferredValue] containing the result of applying [init] + * immediately. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] is + * stopped. + */ + fun <A, B> Events<Stateful<B>>.applyLatestStateful( + init: Stateful<A> + ): Pair<Events<B>, DeferredValue<A>> { + val (events, result) = + mapCheap { spec -> mapOf(Unit to just(spec)) } + .applyLatestStatefulForKey(init = mapOf(Unit to init), numKeys = 1) + val outEvents: Events<B> = + events.mapMaybe { + checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" } + } + val outInit: DeferredValue<A> = deferredTransactionScope { + val initResult: Map<Unit, A> = result.get() + check(Unit in initResult) { + "applyLatest: expected initial result, but none present in: $initResult" + } + @Suppress("UNCHECKED_CAST") + initResult.getOrDefault(Unit) { null } as A + } + return Pair(outEvents, outInit) + } + + /** + * Returns an [Events] containing the results of applying each [Stateful] emitted from the + * original [Events], and a [DeferredValue] containing the result of applying [init] + * immediately. + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [Stateful] will be stopped with no replacement. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] + * with the same key is stopped. + */ + fun <K, A, B> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey( + init: DeferredValue<Map<K, Stateful<B>>>, + numKeys: Int? = null, + ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> + + /** + * Returns an [Events] containing the results of applying each [Stateful] emitted from the + * original [Events], and a [DeferredValue] containing the result of applying [init] + * immediately. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] + * with the same key is stopped. + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [Stateful] will be stopped with no replacement. + */ + fun <K, A, B> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey( + init: Map<K, Stateful<B>>, + numKeys: Int? = null, + ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> = + applyLatestStatefulForKey(deferredOf(init), numKeys) + + fun <K, V> Incremental<K, Stateful<V>>.applyLatestStatefulForKey( + numKeys: Int? = null + ): Incremental<K, V> { + val (events, init) = updates.applyLatestStatefulForKey(sampleDeferred()) + return events.foldStateMapIncrementally(init) + } + + /** + * Returns a [State] containing the latest results of applying each [Stateful] emitted from the + * original [Events]. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] + * with the same key is stopped. + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [Stateful] will be stopped with no replacement. + */ + fun <K, A> Events<Map<K, Maybe<Stateful<A>>>>.holdLatestStatefulForKey( + init: DeferredValue<Map<K, Stateful<A>>>, + numKeys: Int? = null, + ): Incremental<K, A> { + val (changes, initialValues) = applyLatestStatefulForKey(init, numKeys) + return changes.foldStateMapIncrementally(initialValues) + } + + /** + * Returns a [State] containing the latest results of applying each [Stateful] emitted from the + * original [Events]. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] + * with the same key is stopped. + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [Stateful] will be stopped with no replacement. + */ + fun <K, A> Events<Map<K, Maybe<Stateful<A>>>>.holdLatestStatefulForKey( + init: Map<K, Stateful<A>> = emptyMap(), + numKeys: Int? = null, + ): Incremental<K, A> = holdLatestStatefulForKey(deferredOf(init), numKeys) + + /** + * Returns an [Events] containing the results of applying each [Stateful] emitted from the + * original [Events], and a [DeferredValue] containing the result of applying [stateInit] + * immediately. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] + * with the same key is stopped. + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [Stateful] will be stopped with no replacement. + */ + fun <K, A> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey( + numKeys: Int? = null + ): Events<Map<K, Maybe<A>>> = + applyLatestStatefulForKey(init = emptyMap<K, Stateful<*>>(), numKeys = numKeys).first + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events], and a [DeferredValue] containing the result of applying [transform] to + * [initialValues] immediately. + * + * [transform] can perform state accumulation via its [StateScope] receiver. With each + * invocation of [transform], state accumulation from previous invocation is stopped. + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [StateScope] will be stopped with no replacement. + */ + fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestStatefulForKey( + initialValues: DeferredValue<Map<K, A>>, + numKeys: Int? = null, + transform: StateScope.(A) -> B, + ): Pair<Events<Map<K, Maybe<B>>>, DeferredValue<Map<K, B>>> = + map { patch -> patch.mapValues { (_, v) -> v.map { statefully { transform(it) } } } } + .applyLatestStatefulForKey( + deferredStateScope { + initialValues.get().mapValues { (_, v) -> statefully { transform(v) } } + }, + numKeys = numKeys, + ) + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events], and a [DeferredValue] containing the result of applying [transform] to + * [initialValues] immediately. + * + * [transform] can perform state accumulation via its [StateScope] receiver. With each + * invocation of [transform], state accumulation from previous invocation is stopped. + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [StateScope] will be stopped with no replacement. + */ + fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestStatefulForKey( + initialValues: Map<K, A>, + numKeys: Int? = null, + transform: StateScope.(A) -> B, + ): Pair<Events<Map<K, Maybe<B>>>, DeferredValue<Map<K, B>>> = + mapLatestStatefulForKey(deferredOf(initialValues), numKeys, transform) + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events]. + * + * [transform] can perform state accumulation via its [StateScope] receiver. With each + * invocation of [transform], state accumulation from previous invocation is stopped. + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [StateScope] will be stopped with no replacement. + */ + fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestStatefulForKey( + numKeys: Int? = null, + transform: StateScope.(A) -> B, + ): Events<Map<K, Maybe<B>>> = mapLatestStatefulForKey(emptyMap(), numKeys, transform).first + + /** + * Returns an [Events] that will only emit the next event of the original [Events], and then + * will act as [emptyEvents]. + * + * If the original [Events] is emitting an event at this exact time, then it will be the only + * even emitted from the result [Events]. + */ + fun <A> Events<A>.nextOnly(name: String? = null): Events<A> = + if (this === emptyEvents) { + this + } else { + EventsLoop<A>().also { + it.loopback = + it.mapCheap { emptyEvents }.holdState(this@nextOnly).switchEvents(name) + } + } + + /** Returns an [Events] that skips the next emission of the original [Events]. */ + fun <A> Events<A>.skipNext(): Events<A> = + if (this === emptyEvents) { + this + } else { + nextOnly().mapCheap { this@skipNext }.holdState(emptyEvents).switchEvents() + } + + /** + * Returns an [Events] that emits values from the original [Events] up until [stop] emits a + * value. + * + * If the original [Events] emits at the same time as [stop], then the returned [Events] will + * emit that value. + */ + fun <A> Events<A>.takeUntil(stop: Events<*>): Events<A> = + if (stop === emptyEvents) { + this + } else { + stop.mapCheap { emptyEvents }.nextOnly().holdState(this).switchEvents() + } + + /** + * Invokes [stateful] in a new [StateScope] that is a child of this one. + * + * This new scope is stopped when [stop] first emits a value, or when the parent scope is + * stopped. Stopping will end all state accumulation; any [States][State] returned from this + * scope will no longer update. + */ + fun <A> childStateScope(stop: Events<*>, stateful: Stateful<A>): DeferredValue<A> { + val (_, init: DeferredValue<Map<Unit, A>>) = + stop + .nextOnly() + .map { mapOf(Unit to none<Stateful<A>>()) } + .applyLatestStatefulForKey(init = mapOf(Unit to stateful), numKeys = 1) + return deferredStateScope { init.get().getValue(Unit) } + } + + /** + * Returns an [Events] that emits values from the original [Events] up to and including a value + * is emitted that satisfies [predicate]. + */ + fun <A> Events<A>.takeUntil(predicate: TransactionScope.(A) -> Boolean): Events<A> = + takeUntil(filter(predicate)) + + /** + * Returns a [State] that is incrementally updated when this [Events] emits a value, by applying + * [transform] to both the emitted value and the currently tracked state. + * + * Note that the value contained within the [State] is not updated until *after* all [Events] + * have been processed; this keeps the value of the [State] consistent during the entire Kairos + * transaction. + */ + fun <A, B> Events<A>.foldState( + initialValue: B, + transform: TransactionScope.(A, B) -> B, + ): State<B> { + lateinit var state: State<B> + return map { a -> transform(a, state.sample()) }.holdState(initialValue).also { state = it } + } + + /** + * Returns a [State] that is incrementally updated when this [Events] emits a value, by applying + * [transform] to both the emitted value and the currently tracked state. + * + * Note that the value contained within the [State] is not updated until *after* all [Events] + * have been processed; this keeps the value of the [State] consistent during the entire Kairos + * transaction. + */ + fun <A, B> Events<A>.foldStateDeferred( + initialValue: DeferredValue<B>, + transform: TransactionScope.(A, B) -> B, + ): State<B> { + lateinit var state: State<B> + return map { a -> transform(a, state.sample()) } + .holdStateDeferred(initialValue) + .also { state = it } + } + + /** + * Returns a [State] that holds onto the result of applying the most recently emitted [Stateful] + * this [Events], or [init] if nothing has been emitted since it was constructed. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] is + * stopped. + * + * Note that the value contained within the [State] is not updated until *after* all [Events] + * have been processed; this keeps the value of the [State] consistent during the entire Kairos + * transaction. + * + * Shorthand for: + * ```kotlin + * val (changes, initApplied) = applyLatestStateful(init) + * return changes.holdStateDeferred(initApplied) + * ``` + */ + fun <A> Events<Stateful<A>>.holdLatestStateful(init: Stateful<A>): State<A> { + val (changes, initApplied) = applyLatestStateful(init) + return changes.holdStateDeferred(initApplied) + } + + /** + * Returns an [Events] that emits the two most recent emissions from the original [Events]. + * [initialValue] is used as the previous value for the first emission. + * + * Shorthand for `sample(hold(init)) { new, old -> Pair(old, new) }` + */ + fun <S, T : S> Events<T>.pairwise(initialValue: S): Events<WithPrev<S, T>> { + val previous = holdState(initialValue) + return mapCheap { new -> WithPrev(previousValue = previous.sample(), newValue = new) } + } + + /** + * Returns an [Events] that emits the two most recent emissions from the original [Events]. Note + * that the returned [Events] will not emit until the original [Events] has emitted twice. + */ + fun <A> Events<A>.pairwise(): Events<WithPrev<A, A>> = + mapCheap { just(it) } + .pairwise(none) + .mapMaybe { (prev, next) -> prev.zipWith(next, ::WithPrev) } + + /** + * Returns a [State] that holds both the current and previous values of the original [State]. + * [initialPreviousValue] is used as the first previous value. + * + * Shorthand for `sample(hold(init)) { new, old -> Pair(old, new) }` + */ + fun <S, T : S> State<T>.pairwise(initialPreviousValue: S): State<WithPrev<S, T>> = + changes + .pairwise(initialPreviousValue) + .holdStateDeferred( + deferredTransactionScope { WithPrev(initialPreviousValue, sample()) } + ) + + /** + * Returns a [State] holding a [Map] that is updated incrementally whenever this emits a value. + * + * The value emitted is used as a "patch" for the tracked [Map]; for each key [K] in the emitted + * map, an associated value of [Just] will insert or replace the value in the tracked [Map], and + * an associated value of [none] will remove the key from the tracked [Map]. + */ + fun <K, V> Events<Map<K, Maybe<V>>>.foldStateMapIncrementally( + initialValues: Map<K, V> = emptyMap() + ): Incremental<K, V> = foldStateMapIncrementally(deferredOf(initialValues)) + + /** + * Returns an [Events] that wraps each emission of the original [Events] into an [IndexedValue], + * containing the emitted value and its index (starting from zero). + * + * Shorthand for: + * ``` + * val index = fold(0) { _, oldIdx -> oldIdx + 1 } + * sample(index) { a, idx -> IndexedValue(idx, a) } + * ``` + */ + fun <A> Events<A>.withIndex(): Events<IndexedValue<A>> { + val index = foldState(0) { _, old -> old + 1 } + return sample(index) { a, idx -> IndexedValue(idx, a) } + } + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events] and its index (starting from zero). + * + * Shorthand for: + * ``` + * withIndex().map { (idx, a) -> transform(idx, a) } + * ``` + */ + fun <A, B> Events<A>.mapIndexed(transform: TransactionScope.(Int, A) -> B): Events<B> { + val index = foldState(0) { _, i -> i + 1 } + return sample(index) { a, idx -> transform(idx, a) } + } + + /** Returns an [Events] where all subsequent repetitions of the same value are filtered out. */ + fun <A> Events<A>.distinctUntilChanged(): Events<A> { + val state: State<Any?> = holdState(Any()) + return filter { it != state.sample() } + } + + /** + * Returns a new [Events] that emits at the same rate as the original [Events], but combines the + * emitted value with the most recent emission from [other] using [transform]. + * + * Note that the returned [Events] will not emit anything until [other] has emitted at least one + * value. + */ + fun <A, B, C> Events<A>.sample( + other: Events<B>, + transform: TransactionScope.(A, B) -> C, + ): Events<C> { + val state = other.mapCheap { just(it) }.holdState(none) + return sample(state) { a, b -> b.map { transform(a, it) } }.filterJust() + } + + /** + * Returns a [State] that samples the [Transactional] held by the given [State] within the same + * transaction that the state changes. + */ + fun <A> State<Transactional<A>>.sampleTransactionals(): State<A> = + changes + .sampleTransactionals() + .holdStateDeferred(deferredTransactionScope { sample().sample() }) + + /** + * Returns a [State] that transforms the value held inside this [State] by applying it to the + * given function [transform]. + */ + fun <A, B> State<A>.mapTransactionally(transform: TransactionScope.(A) -> B): State<B> = + map { transactionally { transform(it) } }.sampleTransactionals() + + /** + * Returns a [State] whose value is generated with [transform] by combining the current values + * of each given [State]. + * + * @see State.combineWithTransactionally + */ + fun <A, B, Z> combineTransactionally( + stateA: State<A>, + stateB: State<B>, + transform: TransactionScope.(A, B) -> Z, + ): State<Z> = + combine(stateA, stateB) { a, b -> transactionally { transform(a, b) } } + .sampleTransactionals() + + /** + * Returns a [State] whose value is generated with [transform] by combining the current values + * of each given [State]. + * + * @see State.combineWithTransactionally + */ + fun <A, B, C, Z> combineTransactionally( + stateA: State<A>, + stateB: State<B>, + stateC: State<C>, + transform: TransactionScope.(A, B, C) -> Z, + ): State<Z> = + combine(stateA, stateB, stateC) { a, b, c -> transactionally { transform(a, b, c) } } + .sampleTransactionals() + + /** + * Returns a [State] whose value is generated with [transform] by combining the current values + * of each given [State]. + * + * @see State.combineWithTransactionally + */ + fun <A, B, C, D, Z> combineTransactionally( + stateA: State<A>, + stateB: State<B>, + stateC: State<C>, + stateD: State<D>, + transform: TransactionScope.(A, B, C, D) -> Z, + ): State<Z> = + combine(stateA, stateB, stateC, stateD) { a, b, c, d -> + transactionally { transform(a, b, c, d) } + } + .sampleTransactionals() + + /** Returns a [State] by applying [transform] to the value held by the original [State]. */ + fun <A, B> State<A>.flatMapTransactionally( + transform: TransactionScope.(A) -> State<B> + ): State<B> = map { transactionally { transform(it) } }.sampleTransactionals().flatten() + + /** + * Returns a [State] whose value is generated with [transform] by combining the current values + * of each given [State]. + * + * @see State.combineWithTransactionally + */ + fun <A, Z> combineTransactionally( + vararg states: State<A>, + transform: TransactionScope.(List<A>) -> Z, + ): State<Z> = combine(*states).mapTransactionally(transform) + + /** + * Returns a [State] whose value is generated with [transform] by combining the current values + * of each given [State]. + * + * @see State.combineWithTransactionally + */ + fun <A, Z> Iterable<State<A>>.combineTransactionally( + transform: TransactionScope.(List<A>) -> Z + ): State<Z> = combine().mapTransactionally(transform) + + /** + * Returns a [State] by combining the values held inside the given [State]s by applying them to + * the given function [transform]. + */ + fun <A, B, C> State<A>.combineWithTransactionally( + other: State<B>, + transform: TransactionScope.(A, B) -> C, + ): State<C> = combineTransactionally(this, other, transform) + + /** + * Returns an [Incremental] that reflects the state of the original [Incremental], but also adds + * / removes entries based on the state of the original's values. + */ + fun <K, V> Incremental<K, State<Maybe<V>>>.applyStateIncrementally(): Incremental<K, V> = + mapValues { (_, v) -> v.changes } + .mergeEventsIncrementallyPromptly() + .foldStateMapIncrementally( + deferredStateScope { sample().mapMaybeValues { (_, s) -> s.sample() } } + ) + + /** + * Returns an [Incremental] that reflects the state of the original [Incremental], but also adds + * / removes entries based on the [State] returned from applying [transform] to the original's + * entries. + */ + fun <K, V, U> Incremental<K, V>.mapIncrementalState( + transform: KairosScope.(Map.Entry<K, V>) -> State<Maybe<U>> + ): Incremental<K, U> = mapValues { transform(it) }.applyStateIncrementally() + + /** + * Returns an [Incremental] that reflects the state of the original [Incremental], but also adds + * / removes entries based on the [State] returned from applying [transform] to the original's + * entries, such that entries are added when that state is `true`, and removed when `false`. + */ + fun <K, V> Incremental<K, V>.filterIncrementally( + transform: KairosScope.(Map.Entry<K, V>) -> State<Boolean> + ): Incremental<K, V> = mapIncrementalState { entry -> + transform(entry).map { if (it) just(entry.value) else none } + } + + /** + * Returns an [Incremental] that samples the [Transactionals][Transactional] held by the + * original within the same transaction that the incremental [updates]. + */ + fun <K, V> Incremental<K, Transactional<V>>.sampleTransactionals(): Incremental<K, V> = + updates + .map { patch -> patch.mapValues { (k, mv) -> mv.map { it.sample() } } } + .foldStateMapIncrementally( + deferredStateScope { sample().mapValues { (k, v) -> v.sample() } } + ) + + /** + * Returns an [Incremental] that tracks the entries of the original incremental, but values + * replaced with those obtained by applying [transform] to each original entry. + */ + fun <K, V, U> Incremental<K, V>.mapValuesTransactionally( + transform: TransactionScope.(Map.Entry<K, V>) -> U + ): Incremental<K, U> = mapValues { transactionally { transform(it) } }.sampleTransactionals() +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TFlow.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TFlow.kt deleted file mode 100644 index a175e2e20e46..000000000000 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TFlow.kt +++ /dev/null @@ -1,560 +0,0 @@ -/* - * Copyright (C) 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 com.android.systemui.kairos - -import com.android.systemui.kairos.internal.DemuxImpl -import com.android.systemui.kairos.internal.Init -import com.android.systemui.kairos.internal.InitScope -import com.android.systemui.kairos.internal.InputNode -import com.android.systemui.kairos.internal.Network -import com.android.systemui.kairos.internal.NoScope -import com.android.systemui.kairos.internal.TFlowImpl -import com.android.systemui.kairos.internal.activated -import com.android.systemui.kairos.internal.cached -import com.android.systemui.kairos.internal.constInit -import com.android.systemui.kairos.internal.filterNode -import com.android.systemui.kairos.internal.init -import com.android.systemui.kairos.internal.map -import com.android.systemui.kairos.internal.mapImpl -import com.android.systemui.kairos.internal.mapMaybeNode -import com.android.systemui.kairos.internal.mergeNodes -import com.android.systemui.kairos.internal.mergeNodesLeft -import com.android.systemui.kairos.internal.neverImpl -import com.android.systemui.kairos.internal.switchDeferredImplSingle -import com.android.systemui.kairos.internal.switchPromptImpl -import com.android.systemui.kairos.internal.util.hashString -import com.android.systemui.kairos.util.Either -import com.android.systemui.kairos.util.Left -import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.Right -import com.android.systemui.kairos.util.just -import com.android.systemui.kairos.util.map -import com.android.systemui.kairos.util.toMaybe -import java.util.concurrent.atomic.AtomicReference -import kotlin.reflect.KProperty -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Job -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope - -/** A series of values of type [A] available at discrete points in time. */ -@ExperimentalFrpApi -sealed class TFlow<out A> { - companion object { - /** A [TFlow] with no values. */ - val empty: TFlow<Nothing> = EmptyFlow - } -} - -/** A [TFlow] with no values. */ -@ExperimentalFrpApi val emptyTFlow: TFlow<Nothing> = TFlow.empty - -/** - * A forward-reference to a [TFlow]. Useful for recursive definitions. - * - * This reference can be used like a standard [TFlow], but will hold up evaluation of the FRP - * network until the [loopback] reference is set. - */ -@ExperimentalFrpApi -class TFlowLoop<A> : TFlow<A>() { - private val deferred = CompletableDeferred<TFlow<A>>() - - internal val init: Init<TFlowImpl<A>> = - init(name = null) { deferred.await().init.connect(evalScope = this) } - - /** The [TFlow] this reference is referring to. */ - @ExperimentalFrpApi - var loopback: TFlow<A>? = null - set(value) { - value?.let { - check(deferred.complete(value)) { "TFlowLoop.loopback has already been set." } - field = value - } - } - - operator fun getValue(thisRef: Any?, property: KProperty<*>): TFlow<A> = this - - operator fun setValue(thisRef: Any?, property: KProperty<*>, value: TFlow<A>) { - loopback = value - } - - override fun toString(): String = "${this::class.simpleName}@$hashString" -} - -/** TODO */ -@ExperimentalFrpApi fun <A> Lazy<TFlow<A>>.defer(): TFlow<A> = deferInline { value } - -/** TODO */ -@ExperimentalFrpApi -fun <A> FrpDeferredValue<TFlow<A>>.defer(): TFlow<A> = deferInline { unwrapped.await() } - -/** TODO */ -@ExperimentalFrpApi -fun <A> deferTFlow(block: suspend FrpScope.() -> TFlow<A>): TFlow<A> = deferInline { - NoScope.runInFrpScope(block) -} - -/** Returns a [TFlow] that emits the new value of this [TState] when it changes. */ -@ExperimentalFrpApi -val <A> TState<A>.stateChanges: TFlow<A> - get() = TFlowInit(init(name = null) { init.connect(evalScope = this).changes }) - -/** - * Returns a [TFlow] that contains only the [just] results of applying [transform] to each value of - * the original [TFlow]. - * - * @see mapNotNull - */ -@ExperimentalFrpApi -fun <A, B> TFlow<A>.mapMaybe(transform: suspend FrpTransactionScope.(A) -> Maybe<B>): TFlow<B> { - val pulse = - mapMaybeNode({ init.connect(evalScope = this) }) { runInTransactionScope { transform(it) } } - return TFlowInit(constInit(name = null, pulse)) -} - -/** - * Returns a [TFlow] that contains only the non-null results of applying [transform] to each value - * of the original [TFlow]. - * - * @see mapMaybe - */ -@ExperimentalFrpApi -fun <A, B> TFlow<A>.mapNotNull(transform: suspend FrpTransactionScope.(A) -> B?): TFlow<B> = - mapMaybe { - transform(it).toMaybe() - } - -/** Returns a [TFlow] containing only values of the original [TFlow] that are not null. */ -@ExperimentalFrpApi fun <A> TFlow<A?>.filterNotNull(): TFlow<A> = mapNotNull { it } - -/** Shorthand for `mapNotNull { it as? A }`. */ -@ExperimentalFrpApi -inline fun <reified A> TFlow<*>.filterIsInstance(): TFlow<A> = mapNotNull { it as? A } - -/** Shorthand for `mapMaybe { it }`. */ -@ExperimentalFrpApi fun <A> TFlow<Maybe<A>>.filterJust(): TFlow<A> = mapMaybe { it } - -/** - * Returns a [TFlow] containing the results of applying [transform] to each value of the original - * [TFlow]. - */ -@ExperimentalFrpApi -fun <A, B> TFlow<A>.map(transform: suspend FrpTransactionScope.(A) -> B): TFlow<B> { - val mapped: TFlowImpl<B> = - mapImpl({ init.connect(evalScope = this) }) { a -> runInTransactionScope { transform(a) } } - return TFlowInit(constInit(name = null, mapped.cached())) -} - -/** - * Like [map], but the emission is not cached during the transaction. Use only if [transform] is - * fast and pure. - * - * @see map - */ -@ExperimentalFrpApi -fun <A, B> TFlow<A>.mapCheap(transform: suspend FrpTransactionScope.(A) -> B): TFlow<B> = - TFlowInit( - constInit( - name = null, - mapImpl({ init.connect(evalScope = this) }) { a -> - runInTransactionScope { transform(a) } - }, - ) - ) - -/** - * Returns a [TFlow] that invokes [action] before each value of the original [TFlow] is emitted. - * Useful for logging and debugging. - * - * ``` - * pulse.onEach { foo(it) } == pulse.map { foo(it); it } - * ``` - * - * Note that the side effects performed in [onEach] are only performed while the resulting [TFlow] - * is connected to an output of the FRP network. If your goal is to reliably perform side effects in - * response to a [TFlow], use the output combinators available in [FrpBuildScope], such as - * [FrpBuildScope.toSharedFlow] or [FrpBuildScope.observe]. - */ -@ExperimentalFrpApi -fun <A> TFlow<A>.onEach(action: suspend FrpTransactionScope.(A) -> Unit): TFlow<A> = map { - action(it) - it -} - -/** - * Returns a [TFlow] containing only values of the original [TFlow] that satisfy the given - * [predicate]. - */ -@ExperimentalFrpApi -fun <A> TFlow<A>.filter(predicate: suspend FrpTransactionScope.(A) -> Boolean): TFlow<A> { - val pulse = - filterNode({ init.connect(evalScope = this) }) { runInTransactionScope { predicate(it) } } - return TFlowInit(constInit(name = null, pulse.cached())) -} - -/** - * Splits a [TFlow] of pairs into a pair of [TFlows][TFlow], where each returned [TFlow] emits half - * of the original. - * - * Shorthand for: - * ```kotlin - * val lefts = map { it.first } - * val rights = map { it.second } - * return Pair(lefts, rights) - * ``` - */ -@ExperimentalFrpApi -fun <A, B> TFlow<Pair<A, B>>.unzip(): Pair<TFlow<A>, TFlow<B>> { - val lefts = map { it.first } - val rights = map { it.second } - return lefts to rights -} - -/** - * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from both. - * - * Because [TFlow]s can only emit one value per transaction, the provided [transformCoincidence] - * function is used to combine coincident emissions to produce the result value to be emitted by the - * merged [TFlow]. - */ -@ExperimentalFrpApi -fun <A> TFlow<A>.mergeWith( - other: TFlow<A>, - transformCoincidence: suspend FrpTransactionScope.(A, A) -> A = { a, _ -> a }, -): TFlow<A> { - val node = - mergeNodes( - getPulse = { init.connect(evalScope = this) }, - getOther = { other.init.connect(evalScope = this) }, - ) { a, b -> - runInTransactionScope { transformCoincidence(a, b) } - } - return TFlowInit(constInit(name = null, node)) -} - -/** - * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all. All coincident - * emissions are collected into the emitted [List], preserving the input ordering. - * - * @see mergeWith - * @see mergeLeft - */ -@ExperimentalFrpApi -fun <A> merge(vararg flows: TFlow<A>): TFlow<List<A>> = flows.asIterable().merge() - -/** - * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all. In the case of - * coincident emissions, the emission from the left-most [TFlow] is emitted. - * - * @see merge - */ -@ExperimentalFrpApi -fun <A> mergeLeft(vararg flows: TFlow<A>): TFlow<A> = flows.asIterable().mergeLeft() - -/** - * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all. - * - * Because [TFlow]s can only emit one value per transaction, the provided [transformCoincidence] - * function is used to combine coincident emissions to produce the result value to be emitted by the - * merged [TFlow]. - */ -// TODO: can be optimized to avoid creating the intermediate list -fun <A> merge(vararg flows: TFlow<A>, transformCoincidence: (A, A) -> A): TFlow<A> = - merge(*flows).map { l -> l.reduce(transformCoincidence) } - -/** - * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all. All coincident - * emissions are collected into the emitted [List], preserving the input ordering. - * - * @see mergeWith - * @see mergeLeft - */ -@ExperimentalFrpApi -fun <A> Iterable<TFlow<A>>.merge(): TFlow<List<A>> = - TFlowInit(constInit(name = null, mergeNodes { map { it.init.connect(evalScope = this) } })) - -/** - * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all. In the case of - * coincident emissions, the emission from the left-most [TFlow] is emitted. - * - * @see merge - */ -@ExperimentalFrpApi -fun <A> Iterable<TFlow<A>>.mergeLeft(): TFlow<A> = - TFlowInit(constInit(name = null, mergeNodesLeft { map { it.init.connect(evalScope = this) } })) - -/** - * Creates a new [TFlow] that emits events from all given [TFlow]s. All simultaneous emissions are - * collected into the emitted [List], preserving the input ordering. - * - * @see mergeWith - */ -@ExperimentalFrpApi fun <A> Sequence<TFlow<A>>.merge(): TFlow<List<A>> = asIterable().merge() - -/** - * Creates a new [TFlow] that emits events from all given [TFlow]s. All simultaneous emissions are - * collected into the emitted [Map], and are given the same key of the associated [TFlow] in the - * input [Map]. - * - * @see mergeWith - */ -@ExperimentalFrpApi -fun <K, A> Map<K, TFlow<A>>.merge(): TFlow<Map<K, A>> = - asSequence().map { (k, flowA) -> flowA.map { a -> k to a } }.toList().merge().map { it.toMap() } - -/** - * Returns a [GroupedTFlow] that can be used to efficiently split a single [TFlow] into multiple - * downstream [TFlow]s. - * - * The input [TFlow] emits [Map] instances that specify which downstream [TFlow] the associated - * value will be emitted from. These downstream [TFlow]s can be obtained via - * [GroupedTFlow.eventsForKey]. - * - * An example: - * ``` - * val sFoo: TFlow<Map<String, Foo>> = ... - * val fooById: GroupedTFlow<String, Foo> = sFoo.groupByKey() - * val fooBar: TFlow<Foo> = fooById["bar"] - * ``` - * - * This is semantically equivalent to `val fooBar = sFoo.mapNotNull { map -> map["bar"] }` but is - * significantly more efficient; specifically, using [mapNotNull] in this way incurs a `O(n)` - * performance hit, where `n` is the number of different [mapNotNull] operations used to filter on a - * specific key's presence in the emitted [Map]. [groupByKey] internally uses a [HashMap] to lookup - * the appropriate downstream [TFlow], and so operates in `O(1)`. - * - * Note that the result [GroupedTFlow] should be cached and re-used to gain the performance benefit. - * - * @see selector - */ -@ExperimentalFrpApi -fun <K, A> TFlow<Map<K, A>>.groupByKey(numKeys: Int? = null): GroupedTFlow<K, A> = - GroupedTFlow(DemuxImpl({ init.connect(this) }, numKeys)) - -/** - * Shorthand for `map { mapOf(extractKey(it) to it) }.groupByKey()` - * - * @see groupByKey - */ -@ExperimentalFrpApi -fun <K, A> TFlow<A>.groupBy( - numKeys: Int? = null, - extractKey: suspend FrpTransactionScope.(A) -> K, -): GroupedTFlow<K, A> = map { mapOf(extractKey(it) to it) }.groupByKey(numKeys) - -/** - * Returns two new [TFlow]s that contain elements from this [TFlow] that satisfy or don't satisfy - * [predicate]. - * - * Using this is equivalent to `upstream.filter(predicate) to upstream.filter { !predicate(it) }` - * but is more efficient; specifically, [partition] will only invoke [predicate] once per element. - */ -@ExperimentalFrpApi -fun <A> TFlow<A>.partition( - predicate: suspend FrpTransactionScope.(A) -> Boolean -): Pair<TFlow<A>, TFlow<A>> { - val grouped: GroupedTFlow<Boolean, A> = groupBy(numKeys = 2, extractKey = predicate) - return Pair(grouped.eventsForKey(true), grouped.eventsForKey(false)) -} - -/** - * Returns two new [TFlow]s that contain elements from this [TFlow]; [Pair.first] will contain - * [Left] values, and [Pair.second] will contain [Right] values. - * - * Using this is equivalent to using [filterIsInstance] in conjunction with [map] twice, once for - * [Left]s and once for [Right]s, but is slightly more efficient; specifically, the - * [filterIsInstance] check is only performed once per element. - */ -@ExperimentalFrpApi -fun <A, B> TFlow<Either<A, B>>.partitionEither(): Pair<TFlow<A>, TFlow<B>> { - val (left, right) = partition { it is Left } - return Pair(left.mapCheap { (it as Left).value }, right.mapCheap { (it as Right).value }) -} - -/** - * A mapping from keys of type [K] to [TFlow]s emitting values of type [A]. - * - * @see groupByKey - */ -@ExperimentalFrpApi -class GroupedTFlow<in K, out A> internal constructor(internal val impl: DemuxImpl<K, A>) { - /** - * Returns a [TFlow] that emits values of type [A] that correspond to the given [key]. - * - * @see groupByKey - */ - @ExperimentalFrpApi - fun eventsForKey(key: K): TFlow<A> = TFlowInit(constInit(name = null, impl.eventsForKey(key))) - - /** - * Returns a [TFlow] that emits values of type [A] that correspond to the given [key]. - * - * @see groupByKey - */ - @ExperimentalFrpApi operator fun get(key: K): TFlow<A> = eventsForKey(key) -} - -/** - * Returns a [TFlow] that switches to the [TFlow] contained within this [TState] whenever it - * changes. - * - * This switch does take effect until the *next* transaction after [TState] changes. For a switch - * that takes effect immediately, see [switchPromptly]. - */ -@ExperimentalFrpApi -fun <A> TState<TFlow<A>>.switch(): TFlow<A> { - return TFlowInit( - constInit( - name = null, - switchDeferredImplSingle( - getStorage = { - init.connect(this).getCurrentWithEpoch(this).first.init.connect(this) - }, - getPatches = { - mapImpl({ init.connect(this).changes }) { newFlow -> - newFlow.init.connect(this) - } - }, - ), - ) - ) -} - -/** - * Returns a [TFlow] that switches to the [TFlow] contained within this [TState] whenever it - * changes. - * - * This switch takes effect immediately within the same transaction that [TState] changes. In - * general, you should prefer [switch] over this method. It is both safer and more performant. - */ -// TODO: parameter to handle coincidental emission from both old and new -@ExperimentalFrpApi -fun <A> TState<TFlow<A>>.switchPromptly(): TFlow<A> { - val switchNode = - switchPromptImpl( - getStorage = { - mapOf(Unit to init.connect(this).getCurrentWithEpoch(this).first.init.connect(this)) - }, - getPatches = { - val patches = init.connect(this).changes - mapImpl({ patches }) { newFlow -> mapOf(Unit to just(newFlow.init.connect(this))) } - }, - ) - return TFlowInit(constInit(name = null, mapImpl({ switchNode }) { it.getValue(Unit) })) -} - -/** - * A mutable [TFlow] that provides the ability to [emit] values to the flow, handling backpressure - * by coalescing all emissions into batches. - * - * @see FrpNetwork.coalescingMutableTFlow - */ -@ExperimentalFrpApi -class CoalescingMutableTFlow<In, Out> -internal constructor( - internal val name: String?, - internal val coalesce: (old: Out, new: In) -> Out, - internal val network: Network, - private val getInitialValue: () -> Out, - internal val impl: InputNode<Out> = InputNode(), -) : TFlow<Out>() { - internal val storage = AtomicReference(false to getInitialValue()) - - override fun toString(): String = "${this::class.simpleName}@$hashString" - - /** - * Inserts [value] into the current batch, enqueueing it for emission from this [TFlow] if not - * already pending. - * - * Backpressure occurs when [emit] is called while the FRP network is currently in a - * transaction; if called multiple times, then emissions will be coalesced into a single batch - * that is then processed when the network is ready. - */ - @ExperimentalFrpApi - fun emit(value: In) { - val (scheduled, _) = storage.getAndUpdate { (_, old) -> true to coalesce(old, value) } - if (!scheduled) { - @Suppress("DeferredResultUnused") - network.transaction("CoalescingMutableTFlow${name?.let { "($name)" }.orEmpty()}.emit") { - impl.visit(this, storage.getAndSet(false to getInitialValue()).second) - } - } - } -} - -/** - * A mutable [TFlow] that provides the ability to [emit] values to the flow, handling backpressure - * by suspending the emitter. - * - * @see FrpNetwork.coalescingMutableTFlow - */ -@ExperimentalFrpApi -class MutableTFlow<T> -internal constructor(internal val network: Network, internal val impl: InputNode<T> = InputNode()) : - TFlow<T>() { - internal val name: String? = null - - private val storage = AtomicReference<Job?>(null) - - override fun toString(): String = "${this::class.simpleName}@$hashString" - - /** - * Emits a [value] to this [TFlow], suspending the caller until the FRP transaction containing - * the emission has completed. - */ - @ExperimentalFrpApi - suspend fun emit(value: T) { - coroutineScope { - var jobOrNull: Job? = null - val newEmit = - async(start = CoroutineStart.LAZY) { - jobOrNull?.join() - network - .transaction("MutableTFlow($name).emit") { impl.visit(this, value) } - .await() - } - jobOrNull = storage.getAndSet(newEmit) - newEmit.await() - } - } - - // internal suspend fun emitInCurrentTransaction(value: T, evalScope: EvalScope) { - // if (storage.getAndSet(just(value)) is None) { - // impl.visit(evalScope) - // } - // } -} - -private data object EmptyFlow : TFlow<Nothing>() - -internal class TFlowInit<out A>(val init: Init<TFlowImpl<A>>) : TFlow<A>() { - override fun toString(): String = "${this::class.simpleName}@$hashString" -} - -internal val <A> TFlow<A>.init: Init<TFlowImpl<A>> - get() = - when (this) { - is EmptyFlow -> constInit("EmptyFlow", neverImpl) - is TFlowInit -> init - is TFlowLoop -> init - is CoalescingMutableTFlow<*, A> -> constInit(name, impl.activated()) - is MutableTFlow -> constInit(name, impl.activated()) - } - -private inline fun <A> deferInline(crossinline block: suspend InitScope.() -> TFlow<A>): TFlow<A> = - TFlowInit(init(name = null) { block().init.connect(evalScope = this) }) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TState.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TState.kt deleted file mode 100644 index 80e74748a375..000000000000 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TState.kt +++ /dev/null @@ -1,544 +0,0 @@ -/* - * Copyright (C) 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 com.android.systemui.kairos - -import com.android.systemui.kairos.internal.DerivedMapCheap -import com.android.systemui.kairos.internal.Init -import com.android.systemui.kairos.internal.InitScope -import com.android.systemui.kairos.internal.Network -import com.android.systemui.kairos.internal.NoScope -import com.android.systemui.kairos.internal.Schedulable -import com.android.systemui.kairos.internal.TFlowImpl -import com.android.systemui.kairos.internal.TStateImpl -import com.android.systemui.kairos.internal.TStateSource -import com.android.systemui.kairos.internal.activated -import com.android.systemui.kairos.internal.cached -import com.android.systemui.kairos.internal.constInit -import com.android.systemui.kairos.internal.constS -import com.android.systemui.kairos.internal.filterNode -import com.android.systemui.kairos.internal.flatMap -import com.android.systemui.kairos.internal.init -import com.android.systemui.kairos.internal.map -import com.android.systemui.kairos.internal.mapCheap -import com.android.systemui.kairos.internal.mapImpl -import com.android.systemui.kairos.internal.util.hashString -import com.android.systemui.kairos.internal.zipStates -import kotlin.reflect.KProperty -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope - -/** - * A time-varying value with discrete changes. Essentially, a combination of a [Transactional] that - * holds a value, and a [TFlow] that emits when the value changes. - */ -@ExperimentalFrpApi sealed class TState<out A> - -/** A [TState] that never changes. */ -@ExperimentalFrpApi -fun <A> tStateOf(value: A): TState<A> { - val operatorName = "tStateOf" - val name = "$operatorName($value)" - return TStateInit(constInit(name, constS(name, operatorName, value))) -} - -/** TODO */ -@ExperimentalFrpApi fun <A> Lazy<TState<A>>.defer(): TState<A> = deferInline { value } - -/** TODO */ -@ExperimentalFrpApi -fun <A> FrpDeferredValue<TState<A>>.defer(): TState<A> = deferInline { unwrapped.await() } - -/** TODO */ -@ExperimentalFrpApi -fun <A> deferTState(block: suspend FrpScope.() -> TState<A>): TState<A> = deferInline { - NoScope.runInFrpScope(block) -} - -/** - * Returns a [TState] containing the results of applying [transform] to the value held by the - * original [TState]. - */ -@ExperimentalFrpApi -fun <A, B> TState<A>.map(transform: suspend FrpScope.(A) -> B): TState<B> { - val operatorName = "map" - val name = operatorName - return TStateInit( - init(name) { - init.connect(evalScope = this).map(name, operatorName) { - NoScope.runInFrpScope { transform(it) } - } - } - ) -} - -/** - * Returns a [TState] that transforms the value held inside this [TState] by applying it to the - * [transform]. - * - * Note that unlike [map], the result is not cached. This means that not only should [transform] be - * fast and pure, it should be *monomorphic* (1-to-1). Failure to do this means that [stateChanges] - * for the returned [TState] will operate unexpectedly, emitting at rates that do not reflect an - * observable change to the returned [TState]. - */ -@ExperimentalFrpApi -fun <A, B> TState<A>.mapCheapUnsafe(transform: suspend FrpScope.(A) -> B): TState<B> { - val operatorName = "map" - val name = operatorName - return TStateInit( - init(name) { - init.connect(evalScope = this).mapCheap(name, operatorName) { - NoScope.runInFrpScope { transform(it) } - } - } - ) -} - -/** - * Returns a [TState] by combining the values held inside the given [TState]s by applying them to - * the given function [transform]. - */ -@ExperimentalFrpApi -fun <A, B, C> TState<A>.combineWith( - other: TState<B>, - transform: suspend FrpScope.(A, B) -> C, -): TState<C> = combine(this, other, transform) - -/** - * Splits a [TState] of pairs into a pair of [TFlows][TState], where each returned [TState] holds - * half of the original. - * - * Shorthand for: - * ```kotlin - * val lefts = map { it.first } - * val rights = map { it.second } - * return Pair(lefts, rights) - * ``` - */ -@ExperimentalFrpApi -fun <A, B> TState<Pair<A, B>>.unzip(): Pair<TState<A>, TState<B>> { - val left = map { it.first } - val right = map { it.second } - return left to right -} - -/** - * Returns a [TState] by combining the values held inside the given [TStates][TState] into a [List]. - * - * @see TState.combineWith - */ -@ExperimentalFrpApi -fun <A> Iterable<TState<A>>.combine(): TState<List<A>> { - val operatorName = "combine" - val name = operatorName - return TStateInit( - init(name) { - zipStates(name, operatorName, states = map { it.init.connect(evalScope = this) }) - } - ) -} - -/** - * Returns a [TState] by combining the values held inside the given [TStates][TState] into a [Map]. - * - * @see TState.combineWith - */ -@ExperimentalFrpApi -fun <K : Any, A> Map<K, TState<A>>.combine(): TState<Map<K, A>> { - val operatorName = "combine" - val name = operatorName - return TStateInit( - init(name) { - zipStates( - name, - operatorName, - states = mapValues { it.value.init.connect(evalScope = this) }, - ) - } - ) -} - -/** - * Returns a [TState] whose value is generated with [transform] by combining the current values of - * each given [TState]. - * - * @see TState.combineWith - */ -@ExperimentalFrpApi -fun <A, B> Iterable<TState<A>>.combine(transform: suspend FrpScope.(List<A>) -> B): TState<B> = - combine().map(transform) - -/** - * Returns a [TState] by combining the values held inside the given [TState]s into a [List]. - * - * @see TState.combineWith - */ -@ExperimentalFrpApi -fun <A> combine(vararg states: TState<A>): TState<List<A>> = states.asIterable().combine() - -/** - * Returns a [TState] whose value is generated with [transform] by combining the current values of - * each given [TState]. - * - * @see TState.combineWith - */ -@ExperimentalFrpApi -fun <A, B> combine( - vararg states: TState<A>, - transform: suspend FrpScope.(List<A>) -> B, -): TState<B> = states.asIterable().combine(transform) - -/** - * Returns a [TState] whose value is generated with [transform] by combining the current values of - * each given [TState]. - * - * @see TState.combineWith - */ -@ExperimentalFrpApi -fun <A, B, Z> combine( - stateA: TState<A>, - stateB: TState<B>, - transform: suspend FrpScope.(A, B) -> Z, -): TState<Z> { - val operatorName = "combine" - val name = operatorName - return TStateInit( - init(name) { - coroutineScope { - val dl1: Deferred<TStateImpl<A>> = async { - stateA.init.connect(evalScope = this@init) - } - val dl2: Deferred<TStateImpl<B>> = async { - stateB.init.connect(evalScope = this@init) - } - zipStates(name, operatorName, dl1.await(), dl2.await()) { a, b -> - NoScope.runInFrpScope { transform(a, b) } - } - } - } - ) -} - -/** - * Returns a [TState] whose value is generated with [transform] by combining the current values of - * each given [TState]. - * - * @see TState.combineWith - */ -@ExperimentalFrpApi -fun <A, B, C, Z> combine( - stateA: TState<A>, - stateB: TState<B>, - stateC: TState<C>, - transform: suspend FrpScope.(A, B, C) -> Z, -): TState<Z> { - val operatorName = "combine" - val name = operatorName - return TStateInit( - init(name) { - coroutineScope { - val dl1: Deferred<TStateImpl<A>> = async { - stateA.init.connect(evalScope = this@init) - } - val dl2: Deferred<TStateImpl<B>> = async { - stateB.init.connect(evalScope = this@init) - } - val dl3: Deferred<TStateImpl<C>> = async { - stateC.init.connect(evalScope = this@init) - } - zipStates(name, operatorName, dl1.await(), dl2.await(), dl3.await()) { a, b, c -> - NoScope.runInFrpScope { transform(a, b, c) } - } - } - } - ) -} - -/** - * Returns a [TState] whose value is generated with [transform] by combining the current values of - * each given [TState]. - * - * @see TState.combineWith - */ -@ExperimentalFrpApi -fun <A, B, C, D, Z> combine( - stateA: TState<A>, - stateB: TState<B>, - stateC: TState<C>, - stateD: TState<D>, - transform: suspend FrpScope.(A, B, C, D) -> Z, -): TState<Z> { - val operatorName = "combine" - val name = operatorName - return TStateInit( - init(name) { - coroutineScope { - val dl1: Deferred<TStateImpl<A>> = async { - stateA.init.connect(evalScope = this@init) - } - val dl2: Deferred<TStateImpl<B>> = async { - stateB.init.connect(evalScope = this@init) - } - val dl3: Deferred<TStateImpl<C>> = async { - stateC.init.connect(evalScope = this@init) - } - val dl4: Deferred<TStateImpl<D>> = async { - stateD.init.connect(evalScope = this@init) - } - zipStates(name, operatorName, dl1.await(), dl2.await(), dl3.await(), dl4.await()) { - a, - b, - c, - d -> - NoScope.runInFrpScope { transform(a, b, c, d) } - } - } - } - ) -} - -/** - * Returns a [TState] whose value is generated with [transform] by combining the current values of - * each given [TState]. - * - * @see TState.combineWith - */ -@ExperimentalFrpApi -fun <A, B, C, D, E, Z> combine( - stateA: TState<A>, - stateB: TState<B>, - stateC: TState<C>, - stateD: TState<D>, - stateE: TState<E>, - transform: suspend FrpScope.(A, B, C, D, E) -> Z, -): TState<Z> { - val operatorName = "combine" - val name = operatorName - return TStateInit( - init(name) { - coroutineScope { - val dl1: Deferred<TStateImpl<A>> = async { - stateA.init.connect(evalScope = this@init) - } - val dl2: Deferred<TStateImpl<B>> = async { - stateB.init.connect(evalScope = this@init) - } - val dl3: Deferred<TStateImpl<C>> = async { - stateC.init.connect(evalScope = this@init) - } - val dl4: Deferred<TStateImpl<D>> = async { - stateD.init.connect(evalScope = this@init) - } - val dl5: Deferred<TStateImpl<E>> = async { - stateE.init.connect(evalScope = this@init) - } - zipStates( - name, - operatorName, - dl1.await(), - dl2.await(), - dl3.await(), - dl4.await(), - dl5.await(), - ) { a, b, c, d, e -> - NoScope.runInFrpScope { transform(a, b, c, d, e) } - } - } - } - ) -} - -/** Returns a [TState] by applying [transform] to the value held by the original [TState]. */ -@ExperimentalFrpApi -fun <A, B> TState<A>.flatMap(transform: suspend FrpScope.(A) -> TState<B>): TState<B> { - val operatorName = "flatMap" - val name = operatorName - return TStateInit( - init(name) { - init.connect(this).flatMap(name, operatorName) { a -> - NoScope.runInFrpScope { transform(a) }.init.connect(this) - } - } - ) -} - -/** Shorthand for `flatMap { it }` */ -@ExperimentalFrpApi fun <A> TState<TState<A>>.flatten() = flatMap { it } - -/** - * Returns a [TStateSelector] that can be used to efficiently check if the input [TState] is - * currently holding a specific value. - * - * An example: - * ``` - * val lInt: TState<Int> = ... - * val intSelector: TStateSelector<Int> = lInt.selector() - * // Tracks if lInt is holding 1 - * val isOne: TState<Boolean> = intSelector.whenSelected(1) - * ``` - * - * This is semantically equivalent to `val isOne = lInt.map { i -> i == 1 }`, but is significantly - * more efficient; specifically, using [TState.map] in this way incurs a `O(n)` performance hit, - * where `n` is the number of different [TState.map] operations used to track a specific value. - * [selector] internally uses a [HashMap] to lookup the appropriate downstream [TState] to update, - * and so operates in `O(1)`. - * - * Note that the result [TStateSelector] should be cached and re-used to gain the performance - * benefit. - * - * @see groupByKey - */ -@ExperimentalFrpApi -fun <A> TState<A>.selector(numDistinctValues: Int? = null): TStateSelector<A> = - TStateSelector( - this, - stateChanges - .map { new -> mapOf(new to true, sampleDeferred().get() to false) } - .groupByKey(numDistinctValues), - ) - -/** - * Tracks the currently selected value of type [A] from an upstream [TState]. - * - * @see selector - */ -@ExperimentalFrpApi -class TStateSelector<in A> -internal constructor( - private val upstream: TState<A>, - private val groupedChanges: GroupedTFlow<A, Boolean>, -) { - /** - * Returns a [TState] that tracks whether the upstream [TState] is currently holding the given - * [value]. - * - * @see selector - */ - @ExperimentalFrpApi - fun whenSelected(value: A): TState<Boolean> { - val operatorName = "TStateSelector#whenSelected" - val name = "$operatorName[$value]" - return TStateInit( - init(name) { - DerivedMapCheap( - name, - operatorName, - upstream = upstream.init.connect(evalScope = this), - changes = groupedChanges.impl.eventsForKey(value), - ) { - it == value - } - } - ) - } - - @ExperimentalFrpApi operator fun get(value: A): TState<Boolean> = whenSelected(value) -} - -/** TODO */ -@ExperimentalFrpApi -class MutableTState<T> -internal constructor(internal val network: Network, initialValue: Deferred<T>) : TState<T>() { - - private val input: CoalescingMutableTFlow<Deferred<T>, Deferred<T>?> = - CoalescingMutableTFlow( - name = null, - coalesce = { _, new -> new }, - network = network, - getInitialValue = { null }, - ) - - internal val tState = run { - val changes = input.impl - val name = null - val operatorName = "MutableTState" - lateinit var state: TStateSource<T> - val calm: TFlowImpl<T> = - filterNode({ mapImpl(upstream = { changes.activated() }) { it!!.await() } }) { new -> - new != state.getCurrentWithEpoch(evalScope = this).first - } - .cached() - state = TStateSource(name, operatorName, initialValue, calm) - @Suppress("DeferredResultUnused") - network.transaction("MutableTState.init") { - calm.activate(evalScope = this, downstream = Schedulable.S(state))?.let { - (connection, needsEval) -> - state.upstreamConnection = connection - if (needsEval) { - schedule(state) - } - } - } - TStateInit(constInit(name, state)) - } - - /** TODO */ - @ExperimentalFrpApi fun setValue(value: T) = input.emit(CompletableDeferred(value)) - - @ExperimentalFrpApi - fun setValueDeferred(value: FrpDeferredValue<T>) = input.emit(value.unwrapped) -} - -/** A forward-reference to a [TState], allowing for recursive definitions. */ -@ExperimentalFrpApi -class TStateLoop<A> : TState<A>() { - - private val name: String? = null - - private val deferred = CompletableDeferred<TState<A>>() - - internal val init: Init<TStateImpl<A>> = - init(name) { deferred.await().init.connect(evalScope = this) } - - /** The [TState] this [TStateLoop] will forward to. */ - @ExperimentalFrpApi - var loopback: TState<A>? = null - set(value) { - value?.let { - check(deferred.complete(value)) { "TStateLoop.loopback has already been set." } - field = value - } - } - - @ExperimentalFrpApi - operator fun getValue(thisRef: Any?, property: KProperty<*>): TState<A> = this - - @ExperimentalFrpApi - operator fun setValue(thisRef: Any?, property: KProperty<*>, value: TState<A>) { - loopback = value - } - - override fun toString(): String = "${this::class.simpleName}@$hashString" -} - -internal class TStateInit<A> internal constructor(internal val init: Init<TStateImpl<A>>) : - TState<A>() { - override fun toString(): String = "${this::class.simpleName}@$hashString" -} - -internal val <A> TState<A>.init: Init<TStateImpl<A>> - get() = - when (this) { - is TStateInit -> init - is TStateLoop -> init - is MutableTState -> tState.init - } - -private inline fun <A> deferInline( - crossinline block: suspend InitScope.() -> TState<A> -): TState<A> = TStateInit(init(name = null) { block().init.connect(evalScope = this) }) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt new file mode 100644 index 000000000000..225416992d52 --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 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 com.android.systemui.kairos + +/** + * Kairos operations that are available while a transaction is active. + * + * These operations do not accumulate state, which makes [TransactionScope] weaker than + * [StateScope], but allows them to be used in more places. + */ +@ExperimentalKairosApi +interface TransactionScope : KairosScope { + + /** + * Returns the current value of this [Transactional] as a [DeferredValue]. + * + * Compared to [sample], you may want to use this instead if you do not need to inspect the + * sampled value, but instead want to pass it to another Kairos API that accepts a + * [DeferredValue]. In this case, [sampleDeferred] is both safer and more performant. + * + * @see sample + */ + fun <A> Transactional<A>.sampleDeferred(): DeferredValue<A> + + /** + * Returns the current value of this [State] as a [DeferredValue]. + * + * Compared to [sample], you may want to use this instead if you do not need to inspect the + * sampled value, but instead want to pass it to another Kairos API that accepts a + * [DeferredValue]. In this case, [sampleDeferred] is both safer and more performant. + * + * @see sample + */ + fun <A> State<A>.sampleDeferred(): DeferredValue<A> + + /** + * Defers invoking [block] until after the current [TransactionScope] code-path completes, + * returning a [DeferredValue] that can be used to reference the result. + * + * Useful for recursive definitions. + * + * @see DeferredValue + */ + fun <A> deferredTransactionScope(block: TransactionScope.() -> A): DeferredValue<A> + + /** An [Events] that emits once, within this transaction, and then never again. */ + val now: Events<Unit> + + /** + * Returns the current value held by this [State]. Guaranteed to be consistent within the same + * transaction. + * + * @see sampleDeferred + */ + fun <A> State<A>.sample(): A = sampleDeferred().get() + + /** + * Returns the current value held by this [Transactional]. Guaranteed to be consistent within + * the same transaction. + * + * @see sampleDeferred + */ + fun <A> Transactional<A>.sample(): A = sampleDeferred().get() +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt index 6b1c8c8fc3e5..9485cd212603 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt @@ -16,57 +16,80 @@ package com.android.systemui.kairos +import com.android.systemui.kairos.internal.CompletableLazy import com.android.systemui.kairos.internal.InitScope import com.android.systemui.kairos.internal.NoScope import com.android.systemui.kairos.internal.TransactionalImpl import com.android.systemui.kairos.internal.init import com.android.systemui.kairos.internal.transactionalImpl import com.android.systemui.kairos.internal.util.hashString -import kotlinx.coroutines.CompletableDeferred /** * A time-varying value. A [Transactional] encapsulates the idea of some continuous state; each time * it is "sampled", a new result may be produced. * - * Because FRP operates over an "idealized" model of Time that can be passed around as a data type, - * [Transactional]s are guaranteed to produce the same result if queried multiple times at the same - * (conceptual) time, in order to preserve _referential transparency_. + * Because Kairos operates over an "idealized" model of Time that can be passed around as a data + * type, [Transactional]s are guaranteed to produce the same result if queried multiple times at the + * same (conceptual) time, in order to preserve _referential transparency_. */ -@ExperimentalFrpApi -class Transactional<out A> internal constructor(internal val impl: TState<TransactionalImpl<A>>) { +@ExperimentalKairosApi +class Transactional<out A> internal constructor(internal val impl: State<TransactionalImpl<A>>) { override fun toString(): String = "${this::class.simpleName}@$hashString" } /** A constant [Transactional] that produces [value] whenever it is sampled. */ -@ExperimentalFrpApi +@ExperimentalKairosApi fun <A> transactionalOf(value: A): Transactional<A> = - Transactional(tStateOf(TransactionalImpl.Const(CompletableDeferred(value)))) + Transactional(stateOf(TransactionalImpl.Const(CompletableLazy(value)))) -/** TODO */ -@ExperimentalFrpApi -fun <A> FrpDeferredValue<Transactional<A>>.defer(): Transactional<A> = deferInline { - unwrapped.await() -} +/** + * Returns a [Transactional] that acts as a deferred-reference to the [Transactional] produced by + * this [DeferredValue]. + * + * When the returned [Transactional] is accessed by the Kairos network, the [DeferredValue] will be + * queried and used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi +fun <A> DeferredValue<Transactional<A>>.defer(): Transactional<A> = deferInline { unwrapped.value } -/** TODO */ -@ExperimentalFrpApi fun <A> Lazy<Transactional<A>>.defer(): Transactional<A> = deferInline { value } +/** + * Returns a [Transactional] that acts as a deferred-reference to the [Transactional] produced by + * this [Lazy]. + * + * When the returned [Transactional] is accessed by the Kairos network, the [Lazy]'s + * [value][Lazy.value] will be queried and used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi +fun <A> Lazy<Transactional<A>>.defer(): Transactional<A> = deferInline { value } -/** TODO */ -@ExperimentalFrpApi -fun <A> deferTransactional(block: suspend FrpScope.() -> Transactional<A>): Transactional<A> = +/** + * Returns a [Transactional] that acts as a deferred-reference to the [Transactional] produced by + * [block]. + * + * When the returned [Transactional] is accessed by the Kairos network, [block] will be invoked and + * the returned [Transactional] will be used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi +fun <A> deferredTransactional(block: KairosScope.() -> Transactional<A>): Transactional<A> = deferInline { - NoScope.runInFrpScope(block) + NoScope.block() } private inline fun <A> deferInline( - crossinline block: suspend InitScope.() -> Transactional<A> + crossinline block: InitScope.() -> Transactional<A> ): Transactional<A> = - Transactional(TStateInit(init(name = null) { block().impl.init.connect(evalScope = this) })) + Transactional(StateInit(init(name = null) { block().impl.init.connect(evalScope = this) })) /** * Returns a [Transactional]. The passed [block] will be evaluated on demand at most once per * transaction; any subsequent sampling within the same transaction will receive a cached value. */ -@ExperimentalFrpApi -fun <A> transactionally(block: suspend FrpTransactionScope.() -> A): Transactional<A> = - Transactional(tStateOf(transactionalImpl { runInTransactionScope(block) })) +@ExperimentalKairosApi +fun <A> transactionally(block: TransactionScope.() -> A): Transactional<A> = + Transactional(stateOf(transactionalImpl { block() })) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/debug/Debug.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/debug/Debug.kt deleted file mode 100644 index 0674a2e75659..000000000000 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/debug/Debug.kt +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright (C) 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 com.android.systemui.kairos.debug - -import com.android.systemui.kairos.MutableTState -import com.android.systemui.kairos.TState -import com.android.systemui.kairos.TStateInit -import com.android.systemui.kairos.TStateLoop -import com.android.systemui.kairos.internal.DerivedFlatten -import com.android.systemui.kairos.internal.DerivedMap -import com.android.systemui.kairos.internal.DerivedMapCheap -import com.android.systemui.kairos.internal.DerivedZipped -import com.android.systemui.kairos.internal.Init -import com.android.systemui.kairos.internal.TStateDerived -import com.android.systemui.kairos.internal.TStateImpl -import com.android.systemui.kairos.internal.TStateSource -import com.android.systemui.kairos.util.Just -import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.None -import com.android.systemui.kairos.util.flatMap -import com.android.systemui.kairos.util.map -import com.android.systemui.kairos.util.none -import com.android.systemui.kairos.util.orElseGet - -// object IdGen { -// private val counter = AtomicLong() -// fun getId() = counter.getAndIncrement() -// } - -typealias StateGraph = Graph<ActivationInfo> - -sealed class StateInfo( - val name: String, - val value: Maybe<Any?>, - val operator: String, - val epoch: Long?, -) - -class Source(name: String, value: Maybe<Any?>, operator: String, epoch: Long) : - StateInfo(name, value, operator, epoch) - -class Derived( - name: String, - val type: DerivedStateType, - value: Maybe<Any?>, - operator: String, - epoch: Long?, -) : StateInfo(name, value, operator, epoch) - -sealed interface DerivedStateType - -data object Flatten : DerivedStateType - -data class Mapped(val cheap: Boolean) : DerivedStateType - -data object Combine : DerivedStateType - -sealed class InitInfo(val name: String) - -class Uninitialized(name: String) : InitInfo(name) - -class Initialized(val state: StateInfo) : InitInfo(state.name) - -sealed interface ActivationInfo - -class Inactive(val name: String) : ActivationInfo - -class Active(val nodeInfo: StateInfo) : ActivationInfo - -class Dead(val name: String) : ActivationInfo - -data class Edge(val upstream: Any, val downstream: Any, val tag: Any? = null) - -data class Graph<T>(val nodes: Map<Any, T>, val edges: List<Edge>) - -internal fun TState<*>.dump(infoMap: MutableMap<Any, InitInfo>, edges: MutableList<Edge>) { - val init: Init<TStateImpl<Any?>> = - when (this) { - is TStateInit -> init - is TStateLoop -> init - is MutableTState -> tState.init - } - when (val stateMaybe = init.getUnsafe()) { - None -> { - infoMap[this] = Uninitialized(init.name ?: init.toString()) - } - is Just -> { - stateMaybe.value.dump(infoMap, edges) - } - } -} - -internal fun TStateImpl<*>.dump(infoById: MutableMap<Any, InitInfo>, edges: MutableList<Edge>) { - val state = this - if (state in infoById) return - val stateInfo = - when (state) { - is TStateDerived -> { - val type = - when (state) { - is DerivedFlatten -> { - state.upstream.dump(infoById, edges) - edges.add( - Edge(upstream = state.upstream, downstream = state, tag = "outer") - ) - state.upstream - .getUnsafe() - .orElseGet { null } - ?.let { - edges.add( - Edge(upstream = it, downstream = state, tag = "inner") - ) - it.dump(infoById, edges) - } - Flatten - } - is DerivedMap<*, *> -> { - state.upstream.dump(infoById, edges) - edges.add(Edge(upstream = state.upstream, downstream = state)) - Mapped(cheap = false) - } - is DerivedZipped<*, *> -> { - state.upstream.forEach { (key, upstream) -> - edges.add( - Edge(upstream = upstream, downstream = state, tag = "key=$key") - ) - upstream.dump(infoById, edges) - } - Combine - } - } - Derived( - state.name ?: state.operatorName, - type, - state.getCachedUnsafe(), - state.operatorName, - state.invalidatedEpoch, - ) - } - is TStateSource -> - Source( - state.name ?: state.operatorName, - state.getStorageUnsafe(), - state.operatorName, - state.writeEpoch, - ) - is DerivedMapCheap<*, *> -> { - state.upstream.dump(infoById, edges) - edges.add(Edge(upstream = state.upstream, downstream = state)) - val type = Mapped(cheap = true) - Derived( - state.name ?: state.operatorName, - type, - state.getUnsafe(), - state.operatorName, - null, - ) - } - } - infoById[state] = Initialized(stateInfo) -} - -private fun <A> TStateImpl<A>.getUnsafe(): Maybe<A> = - when (this) { - is TStateDerived -> getCachedUnsafe() - is TStateSource -> getStorageUnsafe() - is DerivedMapCheap<*, *> -> none - } - -private fun <A> TStateImpl<A>.getUnsafeWithEpoch(): Maybe<Pair<A, Long>> = - when (this) { - is TStateDerived -> getCachedUnsafe().map { it to invalidatedEpoch } - is TStateSource -> getStorageUnsafe().map { it to writeEpoch } - is DerivedMapCheap<*, *> -> none - } - -/** - * Returns the current value held in this [TState], or [none] if the [TState] has not been - * initialized. - * - * The returned [Long] is the *epoch* at which the internal cache was last updated. This can be used - * to identify values which are out-of-date. - */ -fun <A> TState<A>.sampleUnsafe(): Maybe<Pair<A, Long>> = - when (this) { - is MutableTState -> tState.init.getUnsafe().flatMap { it.getUnsafeWithEpoch() } - is TStateInit -> init.getUnsafe().flatMap { it.getUnsafeWithEpoch() } - is TStateLoop -> this.init.getUnsafe().flatMap { it.getUnsafeWithEpoch() } - } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt index 7e6384925f38..b20e77a31dab 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt @@ -16,154 +16,115 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.CoalescingMutableTFlow -import com.android.systemui.kairos.FrpBuildScope -import com.android.systemui.kairos.FrpCoalescingProducerScope -import com.android.systemui.kairos.FrpDeferredValue -import com.android.systemui.kairos.FrpEffectScope -import com.android.systemui.kairos.FrpNetwork -import com.android.systemui.kairos.FrpProducerScope -import com.android.systemui.kairos.FrpSpec -import com.android.systemui.kairos.FrpStateScope -import com.android.systemui.kairos.FrpTransactionScope -import com.android.systemui.kairos.GroupedTFlow -import com.android.systemui.kairos.LocalFrpNetwork -import com.android.systemui.kairos.MutableTFlow -import com.android.systemui.kairos.TFlow -import com.android.systemui.kairos.TFlowInit +import com.android.systemui.kairos.BuildScope +import com.android.systemui.kairos.BuildSpec +import com.android.systemui.kairos.CoalescingEventProducerScope +import com.android.systemui.kairos.CoalescingMutableEvents +import com.android.systemui.kairos.DeferredValue +import com.android.systemui.kairos.EffectScope +import com.android.systemui.kairos.EventProducerScope +import com.android.systemui.kairos.Events +import com.android.systemui.kairos.EventsInit +import com.android.systemui.kairos.GroupedEvents +import com.android.systemui.kairos.KairosNetwork +import com.android.systemui.kairos.LocalNetwork +import com.android.systemui.kairos.MutableEvents +import com.android.systemui.kairos.TransactionScope import com.android.systemui.kairos.groupByKey import com.android.systemui.kairos.init import com.android.systemui.kairos.internal.util.childScope -import com.android.systemui.kairos.internal.util.mapValuesParallel +import com.android.systemui.kairos.internal.util.launchImmediate import com.android.systemui.kairos.launchEffect import com.android.systemui.kairos.mergeLeft -import com.android.systemui.kairos.util.Just import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.None +import com.android.systemui.kairos.util.Maybe.Just +import com.android.systemui.kairos.util.Maybe.None import com.android.systemui.kairos.util.just import com.android.systemui.kairos.util.map import java.util.concurrent.atomic.AtomicReference -import kotlin.coroutines.Continuation import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.startCoroutine -import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableJob import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred +import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.Job import kotlinx.coroutines.cancel -import kotlinx.coroutines.completeWith import kotlinx.coroutines.job internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope: CoroutineScope) : - BuildScope, StateScope by stateScope { + InternalBuildScope, InternalStateScope by stateScope { private val job: Job get() = coroutineScope.coroutineContext.job - override val frpScope: FrpBuildScope = FrpBuildScopeImpl() - - override suspend fun <R> runInBuildScope(block: suspend FrpBuildScope.() -> R): R { - val complete = CompletableDeferred<R>(parent = coroutineContext.job) - block.startCoroutine( - frpScope, - object : Continuation<R> { - override val context: CoroutineContext - get() = EmptyCoroutineContext - - override fun resumeWith(result: Result<R>) { - complete.completeWith(result) - } - }, - ) - return complete.await() + override val kairosNetwork: KairosNetwork by lazy { + LocalNetwork(network, coroutineScope, endSignal) } - private fun <A, T : TFlow<A>, S> buildTFlow( - constructFlow: (InputNode<A>) -> Pair<T, S>, - builder: suspend S.() -> Unit, - ): TFlow<A> { - var job: Job? = null - val stopEmitter = newStopEmitter("buildTFlow") - // Create a child scope that will be kept alive beyond the end of this transaction. - val childScope = coroutineScope.childScope() - lateinit var emitter: Pair<T, S> - val inputNode = - InputNode<A>( - activate = { - check(job == null) { "already activated" } - job = - reenterBuildScope(this@BuildScopeImpl, childScope).runInBuildScope { - launchEffect { - builder(emitter.second) - stopEmitter.emit(Unit) - } - } - }, - deactivate = { - checkNotNull(job) { "already deactivated" }.cancel() - job = null - }, - ) - emitter = constructFlow(inputNode) - return with(frpScope) { emitter.first.takeUntil(mergeLeft(stopEmitter, endSignal)) } - } - - private fun <T> tFlowInternal(builder: suspend FrpProducerScope<T>.() -> Unit): TFlow<T> = - buildTFlow( - constructFlow = { inputNode -> - val flow = MutableTFlow(network, inputNode) - flow to - object : FrpProducerScope<T> { + override fun <T> events( + name: String?, + builder: suspend EventProducerScope<T>.() -> Unit, + ): Events<T> = + buildEvents( + name, + constructEvents = { inputNode -> + val events = MutableEvents(network, inputNode) + events to + object : EventProducerScope<T> { override suspend fun emit(value: T) { - flow.emit(value) + events.emit(value) } } }, builder = builder, ) - private fun <In, Out> coalescingTFlowInternal( + override fun <In, Out> coalescingEvents( getInitialValue: () -> Out, coalesce: (old: Out, new: In) -> Out, - builder: suspend FrpCoalescingProducerScope<In>.() -> Unit, - ): TFlow<Out> = - buildTFlow( - constructFlow = { inputNode -> - val flow = - CoalescingMutableTFlow(null, coalesce, network, getInitialValue, inputNode) - flow to - object : FrpCoalescingProducerScope<In> { + builder: suspend CoalescingEventProducerScope<In>.() -> Unit, + ): Events<Out> = + buildEvents( + constructEvents = { inputNode -> + val events = + CoalescingMutableEvents( + null, + coalesce = { old, new: In -> coalesce(old.value, new) }, + network, + getInitialValue, + inputNode, + ) + events to + object : CoalescingEventProducerScope<In> { override fun emit(value: In) { - flow.emit(value) + events.emit(value) } } }, builder = builder, ) - private fun <A> asyncScopeInternal(block: FrpSpec<A>): Pair<FrpDeferredValue<A>, Job> { + override fun <A> asyncScope(block: BuildSpec<A>): Pair<DeferredValue<A>, Job> { val childScope = mutableChildBuildScope() - return FrpDeferredValue(deferAsync { childScope.runInBuildScope(block) }) to childScope.job + return DeferredValue(deferAsync { block(childScope) }) to childScope.job } - private fun <R> deferredInternal(block: suspend FrpBuildScope.() -> R): FrpDeferredValue<R> = - FrpDeferredValue(deferAsync { runInBuildScope(block) }) + override fun <R> deferredBuildScope(block: BuildScope.() -> R): DeferredValue<R> = + DeferredValue(deferAsync { block() }) - private fun deferredActionInternal(block: suspend FrpBuildScope.() -> Unit) { - deferAction { runInBuildScope(block) } + override fun deferredBuildScopeAction(block: BuildScope.() -> Unit) { + deferAction { block() } } - private fun <A> TFlow<A>.observeEffectInternal( - context: CoroutineContext, - block: suspend FrpEffectScope.(A) -> Unit, - ): Job { + override fun <A> Events<A>.observe( + coroutineContext: CoroutineContext, + block: EffectScope.(A) -> Unit, + ): DisposableHandle { val subRef = AtomicReference<Maybe<Output<A>>>(null) val childScope = coroutineScope.childScope() - // When our scope is cancelled, deactivate this observer. - childScope.coroutineContext.job.invokeOnCompletion { + lateinit var cancelHandle: DisposableHandle + val handle = DisposableHandle { subRef.getAndSet(None)?.let { output -> + cancelHandle.dispose() if (output is Just) { @Suppress("DeferredResultUnused") network.transaction("observeEffect cancelled") { @@ -172,38 +133,30 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope } } } - // Defer so that we don't suspend the caller + // When our scope is cancelled, deactivate this observer. + cancelHandle = childScope.coroutineContext.job.invokeOnCompletion { handle.dispose() } + val localNetwork = LocalNetwork(network, childScope, endSignal) + val outputNode = + Output<A>( + context = coroutineContext, + onDeath = { subRef.set(None) }, + onEmit = { output -> + if (subRef.get() is Just) { + // Not cancelled, safe to emit + val scope = + object : EffectScope, TransactionScope by this { + override val effectCoroutineScope: CoroutineScope = childScope + override val kairosNetwork: KairosNetwork = localNetwork + } + scope.block(output) + } + }, + ) + // Defer, in case any EventsLoops / StateLoops still need to be set deferAction { - val outputNode = - Output<A>( - context = context, - onDeath = { subRef.getAndSet(None)?.let { childScope.cancel() } }, - onEmit = { output -> - if (subRef.get() is Just) { - // Not cancelled, safe to emit - val coroutine: suspend FrpEffectScope.() -> Unit = { block(output) } - val complete = CompletableDeferred<Unit>(parent = coroutineContext.job) - coroutine.startCoroutine( - object : FrpEffectScope, FrpTransactionScope by frpScope { - override val frpCoroutineScope: CoroutineScope = childScope - override val frpNetwork: FrpNetwork = - LocalFrpNetwork(network, childScope, endSignal) - }, - completion = - object : Continuation<Unit> { - override val context: CoroutineContext - get() = EmptyCoroutineContext - - override fun resumeWith(result: Result<Unit>) { - complete.completeWith(result) - } - }, - ) - complete.await() - } - }, - ) - with(frpScope) { this@observeEffectInternal.takeUntil(endSignal) } + // Check for immediate cancellation + if (subRef.get() != null) return@deferAction + this@observe.takeUntil(endSignal) .init .connect(evalScope = stateScope.evalScope) .activate(evalScope = stateScope.evalScope, outputNode.schedulable) @@ -213,71 +166,110 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope // Job's already been cancelled, schedule deactivation scheduleDeactivation(outputNode) } else if (needsEval) { - outputNode.schedule(evalScope = stateScope.evalScope) + outputNode.schedule(0, evalScope = stateScope.evalScope) } } ?: run { childScope.cancel() } } - return childScope.coroutineContext.job + return handle } - private fun <A, B> TFlow<A>.mapBuildInternal( - transform: suspend FrpBuildScope.(A) -> B - ): TFlow<B> { + override fun <A, B> Events<A>.mapBuild(transform: BuildScope.(A) -> B): Events<B> { val childScope = coroutineScope.childScope() - return TFlowInit( + return EventsInit( constInit( "mapBuild", - mapImpl({ init.connect(evalScope = this) }) { spec -> + mapImpl({ init.connect(evalScope = this) }) { spec, _ -> reenterBuildScope(outerScope = this@BuildScopeImpl, childScope) - .runInBuildScope { transform(spec) } + .transform(spec) } .cached(), ) ) } - private fun <K, A, B> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestForKeyInternal( - init: FrpDeferredValue<Map<K, FrpSpec<B>>>, + override fun <K, A, B> Events<Map<K, Maybe<BuildSpec<A>>>>.applyLatestSpecForKey( + initialSpecs: DeferredValue<Map<K, BuildSpec<B>>>, numKeys: Int?, - ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> { - val eventsByKey: GroupedTFlow<K, Maybe<FrpSpec<A>>> = groupByKey(numKeys) - val initOut: Deferred<Map<K, B>> = deferAsync { - init.unwrapped.await().mapValuesParallel { (k, spec) -> - val newEnd = with(frpScope) { eventsByKey[k].skipNext() } + ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> { + val eventsByKey: GroupedEvents<K, Maybe<BuildSpec<A>>> = groupByKey(numKeys) + val initOut: Lazy<Map<K, B>> = deferAsync { + initialSpecs.unwrapped.value.mapValues { (k, spec) -> + val newEnd = eventsByKey[k] val newScope = childBuildScope(newEnd) - newScope.runInBuildScope(spec) + newScope.spec() } } val childScope = coroutineScope.childScope() - val changesNode: TFlowImpl<Map<K, Maybe<A>>> = - mapImpl(upstream = { this@applyLatestForKeyInternal.init.connect(evalScope = this) }) { - upstreamMap -> + val changesNode: EventsImpl<Map<K, Maybe<A>>> = + mapImpl(upstream = { this@applyLatestSpecForKey.init.connect(evalScope = this) }) { + upstreamMap, + _ -> reenterBuildScope(this@BuildScopeImpl, childScope).run { - upstreamMap.mapValuesParallel { (k: K, ma: Maybe<FrpSpec<A>>) -> + upstreamMap.mapValues { (k: K, ma: Maybe<BuildSpec<A>>) -> ma.map { spec -> - val newEnd = with(frpScope) { eventsByKey[k].skipNext() } + val newEnd = eventsByKey[k].skipNext() val newScope = childBuildScope(newEnd) - newScope.runInBuildScope(spec) + newScope.spec() } } } } - val changes: TFlow<Map<K, Maybe<A>>> = - TFlowInit(constInit("applyLatestForKey", changesNode.cached())) + val changes: Events<Map<K, Maybe<A>>> = + EventsInit(constInit("applyLatestForKey", changesNode.cached())) // Ensure effects are observed; otherwise init will stay alive longer than expected - changes.observeEffectInternal(EmptyCoroutineContext) {} - return changes to FrpDeferredValue(initOut) + changes.observe() + return changes to DeferredValue(initOut) + } + + private fun <A, T : Events<A>, S> buildEvents( + name: String? = null, + constructEvents: (InputNode<A>) -> Pair<T, S>, + builder: suspend S.() -> Unit, + ): Events<A> { + var job: Job? = null + val stopEmitter = newStopEmitter("buildEvents[$name]") + // Create a child scope that will be kept alive beyond the end of this transaction. + val childScope = coroutineScope.childScope() + lateinit var emitter: Pair<T, S> + val inputNode = + InputNode<A>( + activate = { + // It's possible that activation occurs after all effects have been run, due + // to a MuxDeferred switch-in. For this reason, we need to activate in a new + // transaction. + check(job == null) { "[$name] already activated" } + job = + childScope.launchImmediate { + network + .transaction("buildEvents") { + reenterBuildScope(this@BuildScopeImpl, childScope) + .launchEffect { + builder(emitter.second) + stopEmitter.emit(Unit) + } + } + .await() + .join() + } + }, + deactivate = { + checkNotNull(job) { "[$name] already deactivated" }.cancel() + job = null + }, + ) + emitter = constructEvents(inputNode) + return emitter.first.takeUntil(mergeLeft(stopEmitter, endSignal)) } - private fun newStopEmitter(name: String): CoalescingMutableTFlow<Unit, Unit> = - CoalescingMutableTFlow( + private fun newStopEmitter(name: String): CoalescingMutableEvents<Unit, Unit> = + CoalescingMutableEvents( name = name, coalesce = { _, _: Unit -> }, network = network, getInitialValue = {}, ) - private suspend fun childBuildScope(newEnd: TFlow<Any>): BuildScopeImpl { + private fun childBuildScope(newEnd: Events<Any>): BuildScopeImpl { val newCoroutineScope: CoroutineScope = coroutineScope.childScope() return BuildScopeImpl( stateScope = stateScope.childStateScope(newEnd), @@ -292,7 +284,7 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope (newCoroutineScope.coroutineContext.job as CompletableJob).complete() } ) - runInBuildScope { endSignal.nextOnly().observe { newCoroutineScope.cancel() } } + endSignalOnce.observe { newCoroutineScope.cancel() } } } @@ -315,42 +307,6 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope coroutineScope = childScope, ) } - - private inner class FrpBuildScopeImpl : FrpBuildScope, FrpStateScope by stateScope.frpScope { - - override fun <T> tFlow(builder: suspend FrpProducerScope<T>.() -> Unit): TFlow<T> = - tFlowInternal(builder) - - override fun <In, Out> coalescingTFlow( - getInitialValue: () -> Out, - coalesce: (old: Out, new: In) -> Out, - builder: suspend FrpCoalescingProducerScope<In>.() -> Unit, - ): TFlow<Out> = coalescingTFlowInternal(getInitialValue, coalesce, builder) - - override fun <A> asyncScope(block: FrpSpec<A>): Pair<FrpDeferredValue<A>, Job> = - asyncScopeInternal(block) - - override fun <R> deferredBuildScope( - block: suspend FrpBuildScope.() -> R - ): FrpDeferredValue<R> = deferredInternal(block) - - override fun deferredBuildScopeAction(block: suspend FrpBuildScope.() -> Unit) = - deferredActionInternal(block) - - override fun <A> TFlow<A>.observe( - coroutineContext: CoroutineContext, - block: suspend FrpEffectScope.(A) -> Unit, - ): Job = observeEffectInternal(coroutineContext, block) - - override fun <A, B> TFlow<A>.mapBuild(transform: suspend FrpBuildScope.(A) -> B): TFlow<B> = - mapBuildInternal(transform) - - override fun <K, A, B> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestSpecForKey( - initialSpecs: FrpDeferredValue<Map<K, FrpSpec<B>>>, - numKeys: Int?, - ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> = - applyLatestForKeyInternal(initialSpecs, numKeys) - } } private fun EvalScope.reenterBuildScope( diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/DeferScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/DeferScope.kt index f65307c6106f..d2c154f05b37 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/DeferScope.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/DeferScope.kt @@ -16,33 +16,51 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.internal.util.asyncImmediate -import com.android.systemui.kairos.internal.util.launchImmediate -import kotlinx.coroutines.CoroutineName -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Job -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.isActive - -internal typealias DeferScope = CoroutineScope - -internal inline fun DeferScope.deferAction( - start: CoroutineStart = CoroutineStart.UNDISPATCHED, - crossinline block: suspend () -> Unit, -): Job { - check(isActive) { "Cannot perform deferral, scope already closed." } - return launchImmediate(start, CoroutineName("deferAction")) { block() } +internal interface DeferScope { + fun deferAction(block: () -> Unit) + + fun <R> deferAsync(block: () -> R): Lazy<R> } -internal inline fun <R> DeferScope.deferAsync( - start: CoroutineStart = CoroutineStart.UNDISPATCHED, - crossinline block: suspend () -> R, -): Deferred<R> { - check(isActive) { "Cannot perform deferral, scope already closed." } - return asyncImmediate(start, CoroutineName("deferAsync")) { block() } +internal inline fun <A> deferScope(block: DeferScope.() -> A): A { + val scope = + object : DeferScope { + val deferrals = ArrayDeque<() -> Unit>() // TODO: store lazies instead? + + fun drainDeferrals() { + while (deferrals.isNotEmpty()) { + deferrals.removeFirst().invoke() + } + } + + override fun deferAction(block: () -> Unit) { + deferrals.add(block) + } + + override fun <R> deferAsync(block: () -> R): Lazy<R> = + lazy(block).also { deferrals.add { it.value } } + } + return scope.block().also { scope.drainDeferrals() } } -internal suspend inline fun <A> deferScope(noinline block: suspend DeferScope.() -> A): A = - coroutineScope(block) +internal object NoValue + +internal class CompletableLazy<T>( + private var _value: Any? = NoValue, + private val name: String? = null, +) : Lazy<T> { + + fun setValue(value: T) { + check(_value === NoValue) { "CompletableLazy value already set" } + _value = value + } + + override val value: T + get() { + check(_value !== NoValue) { "CompletableLazy($name) accessed before initialized" } + @Suppress("UNCHECKED_CAST") + return _value as T + } + + override fun isInitialized(): Boolean = _value !== NoValue +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Demux.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Demux.kt index e7b99528fdfc..4cf24580fa32 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Demux.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Demux.kt @@ -14,270 +14,242 @@ * limitations under the License. */ -@file:Suppress("NOTHING_TO_INLINE") - package com.android.systemui.kairos.internal +import com.android.systemui.kairos.internal.store.ConcurrentHashMapK +import com.android.systemui.kairos.internal.store.MapHolder +import com.android.systemui.kairos.internal.store.MapK +import com.android.systemui.kairos.internal.store.MutableMapK import com.android.systemui.kairos.internal.util.hashString -import com.android.systemui.kairos.util.Just -import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.flatMap -import com.android.systemui.kairos.util.getMaybe -import java.util.concurrent.ConcurrentHashMap -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch +import com.android.systemui.kairos.internal.util.logDuration import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -internal class DemuxNode<K, A>( - private val branchNodeByKey: ConcurrentHashMap<K, DemuxBranchNode<K, A>>, +internal class DemuxNode<W, K, A>( + private val branchNodeByKey: MutableMapK<W, K, DemuxNode<W, K, A>.BranchNode>, val lifecycle: DemuxLifecycle<K, A>, - private val spec: DemuxActivator<K, A>, + private val spec: DemuxActivator<W, K, A>, ) : SchedulableNode { val schedulable = Schedulable.N(this) - inline val mutex - get() = lifecycle.mutex - - lateinit var upstreamConnection: NodeConnection<Map<K, A>> - - fun getAndMaybeAddDownstream(key: K): DemuxBranchNode<K, A> = - branchNodeByKey.getOrPut(key) { DemuxBranchNode(key, this) } - - override suspend fun schedule(evalScope: EvalScope) { - val upstreamResult = upstreamConnection.getPushEvent(evalScope) - if (upstreamResult is Just) { - coroutineScope { - val outerScope = this - mutex.withLock { - coroutineScope { - for ((key, _) in upstreamResult.value) { - launch { - branchNodeByKey[key]?.let { branch -> - outerScope.launch { branch.schedule(evalScope) } - } - } - } - } + lateinit var upstreamConnection: NodeConnection<MapK<W, K, A>> + + @Volatile private var epoch: Long = Long.MIN_VALUE + + fun hasCurrentValueLocked(logIndent: Int, evalScope: EvalScope, key: K): Boolean = + evalScope.epoch == epoch && + upstreamConnection.getPushEvent(logIndent, evalScope).contains(key) + + fun hasCurrentValue(logIndent: Int, evalScope: EvalScope, key: K): Boolean = + hasCurrentValueLocked(logIndent, evalScope, key) + + fun getAndMaybeAddDownstream(key: K): BranchNode = + branchNodeByKey.getOrPut(key) { BranchNode(key) } + + override fun schedule(logIndent: Int, evalScope: EvalScope) = + logDuration(logIndent, "DemuxNode.schedule") { + val upstreamResult = + logDuration("upstream.getPushEvent") { + upstreamConnection.getPushEvent(currentLogIndent, evalScope) } + updateEpoch(evalScope) + for ((key, _) in upstreamResult) { + if (!branchNodeByKey.contains(key)) continue + val branch = branchNodeByKey.getValue(key) + branch.schedule(currentLogIndent, evalScope) } } - } - override suspend fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) { - coroutineScope { - mutex.withLock { - for ((_, branchNode) in branchNodeByKey) { - branchNode.downstreamSet.adjustDirectUpstream( - coroutineScope = this, - scheduler, - oldDepth, - newDepth, - ) - } - } + override fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) { + for ((_, branchNode) in branchNodeByKey) { + branchNode.downstreamSet.adjustDirectUpstream(scheduler, oldDepth, newDepth) } } - override suspend fun moveIndirectUpstreamToDirect( + override fun moveIndirectUpstreamToDirect( scheduler: Scheduler, oldIndirectDepth: Int, - oldIndirectSet: Set<MuxDeferredNode<*, *>>, + oldIndirectSet: Set<MuxDeferredNode<*, *, *>>, newDirectDepth: Int, ) { - coroutineScope { - mutex.withLock { - for ((_, branchNode) in branchNodeByKey) { - branchNode.downstreamSet.moveIndirectUpstreamToDirect( - coroutineScope = this, - scheduler, - oldIndirectDepth, - oldIndirectSet, - newDirectDepth, - ) - } - } + for ((_, branchNode) in branchNodeByKey) { + branchNode.downstreamSet.moveIndirectUpstreamToDirect( + scheduler, + oldIndirectDepth, + oldIndirectSet, + newDirectDepth, + ) } } - override suspend fun adjustIndirectUpstream( + override fun adjustIndirectUpstream( scheduler: Scheduler, oldDepth: Int, newDepth: Int, - removals: Set<MuxDeferredNode<*, *>>, - additions: Set<MuxDeferredNode<*, *>>, + removals: Set<MuxDeferredNode<*, *, *>>, + additions: Set<MuxDeferredNode<*, *, *>>, ) { - coroutineScope { - mutex.withLock { - for ((_, branchNode) in branchNodeByKey) { - branchNode.downstreamSet.adjustIndirectUpstream( - coroutineScope = this, - scheduler, - oldDepth, - newDepth, - removals, - additions, - ) - } - } + for ((_, branchNode) in branchNodeByKey) { + branchNode.downstreamSet.adjustIndirectUpstream( + scheduler, + oldDepth, + newDepth, + removals, + additions, + ) } } - override suspend fun moveDirectUpstreamToIndirect( + override fun moveDirectUpstreamToIndirect( scheduler: Scheduler, oldDirectDepth: Int, newIndirectDepth: Int, - newIndirectSet: Set<MuxDeferredNode<*, *>>, + newIndirectSet: Set<MuxDeferredNode<*, *, *>>, ) { - coroutineScope { - mutex.withLock { - for ((_, branchNode) in branchNodeByKey) { - branchNode.downstreamSet.moveDirectUpstreamToIndirect( - coroutineScope = this, - scheduler, - oldDirectDepth, - newIndirectDepth, - newIndirectSet, - ) - } - } + for ((_, branchNode) in branchNodeByKey) { + branchNode.downstreamSet.moveDirectUpstreamToIndirect( + scheduler, + oldDirectDepth, + newIndirectDepth, + newIndirectSet, + ) } } - override suspend fun removeIndirectUpstream( + override fun removeIndirectUpstream( scheduler: Scheduler, depth: Int, - indirectSet: Set<MuxDeferredNode<*, *>>, + indirectSet: Set<MuxDeferredNode<*, *, *>>, ) { - coroutineScope { - mutex.withLock { - lifecycle.lifecycleState = DemuxLifecycleState.Dead - for ((_, branchNode) in branchNodeByKey) { - branchNode.downstreamSet.removeIndirectUpstream( - coroutineScope = this, - scheduler, - depth, - indirectSet, - ) - } - } + lifecycle.lifecycleState = DemuxLifecycleState.Dead + for ((_, branchNode) in branchNodeByKey) { + branchNode.downstreamSet.removeIndirectUpstream(scheduler, depth, indirectSet) } } - override suspend fun removeDirectUpstream(scheduler: Scheduler, depth: Int) { - coroutineScope { - mutex.withLock { - lifecycle.lifecycleState = DemuxLifecycleState.Dead - for ((_, branchNode) in branchNodeByKey) { - branchNode.downstreamSet.removeDirectUpstream( - coroutineScope = this, - scheduler, - depth, - ) - } - } + override fun removeDirectUpstream(scheduler: Scheduler, depth: Int) { + lifecycle.lifecycleState = DemuxLifecycleState.Dead + for ((_, branchNode) in branchNodeByKey) { + branchNode.downstreamSet.removeDirectUpstream(scheduler, depth) } } - suspend fun removeDownstreamAndDeactivateIfNeeded(key: K) { - val deactivate = - mutex.withLock { - branchNodeByKey.remove(key) - branchNodeByKey.isEmpty() - } + fun removeDownstreamAndDeactivateIfNeeded(key: K) { + branchNodeByKey.remove(key) + val deactivate = branchNodeByKey.isEmpty() if (deactivate) { // No need for mutex here; no more concurrent changes to can occur during this phase lifecycle.lifecycleState = DemuxLifecycleState.Inactive(spec) upstreamConnection.removeDownstreamAndDeactivateIfNeeded(downstream = schedulable) } } -} -internal class DemuxBranchNode<K, A>(val key: K, private val demuxNode: DemuxNode<K, A>) : - PushNode<A> { + fun updateEpoch(evalScope: EvalScope) { + epoch = evalScope.epoch + } - private val mutex = Mutex() + fun getPushEvent(logIndent: Int, evalScope: EvalScope, key: K): A = + logDuration(logIndent, "Demux.getPushEvent($key)") { + upstreamConnection.getPushEvent(currentLogIndent, evalScope).getValue(key) + } - val downstreamSet = DownstreamSet() + inner class BranchNode(val key: K) : PushNode<A> { - override val depthTracker: DepthTracker - get() = demuxNode.upstreamConnection.depthTracker + val downstreamSet = DownstreamSet() - override suspend fun hasCurrentValue(transactionStore: TransactionStore): Boolean = - demuxNode.upstreamConnection.hasCurrentValue(transactionStore) + override val depthTracker: DepthTracker + get() = upstreamConnection.depthTracker - override suspend fun getPushEvent(evalScope: EvalScope): Maybe<A> = - demuxNode.upstreamConnection.getPushEvent(evalScope).flatMap { it.getMaybe(key) } + override fun hasCurrentValue(logIndent: Int, evalScope: EvalScope): Boolean = + hasCurrentValue(logIndent, evalScope, key) - override suspend fun addDownstream(downstream: Schedulable) { - mutex.withLock { downstreamSet.add(downstream) } - } + override fun getPushEvent(logIndent: Int, evalScope: EvalScope): A = + getPushEvent(logIndent, evalScope, key) - override suspend fun removeDownstream(downstream: Schedulable) { - mutex.withLock { downstreamSet.remove(downstream) } - } + override fun addDownstream(downstream: Schedulable) { + downstreamSet.add(downstream) + } + + override fun removeDownstream(downstream: Schedulable) { + downstreamSet.remove(downstream) + } - override suspend fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) { - val canDeactivate = - mutex.withLock { - downstreamSet.remove(downstream) - downstreamSet.isEmpty() + override fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) { + downstreamSet.remove(downstream) + val canDeactivate = downstreamSet.isEmpty() + if (canDeactivate) { + removeDownstreamAndDeactivateIfNeeded(key) } - if (canDeactivate) { - demuxNode.removeDownstreamAndDeactivateIfNeeded(key) } - } - override suspend fun deactivateIfNeeded() { - if (mutex.withLock { downstreamSet.isEmpty() }) { - demuxNode.removeDownstreamAndDeactivateIfNeeded(key) + override fun deactivateIfNeeded() { + if (downstreamSet.isEmpty()) { + removeDownstreamAndDeactivateIfNeeded(key) + } } - } - override suspend fun scheduleDeactivationIfNeeded(evalScope: EvalScope) { - if (mutex.withLock { downstreamSet.isEmpty() }) { - evalScope.scheduleDeactivation(this) + override fun scheduleDeactivationIfNeeded(evalScope: EvalScope) { + if (downstreamSet.isEmpty()) { + evalScope.scheduleDeactivation(this) + } } - } - suspend fun schedule(evalScope: EvalScope) { - if (!coroutineScope { mutex.withLock { scheduleAll(downstreamSet, evalScope) } }) { - evalScope.scheduleDeactivation(this) + fun schedule(logIndent: Int, evalScope: EvalScope) { + logDuration(logIndent, "DemuxBranchNode($key).schedule") { + if (!scheduleAll(currentLogIndent, downstreamSet, evalScope)) { + evalScope.scheduleDeactivation(this@BranchNode) + } + } } } } -internal fun <K, A> DemuxImpl( - upstream: suspend EvalScope.() -> TFlowImpl<Map<K, A>>, +internal fun <W, K, A> DemuxImpl( + upstream: EventsImpl<MapK<W, K, A>>, numKeys: Int?, + storeFactory: MutableMapK.Factory<W, K>, ): DemuxImpl<K, A> = DemuxImpl( DemuxLifecycle( - object : DemuxActivator<K, A> { - override suspend fun activate( - evalScope: EvalScope, - lifecycle: DemuxLifecycle<K, A>, - ): Pair<DemuxNode<K, A>, Boolean>? { - val dmux = DemuxNode(ConcurrentHashMap(numKeys ?: 16), lifecycle, this) - return upstream - .invoke(evalScope) - .activate(evalScope, downstream = dmux.schedulable) - ?.let { (conn, needsEval) -> - dmux.apply { upstreamConnection = conn } to needsEval - } - } - } + DemuxLifecycleState.Inactive(DemuxActivator(numKeys, upstream, storeFactory)) ) ) +internal fun <K, A> demuxMap( + upstream: EvalScope.() -> EventsImpl<Map<K, A>>, + numKeys: Int?, +): DemuxImpl<K, A> = + DemuxImpl(mapImpl(upstream) { it, _ -> MapHolder(it) }, numKeys, ConcurrentHashMapK.Factory()) + +internal class DemuxActivator<W, K, A>( + private val numKeys: Int?, + private val upstream: EventsImpl<MapK<W, K, A>>, + private val storeFactory: MutableMapK.Factory<W, K>, +) { + fun activate( + evalScope: EvalScope, + lifecycle: DemuxLifecycle<K, A>, + ): Pair<DemuxNode<W, K, A>, Set<K>>? { + val demux = DemuxNode(storeFactory.create(numKeys), lifecycle, this) + return upstream.activate(evalScope, demux.schedulable)?.let { (conn, needsEval) -> + Pair( + demux.apply { upstreamConnection = conn }, + if (needsEval) { + demux.updateEpoch(evalScope) + conn.getPushEvent(0, evalScope).keys + } else { + emptySet() + }, + ) + } + } +} + internal class DemuxImpl<in K, out A>(private val dmux: DemuxLifecycle<K, A>) { - fun eventsForKey(key: K): TFlowImpl<A> = TFlowCheap { downstream -> + fun eventsForKey(key: K): EventsImpl<A> = EventsImplCheap { downstream -> dmux.activate(evalScope = this, key)?.let { (branchNode, needsEval) -> branchNode.addDownstream(downstream) - val branchNeedsEval = needsEval && branchNode.getPushEvent(evalScope = this) is Just + val branchNeedsEval = needsEval && branchNode.hasCurrentValue(0, evalScope = this) ActivationResult( connection = NodeConnection(branchNode, branchNode), needsEval = branchNeedsEval, @@ -289,61 +261,45 @@ internal class DemuxImpl<in K, out A>(private val dmux: DemuxLifecycle<K, A>) { internal class DemuxLifecycle<K, A>(@Volatile var lifecycleState: DemuxLifecycleState<K, A>) { val mutex = Mutex() - override fun toString(): String = "TFlowDmuxState[$hashString][$lifecycleState][$mutex]" - - suspend fun activate(evalScope: EvalScope, key: K): Pair<DemuxBranchNode<K, A>, Boolean>? = - coroutineScope { - mutex - .withLock { - when (val state = lifecycleState) { - is DemuxLifecycleState.Dead -> null - is DemuxLifecycleState.Active -> - state.node.getAndMaybeAddDownstream(key) to - async { - state.node.upstreamConnection.hasCurrentValue( - evalScope.transactionStore - ) - } - is DemuxLifecycleState.Inactive -> { - state.spec - .activate(evalScope, this@DemuxLifecycle) - .also { result -> - lifecycleState = - if (result == null) { - DemuxLifecycleState.Dead - } else { - DemuxLifecycleState.Active(result.first) - } - } - ?.let { (node, needsEval) -> - node.getAndMaybeAddDownstream(key) to - CompletableDeferred(needsEval) - } - } + override fun toString(): String = "EventsDmuxState[$hashString][$lifecycleState][$mutex]" + + fun activate(evalScope: EvalScope, key: K): Pair<DemuxNode<*, K, A>.BranchNode, Boolean>? = + when (val state = lifecycleState) { + is DemuxLifecycleState.Dead -> { + null + } + + is DemuxLifecycleState.Active -> { + state.node.getAndMaybeAddDownstream(key) to + state.node.hasCurrentValueLocked(0, evalScope, key) + } + + is DemuxLifecycleState.Inactive -> { + state.spec + .activate(evalScope, this@DemuxLifecycle) + .also { result -> + lifecycleState = + if (result == null) { + DemuxLifecycleState.Dead + } else { + DemuxLifecycleState.Active(result.first) + } } - } - ?.let { (branch, result) -> branch to result.await() } + ?.let { (node, needsEval) -> + node.getAndMaybeAddDownstream(key) to (key in needsEval) + } + } } } internal sealed interface DemuxLifecycleState<out K, out A> { - class Inactive<K, A>(val spec: DemuxActivator<K, A>) : DemuxLifecycleState<K, A> { + class Inactive<K, A>(val spec: DemuxActivator<*, K, A>) : DemuxLifecycleState<K, A> { override fun toString(): String = "Inactive" } - class Active<K, A>(val node: DemuxNode<K, A>) : DemuxLifecycleState<K, A> { + class Active<K, A>(val node: DemuxNode<*, K, A>) : DemuxLifecycleState<K, A> { override fun toString(): String = "Active(node=$node)" } data object Dead : DemuxLifecycleState<Nothing, Nothing> } - -internal interface DemuxActivator<K, A> { - suspend fun activate( - evalScope: EvalScope, - lifecycle: DemuxLifecycle<K, A>, - ): Pair<DemuxNode<K, A>, Boolean>? -} - -internal inline fun <K, A> DemuxLifecycle(onSubscribe: DemuxActivator<K, A>) = - DemuxLifecycle(DemuxLifecycleState.Inactive(onSubscribe)) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EvalScopeImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EvalScopeImpl.kt index 815473fe900f..80a294819fac 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EvalScopeImpl.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EvalScopeImpl.kt @@ -16,57 +16,54 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.FrpDeferredValue -import com.android.systemui.kairos.FrpTransactionScope -import com.android.systemui.kairos.TFlow -import com.android.systemui.kairos.TFlowInit -import com.android.systemui.kairos.TFlowLoop -import com.android.systemui.kairos.TState -import com.android.systemui.kairos.TStateInit +import com.android.systemui.kairos.DeferredValue +import com.android.systemui.kairos.Events +import com.android.systemui.kairos.EventsInit +import com.android.systemui.kairos.EventsLoop +import com.android.systemui.kairos.State +import com.android.systemui.kairos.StateInit +import com.android.systemui.kairos.TransactionScope import com.android.systemui.kairos.Transactional -import com.android.systemui.kairos.emptyTFlow +import com.android.systemui.kairos.emptyEvents import com.android.systemui.kairos.init import com.android.systemui.kairos.mapCheap -import com.android.systemui.kairos.switch -import kotlin.coroutines.Continuation -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.startCoroutine -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.completeWith -import kotlinx.coroutines.job +import com.android.systemui.kairos.switchEvents internal class EvalScopeImpl(networkScope: NetworkScope, deferScope: DeferScope) : - EvalScope, NetworkScope by networkScope, DeferScope by deferScope { - - private suspend fun <A> Transactional<A>.sample(): A = - impl.sample().sample(this@EvalScopeImpl).await() - - private suspend fun <A> TState<A>.sample(): A = - init.connect(evalScope = this@EvalScopeImpl).getCurrentWithEpoch(this@EvalScopeImpl).first - - private val <A> Transactional<A>.deferredValue: FrpDeferredValue<A> - get() = FrpDeferredValue(deferAsync { sample() }) + EvalScope, NetworkScope by networkScope, DeferScope by deferScope, TransactionScope { + + override fun <A> Transactional<A>.sampleDeferred(): DeferredValue<A> = + DeferredValue(deferAsync { impl.sample().sample(this@EvalScopeImpl).value }) + + override fun <A> State<A>.sampleDeferred(): DeferredValue<A> = + DeferredValue( + deferAsync { + init + .connect(evalScope = this@EvalScopeImpl) + .getCurrentWithEpoch(this@EvalScopeImpl) + .first + } + ) - private val <A> TState<A>.deferredValue: FrpDeferredValue<A> - get() = FrpDeferredValue(deferAsync { sample() }) + override fun <R> deferredTransactionScope(block: TransactionScope.() -> R): DeferredValue<R> = + DeferredValue(deferAsync { block() }) - private val nowInternal: TFlow<Unit> by lazy { - var result by TFlowLoop<Unit>() + override val now: Events<Unit> by lazy { + var result by EventsLoop<Unit>() result = - TStateInit( + StateInit( constInit( "now", - mkState( + activatedStateSource( "now", "now", this, - { result.mapCheap { emptyTFlow }.init.connect(evalScope = this) }, - CompletableDeferred( - TFlowInit( + { result.mapCheap { emptyEvents }.init.connect(evalScope = this) }, + CompletableLazy( + EventsInit( constInit( "now", - TFlowCheap { + EventsImplCheap { ActivationResult( connection = NodeConnection(AlwaysNode, AlwaysNode), needsEval = true, @@ -78,42 +75,7 @@ internal class EvalScopeImpl(networkScope: NetworkScope, deferScope: DeferScope) ), ) ) - .switch() + .switchEvents() result } - - private fun <R> deferredInternal( - block: suspend FrpTransactionScope.() -> R - ): FrpDeferredValue<R> = FrpDeferredValue(deferAsync { runInTransactionScope(block) }) - - override suspend fun <R> runInTransactionScope(block: suspend FrpTransactionScope.() -> R): R { - val complete = CompletableDeferred<R>(parent = coroutineContext.job) - block.startCoroutine( - frpScope, - object : Continuation<R> { - override val context: CoroutineContext - get() = EmptyCoroutineContext - - override fun resumeWith(result: Result<R>) { - complete.completeWith(result) - } - }, - ) - return complete.await() - } - - override val frpScope: FrpTransactionScope = FrpTransactionScopeImpl() - - inner class FrpTransactionScopeImpl : FrpTransactionScope { - override fun <A> Transactional<A>.sampleDeferred(): FrpDeferredValue<A> = deferredValue - - override fun <A> TState<A>.sampleDeferred(): FrpDeferredValue<A> = deferredValue - - override fun <R> deferredTransactionScope( - block: suspend FrpTransactionScope.() -> R - ): FrpDeferredValue<R> = deferredInternal(block) - - override val now: TFlow<Unit> - get() = nowInternal - } } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TFlowImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EventsImpl.kt index b904b48f7f9c..59c5e7244aa2 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TFlowImpl.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EventsImpl.kt @@ -16,11 +16,9 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.util.Maybe - -/* Initialized TFlow */ -internal fun interface TFlowImpl<out A> { - suspend fun activate(evalScope: EvalScope, downstream: Schedulable): ActivationResult<A>? +/* Initialized Events */ +internal fun interface EventsImpl<out A> { + fun activate(evalScope: EvalScope, downstream: Schedulable): ActivationResult<A>? } internal data class ActivationResult<out A>( @@ -28,35 +26,33 @@ internal data class ActivationResult<out A>( val needsEval: Boolean, ) -internal inline fun <A> TFlowCheap(crossinline cheap: CheapNodeSubscribe<A>) = - TFlowImpl { scope, ds -> +internal inline fun <A> EventsImplCheap(crossinline cheap: CheapNodeSubscribe<A>) = + EventsImpl { scope, ds -> scope.cheap(ds) } internal typealias CheapNodeSubscribe<A> = - suspend EvalScope.(downstream: Schedulable) -> ActivationResult<A>? + EvalScope.(downstream: Schedulable) -> ActivationResult<A>? internal data class NodeConnection<out A>( val directUpstream: PullNode<A>, val schedulerUpstream: PushNode<*>, ) -internal suspend fun <A> NodeConnection<A>.hasCurrentValue( - transactionStore: TransactionStore -): Boolean = schedulerUpstream.hasCurrentValue(transactionStore) +internal fun <A> NodeConnection<A>.hasCurrentValue(logIndent: Int, evalScope: EvalScope): Boolean = + schedulerUpstream.hasCurrentValue(logIndent, evalScope) -internal suspend fun <A> NodeConnection<A>.removeDownstreamAndDeactivateIfNeeded( - downstream: Schedulable -) = schedulerUpstream.removeDownstreamAndDeactivateIfNeeded(downstream) +internal fun <A> NodeConnection<A>.removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) = + schedulerUpstream.removeDownstreamAndDeactivateIfNeeded(downstream) -internal suspend fun <A> NodeConnection<A>.scheduleDeactivationIfNeeded(evalScope: EvalScope) = +internal fun <A> NodeConnection<A>.scheduleDeactivationIfNeeded(evalScope: EvalScope) = schedulerUpstream.scheduleDeactivationIfNeeded(evalScope) -internal suspend fun <A> NodeConnection<A>.removeDownstream(downstream: Schedulable) = +internal fun <A> NodeConnection<A>.removeDownstream(downstream: Schedulable) = schedulerUpstream.removeDownstream(downstream) -internal suspend fun <A> NodeConnection<A>.getPushEvent(evalScope: EvalScope): Maybe<A> = - directUpstream.getPushEvent(evalScope) +internal fun <A> NodeConnection<A>.getPushEvent(logIndent: Int, evalScope: EvalScope): A = + directUpstream.getPushEvent(logIndent, evalScope) internal val <A> NodeConnection<A>.depthTracker: DepthTracker get() = schedulerUpstream.depthTracker diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt index bc06a3679d5c..9496b06c6bd1 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt @@ -16,32 +16,33 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.util.Just +import com.android.systemui.kairos.internal.store.Single +import com.android.systemui.kairos.internal.store.SingletonMapK import com.android.systemui.kairos.util.Maybe +import com.android.systemui.kairos.util.Maybe.Just import com.android.systemui.kairos.util.just import com.android.systemui.kairos.util.none -internal inline fun <A, B> mapMaybeNode( - crossinline getPulse: suspend EvalScope.() -> TFlowImpl<A>, - crossinline f: suspend EvalScope.(A) -> Maybe<B>, -): TFlowImpl<B> { - return DemuxImpl( - { - mapImpl(getPulse) { - val maybeResult = f(it) - if (maybeResult is Just) { - mapOf(Unit to maybeResult.value) - } else { - emptyMap() - } +internal inline fun <A> filterJustImpl( + crossinline getPulse: EvalScope.() -> EventsImpl<Maybe<A>> +): EventsImpl<A> = + DemuxImpl( + mapImpl(getPulse) { maybeResult, _ -> + if (maybeResult is Just) { + Single(maybeResult.value) + } else { + Single<A>() } }, numKeys = 1, + storeFactory = SingletonMapK.Factory(), ) .eventsForKey(Unit) -} -internal inline fun <A> filterNode( - crossinline getPulse: suspend EvalScope.() -> TFlowImpl<A>, - crossinline f: suspend EvalScope.(A) -> Boolean, -): TFlowImpl<A> = mapMaybeNode(getPulse) { if (f(it)) just(it) else none } +internal inline fun <A> filterImpl( + crossinline getPulse: EvalScope.() -> EventsImpl<A>, + crossinline f: EvalScope.(A) -> Boolean, +): EventsImpl<A> { + val mapped = mapImpl(getPulse) { it, _ -> if (f(it)) just(it) else none }.cached() + return filterJustImpl { mapped } +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Graph.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Graph.kt index 04ce5b6d8785..b956e44e0618 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Graph.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Graph.kt @@ -18,8 +18,6 @@ package com.android.systemui.kairos.internal import com.android.systemui.kairos.internal.util.Bag import java.util.TreeMap -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch /** * Tracks all upstream connections for Mux nodes. @@ -72,15 +70,15 @@ internal class DepthTracker { @Volatile var snapshotIndirectDepth: Int = 0 @Volatile var snapshotDirectDepth: Int = 0 - private val _snapshotIndirectRoots = HashSet<MuxDeferredNode<*, *>>() + private val _snapshotIndirectRoots = HashSet<MuxDeferredNode<*, *, *>>() val snapshotIndirectRoots get() = _snapshotIndirectRoots.toSet() - private val indirectAdditions = HashSet<MuxDeferredNode<*, *>>() - private val indirectRemovals = HashSet<MuxDeferredNode<*, *>>() + private val indirectAdditions = HashSet<MuxDeferredNode<*, *, *>>() + private val indirectRemovals = HashSet<MuxDeferredNode<*, *, *>>() private val dirty_directUpstreamDepths = TreeMap<Int, Int>() private val dirty_indirectUpstreamDepths = TreeMap<Int, Int>() - private val dirty_indirectUpstreamRoots = Bag<MuxDeferredNode<*, *>>() + private val dirty_indirectUpstreamRoots = Bag<MuxDeferredNode<*, *, *>>() @Volatile var dirty_directDepth = 0 @Volatile private var dirty_indirectDepth = 0 @Volatile private var dirty_depthIsDirect = true @@ -161,9 +159,9 @@ internal class DepthTracker { } fun updateIndirectRoots( - additions: Set<MuxDeferredNode<*, *>>? = null, - removals: Set<MuxDeferredNode<*, *>>? = null, - butNot: MuxDeferredNode<*, *>? = null, + additions: Set<MuxDeferredNode<*, *, *>>? = null, + removals: Set<MuxDeferredNode<*, *, *>>? = null, + butNot: MuxDeferredNode<*, *, *>? = null, ): Boolean { val addsChanged = additions @@ -192,14 +190,13 @@ internal class DepthTracker { return remainder } - suspend fun propagateChanges(scheduler: Scheduler, muxNode: MuxNode<*, *, *>) { + fun propagateChanges(scheduler: Scheduler, muxNode: MuxNode<*, *, *>) { if (isDirty()) { schedule(scheduler, muxNode) } } fun applyChanges( - coroutineScope: CoroutineScope, scheduler: Scheduler, downstreamSet: DownstreamSet, muxNode: MuxNode<*, *, *>, @@ -208,21 +205,19 @@ internal class DepthTracker { dirty_depthIsDirect -> { if (snapshotIsDirect) { downstreamSet.adjustDirectUpstream( - coroutineScope, scheduler, oldDepth = snapshotDirectDepth, newDepth = dirty_directDepth, ) } else { downstreamSet.moveIndirectUpstreamToDirect( - coroutineScope, scheduler, oldIndirectDepth = snapshotIndirectDepth, oldIndirectSet = buildSet { addAll(snapshotIndirectRoots) if (snapshotIsIndirectRoot) { - add(muxNode as MuxDeferredNode<*, *>) + add(muxNode as MuxDeferredNode<*, *, *>) } }, newDirectDepth = dirty_directDepth, @@ -233,7 +228,6 @@ internal class DepthTracker { dirty_hasIndirectUpstream() || dirty_isIndirectRoot -> { if (snapshotIsDirect) { downstreamSet.moveDirectUpstreamToIndirect( - coroutineScope, scheduler, oldDirectDepth = snapshotDirectDepth, newIndirectDepth = dirty_indirectDepth, @@ -241,13 +235,12 @@ internal class DepthTracker { buildSet { addAll(dirty_indirectUpstreamRoots) if (dirty_isIndirectRoot) { - add(muxNode as MuxDeferredNode<*, *>) + add(muxNode as MuxDeferredNode<*, *, *>) } }, ) } else { downstreamSet.adjustIndirectUpstream( - coroutineScope, scheduler, oldDepth = snapshotIndirectDepth, newDepth = dirty_indirectDepth, @@ -255,14 +248,14 @@ internal class DepthTracker { buildSet { addAll(indirectRemovals) if (snapshotIsIndirectRoot && !dirty_isIndirectRoot) { - add(muxNode as MuxDeferredNode<*, *>) + add(muxNode as MuxDeferredNode<*, *, *>) } }, additions = buildSet { addAll(indirectAdditions) if (!snapshotIsIndirectRoot && dirty_isIndirectRoot) { - add(muxNode as MuxDeferredNode<*, *>) + add(muxNode as MuxDeferredNode<*, *, *>) } }, ) @@ -274,21 +267,16 @@ internal class DepthTracker { muxNode.lifecycle.lifecycleState = MuxLifecycleState.Dead if (snapshotIsDirect) { - downstreamSet.removeDirectUpstream( - coroutineScope, - scheduler, - depth = snapshotDirectDepth, - ) + downstreamSet.removeDirectUpstream(scheduler, depth = snapshotDirectDepth) } else { downstreamSet.removeIndirectUpstream( - coroutineScope, scheduler, depth = snapshotIndirectDepth, indirectSet = buildSet { addAll(snapshotIndirectRoots) if (snapshotIsIndirectRoot) { - add(muxNode as MuxDeferredNode<*, *>) + add(muxNode as MuxDeferredNode<*, *, *>) } }, ) @@ -352,8 +340,8 @@ internal class DepthTracker { internal class DownstreamSet { val outputs = HashSet<Output<*>>() - val stateWriters = mutableListOf<TStateSource<*>>() - val muxMovers = HashSet<MuxDeferredNode<*, *>>() + val stateWriters = mutableListOf<StateSource<*>>() + val muxMovers = HashSet<MuxDeferredNode<*, *, *>>() val nodes = HashSet<SchedulableNode>() fun add(schedulable: Schedulable) { @@ -374,125 +362,92 @@ internal class DownstreamSet { } } - fun adjustDirectUpstream( - coroutineScope: CoroutineScope, - scheduler: Scheduler, - oldDepth: Int, - newDepth: Int, - ) = - coroutineScope.run { - for (node in nodes) { - launch { node.adjustDirectUpstream(scheduler, oldDepth, newDepth) } - } + fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) { + for (node in nodes) { + node.adjustDirectUpstream(scheduler, oldDepth, newDepth) } + } fun moveIndirectUpstreamToDirect( - coroutineScope: CoroutineScope, scheduler: Scheduler, oldIndirectDepth: Int, - oldIndirectSet: Set<MuxDeferredNode<*, *>>, + oldIndirectSet: Set<MuxDeferredNode<*, *, *>>, newDirectDepth: Int, - ) = - coroutineScope.run { - for (node in nodes) { - launch { - node.moveIndirectUpstreamToDirect( - scheduler, - oldIndirectDepth, - oldIndirectSet, - newDirectDepth, - ) - } - } - for (mover in muxMovers) { - launch { - mover.moveIndirectPatchNodeToDirect(scheduler, oldIndirectDepth, oldIndirectSet) - } - } + ) { + for (node in nodes) { + node.moveIndirectUpstreamToDirect( + scheduler, + oldIndirectDepth, + oldIndirectSet, + newDirectDepth, + ) + } + for (mover in muxMovers) { + mover.moveIndirectPatchNodeToDirect(scheduler, oldIndirectDepth, oldIndirectSet) } + } fun adjustIndirectUpstream( - coroutineScope: CoroutineScope, scheduler: Scheduler, oldDepth: Int, newDepth: Int, - removals: Set<MuxDeferredNode<*, *>>, - additions: Set<MuxDeferredNode<*, *>>, - ) = - coroutineScope.run { - for (node in nodes) { - launch { - node.adjustIndirectUpstream(scheduler, oldDepth, newDepth, removals, additions) - } - } - for (mover in muxMovers) { - launch { - mover.adjustIndirectPatchNode( - scheduler, - oldDepth, - newDepth, - removals, - additions, - ) - } - } + removals: Set<MuxDeferredNode<*, *, *>>, + additions: Set<MuxDeferredNode<*, *, *>>, + ) { + for (node in nodes) { + node.adjustIndirectUpstream(scheduler, oldDepth, newDepth, removals, additions) + } + for (mover in muxMovers) { + mover.adjustIndirectPatchNode(scheduler, oldDepth, newDepth, removals, additions) } + } fun moveDirectUpstreamToIndirect( - coroutineScope: CoroutineScope, scheduler: Scheduler, oldDirectDepth: Int, newIndirectDepth: Int, - newIndirectSet: Set<MuxDeferredNode<*, *>>, - ) = - coroutineScope.run { - for (node in nodes) { - launch { - node.moveDirectUpstreamToIndirect( - scheduler, - oldDirectDepth, - newIndirectDepth, - newIndirectSet, - ) - } - } - for (mover in muxMovers) { - launch { - mover.moveDirectPatchNodeToIndirect(scheduler, newIndirectDepth, newIndirectSet) - } - } + newIndirectSet: Set<MuxDeferredNode<*, *, *>>, + ) { + for (node in nodes) { + node.moveDirectUpstreamToIndirect( + scheduler, + oldDirectDepth, + newIndirectDepth, + newIndirectSet, + ) + } + for (mover in muxMovers) { + mover.moveDirectPatchNodeToIndirect(scheduler, newIndirectDepth, newIndirectSet) } + } fun removeIndirectUpstream( - coroutineScope: CoroutineScope, scheduler: Scheduler, depth: Int, - indirectSet: Set<MuxDeferredNode<*, *>>, - ) = - coroutineScope.run { - for (node in nodes) { - launch { node.removeIndirectUpstream(scheduler, depth, indirectSet) } - } - for (mover in muxMovers) { - launch { mover.removeIndirectPatchNode(scheduler, depth, indirectSet) } - } - for (output in outputs) { - launch { output.kill() } - } + indirectSet: Set<MuxDeferredNode<*, *, *>>, + ) { + for (node in nodes) { + node.removeIndirectUpstream(scheduler, depth, indirectSet) + } + for (mover in muxMovers) { + mover.removeIndirectPatchNode(scheduler, depth, indirectSet) } + for (output in outputs) { + output.kill() + } + } - fun removeDirectUpstream(coroutineScope: CoroutineScope, scheduler: Scheduler, depth: Int) = - coroutineScope.run { - for (node in nodes) { - launch { node.removeDirectUpstream(scheduler, depth) } - } - for (mover in muxMovers) { - launch { mover.removeDirectPatchNode(scheduler) } - } - for (output in outputs) { - launch { output.kill() } - } + fun removeDirectUpstream(scheduler: Scheduler, depth: Int) { + for (node in nodes) { + node.removeDirectUpstream(scheduler, depth) + } + for (mover in muxMovers) { + mover.removeDirectPatchNode(scheduler) } + for (output in outputs) { + output.kill() + } + } fun clear() { outputs.clear() @@ -504,9 +459,9 @@ internal class DownstreamSet { // TODO: remove this indirection internal sealed interface Schedulable { - data class S constructor(val state: TStateSource<*>) : Schedulable + data class S constructor(val state: StateSource<*>) : Schedulable - data class M constructor(val muxMover: MuxDeferredNode<*, *>) : Schedulable + data class M constructor(val muxMover: MuxDeferredNode<*, *, *>) : Schedulable data class N constructor(val node: SchedulableNode) : Schedulable @@ -518,13 +473,14 @@ internal fun DownstreamSet.isEmpty() = @Suppress("NOTHING_TO_INLINE") internal inline fun DownstreamSet.isNotEmpty() = !isEmpty() -internal fun CoroutineScope.scheduleAll( +internal fun scheduleAll( + logIndent: Int, downstreamSet: DownstreamSet, evalScope: EvalScope, ): Boolean { - downstreamSet.nodes.forEach { launch { it.schedule(evalScope) } } - downstreamSet.muxMovers.forEach { launch { it.scheduleMover(evalScope) } } - downstreamSet.outputs.forEach { launch { it.schedule(evalScope) } } + downstreamSet.nodes.forEach { it.schedule(logIndent, evalScope) } + downstreamSet.muxMovers.forEach { it.scheduleMover(logIndent, evalScope) } + downstreamSet.outputs.forEach { it.schedule(logIndent, evalScope) } downstreamSet.stateWriters.forEach { evalScope.schedule(it) } return downstreamSet.isNotEmpty() } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/IncrementalImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/IncrementalImpl.kt new file mode 100644 index 000000000000..8a3e01af6565 --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/IncrementalImpl.kt @@ -0,0 +1,108 @@ +/* + * Copyright (C) 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 com.android.systemui.kairos.internal + +import com.android.systemui.kairos.internal.store.StoreEntry +import com.android.systemui.kairos.util.Maybe +import com.android.systemui.kairos.util.applyPatch +import com.android.systemui.kairos.util.just +import com.android.systemui.kairos.util.map +import com.android.systemui.kairos.util.none + +internal class IncrementalImpl<K, out V>( + name: String?, + operatorName: String, + changes: EventsImpl<Map<K, V>>, + val patches: EventsImpl<Map<K, Maybe<V>>>, + store: StateStore<Map<K, V>>, +) : StateImpl<Map<K, V>>(name, operatorName, changes, store) + +internal fun <K, V> constIncremental( + name: String?, + operatorName: String, + init: Map<K, V>, +): IncrementalImpl<K, V> = + IncrementalImpl(name, operatorName, neverImpl, neverImpl, StateSource(init)) + +internal inline fun <K, V> activatedIncremental( + name: String?, + operatorName: String, + evalScope: EvalScope, + crossinline getPatches: EvalScope.() -> EventsImpl<Map<K, Maybe<V>>>, + init: Lazy<Map<K, V>>, +): IncrementalImpl<K, V> { + val store = StateSource(init) + val maybeChanges = + mapImpl(getPatches) { patch, _ -> + val (old, _) = store.getCurrentWithEpoch(evalScope = this) + val new = old.applyPatch(patch) + if (new != old) just(patch to new) else none + } + .cached() + val calm = filterJustImpl { maybeChanges } + val changes = mapImpl({ calm }) { (_, change), _ -> change } + val patches = mapImpl({ calm }) { (patch, _), _ -> patch } + evalScope.scheduleOutput( + OneShot { + changes.activate(evalScope = this, downstream = Schedulable.S(store))?.let { + (connection, needsEval) -> + store.upstreamConnection = connection + if (needsEval) { + schedule(store) + } + } + } + ) + return IncrementalImpl(name, operatorName, changes, patches, store) +} + +internal inline fun <K, V> EventsImpl<Map<K, Maybe<V>>>.calmUpdates( + state: StateDerived<Map<K, V>> +): Pair<EventsImpl<Map<K, Maybe<V>>>, EventsImpl<Map<K, V>>> { + val maybeUpdate = + mapImpl({ this@calmUpdates }) { patch, _ -> + val (current, _) = state.getCurrentWithEpoch(evalScope = this) + val new = current.applyPatch(patch) + if (new != current) { + state.setCacheFromPush(new, epoch) + just(patch to new) + } else { + none + } + } + .cached() + val calm = filterJustImpl { maybeUpdate } + val patches = mapImpl({ calm }) { (p, _), _ -> p } + val changes = mapImpl({ calm }) { (_, s), _ -> s } + return patches to changes +} + +internal fun <K, A, B> mapValuesImpl( + incrementalImpl: InitScope.() -> IncrementalImpl<K, A>, + name: String?, + operatorName: String, + transform: EvalScope.(Map.Entry<K, A>) -> B, +): IncrementalImpl<K, B> { + val store = DerivedMap(incrementalImpl) { map -> map.mapValues { transform(it) } } + val mappedPatches = + mapImpl({ incrementalImpl().patches }) { patch, _ -> + patch.mapValues { (k, mv) -> mv.map { v -> transform(StoreEntry(k, v)) } } + } + .cached() + val (calmPatches, calmChanges) = mappedPatches.calmUpdates(store) + return IncrementalImpl(name, operatorName, calmChanges, calmPatches, store) +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt index 57db9a493e21..640c561a21eb 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt @@ -19,42 +19,40 @@ package com.android.systemui.kairos.internal import com.android.systemui.kairos.util.Maybe import com.android.systemui.kairos.util.just import com.android.systemui.kairos.util.none -import java.util.concurrent.atomic.AtomicBoolean -import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.ExperimentalCoroutinesApi /** Performs actions once, when the reactive component is first connected to the network. */ -internal class Init<out A>(val name: String?, private val block: suspend InitScope.() -> A) { - - /** Has the initialization logic been evaluated yet? */ - private val initialized = AtomicBoolean() +internal class Init<out A>(val name: String?, private val block: InitScope.() -> A) { /** * Stores the result after initialization, as well as the id of the [Network] it's been * initialized with. */ - private val cache = CompletableDeferred<Pair<Any, A>>() + private val cache = CompletableLazy<Pair<Any, A>>() - suspend fun connect(evalScope: InitScope): A = - if (initialized.getAndSet(true)) { + fun connect(evalScope: InitScope): A = + if (cache.isInitialized()) { // Read from cache - val (networkId, result) = cache.await() + val (networkId, result) = cache.value check(networkId == evalScope.networkId) { "Network mismatch" } result } else { // Write to cache - block(evalScope).also { cache.complete(evalScope.networkId to it) } + block(evalScope).also { cache.setValue(evalScope.networkId to it) } } @OptIn(ExperimentalCoroutinesApi::class) fun getUnsafe(): Maybe<A> = - if (cache.isCompleted) { - just(cache.getCompleted().second) + if (cache.isInitialized()) { + just(cache.value.second) } else { none } } -internal fun <A> init(name: String?, block: suspend InitScope.() -> A) = Init(name, block) +@Suppress("NOTHING_TO_INLINE") +internal inline fun <A> init(name: String?, noinline block: InitScope.() -> A): Init<A> = + Init(name, block) -internal fun <A> constInit(name: String?, value: A) = init(name) { value } +@Suppress("NOTHING_TO_INLINE") +internal inline fun <A> constInit(name: String?, value: A): Init<A> = init(name) { value } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Inputs.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Inputs.kt index 8efaf79b18b2..d7cbe8087f52 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Inputs.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Inputs.kt @@ -16,105 +16,103 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.internal.util.Key -import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.just +import com.android.systemui.kairos.internal.util.logDuration import java.util.concurrent.atomic.AtomicBoolean -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock internal class InputNode<A>( - private val activate: suspend EvalScope.() -> Unit = {}, + private val activate: EvalScope.() -> Unit = {}, private val deactivate: () -> Unit = {}, -) : PushNode<A>, Key<A> { +) : PushNode<A> { - internal val downstreamSet = DownstreamSet() - private val mutex = Mutex() - private val activated = AtomicBoolean(false) + private val downstreamSet = DownstreamSet() + val activated = AtomicBoolean(false) + + private val transactionCache = TransactionCache<A>() + private val epoch + get() = transactionCache.epoch override val depthTracker: DepthTracker = DepthTracker() - override suspend fun hasCurrentValue(transactionStore: TransactionStore): Boolean = - transactionStore.contains(this) + override fun hasCurrentValue(logIndent: Int, evalScope: EvalScope): Boolean = + epoch == evalScope.epoch - suspend fun visit(evalScope: EvalScope, value: A) { - evalScope.setResult(this, value) - coroutineScope { - if (!mutex.withLock { scheduleAll(downstreamSet, evalScope) }) { - evalScope.scheduleDeactivation(this@InputNode) - } + fun visit(evalScope: EvalScope, value: A) { + transactionCache.put(evalScope, value) + if (!scheduleAll(0, downstreamSet, evalScope)) { + evalScope.scheduleDeactivation(this@InputNode) } } - override suspend fun removeDownstream(downstream: Schedulable) { - mutex.withLock { downstreamSet.remove(downstream) } + override fun removeDownstream(downstream: Schedulable) { + downstreamSet.remove(downstream) } - override suspend fun deactivateIfNeeded() { - if (mutex.withLock { downstreamSet.isEmpty() && activated.getAndSet(false) }) { + override fun deactivateIfNeeded() { + if (downstreamSet.isEmpty() && activated.getAndSet(false)) { deactivate() } } - override suspend fun scheduleDeactivationIfNeeded(evalScope: EvalScope) { - if (mutex.withLock { downstreamSet.isEmpty() }) { + override fun scheduleDeactivationIfNeeded(evalScope: EvalScope) { + if (downstreamSet.isEmpty()) { evalScope.scheduleDeactivation(this) } } - override suspend fun addDownstream(downstream: Schedulable) { - mutex.withLock { downstreamSet.add(downstream) } + override fun addDownstream(downstream: Schedulable) { + downstreamSet.add(downstream) } - suspend fun addDownstreamAndActivateIfNeeded(downstream: Schedulable, evalScope: EvalScope) { - val needsActivation = - mutex.withLock { - val wasEmpty = downstreamSet.isEmpty() - downstreamSet.add(downstream) - wasEmpty && !activated.getAndSet(true) - } + fun addDownstreamAndActivateIfNeeded(downstream: Schedulable, evalScope: EvalScope) { + val needsActivation = run { + val wasEmpty = downstreamSet.isEmpty() + downstreamSet.add(downstream) + wasEmpty && !activated.getAndSet(true) + } if (needsActivation) { activate(evalScope) } } - override suspend fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) { - val needsDeactivation = - mutex.withLock { - downstreamSet.remove(downstream) - downstreamSet.isEmpty() && activated.getAndSet(false) - } + override fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) { + downstreamSet.remove(downstream) + val needsDeactivation = downstreamSet.isEmpty() && activated.getAndSet(false) if (needsDeactivation) { deactivate() } } - override suspend fun getPushEvent(evalScope: EvalScope): Maybe<A> = - evalScope.getCurrentValue(this) + override fun getPushEvent(logIndent: Int, evalScope: EvalScope): A = + logDuration(logIndent, "Input.getPushEvent", false) { + transactionCache.getCurrentValue(evalScope) + } } -internal fun <A> InputNode<A>.activated() = TFlowCheap { downstream -> +internal fun <A> InputNode<A>.activated() = EventsImplCheap { downstream -> val input = this@activated addDownstreamAndActivateIfNeeded(downstream, evalScope = this) - ActivationResult(connection = NodeConnection(input, input), needsEval = hasCurrentValue(input)) + ActivationResult( + connection = NodeConnection(input, input), + needsEval = input.hasCurrentValue(0, evalScope = this), + ) } internal data object AlwaysNode : PushNode<Unit> { override val depthTracker = DepthTracker() - override suspend fun hasCurrentValue(transactionStore: TransactionStore): Boolean = true + override fun hasCurrentValue(logIndent: Int, evalScope: EvalScope): Boolean = true - override suspend fun removeDownstream(downstream: Schedulable) {} + override fun removeDownstream(downstream: Schedulable) {} - override suspend fun deactivateIfNeeded() {} + override fun deactivateIfNeeded() {} - override suspend fun scheduleDeactivationIfNeeded(evalScope: EvalScope) {} + override fun scheduleDeactivationIfNeeded(evalScope: EvalScope) {} - override suspend fun addDownstream(downstream: Schedulable) {} + override fun addDownstream(downstream: Schedulable) {} - override suspend fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) {} + override fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) {} - override suspend fun getPushEvent(evalScope: EvalScope): Maybe<Unit> = just(Unit) + override fun getPushEvent(logIndent: Int, evalScope: EvalScope) = + logDuration(logIndent, "Always.getPushEvent", false) { Unit } } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/InternalScopes.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/InternalScopes.kt index 69994ba6e866..cd2214370d62 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/InternalScopes.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/InternalScopes.kt @@ -16,39 +16,25 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.FrpBuildScope -import com.android.systemui.kairos.FrpStateScope -import com.android.systemui.kairos.FrpTransactionScope -import com.android.systemui.kairos.TFlow -import com.android.systemui.kairos.internal.util.HeteroMap -import com.android.systemui.kairos.internal.util.Key -import com.android.systemui.kairos.util.Maybe +import com.android.systemui.kairos.BuildScope +import com.android.systemui.kairos.Events +import com.android.systemui.kairos.StateScope +import com.android.systemui.kairos.TransactionScope internal interface InitScope { val networkId: Any } -internal interface EvalScope : NetworkScope, DeferScope { - val frpScope: FrpTransactionScope +internal interface EvalScope : NetworkScope, DeferScope, TransactionScope - suspend fun <R> runInTransactionScope(block: suspend FrpTransactionScope.() -> R): R -} - -internal interface StateScope : EvalScope { - override val frpScope: FrpStateScope - - suspend fun <R> runInStateScope(block: suspend FrpStateScope.() -> R): R +internal interface InternalStateScope : EvalScope, StateScope { + val endSignal: Events<Any> + val endSignalOnce: Events<Any> - val endSignal: TFlow<Any> - - fun childStateScope(newEnd: TFlow<Any>): StateScope + fun childStateScope(newEnd: Events<Any>): InternalStateScope } -internal interface BuildScope : StateScope { - override val frpScope: FrpBuildScope - - suspend fun <R> runInBuildScope(block: suspend FrpBuildScope.() -> R): R -} +internal interface InternalBuildScope : InternalStateScope, BuildScope internal interface NetworkScope : InitScope { @@ -58,23 +44,15 @@ internal interface NetworkScope : InitScope { val compactor: Scheduler val scheduler: Scheduler - val transactionStore: HeteroMap + val transactionStore: TransactionStore fun scheduleOutput(output: Output<*>) - fun scheduleMuxMover(muxMover: MuxDeferredNode<*, *>) + fun scheduleMuxMover(muxMover: MuxDeferredNode<*, *, *>) - fun schedule(state: TStateSource<*>) + fun schedule(state: StateSource<*>) fun scheduleDeactivation(node: PushNode<*>) fun scheduleDeactivation(output: Output<*>) } - -internal fun <A> NetworkScope.setResult(node: Key<A>, result: A) { - transactionStore[node] = result -} - -internal fun <A> NetworkScope.getCurrentValue(key: Key<A>): Maybe<A> = transactionStore[key] - -internal fun NetworkScope.hasCurrentValue(key: Key<*>): Boolean = transactionStore.contains(key) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Mux.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Mux.kt index af68a1e3d83c..268fec4fa486 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Mux.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Mux.kt @@ -18,31 +18,43 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.internal.util.ConcurrentNullableHashMap +import com.android.systemui.kairos.internal.store.MapHolder +import com.android.systemui.kairos.internal.store.MapK +import com.android.systemui.kairos.internal.store.MutableMapK +import com.android.systemui.kairos.internal.store.asMapHolder import com.android.systemui.kairos.internal.util.hashString -import com.android.systemui.kairos.util.Just -import java.util.concurrent.ConcurrentHashMap -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock - -/** Base class for muxing nodes, which have a potentially dynamic collection of upstream nodes. */ -internal sealed class MuxNode<K : Any, V, Output>(val lifecycle: MuxLifecycle<Output>) : - PushNode<Output> { - - inline val mutex - get() = lifecycle.mutex - - // TODO: preserve insertion order? - val upstreamData = ConcurrentNullableHashMap<K, V>() - val switchedIn = ConcurrentHashMap<K, MuxBranchNode<K, V>>() +import com.android.systemui.kairos.internal.util.logDuration + +internal typealias MuxResult<W, K, V> = MapK<W, K, PullNode<V>> + +/** Base class for muxing nodes, which have a (potentially dynamic) collection of upstream nodes. */ +internal sealed class MuxNode<W, K, V>( + val lifecycle: MuxLifecycle<W, K, V>, + protected val storeFactory: MutableMapK.Factory<W, K>, +) : PushNode<MuxResult<W, K, V>> { + + lateinit var upstreamData: MutableMapK<W, K, PullNode<V>> + lateinit var switchedIn: MutableMapK<W, K, BranchNode> + + @Volatile var markedForCompaction = false + @Volatile var markedForEvaluation = false + val downstreamSet: DownstreamSet = DownstreamSet() // TODO: inline DepthTracker? would need to be added to PushNode signature final override val depthTracker = DepthTracker() - final override suspend fun addDownstream(downstream: Schedulable) { - mutex.withLock { addDownstreamLocked(downstream) } + val transactionCache = TransactionCache<MuxResult<W, K, V>>() + val epoch + get() = transactionCache.epoch + + inline fun hasCurrentValueLocked(evalScope: EvalScope): Boolean = epoch == evalScope.epoch + + override fun hasCurrentValue(logIndent: Int, evalScope: EvalScope): Boolean = + hasCurrentValueLocked(evalScope) + + final override fun addDownstream(downstream: Schedulable) { + addDownstreamLocked(downstream) } /** @@ -55,140 +67,124 @@ internal sealed class MuxNode<K : Any, V, Output>(val lifecycle: MuxLifecycle<Ou downstreamSet.add(downstream) } - final override suspend fun removeDownstream(downstream: Schedulable) { + final override fun removeDownstream(downstream: Schedulable) { // TODO: return boolean? - mutex.withLock { downstreamSet.remove(downstream) } + downstreamSet.remove(downstream) } - final override suspend fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) { - val deactivate = - mutex.withLock { - downstreamSet.remove(downstream) - downstreamSet.isEmpty() - } + final override fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) { + downstreamSet.remove(downstream) + val deactivate = downstreamSet.isEmpty() if (deactivate) { doDeactivate() } } - final override suspend fun deactivateIfNeeded() { - if (mutex.withLock { downstreamSet.isEmpty() }) { + final override fun deactivateIfNeeded() { + if (downstreamSet.isEmpty()) { doDeactivate() } } /** visit this node from the scheduler (push eval) */ - abstract suspend fun visit(evalScope: EvalScope) + abstract fun visit(logIndent: Int, evalScope: EvalScope) /** perform deactivation logic, propagating to all upstream nodes. */ - protected abstract suspend fun doDeactivate() + protected abstract fun doDeactivate() - final override suspend fun scheduleDeactivationIfNeeded(evalScope: EvalScope) { - if (mutex.withLock { downstreamSet.isEmpty() }) { + final override fun scheduleDeactivationIfNeeded(evalScope: EvalScope) { + if (downstreamSet.isEmpty()) { evalScope.scheduleDeactivation(this) } } - suspend fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) { - mutex.withLock { - if (depthTracker.addDirectUpstream(oldDepth, newDepth)) { - depthTracker.schedule(scheduler, this) - } + fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) { + + if (depthTracker.addDirectUpstream(oldDepth, newDepth)) { + depthTracker.schedule(scheduler, this) } } - suspend fun moveIndirectUpstreamToDirect( + fun moveIndirectUpstreamToDirect( scheduler: Scheduler, oldIndirectDepth: Int, - oldIndirectRoots: Set<MuxDeferredNode<*, *>>, + oldIndirectRoots: Set<MuxDeferredNode<*, *, *>>, newDepth: Int, ) { - mutex.withLock { - if ( - depthTracker.addDirectUpstream(oldDepth = null, newDepth) or - depthTracker.removeIndirectUpstream(depth = oldIndirectDepth) or - depthTracker.updateIndirectRoots(removals = oldIndirectRoots) - ) { - depthTracker.schedule(scheduler, this) - } + if ( + depthTracker.addDirectUpstream(oldDepth = null, newDepth) or + depthTracker.removeIndirectUpstream(depth = oldIndirectDepth) or + depthTracker.updateIndirectRoots(removals = oldIndirectRoots) + ) { + depthTracker.schedule(scheduler, this) } } - suspend fun adjustIndirectUpstream( + fun adjustIndirectUpstream( scheduler: Scheduler, oldDepth: Int, newDepth: Int, - removals: Set<MuxDeferredNode<*, *>>, - additions: Set<MuxDeferredNode<*, *>>, + removals: Set<MuxDeferredNode<*, *, *>>, + additions: Set<MuxDeferredNode<*, *, *>>, ) { - mutex.withLock { - if ( - depthTracker.addIndirectUpstream(oldDepth, newDepth) or - depthTracker.updateIndirectRoots( - additions, - removals, - butNot = this as? MuxDeferredNode<*, *>, - ) - ) { - depthTracker.schedule(scheduler, this) - } + if ( + depthTracker.addIndirectUpstream(oldDepth, newDepth) or + depthTracker.updateIndirectRoots( + additions, + removals, + butNot = this as? MuxDeferredNode<*, *, *>, + ) + ) { + depthTracker.schedule(scheduler, this) } } - suspend fun moveDirectUpstreamToIndirect( + fun moveDirectUpstreamToIndirect( scheduler: Scheduler, oldDepth: Int, newDepth: Int, - newIndirectSet: Set<MuxDeferredNode<*, *>>, + newIndirectSet: Set<MuxDeferredNode<*, *, *>>, ) { - mutex.withLock { - if ( - depthTracker.addIndirectUpstream(oldDepth = null, newDepth) or - depthTracker.removeDirectUpstream(oldDepth) or - depthTracker.updateIndirectRoots( - additions = newIndirectSet, - butNot = this as? MuxDeferredNode<*, *>, - ) - ) { - depthTracker.schedule(scheduler, this) - } + if ( + depthTracker.addIndirectUpstream(oldDepth = null, newDepth) or + depthTracker.removeDirectUpstream(oldDepth) or + depthTracker.updateIndirectRoots( + additions = newIndirectSet, + butNot = this as? MuxDeferredNode<*, *, *>, + ) + ) { + depthTracker.schedule(scheduler, this) } } - suspend fun removeDirectUpstream(scheduler: Scheduler, depth: Int, key: K) { - mutex.withLock { - switchedIn.remove(key) - if (depthTracker.removeDirectUpstream(depth)) { - depthTracker.schedule(scheduler, this) - } + fun removeDirectUpstream(scheduler: Scheduler, depth: Int, key: K) { + switchedIn.remove(key) + if (depthTracker.removeDirectUpstream(depth)) { + depthTracker.schedule(scheduler, this) } } - suspend fun removeIndirectUpstream( + fun removeIndirectUpstream( scheduler: Scheduler, oldDepth: Int, - indirectSet: Set<MuxDeferredNode<*, *>>, + indirectSet: Set<MuxDeferredNode<*, *, *>>, key: K, ) { - mutex.withLock { - switchedIn.remove(key) - if ( - depthTracker.removeIndirectUpstream(oldDepth) or - depthTracker.updateIndirectRoots(removals = indirectSet) - ) { - depthTracker.schedule(scheduler, this) - } + switchedIn.remove(key) + if ( + depthTracker.removeIndirectUpstream(oldDepth) or + depthTracker.updateIndirectRoots(removals = indirectSet) + ) { + depthTracker.schedule(scheduler, this) } } - suspend fun visitCompact(scheduler: Scheduler) = coroutineScope { + fun visitCompact(scheduler: Scheduler) { if (depthTracker.isDirty()) { - depthTracker.applyChanges(coroutineScope = this, scheduler, downstreamSet, this@MuxNode) + depthTracker.applyChanges(scheduler, downstreamSet, this@MuxNode) } } - abstract fun hasCurrentValueLocked(transactionStore: TransactionStore): Boolean - fun schedule(evalScope: EvalScope) { // TODO: Potential optimization // Detect if this node is guaranteed to have a single upstream within this transaction, @@ -196,139 +192,202 @@ internal sealed class MuxNode<K : Any, V, Output>(val lifecycle: MuxLifecycle<Ou // MuxNode as a Pull (effectively making it a mapCheap). depthTracker.schedule(evalScope.scheduler, this) } -} -/** An input branch of a mux node, associated with a key. */ -internal class MuxBranchNode<K : Any, V>(private val muxNode: MuxNode<K, V, *>, val key: K) : - SchedulableNode { + /** An input branch of a mux node, associated with a key. */ + inner class BranchNode(val key: K) : SchedulableNode { - val schedulable = Schedulable.N(this) + val schedulable = Schedulable.N(this) - @Volatile lateinit var upstream: NodeConnection<V> + lateinit var upstream: NodeConnection<V> - override suspend fun schedule(evalScope: EvalScope) { - val upstreamResult = upstream.getPushEvent(evalScope) - if (upstreamResult is Just) { - muxNode.upstreamData[key] = upstreamResult.value - muxNode.schedule(evalScope) + override fun schedule(logIndent: Int, evalScope: EvalScope) { + logDuration(logIndent, "MuxBranchNode.schedule") { + if (this@MuxNode is MuxPromptNode && this@MuxNode.name != null) { + logLn("[${this@MuxNode}] scheduling $key") + } + upstreamData[key] = upstream.directUpstream + this@MuxNode.schedule(evalScope) + } } - } - override suspend fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) { - muxNode.adjustDirectUpstream(scheduler, oldDepth, newDepth) - } + override fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) { + this@MuxNode.adjustDirectUpstream(scheduler, oldDepth, newDepth) + } - override suspend fun moveIndirectUpstreamToDirect( - scheduler: Scheduler, - oldIndirectDepth: Int, - oldIndirectSet: Set<MuxDeferredNode<*, *>>, - newDirectDepth: Int, - ) { - muxNode.moveIndirectUpstreamToDirect( - scheduler, - oldIndirectDepth, - oldIndirectSet, - newDirectDepth, - ) - } + override fun moveIndirectUpstreamToDirect( + scheduler: Scheduler, + oldIndirectDepth: Int, + oldIndirectSet: Set<MuxDeferredNode<*, *, *>>, + newDirectDepth: Int, + ) { + this@MuxNode.moveIndirectUpstreamToDirect( + scheduler, + oldIndirectDepth, + oldIndirectSet, + newDirectDepth, + ) + } - override suspend fun adjustIndirectUpstream( - scheduler: Scheduler, - oldDepth: Int, - newDepth: Int, - removals: Set<MuxDeferredNode<*, *>>, - additions: Set<MuxDeferredNode<*, *>>, - ) { - muxNode.adjustIndirectUpstream(scheduler, oldDepth, newDepth, removals, additions) - } + override fun adjustIndirectUpstream( + scheduler: Scheduler, + oldDepth: Int, + newDepth: Int, + removals: Set<MuxDeferredNode<*, *, *>>, + additions: Set<MuxDeferredNode<*, *, *>>, + ) { + this@MuxNode.adjustIndirectUpstream(scheduler, oldDepth, newDepth, removals, additions) + } - override suspend fun moveDirectUpstreamToIndirect( - scheduler: Scheduler, - oldDirectDepth: Int, - newIndirectDepth: Int, - newIndirectSet: Set<MuxDeferredNode<*, *>>, - ) { - muxNode.moveDirectUpstreamToIndirect( - scheduler, - oldDirectDepth, - newIndirectDepth, - newIndirectSet, - ) - } + override fun moveDirectUpstreamToIndirect( + scheduler: Scheduler, + oldDirectDepth: Int, + newIndirectDepth: Int, + newIndirectSet: Set<MuxDeferredNode<*, *, *>>, + ) { + this@MuxNode.moveDirectUpstreamToIndirect( + scheduler, + oldDirectDepth, + newIndirectDepth, + newIndirectSet, + ) + } - override suspend fun removeDirectUpstream(scheduler: Scheduler, depth: Int) { - muxNode.removeDirectUpstream(scheduler, depth, key) - } + override fun removeDirectUpstream(scheduler: Scheduler, depth: Int) { + removeDirectUpstream(scheduler, depth, key) + } - override suspend fun removeIndirectUpstream( - scheduler: Scheduler, - depth: Int, - indirectSet: Set<MuxDeferredNode<*, *>>, - ) { - muxNode.removeIndirectUpstream(scheduler, depth, indirectSet, key) - } + override fun removeIndirectUpstream( + scheduler: Scheduler, + depth: Int, + indirectSet: Set<MuxDeferredNode<*, *, *>>, + ) { + removeIndirectUpstream(scheduler, depth, indirectSet, key) + } - override fun toString(): String = "MuxBranchNode(key=$key, mux=$muxNode)" + override fun toString(): String = "MuxBranchNode(key=$key, mux=${this@MuxNode})" + } } +internal typealias BranchNode<W, K, V> = MuxNode<W, K, V>.BranchNode + /** Tracks lifecycle of MuxNode in the network. Essentially a mutable ref for MuxLifecycleState. */ -internal class MuxLifecycle<A>(@Volatile var lifecycleState: MuxLifecycleState<A>) : TFlowImpl<A> { - val mutex = Mutex() +internal class MuxLifecycle<W, K, V>(var lifecycleState: MuxLifecycleState<W, K, V>) : + EventsImpl<MuxResult<W, K, V>> { - override fun toString(): String = "TFlowLifecycle[$hashString][$lifecycleState][$mutex]" + override fun toString(): String = "MuxLifecycle[$hashString][$lifecycleState]" - override suspend fun activate( + override fun activate( evalScope: EvalScope, downstream: Schedulable, - ): ActivationResult<A>? = - mutex.withLock { - when (val state = lifecycleState) { - is MuxLifecycleState.Dead -> null - is MuxLifecycleState.Active -> { - state.node.addDownstreamLocked(downstream) - ActivationResult( - connection = NodeConnection(state.node, state.node), - needsEval = state.node.hasCurrentValueLocked(evalScope.transactionStore), - ) - } - is MuxLifecycleState.Inactive -> { - state.spec - .activate(evalScope, this@MuxLifecycle) - .also { node -> - lifecycleState = - if (node == null) { - MuxLifecycleState.Dead - } else { - MuxLifecycleState.Active(node) - } - } - ?.let { node -> - node.addDownstreamLocked(downstream) - ActivationResult( - connection = NodeConnection(node, node), - needsEval = false, - ) - } - } + ): ActivationResult<MuxResult<W, K, V>>? = + when (val state = lifecycleState) { + is MuxLifecycleState.Dead -> { + null + } + is MuxLifecycleState.Active -> { + state.node.addDownstreamLocked(downstream) + ActivationResult( + connection = NodeConnection(state.node, state.node), + needsEval = state.node.hasCurrentValueLocked(evalScope), + ) + } + is MuxLifecycleState.Inactive -> { + state.spec + .activate(evalScope, this@MuxLifecycle) + .also { node -> + lifecycleState = + if (node == null) { + MuxLifecycleState.Dead + } else { + MuxLifecycleState.Active(node.first) + } + } + ?.let { (node, postActivate) -> + postActivate?.invoke() + node.addDownstreamLocked(downstream) + ActivationResult(connection = NodeConnection(node, node), needsEval = false) + } } } } -internal sealed interface MuxLifecycleState<out A> { - class Inactive<A>(val spec: MuxActivator<A>) : MuxLifecycleState<A> { +internal sealed interface MuxLifecycleState<out W, out K, out V> { + class Inactive<W, K, V>(val spec: MuxActivator<W, K, V>) : MuxLifecycleState<W, K, V> { override fun toString(): String = "Inactive" } - class Active<A>(val node: MuxNode<*, *, A>) : MuxLifecycleState<A> { + class Active<W, K, V>(val node: MuxNode<W, K, V>) : MuxLifecycleState<W, K, V> { override fun toString(): String = "Active(node=$node)" } - data object Dead : MuxLifecycleState<Nothing> + data object Dead : MuxLifecycleState<Nothing, Nothing, Nothing> } -internal interface MuxActivator<A> { - suspend fun activate(evalScope: EvalScope, lifecycle: MuxLifecycle<A>): MuxNode<*, *, A>? +internal interface MuxActivator<W, K, V> { + fun activate( + evalScope: EvalScope, + lifecycle: MuxLifecycle<W, K, V>, + ): Pair<MuxNode<W, K, V>, (() -> Unit)?>? } -internal inline fun <A> MuxLifecycle(onSubscribe: MuxActivator<A>): TFlowImpl<A> = - MuxLifecycle(MuxLifecycleState.Inactive(onSubscribe)) +internal inline fun <W, K, V> MuxLifecycle( + onSubscribe: MuxActivator<W, K, V> +): EventsImpl<MuxResult<W, K, V>> = MuxLifecycle(MuxLifecycleState.Inactive(onSubscribe)) + +internal fun <K, V> EventsImpl<MuxResult<MapHolder.W, K, V>>.awaitValues(): EventsImpl<Map<K, V>> = + mapImpl({ this@awaitValues }) { results, logIndent -> + results.asMapHolder().unwrapped.mapValues { it.value.getPushEvent(logIndent, this) } + } + +// activation logic + +internal fun <W, K, V> MuxNode<W, K, V>.initializeUpstream( + evalScope: EvalScope, + getStorage: EvalScope.() -> Iterable<Map.Entry<K, EventsImpl<V>>>, + storeFactory: MutableMapK.Factory<W, K>, +) { + val storage = getStorage(evalScope) + val initUpstream = buildList { + storage.forEach { (key, events) -> + val branchNode = BranchNode(key) + add( + events.activate(evalScope, branchNode.schedulable)?.let { (conn, needsEval) -> + Triple( + key, + branchNode.apply { upstream = conn }, + if (needsEval) conn.directUpstream else null, + ) + } + ) + } + } + switchedIn = storeFactory.create(initUpstream.size) + upstreamData = storeFactory.create(initUpstream.size) + for (triple in initUpstream) { + triple?.let { (key, branch, upstream) -> + switchedIn[key] = branch + upstream?.let { upstreamData[key] = upstream } + } + } +} + +internal fun <W, K, V> MuxNode<W, K, V>.initializeDepth() { + switchedIn.forEach { (_, branch) -> + val conn = branch.upstream + if (conn.depthTracker.snapshotIsDirect) { + depthTracker.addDirectUpstream( + oldDepth = null, + newDepth = conn.depthTracker.snapshotDirectDepth, + ) + } else { + depthTracker.addIndirectUpstream( + oldDepth = null, + newDepth = conn.depthTracker.snapshotIndirectDepth, + ) + depthTracker.updateIndirectRoots( + additions = conn.depthTracker.snapshotIndirectRoots, + butNot = null, + ) + } + } +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt index 3b9502a5d812..cf74f755c98b 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt @@ -16,16 +16,20 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.internal.util.Key -import com.android.systemui.kairos.internal.util.associateByIndexTo +import com.android.systemui.kairos.internal.store.MapK +import com.android.systemui.kairos.internal.store.MutableArrayMapK +import com.android.systemui.kairos.internal.store.MutableMapK +import com.android.systemui.kairos.internal.store.SingletonMapK +import com.android.systemui.kairos.internal.store.StoreEntry +import com.android.systemui.kairos.internal.store.asArrayHolder +import com.android.systemui.kairos.internal.store.asSingle +import com.android.systemui.kairos.internal.store.singleOf import com.android.systemui.kairos.internal.util.hashString -import com.android.systemui.kairos.internal.util.mapParallel -import com.android.systemui.kairos.internal.util.mapValuesNotNullParallelTo -import com.android.systemui.kairos.util.Just -import com.android.systemui.kairos.util.Left +import com.android.systemui.kairos.internal.util.logDuration +import com.android.systemui.kairos.internal.util.logLn import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.None -import com.android.systemui.kairos.util.Right +import com.android.systemui.kairos.util.Maybe.Just +import com.android.systemui.kairos.util.Maybe.None import com.android.systemui.kairos.util.These import com.android.systemui.kairos.util.flatMap import com.android.systemui.kairos.util.getMaybe @@ -33,49 +37,51 @@ import com.android.systemui.kairos.util.just import com.android.systemui.kairos.util.maybeThat import com.android.systemui.kairos.util.maybeThis import com.android.systemui.kairos.util.merge -import com.android.systemui.kairos.util.orElseGet -import com.android.systemui.kairos.util.partitionEithers +import com.android.systemui.kairos.util.orError import com.android.systemui.kairos.util.these -import java.util.TreeMap -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.withLock -internal class MuxDeferredNode<K : Any, V>( - lifecycle: MuxLifecycle<Map<K, V>>, - val spec: MuxActivator<Map<K, V>>, -) : MuxNode<K, V, Map<K, V>>(lifecycle), Key<Map<K, V>> { +internal class MuxDeferredNode<W, K, V>( + val name: String?, + lifecycle: MuxLifecycle<W, K, V>, + val spec: MuxActivator<W, K, V>, + factory: MutableMapK.Factory<W, K>, +) : MuxNode<W, K, V>(lifecycle, factory) { val schedulable = Schedulable.M(this) - - @Volatile var patches: NodeConnection<Map<K, Maybe<TFlowImpl<V>>>>? = null - @Volatile var patchData: Map<K, Maybe<TFlowImpl<V>>>? = null - - override fun hasCurrentValueLocked(transactionStore: TransactionStore): Boolean = - transactionStore.contains(this) - - override suspend fun hasCurrentValue(transactionStore: TransactionStore): Boolean = - mutex.withLock { hasCurrentValueLocked(transactionStore) } - - override suspend fun visit(evalScope: EvalScope) { - val result = upstreamData.toMap() - upstreamData.clear() - val scheduleDownstream = result.isNotEmpty() - val compactDownstream = depthTracker.isDirty() - if (scheduleDownstream || compactDownstream) { - coroutineScope { - mutex.withLock { - if (compactDownstream) { + var patches: NodeConnection<Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>>? = null + var patchData: Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>? = null + + override fun visit(logIndent: Int, evalScope: EvalScope) { + check(epoch < evalScope.epoch) { "node unexpectedly visited multiple times in transaction" } + logDuration(logIndent, "MuxDeferred[$name].visit") { + val scheduleDownstream: Boolean + val result: MapK<W, K, PullNode<V>> + logDuration("copying upstream data", false) { + scheduleDownstream = upstreamData.isNotEmpty() + result = upstreamData.readOnlyCopy() + upstreamData.clear() + } + if (name != null) { + logLn("[${this@MuxDeferredNode}] result = $result") + } + val compactDownstream = depthTracker.isDirty() + if (scheduleDownstream || compactDownstream) { + if (compactDownstream) { + logDuration("compactDownstream", false) { depthTracker.applyChanges( - coroutineScope = this, evalScope.scheduler, downstreamSet, muxNode = this@MuxDeferredNode, ) } - if (scheduleDownstream) { - evalScope.setResult(this@MuxDeferredNode, result) - if (!scheduleAll(downstreamSet, evalScope)) { + } + if (scheduleDownstream) { + logDuration("scheduleDownstream") { + if (name != null) { + logLn("[${this@MuxDeferredNode}] scheduling") + } + transactionCache.put(evalScope, result) + if (!scheduleAll(currentLogIndent, downstreamSet, evalScope)) { evalScope.scheduleDeactivation(this@MuxDeferredNode) } } @@ -84,26 +90,26 @@ internal class MuxDeferredNode<K : Any, V>( } } - override suspend fun getPushEvent(evalScope: EvalScope): Maybe<Map<K, V>> = - evalScope.getCurrentValue(key = this) + override fun getPushEvent(logIndent: Int, evalScope: EvalScope): MuxResult<W, K, V> = + logDuration(logIndent, "MuxDeferred.getPushEvent") { + transactionCache.getCurrentValue(evalScope).also { + if (name != null) { + logLn("[${this@MuxDeferredNode}] getPushEvent = $it") + } + } + } - private suspend fun compactIfNeeded(evalScope: EvalScope) { + private fun compactIfNeeded(evalScope: EvalScope) { depthTracker.propagateChanges(evalScope.compactor, this) } - override suspend fun doDeactivate() { + override fun doDeactivate() { // Update lifecycle - lifecycle.mutex.withLock { - if (lifecycle.lifecycleState !is MuxLifecycleState.Active) return@doDeactivate - lifecycle.lifecycleState = MuxLifecycleState.Inactive(spec) - } + if (lifecycle.lifecycleState !is MuxLifecycleState.Active) return@doDeactivate + lifecycle.lifecycleState = MuxLifecycleState.Inactive(spec) // Process branch nodes - coroutineScope { - switchedIn.values.forEach { branchNode -> - branchNode.upstream.let { - launch { it.removeDownstreamAndDeactivateIfNeeded(branchNode.schedulable) } - } - } + switchedIn.forEach { (_, branchNode) -> + branchNode.upstream.removeDownstreamAndDeactivateIfNeeded(branchNode.schedulable) } // Process patch node patches?.removeDownstreamAndDeactivateIfNeeded(schedulable) @@ -112,362 +118,343 @@ internal class MuxDeferredNode<K : Any, V>( // MOVE phase // - concurrent moves may be occurring, but no more evals. all depth recalculations are // deferred to the end of this phase. - suspend fun performMove(evalScope: EvalScope) { + fun performMove(logIndent: Int, evalScope: EvalScope) { + if (name != null) { + logLn(logIndent, "[${this@MuxDeferredNode}] performMove (patchData = $patchData)") + } + val patch = patchData ?: return patchData = null - // TODO: this logic is very similar to what's in MuxPromptMoving, maybe turn into an inline - // fun? + // TODO: this logic is very similar to what's in MuxPrompt, maybe turn into an inline fun? // We have a patch, process additions/updates and removals - val (adds, removes) = - patch - .asSequence() - .map { (k, newUpstream: Maybe<TFlowImpl<V>>) -> - when (newUpstream) { - is Just -> Left(k to newUpstream.value) - None -> Right(k) - } - } - .partitionEithers() + val adds = mutableListOf<Pair<K, EventsImpl<V>>>() + val removes = mutableListOf<K>() + patch.forEach { (k, newUpstream) -> + when (newUpstream) { + is Just -> adds.add(k to newUpstream.value) + None -> removes.add(k) + } + } val severed = mutableListOf<NodeConnection<*>>() - coroutineScope { - // remove and sever - removes.forEach { k -> - switchedIn.remove(k)?.let { branchNode: MuxBranchNode<K, V> -> - val conn = branchNode.upstream - severed.add(conn) - launch { conn.removeDownstream(downstream = branchNode.schedulable) } - depthTracker.removeDirectUpstream(conn.depthTracker.snapshotDirectDepth) - } + // remove and sever + removes.forEach { k -> + switchedIn.remove(k)?.let { branchNode: BranchNode -> + val conn = branchNode.upstream + severed.add(conn) + conn.removeDownstream(downstream = branchNode.schedulable) + depthTracker.removeDirectUpstream(conn.depthTracker.snapshotDirectDepth) } + } - // add or replace - adds - .mapParallel { (k, newUpstream: TFlowImpl<V>) -> - val branchNode = MuxBranchNode(this@MuxDeferredNode, k) - k to - newUpstream.activate(evalScope, branchNode.schedulable)?.let { (conn, _) -> - branchNode.apply { upstream = conn } - } - } - .forEach { (k, newBranch: MuxBranchNode<K, V>?) -> - // remove old and sever, if present - switchedIn.remove(k)?.let { branchNode -> - val conn = branchNode.upstream - severed.add(conn) - launch { conn.removeDownstream(downstream = branchNode.schedulable) } - depthTracker.removeDirectUpstream(conn.depthTracker.snapshotDirectDepth) - } + // add or replace + adds.forEach { (k, newUpstream: EventsImpl<V>) -> + // remove old and sever, if present + switchedIn.remove(k)?.let { branchNode -> + val conn = branchNode.upstream + severed.add(conn) + conn.removeDownstream(downstream = branchNode.schedulable) + depthTracker.removeDirectUpstream(conn.depthTracker.snapshotDirectDepth) + } - // add new - newBranch?.let { - switchedIn[k] = newBranch - val branchDepthTracker = newBranch.upstream.depthTracker - if (branchDepthTracker.snapshotIsDirect) { - depthTracker.addDirectUpstream( - oldDepth = null, - newDepth = branchDepthTracker.snapshotDirectDepth, - ) - } else { - depthTracker.addIndirectUpstream( - oldDepth = null, - newDepth = branchDepthTracker.snapshotIndirectDepth, - ) - depthTracker.updateIndirectRoots( - additions = branchDepthTracker.snapshotIndirectRoots, - butNot = this@MuxDeferredNode, - ) - } - } + // add new + val newBranch = BranchNode(k) + newUpstream.activate(evalScope, newBranch.schedulable)?.let { (conn, _) -> + newBranch.upstream = conn + switchedIn[k] = newBranch + val branchDepthTracker = newBranch.upstream.depthTracker + if (branchDepthTracker.snapshotIsDirect) { + depthTracker.addDirectUpstream( + oldDepth = null, + newDepth = branchDepthTracker.snapshotDirectDepth, + ) + } else { + depthTracker.addIndirectUpstream( + oldDepth = null, + newDepth = branchDepthTracker.snapshotIndirectDepth, + ) + depthTracker.updateIndirectRoots( + additions = branchDepthTracker.snapshotIndirectRoots, + butNot = this@MuxDeferredNode, + ) } + } } - coroutineScope { - for (severedNode in severed) { - launch { severedNode.scheduleDeactivationIfNeeded(evalScope) } - } + for (severedNode in severed) { + severedNode.scheduleDeactivationIfNeeded(evalScope) } compactIfNeeded(evalScope) } - suspend fun removeDirectPatchNode(scheduler: Scheduler) { - mutex.withLock { - if ( - depthTracker.removeIndirectUpstream(depth = 0) or - depthTracker.setIsIndirectRoot(false) - ) { - depthTracker.schedule(scheduler, this) - } - patches = null + fun removeDirectPatchNode(scheduler: Scheduler) { + if ( + depthTracker.removeIndirectUpstream(depth = 0) or depthTracker.setIsIndirectRoot(false) + ) { + depthTracker.schedule(scheduler, this) } + patches = null } - suspend fun removeIndirectPatchNode( + fun removeIndirectPatchNode( scheduler: Scheduler, depth: Int, - indirectSet: Set<MuxDeferredNode<*, *>>, + indirectSet: Set<MuxDeferredNode<*, *, *>>, ) { // indirectly connected patches forward the indirectSet - mutex.withLock { - if ( - depthTracker.updateIndirectRoots(removals = indirectSet) or - depthTracker.removeIndirectUpstream(depth) - ) { - depthTracker.schedule(scheduler, this) - } - patches = null + if ( + depthTracker.updateIndirectRoots(removals = indirectSet) or + depthTracker.removeIndirectUpstream(depth) + ) { + depthTracker.schedule(scheduler, this) } + patches = null } - suspend fun moveIndirectPatchNodeToDirect( + fun moveIndirectPatchNodeToDirect( scheduler: Scheduler, oldIndirectDepth: Int, - oldIndirectSet: Set<MuxDeferredNode<*, *>>, + oldIndirectSet: Set<MuxDeferredNode<*, *, *>>, ) { // directly connected patches are stored as an indirect singleton set of the patchNode - mutex.withLock { - if ( - depthTracker.updateIndirectRoots(removals = oldIndirectSet) or - depthTracker.removeIndirectUpstream(oldIndirectDepth) or - depthTracker.setIsIndirectRoot(true) - ) { - depthTracker.schedule(scheduler, this) - } + if ( + depthTracker.updateIndirectRoots(removals = oldIndirectSet) or + depthTracker.removeIndirectUpstream(oldIndirectDepth) or + depthTracker.setIsIndirectRoot(true) + ) { + depthTracker.schedule(scheduler, this) } } - suspend fun moveDirectPatchNodeToIndirect( + fun moveDirectPatchNodeToIndirect( scheduler: Scheduler, newIndirectDepth: Int, - newIndirectSet: Set<MuxDeferredNode<*, *>>, + newIndirectSet: Set<MuxDeferredNode<*, *, *>>, ) { // indirectly connected patches forward the indirectSet - mutex.withLock { - if ( - depthTracker.setIsIndirectRoot(false) or - depthTracker.updateIndirectRoots(additions = newIndirectSet, butNot = this) or - depthTracker.addIndirectUpstream(oldDepth = null, newDepth = newIndirectDepth) - ) { - depthTracker.schedule(scheduler, this) - } + if ( + depthTracker.setIsIndirectRoot(false) or + depthTracker.updateIndirectRoots(additions = newIndirectSet, butNot = this) or + depthTracker.addIndirectUpstream(oldDepth = null, newDepth = newIndirectDepth) + ) { + depthTracker.schedule(scheduler, this) } } - suspend fun adjustIndirectPatchNode( + fun adjustIndirectPatchNode( scheduler: Scheduler, oldDepth: Int, newDepth: Int, - removals: Set<MuxDeferredNode<*, *>>, - additions: Set<MuxDeferredNode<*, *>>, + removals: Set<MuxDeferredNode<*, *, *>>, + additions: Set<MuxDeferredNode<*, *, *>>, ) { // indirectly connected patches forward the indirectSet - mutex.withLock { - if ( - depthTracker.updateIndirectRoots( - additions = additions, - removals = removals, - butNot = this, - ) or depthTracker.addIndirectUpstream(oldDepth = oldDepth, newDepth = newDepth) - ) { - depthTracker.schedule(scheduler, this) - } + if ( + depthTracker.updateIndirectRoots( + additions = additions, + removals = removals, + butNot = this, + ) or depthTracker.addIndirectUpstream(oldDepth = oldDepth, newDepth = newDepth) + ) { + depthTracker.schedule(scheduler, this) } } - suspend fun scheduleMover(evalScope: EvalScope) { - patchData = - checkNotNull(patches) { "mux mover scheduled with unset patches upstream node" } - .getPushEvent(evalScope) - .orElseGet { null } - evalScope.scheduleMuxMover(this) + fun scheduleMover(logIndent: Int, evalScope: EvalScope) { + logDuration(logIndent, "MuxDeferred.scheduleMover") { + patchData = + checkNotNull(patches) { "mux mover scheduled with unset patches upstream node" } + .getPushEvent(currentLogIndent, evalScope) + evalScope.scheduleMuxMover(this@MuxDeferredNode) + } } - override fun toString(): String = "${this::class.simpleName}@$hashString" + override fun toString(): String = + "${this::class.simpleName}@$hashString${name?.let { "[$it]" }.orEmpty()}" } internal inline fun <A> switchDeferredImplSingle( - crossinline getStorage: suspend EvalScope.() -> TFlowImpl<A>, - crossinline getPatches: suspend EvalScope.() -> TFlowImpl<TFlowImpl<A>>, -): TFlowImpl<A> = - mapImpl({ + name: String? = null, + crossinline getStorage: EvalScope.() -> EventsImpl<A>, + crossinline getPatches: EvalScope.() -> EventsImpl<EventsImpl<A>>, +): EventsImpl<A> { + val patches = mapImpl(getPatches) { newEvents, _ -> singleOf(just(newEvents)).asIterable() } + val switchDeferredImpl = switchDeferredImpl( - getStorage = { mapOf(Unit to getStorage()) }, - getPatches = { mapImpl(getPatches) { newFlow -> mapOf(Unit to just(newFlow)) } }, + name = name, + getStorage = { singleOf(getStorage()).asIterable() }, + getPatches = { patches }, + storeFactory = SingletonMapK.Factory(), ) - }) { map -> - map.getValue(Unit) + return mapImpl({ switchDeferredImpl }) { map, logIndent -> + map.asSingle().getValue(Unit).getPushEvent(logIndent, this).also { + if (name != null) { + logLn(logIndent, "[$name] extracting single mux: $it") + } + } } +} -internal fun <K : Any, A> switchDeferredImpl( - getStorage: suspend EvalScope.() -> Map<K, TFlowImpl<A>>, - getPatches: suspend EvalScope.() -> TFlowImpl<Map<K, Maybe<TFlowImpl<A>>>>, -): TFlowImpl<Map<K, A>> = - MuxLifecycle( - object : MuxActivator<Map<K, A>> { - override suspend fun activate( - evalScope: EvalScope, - lifecycle: MuxLifecycle<Map<K, A>>, - ): MuxNode<*, *, Map<K, A>>? { - val storage: Map<K, TFlowImpl<A>> = getStorage(evalScope) - // Initialize mux node and switched-in connections. - val muxNode = - MuxDeferredNode(lifecycle, this).apply { - storage.mapValuesNotNullParallelTo(switchedIn) { (key, flow) -> - val branchNode = MuxBranchNode(this@apply, key) - flow.activate(evalScope, branchNode.schedulable)?.let { - (conn, needsEval) -> - branchNode - .apply { upstream = conn } - .also { - if (needsEval) { - val result = conn.getPushEvent(evalScope) - if (result is Just) { - upstreamData[key] = result.value - } - } - } - } - } - } +internal fun <W, K, V> switchDeferredImpl( + name: String? = null, + getStorage: EvalScope.() -> Iterable<Map.Entry<K, EventsImpl<V>>>, + getPatches: EvalScope.() -> EventsImpl<Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>>, + storeFactory: MutableMapK.Factory<W, K>, +): EventsImpl<MuxResult<W, K, V>> = + MuxLifecycle(MuxDeferredActivator(name, getStorage, storeFactory, getPatches)) + +private class MuxDeferredActivator<W, K, V>( + private val name: String?, + private val getStorage: EvalScope.() -> Iterable<Map.Entry<K, EventsImpl<V>>>, + private val storeFactory: MutableMapK.Factory<W, K>, + private val getPatches: EvalScope.() -> EventsImpl<Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>>, +) : MuxActivator<W, K, V> { + override fun activate( + evalScope: EvalScope, + lifecycle: MuxLifecycle<W, K, V>, + ): Pair<MuxNode<W, K, V>, (() -> Unit)?>? { + // Initialize mux node and switched-in connections. + val muxNode = + MuxDeferredNode(name, lifecycle, this, storeFactory).apply { + initializeUpstream(evalScope, getStorage, storeFactory) // Update depth based on all initial switched-in nodes. - muxNode.switchedIn.values.forEach { branch -> - val conn = branch.upstream - if (conn.depthTracker.snapshotIsDirect) { - muxNode.depthTracker.addDirectUpstream( - oldDepth = null, - newDepth = conn.depthTracker.snapshotDirectDepth, - ) - } else { - muxNode.depthTracker.addIndirectUpstream( - oldDepth = null, - newDepth = conn.depthTracker.snapshotIndirectDepth, - ) - muxNode.depthTracker.updateIndirectRoots( - additions = conn.depthTracker.snapshotIndirectRoots, - butNot = muxNode, - ) - } - } + initializeDepth() // We don't have our patches connection established yet, so for now pretend we have // a direct connection to patches. We will update downstream nodes later if this // turns out to be a lie. - muxNode.depthTracker.setIsIndirectRoot(true) - muxNode.depthTracker.reset() + depthTracker.setIsIndirectRoot(true) + depthTracker.reset() + } + + // Schedule for evaluation if any switched-in nodes have already emitted within + // this transaction. + if (muxNode.upstreamData.isNotEmpty()) { + muxNode.schedule(evalScope) + } + return muxNode to + fun() { // Setup patches connection; deferring allows for a recursive connection, where // muxNode is downstream of itself via patches. - var isIndirect = true - evalScope.deferAction { - val (patchesConn, needsEval) = - getPatches(evalScope).activate(evalScope, downstream = muxNode.schedulable) - ?: run { - isIndirect = false - // Turns out we can't connect to patches, so update our depth and - // propagate - muxNode.mutex.withLock { - if (muxNode.depthTracker.setIsIndirectRoot(false)) { - muxNode.depthTracker.schedule(evalScope.scheduler, muxNode) - } - } - return@deferAction - } - muxNode.patches = patchesConn - - if (!patchesConn.schedulerUpstream.depthTracker.snapshotIsDirect) { - // Turns out patches is indirect, so we are not a root. Update depth and - // propagate. - muxNode.mutex.withLock { - if ( - muxNode.depthTracker.setIsIndirectRoot(false) or - muxNode.depthTracker.addIndirectUpstream( - oldDepth = null, - newDepth = patchesConn.depthTracker.snapshotIndirectDepth, - ) or - muxNode.depthTracker.updateIndirectRoots( - additions = patchesConn.depthTracker.snapshotIndirectRoots - ) - ) { + val (patchesConn, needsEval) = + getPatches(evalScope).activate(evalScope, downstream = muxNode.schedulable) + ?: run { + // Turns out we can't connect to patches, so update our depth and + // propagate + if (muxNode.depthTracker.setIsIndirectRoot(false)) { + // TODO: schedules might not be necessary now that we're not + // parallel? muxNode.depthTracker.schedule(evalScope.scheduler, muxNode) } + return } - } - // Schedule mover to process patch emission at the end of this transaction, if - // needed. - if (needsEval) { - val result = patchesConn.getPushEvent(evalScope) - if (result is Just) { - muxNode.patchData = result.value - evalScope.scheduleMuxMover(muxNode) - } + muxNode.patches = patchesConn + + if (!patchesConn.schedulerUpstream.depthTracker.snapshotIsDirect) { + // Turns out patches is indirect, so we are not a root. Update depth and + // propagate. + if ( + muxNode.depthTracker.setIsIndirectRoot(false) or + muxNode.depthTracker.addIndirectUpstream( + oldDepth = null, + newDepth = patchesConn.depthTracker.snapshotIndirectDepth, + ) or + muxNode.depthTracker.updateIndirectRoots( + additions = patchesConn.depthTracker.snapshotIndirectRoots + ) + ) { + muxNode.depthTracker.schedule(evalScope.scheduler, muxNode) } } - - // Schedule for evaluation if any switched-in nodes have already emitted within - // this transaction. - if (muxNode.upstreamData.isNotEmpty()) { - muxNode.schedule(evalScope) + // Schedule mover to process patch emission at the end of this transaction, if + // needed. + if (needsEval) { + muxNode.patchData = patchesConn.getPushEvent(0, evalScope) + evalScope.scheduleMuxMover(muxNode) } - return muxNode.takeUnless { muxNode.switchedIn.isEmpty() && !isIndirect } } - } - ) + } +} internal inline fun <A> mergeNodes( - crossinline getPulse: suspend EvalScope.() -> TFlowImpl<A>, - crossinline getOther: suspend EvalScope.() -> TFlowImpl<A>, - crossinline f: suspend EvalScope.(A, A) -> A, -): TFlowImpl<A> { + crossinline getPulse: EvalScope.() -> EventsImpl<A>, + crossinline getOther: EvalScope.() -> EventsImpl<A>, + name: String? = null, + crossinline f: EvalScope.(A, A) -> A, +): EventsImpl<A> { + val mergedThese = mergeNodes(name, getPulse, getOther) val merged = - mapImpl({ mergeNodes(getPulse, getOther) }) { these -> - these.merge { thiz, that -> f(thiz, that) } - } + mapImpl({ mergedThese }) { these, _ -> these.merge { thiz, that -> f(thiz, that) } } return merged.cached() } +internal fun <T> Iterable<T>.asIterableWithIndex(): Iterable<Map.Entry<Int, T>> = + asSequence().mapIndexed { i, t -> StoreEntry(i, t) }.asIterable() + internal inline fun <A, B> mergeNodes( - crossinline getPulse: suspend EvalScope.() -> TFlowImpl<A>, - crossinline getOther: suspend EvalScope.() -> TFlowImpl<B>, -): TFlowImpl<These<A, B>> { + name: String? = null, + crossinline getPulse: EvalScope.() -> EventsImpl<A>, + crossinline getOther: EvalScope.() -> EventsImpl<B>, +): EventsImpl<These<A, B>> { val storage = - mapOf( - 0 to mapImpl(getPulse) { These.thiz<A, B>(it) }, - 1 to mapImpl(getOther) { These.that(it) }, + listOf( + mapImpl(getPulse) { it, _ -> These.thiz(it) }, + mapImpl(getOther) { it, _ -> These.that(it) }, + ) + .asIterableWithIndex() + val switchNode = + switchDeferredImpl( + name = name, + getStorage = { storage }, + getPatches = { neverImpl }, + storeFactory = MutableArrayMapK.Factory(), ) - val switchNode = switchDeferredImpl(getStorage = { storage }, getPatches = { neverImpl }) val merged = - mapImpl({ switchNode }) { mergeResults -> - val first = mergeResults.getMaybe(0).flatMap { it.maybeThis() } - val second = mergeResults.getMaybe(1).flatMap { it.maybeThat() } - these(first, second).orElseGet { error("unexpected missing merge result") } + mapImpl({ switchNode }) { it, logIndent -> + val mergeResults = it.asArrayHolder() + val first = + mergeResults.getMaybe(0).flatMap { it.getPushEvent(logIndent, this).maybeThis() } + val second = + mergeResults.getMaybe(1).flatMap { it.getPushEvent(logIndent, this).maybeThat() } + these(first, second).orError { "unexpected missing merge result" } } return merged.cached() } internal inline fun <A> mergeNodes( - crossinline getPulses: suspend EvalScope.() -> Iterable<TFlowImpl<A>> -): TFlowImpl<List<A>> { + crossinline getPulses: EvalScope.() -> Iterable<EventsImpl<A>> +): EventsImpl<List<A>> { val switchNode = switchDeferredImpl( - getStorage = { getPulses().associateByIndexTo(TreeMap()) }, + getStorage = { getPulses().asIterableWithIndex() }, getPatches = { neverImpl }, + storeFactory = MutableArrayMapK.Factory(), ) - val merged = mapImpl({ switchNode }) { mergeResults -> mergeResults.values.toList() } + val merged = + mapImpl({ switchNode }) { it, logIndent -> + val mergeResults = it.asArrayHolder() + mergeResults.map { (_, node) -> node.getPushEvent(logIndent, this) } + } return merged.cached() } internal inline fun <A> mergeNodesLeft( - crossinline getPulses: suspend EvalScope.() -> Iterable<TFlowImpl<A>> -): TFlowImpl<A> { + crossinline getPulses: EvalScope.() -> Iterable<EventsImpl<A>> +): EventsImpl<A> { val switchNode = switchDeferredImpl( - getStorage = { getPulses().associateByIndexTo(TreeMap()) }, + getStorage = { getPulses().asIterableWithIndex() }, getPatches = { neverImpl }, + storeFactory = MutableArrayMapK.Factory(), ) val merged = - mapImpl({ switchNode }) { mergeResults: Map<Int, A> -> mergeResults.values.first() } + mapImpl({ switchNode }) { it, logIndent -> + val mergeResults = it.asArrayHolder() + mergeResults.values.first().getPushEvent(logIndent, this) + } return merged.cached() } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt index b291c879b449..32aef5c7041b 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt @@ -16,217 +16,184 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.internal.util.Key -import com.android.systemui.kairos.internal.util.launchImmediate -import com.android.systemui.kairos.internal.util.mapParallel -import com.android.systemui.kairos.internal.util.mapValuesNotNullParallelTo -import com.android.systemui.kairos.util.Just -import com.android.systemui.kairos.util.Left +import com.android.systemui.kairos.internal.store.MutableMapK +import com.android.systemui.kairos.internal.store.SingletonMapK +import com.android.systemui.kairos.internal.store.asSingle +import com.android.systemui.kairos.internal.store.singleOf +import com.android.systemui.kairos.internal.util.LogIndent +import com.android.systemui.kairos.internal.util.hashString +import com.android.systemui.kairos.internal.util.logDuration import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.None -import com.android.systemui.kairos.util.Right -import com.android.systemui.kairos.util.filterJust -import com.android.systemui.kairos.util.map -import com.android.systemui.kairos.util.partitionEithers -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.withLock - -internal class MuxPromptMovingNode<K : Any, V>( - lifecycle: MuxLifecycle<Pair<Map<K, V>, Map<K, PullNode<V>>?>>, - private val spec: MuxActivator<Pair<Map<K, V>, Map<K, PullNode<V>>?>>, -) : - MuxNode<K, V, Pair<Map<K, V>, Map<K, PullNode<V>>?>>(lifecycle), - Key<Pair<Map<K, V>, Map<K, PullNode<V>>?>> { - - @Volatile var patchData: Map<K, Maybe<TFlowImpl<V>>>? = null - @Volatile var patches: MuxPromptPatchNode<K, V>? = null - - @Volatile private var reEval: Pair<Map<K, V>, Map<K, PullNode<V>>?>? = null - - override fun hasCurrentValueLocked(transactionStore: TransactionStore): Boolean = - transactionStore.contains(this) - - override suspend fun hasCurrentValue(transactionStore: TransactionStore): Boolean = - mutex.withLock { hasCurrentValueLocked(transactionStore) } - - override suspend fun visit(evalScope: EvalScope) { - val preSwitchResults: Map<K, V> = upstreamData.toMap() - upstreamData.clear() - - val patch: Map<K, Maybe<TFlowImpl<V>>>? = patchData - patchData = null - - val (reschedule, evalResult) = - reEval?.let { false to it } - ?: if (preSwitchResults.isNotEmpty() || patch?.isNotEmpty() == true) { - doEval(preSwitchResults, patch, evalScope) - } else { - false to null +import com.android.systemui.kairos.util.Maybe.Just +import com.android.systemui.kairos.util.Maybe.None +import com.android.systemui.kairos.util.just + +internal class MuxPromptNode<W, K, V>( + val name: String?, + lifecycle: MuxLifecycle<W, K, V>, + private val spec: MuxActivator<W, K, V>, + factory: MutableMapK.Factory<W, K>, +) : MuxNode<W, K, V>(lifecycle, factory) { + + var patchData: Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>? = null + var patches: PatchNode? = null + + override fun visit(logIndent: Int, evalScope: EvalScope) { + check(epoch < evalScope.epoch) { "node unexpectedly visited multiple times in transaction" } + logDuration(logIndent, "MuxPrompt.visit") { + val patch: Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>? = patchData + patchData = null + + // If there's a patch, process it. + patch?.let { + val needsReschedule = processPatch(patch, evalScope) + // We may need to reschedule if newly-switched-in nodes have not yet been + // visited within this transaction. + val depthIncreased = depthTracker.dirty_depthIncreased() + if (needsReschedule || depthIncreased) { + if (depthIncreased) { + depthTracker.schedule(evalScope.compactor, this@MuxPromptNode) + } + if (name != null) { + logLn( + "[${this@MuxPromptNode}] rescheduling (reschedule=$needsReschedule, depthIncrease=$depthIncreased)" + ) + } + schedule(evalScope) + return } - reEval = null - - if (reschedule || depthTracker.dirty_depthIncreased()) { - reEval = evalResult - // Can't schedule downstream yet, need to compact first - if (depthTracker.dirty_depthIncreased()) { - depthTracker.schedule(evalScope.compactor, node = this) } - schedule(evalScope) - } else { + val results = upstreamData.readOnlyCopy().also { upstreamData.clear() } + + // If we don't need to reschedule, or there wasn't a patch at all, then we proceed + // with merging pre-switch and post-switch results + val hasResult = results.isNotEmpty() val compactDownstream = depthTracker.isDirty() - if (evalResult != null || compactDownstream) { - coroutineScope { - mutex.withLock { - if (compactDownstream) { - adjustDownstreamDepths(evalScope, coroutineScope = this) - } - if (evalResult != null) { - evalScope.setResult(this@MuxPromptMovingNode, evalResult) - if (!scheduleAll(downstreamSet, evalScope)) { - evalScope.scheduleDeactivation(this@MuxPromptMovingNode) - } - } + if (hasResult || compactDownstream) { + if (compactDownstream) { + adjustDownstreamDepths(evalScope) + } + if (hasResult) { + transactionCache.put(evalScope, results) + if (!scheduleAll(currentLogIndent, downstreamSet, evalScope)) { + evalScope.scheduleDeactivation(this@MuxPromptNode) } } } } } - private suspend fun doEval( - preSwitchResults: Map<K, V>, - patch: Map<K, Maybe<TFlowImpl<V>>>?, + // side-effect: this will populate `upstreamData` with any immediately available results + private fun LogIndent.processPatch( + patch: Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>, evalScope: EvalScope, - ): Pair<Boolean, Pair<Map<K, V>, Map<K, PullNode<V>>?>?> { - val newlySwitchedIn: Map<K, PullNode<V>>? = - patch?.let { - // We have a patch, process additions/updates and removals - val (adds, removes) = - patch - .asSequence() - .map { (k, newUpstream: Maybe<TFlowImpl<V>>) -> - when (newUpstream) { - is Just -> Left(k to newUpstream.value) - None -> Right(k) - } - } - .partitionEithers() - - val additionsAndUpdates = mutableMapOf<K, PullNode<V>>() - val severed = mutableListOf<NodeConnection<*>>() - - coroutineScope { - // remove and sever - removes.forEach { k -> - switchedIn.remove(k)?.let { branchNode: MuxBranchNode<K, V> -> - val conn: NodeConnection<V> = branchNode.upstream - severed.add(conn) - launchImmediate { - conn.removeDownstream(downstream = branchNode.schedulable) - } - depthTracker.removeDirectUpstream(conn.depthTracker.snapshotDirectDepth) - } - } + ): Boolean { + var needsReschedule = false + // We have a patch, process additions/updates and removals + val adds = mutableListOf<Pair<K, EventsImpl<V>>>() + val removes = mutableListOf<K>() + patch.forEach { (k, newUpstream) -> + when (newUpstream) { + is Just -> adds.add(k to newUpstream.value) + None -> removes.add(k) + } + } - // add or replace - adds - .mapParallel { (k, newUpstream: TFlowImpl<V>) -> - val branchNode = MuxBranchNode(this@MuxPromptMovingNode, k) - k to - newUpstream.activate(evalScope, branchNode.schedulable)?.let { - (conn, _) -> - branchNode.apply { upstream = conn } - } - } - .forEach { (k, newBranch: MuxBranchNode<K, V>?) -> - // remove old and sever, if present - switchedIn.remove(k)?.let { oldBranch: MuxBranchNode<K, V> -> - val conn: NodeConnection<V> = oldBranch.upstream - severed.add(conn) - launchImmediate { - conn.removeDownstream(downstream = oldBranch.schedulable) - } - depthTracker.removeDirectUpstream( - conn.depthTracker.snapshotDirectDepth - ) - } - - // add new - newBranch?.let { - switchedIn[k] = newBranch - additionsAndUpdates[k] = newBranch.upstream.directUpstream - val branchDepthTracker = newBranch.upstream.depthTracker - if (branchDepthTracker.snapshotIsDirect) { - depthTracker.addDirectUpstream( - oldDepth = null, - newDepth = branchDepthTracker.snapshotDirectDepth, - ) - } else { - depthTracker.addIndirectUpstream( - oldDepth = null, - newDepth = branchDepthTracker.snapshotIndirectDepth, - ) - depthTracker.updateIndirectRoots( - additions = branchDepthTracker.snapshotIndirectRoots, - butNot = null, - ) - } - } - } + val severed = mutableListOf<NodeConnection<*>>() + + // remove and sever + removes.forEach { k -> + switchedIn.remove(k)?.let { branchNode: BranchNode -> + if (name != null) { + logLn("[${this@MuxPromptNode}] removing $k") } + val conn: NodeConnection<V> = branchNode.upstream + severed.add(conn) + conn.removeDownstream(downstream = branchNode.schedulable) + depthTracker.removeDirectUpstream(conn.depthTracker.snapshotDirectDepth) + } + } - coroutineScope { - for (severedNode in severed) { - launch { severedNode.scheduleDeactivationIfNeeded(evalScope) } - } + // add or replace + adds.forEach { (k, newUpstream: EventsImpl<V>) -> + // remove old and sever, if present + switchedIn.remove(k)?.let { oldBranch: BranchNode -> + if (name != null) { + logLn("[${this@MuxPromptNode}] replacing $k") } + val conn: NodeConnection<V> = oldBranch.upstream + severed.add(conn) + conn.removeDownstream(downstream = oldBranch.schedulable) + depthTracker.removeDirectUpstream(conn.depthTracker.snapshotDirectDepth) + } - additionsAndUpdates.takeIf { it.isNotEmpty() } + // add new + val newBranch = BranchNode(k) + newUpstream.activate(evalScope, newBranch.schedulable)?.let { (conn, needsEval) -> + newBranch.upstream = conn + if (name != null) { + logLn("[${this@MuxPromptNode}] switching in $k") + } + switchedIn[k] = newBranch + if (needsEval) { + upstreamData[k] = newBranch.upstream.directUpstream + } else { + needsReschedule = true + } + val branchDepthTracker = newBranch.upstream.depthTracker + if (branchDepthTracker.snapshotIsDirect) { + depthTracker.addDirectUpstream( + oldDepth = null, + newDepth = branchDepthTracker.snapshotDirectDepth, + ) + } else { + depthTracker.addIndirectUpstream( + oldDepth = null, + newDepth = branchDepthTracker.snapshotIndirectDepth, + ) + depthTracker.updateIndirectRoots( + additions = branchDepthTracker.snapshotIndirectRoots, + butNot = null, + ) + } } + } - return if (preSwitchResults.isNotEmpty() || newlySwitchedIn != null) { - (newlySwitchedIn != null) to (preSwitchResults to newlySwitchedIn) - } else { - false to null + for (severedNode in severed) { + severedNode.scheduleDeactivationIfNeeded(evalScope) } + + return needsReschedule } - private suspend fun adjustDownstreamDepths( - evalScope: EvalScope, - coroutineScope: CoroutineScope, - ) { + private fun adjustDownstreamDepths(evalScope: EvalScope) { if (depthTracker.dirty_depthIncreased()) { // schedule downstream nodes on the compaction scheduler; this scheduler is drained at // the end of this eval depth, so that all depth increases are applied before we advance // the eval step - depthTracker.schedule(evalScope.compactor, node = this@MuxPromptMovingNode) + depthTracker.schedule(evalScope.compactor, node = this@MuxPromptNode) } else if (depthTracker.isDirty()) { // schedule downstream nodes on the eval scheduler; this is more efficient and is only // safe if the depth hasn't increased depthTracker.applyChanges( - coroutineScope, evalScope.scheduler, downstreamSet, - muxNode = this@MuxPromptMovingNode, + muxNode = this@MuxPromptNode, ) } } - override suspend fun getPushEvent( - evalScope: EvalScope - ): Maybe<Pair<Map<K, V>, Map<K, PullNode<V>>?>> = evalScope.getCurrentValue(key = this) + override fun getPushEvent(logIndent: Int, evalScope: EvalScope): MuxResult<W, K, V> = + logDuration(logIndent, "MuxPrompt.getPushEvent") { + transactionCache.getCurrentValue(evalScope) + } - override suspend fun doDeactivate() { + override fun doDeactivate() { // Update lifecycle - lifecycle.mutex.withLock { - if (lifecycle.lifecycleState !is MuxLifecycleState.Active) return@doDeactivate - lifecycle.lifecycleState = MuxLifecycleState.Inactive(spec) - } + if (lifecycle.lifecycleState !is MuxLifecycleState.Active) return + lifecycle.lifecycleState = MuxLifecycleState.Inactive(spec) // Process branch nodes - switchedIn.values.forEach { branchNode -> + switchedIn.forEach { (_, branchNode) -> branchNode.upstream.removeDownstreamAndDeactivateIfNeeded( downstream = branchNode.schedulable ) @@ -237,236 +204,189 @@ internal class MuxPromptMovingNode<K : Any, V>( } } - suspend fun removeIndirectPatchNode( + fun removeIndirectPatchNode( scheduler: Scheduler, oldDepth: Int, - indirectSet: Set<MuxDeferredNode<*, *>>, + indirectSet: Set<MuxDeferredNode<*, *, *>>, ) { - mutex.withLock { - patches = null - if ( - depthTracker.removeIndirectUpstream(oldDepth) or - depthTracker.updateIndirectRoots(removals = indirectSet) - ) { - depthTracker.schedule(scheduler, this) - } + patches = null + if ( + depthTracker.removeIndirectUpstream(oldDepth) or + depthTracker.updateIndirectRoots(removals = indirectSet) + ) { + depthTracker.schedule(scheduler, this) } } - suspend fun removeDirectPatchNode(scheduler: Scheduler, depth: Int) { - mutex.withLock { - patches = null - if (depthTracker.removeDirectUpstream(depth)) { - depthTracker.schedule(scheduler, this) - } + fun removeDirectPatchNode(scheduler: Scheduler, depth: Int) { + patches = null + if (depthTracker.removeDirectUpstream(depth)) { + depthTracker.schedule(scheduler, this) } } -} -internal class MuxPromptEvalNode<K, V>( - private val movingNode: PullNode<Pair<Map<K, V>, Map<K, PullNode<V>>?>> -) : PullNode<Map<K, V>> { - override suspend fun getPushEvent(evalScope: EvalScope): Maybe<Map<K, V>> = - movingNode.getPushEvent(evalScope).map { (preSwitchResults, newlySwitchedIn) -> - coroutineScope { - newlySwitchedIn - ?.map { (k, v) -> async { v.getPushEvent(evalScope).map { k to it } } } - ?.awaitAll() - ?.asSequence() - ?.filterJust() - ?.toMap(preSwitchResults.toMutableMap()) ?: preSwitchResults - } - } -} + override fun toString(): String = + "${this::class.simpleName}@$hashString${name?.let { "[$it]" }.orEmpty()}" -// TODO: inner class? -internal class MuxPromptPatchNode<K : Any, V>(private val muxNode: MuxPromptMovingNode<K, V>) : - SchedulableNode { + inner class PatchNode : SchedulableNode { - val schedulable = Schedulable.N(this) + val schedulable = Schedulable.N(this) - lateinit var upstream: NodeConnection<Map<K, Maybe<TFlowImpl<V>>>> + lateinit var upstream: NodeConnection<Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>> - override suspend fun schedule(evalScope: EvalScope) { - val upstreamResult = upstream.getPushEvent(evalScope) - if (upstreamResult is Just) { - muxNode.patchData = upstreamResult.value - muxNode.schedule(evalScope) + override fun schedule(logIndent: Int, evalScope: EvalScope) { + logDuration(logIndent, "MuxPromptPatchNode.schedule") { + patchData = upstream.getPushEvent(currentLogIndent, evalScope) + this@MuxPromptNode.schedule(evalScope) + } } - } - override suspend fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) { - muxNode.adjustDirectUpstream(scheduler, oldDepth, newDepth) - } + override fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) { + this@MuxPromptNode.adjustDirectUpstream(scheduler, oldDepth, newDepth) + } - override suspend fun moveIndirectUpstreamToDirect( - scheduler: Scheduler, - oldIndirectDepth: Int, - oldIndirectSet: Set<MuxDeferredNode<*, *>>, - newDirectDepth: Int, - ) { - muxNode.moveIndirectUpstreamToDirect( - scheduler, - oldIndirectDepth, - oldIndirectSet, - newDirectDepth, - ) - } + override fun moveIndirectUpstreamToDirect( + scheduler: Scheduler, + oldIndirectDepth: Int, + oldIndirectSet: Set<MuxDeferredNode<*, *, *>>, + newDirectDepth: Int, + ) { + this@MuxPromptNode.moveIndirectUpstreamToDirect( + scheduler, + oldIndirectDepth, + oldIndirectSet, + newDirectDepth, + ) + } - override suspend fun adjustIndirectUpstream( - scheduler: Scheduler, - oldDepth: Int, - newDepth: Int, - removals: Set<MuxDeferredNode<*, *>>, - additions: Set<MuxDeferredNode<*, *>>, - ) { - muxNode.adjustIndirectUpstream(scheduler, oldDepth, newDepth, removals, additions) - } + override fun adjustIndirectUpstream( + scheduler: Scheduler, + oldDepth: Int, + newDepth: Int, + removals: Set<MuxDeferredNode<*, *, *>>, + additions: Set<MuxDeferredNode<*, *, *>>, + ) { + this@MuxPromptNode.adjustIndirectUpstream( + scheduler, + oldDepth, + newDepth, + removals, + additions, + ) + } - override suspend fun moveDirectUpstreamToIndirect( - scheduler: Scheduler, - oldDirectDepth: Int, - newIndirectDepth: Int, - newIndirectSet: Set<MuxDeferredNode<*, *>>, - ) { - muxNode.moveDirectUpstreamToIndirect( - scheduler, - oldDirectDepth, - newIndirectDepth, - newIndirectSet, - ) - } + override fun moveDirectUpstreamToIndirect( + scheduler: Scheduler, + oldDirectDepth: Int, + newIndirectDepth: Int, + newIndirectSet: Set<MuxDeferredNode<*, *, *>>, + ) { + this@MuxPromptNode.moveDirectUpstreamToIndirect( + scheduler, + oldDirectDepth, + newIndirectDepth, + newIndirectSet, + ) + } - override suspend fun removeDirectUpstream(scheduler: Scheduler, depth: Int) { - muxNode.removeDirectPatchNode(scheduler, depth) + override fun removeDirectUpstream(scheduler: Scheduler, depth: Int) { + this@MuxPromptNode.removeDirectPatchNode(scheduler, depth) + } + + override fun removeIndirectUpstream( + scheduler: Scheduler, + depth: Int, + indirectSet: Set<MuxDeferredNode<*, *, *>>, + ) { + this@MuxPromptNode.removeIndirectPatchNode(scheduler, depth, indirectSet) + } } +} - override suspend fun removeIndirectUpstream( - scheduler: Scheduler, - depth: Int, - indirectSet: Set<MuxDeferredNode<*, *>>, - ) { - muxNode.removeIndirectPatchNode(scheduler, depth, indirectSet) +internal inline fun <A> switchPromptImplSingle( + crossinline getStorage: EvalScope.() -> EventsImpl<A>, + crossinline getPatches: EvalScope.() -> EventsImpl<EventsImpl<A>>, +): EventsImpl<A> { + val switchPromptImpl = + switchPromptImpl( + getStorage = { singleOf(getStorage()).asIterable() }, + getPatches = { + mapImpl(getPatches) { newEvents, _ -> singleOf(just(newEvents)).asIterable() } + }, + storeFactory = SingletonMapK.Factory(), + ) + return mapImpl({ switchPromptImpl }) { map, logIndent -> + map.asSingle().getValue(Unit).getPushEvent(logIndent, this) } } -internal fun <K : Any, A> switchPromptImpl( - getStorage: suspend EvalScope.() -> Map<K, TFlowImpl<A>>, - getPatches: suspend EvalScope.() -> TFlowImpl<Map<K, Maybe<TFlowImpl<A>>>>, -): TFlowImpl<Map<K, A>> { - val moving = - MuxLifecycle( - object : MuxActivator<Pair<Map<K, A>, Map<K, PullNode<A>>?>> { - override suspend fun activate( - evalScope: EvalScope, - lifecycle: MuxLifecycle<Pair<Map<K, A>, Map<K, PullNode<A>>?>>, - ): MuxNode<*, *, Pair<Map<K, A>, Map<K, PullNode<A>>?>>? { - val storage: Map<K, TFlowImpl<A>> = getStorage(evalScope) - // Initialize mux node and switched-in connections. - val movingNode = - MuxPromptMovingNode(lifecycle, this).apply { - coroutineScope { - launch { - storage.mapValuesNotNullParallelTo(switchedIn) { (key, flow) -> - val branchNode = MuxBranchNode(this@apply, key) - flow - .activate( - evalScope = evalScope, - downstream = branchNode.schedulable, - ) - ?.let { (conn, needsEval) -> - branchNode - .apply { upstream = conn } - .also { - if (needsEval) { - val result = - conn.getPushEvent(evalScope) - if (result is Just) { - upstreamData[key] = result.value - } - } - } - } - } - } - // Setup patches connection - val patchNode = MuxPromptPatchNode(this@apply) - getPatches(evalScope) - .activate( - evalScope = evalScope, - downstream = patchNode.schedulable, - ) - ?.let { (conn, needsEval) -> - patchNode.upstream = conn - patches = patchNode - - if (needsEval) { - val result = conn.getPushEvent(evalScope) - if (result is Just) { - patchData = result.value - } - } - } - } - } - // Update depth based on all initial switched-in nodes. - movingNode.switchedIn.values.forEach { branch -> - val conn = branch.upstream - if (conn.depthTracker.snapshotIsDirect) { - movingNode.depthTracker.addDirectUpstream( - oldDepth = null, - newDepth = conn.depthTracker.snapshotDirectDepth, - ) - } else { - movingNode.depthTracker.addIndirectUpstream( - oldDepth = null, - newDepth = conn.depthTracker.snapshotIndirectDepth, - ) - movingNode.depthTracker.updateIndirectRoots( - additions = conn.depthTracker.snapshotIndirectRoots, - butNot = null, - ) +internal fun <W, K, V> switchPromptImpl( + name: String? = null, + getStorage: EvalScope.() -> Iterable<Map.Entry<K, EventsImpl<V>>>, + getPatches: EvalScope.() -> EventsImpl<Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>>, + storeFactory: MutableMapK.Factory<W, K>, +): EventsImpl<MuxResult<W, K, V>> = + MuxLifecycle(MuxPromptActivator(name, getStorage, storeFactory, getPatches)) + +private class MuxPromptActivator<W, K, V>( + private val name: String?, + private val getStorage: EvalScope.() -> Iterable<Map.Entry<K, EventsImpl<V>>>, + private val storeFactory: MutableMapK.Factory<W, K>, + private val getPatches: EvalScope.() -> EventsImpl<Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>>, +) : MuxActivator<W, K, V> { + override fun activate( + evalScope: EvalScope, + lifecycle: MuxLifecycle<W, K, V>, + ): Pair<MuxNode<W, K, V>, (() -> Unit)?>? { + // Initialize mux node and switched-in connections. + val movingNode = + MuxPromptNode(name, lifecycle, this, storeFactory).apply { + initializeUpstream(evalScope, getStorage, storeFactory) + // Setup patches connection + val patchNode = PatchNode() + getPatches(evalScope) + .activate(evalScope = evalScope, downstream = patchNode.schedulable) + ?.let { (conn, needsEval) -> + patchNode.upstream = conn + patches = patchNode + if (needsEval) { + patchData = conn.getPushEvent(0, evalScope) } } - // Update depth based on patches node. - movingNode.patches?.upstream?.let { conn -> - if (conn.depthTracker.snapshotIsDirect) { - movingNode.depthTracker.addDirectUpstream( - oldDepth = null, - newDepth = conn.depthTracker.snapshotDirectDepth, - ) - } else { - movingNode.depthTracker.addIndirectUpstream( - oldDepth = null, - newDepth = conn.depthTracker.snapshotIndirectDepth, - ) - movingNode.depthTracker.updateIndirectRoots( - additions = conn.depthTracker.snapshotIndirectRoots, - butNot = null, - ) - } + // Update depth based on all initial switched-in nodes. + initializeDepth() + // Update depth based on patches node. + patches?.upstream?.let { conn -> + if (conn.depthTracker.snapshotIsDirect) { + depthTracker.addDirectUpstream( + oldDepth = null, + newDepth = conn.depthTracker.snapshotDirectDepth, + ) + } else { + depthTracker.addIndirectUpstream( + oldDepth = null, + newDepth = conn.depthTracker.snapshotIndirectDepth, + ) + depthTracker.updateIndirectRoots( + additions = conn.depthTracker.snapshotIndirectRoots, + butNot = null, + ) } - movingNode.depthTracker.reset() - - // Schedule for evaluation if any switched-in nodes or the patches node have - // already emitted within this transaction. - if (movingNode.patchData != null || movingNode.upstreamData.isNotEmpty()) { - movingNode.schedule(evalScope) - } - - return movingNode.takeUnless { it.patches == null && it.switchedIn.isEmpty() } } + // Reset all depth adjustments, since no downstream has been notified + depthTracker.reset() } - ) - val eval = TFlowCheap { downstream -> - moving.activate(evalScope = this, downstream)?.let { (connection, needsEval) -> - val evalNode = MuxPromptEvalNode(connection.directUpstream) - ActivationResult( - connection = NodeConnection(evalNode, connection.schedulerUpstream), - needsEval = needsEval, - ) + // Schedule for evaluation if any switched-in nodes or the patches node have + // already emitted within this transaction. + if (movingNode.patchData != null || movingNode.upstreamData.isNotEmpty()) { + movingNode.schedule(evalScope) + } + + return if (movingNode.patches == null && movingNode.switchedIn.isEmpty()) { + null + } else { + movingNode to null } } - return eval.cached() } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt index 599b18695034..fbc2b3644701 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt @@ -16,17 +16,17 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.TState +import com.android.systemui.kairos.State import com.android.systemui.kairos.internal.util.HeteroMap -import com.android.systemui.kairos.util.Just +import com.android.systemui.kairos.internal.util.logDuration +import com.android.systemui.kairos.internal.util.logLn import com.android.systemui.kairos.util.Maybe +import com.android.systemui.kairos.util.Maybe.Just import com.android.systemui.kairos.util.just import com.android.systemui.kairos.util.none -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.ConcurrentLinkedDeque -import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.atomic.AtomicLong import kotlin.coroutines.ContinuationInterceptor +import kotlin.time.measureTime import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred @@ -52,32 +52,41 @@ internal class Network(val coroutineScope: CoroutineScope) : NetworkScope { override val network get() = this - override val compactor = SchedulerImpl() - override val scheduler = SchedulerImpl() - override val transactionStore = HeteroMap() + override val compactor = SchedulerImpl { + if (it.markedForCompaction) false + else { + it.markedForCompaction = true + true + } + } + override val scheduler = SchedulerImpl { + if (it.markedForEvaluation) false + else { + it.markedForEvaluation = true + true + } + } + override val transactionStore = TransactionStore() - private val stateWrites = ConcurrentLinkedQueue<TStateSource<*>>() - private val outputsByDispatcher = - ConcurrentHashMap<ContinuationInterceptor, ConcurrentLinkedQueue<Output<*>>>() - private val muxMovers = ConcurrentLinkedQueue<MuxDeferredNode<*, *>>() - private val deactivations = ConcurrentLinkedDeque<PushNode<*>>() - private val outputDeactivations = ConcurrentLinkedQueue<Output<*>>() + private val stateWrites = ArrayDeque<StateSource<*>>() + private val outputsByDispatcher = HashMap<ContinuationInterceptor, ArrayDeque<Output<*>>>() + private val muxMovers = ArrayDeque<MuxDeferredNode<*, *, *>>() + private val deactivations = ArrayDeque<PushNode<*>>() + private val outputDeactivations = ArrayDeque<Output<*>>() private val transactionMutex = Mutex() private val inputScheduleChan = Channel<ScheduledAction<*>>() override fun scheduleOutput(output: Output<*>) { val continuationInterceptor = output.context[ContinuationInterceptor] ?: Dispatchers.Unconfined - outputsByDispatcher - .computeIfAbsent(continuationInterceptor) { ConcurrentLinkedQueue() } - .add(output) + outputsByDispatcher.computeIfAbsent(continuationInterceptor) { ArrayDeque() }.add(output) } - override fun scheduleMuxMover(muxMover: MuxDeferredNode<*, *>) { + override fun scheduleMuxMover(muxMover: MuxDeferredNode<*, *, *>) { muxMovers.add(muxMover) } - override fun schedule(state: TStateSource<*>) { + override fun schedule(state: StateSource<*>) { stateWrites.add(state) } @@ -89,7 +98,7 @@ internal class Network(val coroutineScope: CoroutineScope) : NetworkScope { outputDeactivations.add(output) } - /** Listens for external events and starts FRP transactions. Runs forever. */ + /** Listens for external events and starts Kairos transactions. Runs forever. */ suspend fun runInputScheduler() { val actions = mutableListOf<ScheduledAction<*>>() for (first in inputScheduleChan) { @@ -101,18 +110,37 @@ internal class Network(val coroutineScope: CoroutineScope) : NetworkScope { actions.add(func) } transactionMutex.withLock { - // Run all actions - evalScope { - for (action in actions) { - launch { action.started(evalScope = this@evalScope) } + val e = epoch + val duration = measureTime { + logLn(0, "===starting transaction $e===") + try { + logDuration(1, "init actions") { + // Run all actions + evalScope { + for (action in actions) { + action.started(evalScope = this@evalScope) + } + } + } + // Step through the network + doTransaction(1) + } catch (e: Exception) { + // Signal failure + while (actions.isNotEmpty()) { + actions.removeLast().fail(e) + } + // re-throw, cancelling this coroutine + throw e + } finally { + logDuration(1, "signal completions") { + // Signal completion + while (actions.isNotEmpty()) { + actions.removeLast().completed() + } + } } } - // Step through the network - doTransaction() - // Signal completion - while (actions.isNotEmpty()) { - actions.removeLast().completed() - } + logLn(0, "===transaction $e took $duration===") } } } @@ -129,33 +157,47 @@ internal class Network(val coroutineScope: CoroutineScope) : NetworkScope { onResult.invokeOnCompletion { job.cancel() } } - suspend fun <R> evalScope(block: suspend EvalScope.() -> R): R = deferScope { + inline fun <R> evalScope(block: EvalScope.() -> R): R = deferScope { block(EvalScopeImpl(this@Network, this)) } - /** Performs a transactional update of the FRP network. */ - private suspend fun doTransaction() { + /** Performs a transactional update of the Kairos network. */ + private suspend fun doTransaction(logIndent: Int) { // Traverse network, then run outputs - do { - scheduler.drainEval(this) - } while (evalScope { evalOutputs(this) }) + logDuration(logIndent, "traverse network") { + do { + val numNodes = + logDuration("drainEval") { scheduler.drainEval(currentLogIndent, this@Network) } + logLn("drained $numNodes nodes") + } while (logDuration("evalOutputs") { evalScope { evalOutputs(this) } }) + } // Update states - evalScope { evalStateWriters(this) } - transactionStore.clear() + logDuration(logIndent, "update states") { + evalScope { evalStateWriters(currentLogIndent, this) } + } + // Invalidate caches + // Note: this needs to occur before deferred switches + logDuration(logIndent, "clear store") { transactionStore.clear() } + epoch++ // Perform deferred switches - evalScope { evalMuxMovers(this) } + logDuration(logIndent, "evalMuxMovers") { + evalScope { evalMuxMovers(currentLogIndent, this) } + } // Compact depths - scheduler.drainCompact() - compactor.drainCompact() + logDuration(logIndent, "compact") { + scheduler.drainCompact(currentLogIndent) + compactor.drainCompact(currentLogIndent) + } // Deactivate nodes with no downstream - evalDeactivations() - epoch++ + logDuration(logIndent, "deactivations") { evalDeactivations() } } /** Invokes all [Output]s that have received data within this transaction. */ private suspend fun evalOutputs(evalScope: EvalScope): Boolean { + if (outputsByDispatcher.isEmpty()) { + return false + } // Outputs can enqueue other outputs, so we need two loops - if (outputsByDispatcher.isEmpty()) return false while (outputsByDispatcher.isNotEmpty()) { var launchedAny = false coroutineScope { @@ -164,57 +206,50 @@ internal class Network(val coroutineScope: CoroutineScope) : NetworkScope { launchedAny = true launch(key) { while (outputs.isNotEmpty()) { - val output = outputs.remove() + val output = outputs.removeFirst() launch { output.visit(evalScope) } } } } } } - if (!launchedAny) outputsByDispatcher.clear() + if (!launchedAny) { + outputsByDispatcher.clear() + } } return true } - private suspend fun evalMuxMovers(evalScope: EvalScope) { + private fun evalMuxMovers(logIndent: Int, evalScope: EvalScope) { while (muxMovers.isNotEmpty()) { - coroutineScope { - val toMove = muxMovers.remove() - launch { toMove.performMove(evalScope) } - } + val toMove = muxMovers.removeFirst() + toMove.performMove(logIndent, evalScope) } } - /** Updates all [TState]es that have changed within this transaction. */ - private suspend fun evalStateWriters(evalScope: EvalScope) { - coroutineScope { - while (stateWrites.isNotEmpty()) { - val latch = stateWrites.remove() - launch { latch.updateState(evalScope) } - } + /** Updates all [State]es that have changed within this transaction. */ + private fun evalStateWriters(logIndent: Int, evalScope: EvalScope) { + while (stateWrites.isNotEmpty()) { + val latch = stateWrites.removeFirst() + latch.updateState(logIndent, evalScope) } } - private suspend fun evalDeactivations() { - coroutineScope { - launch { - while (deactivations.isNotEmpty()) { - // traverse in reverse order - // - deactivations are added in depth-order during the node traversal phase - // - perform deactivations in reverse order, in case later ones propagate to - // earlier ones - val toDeactivate = deactivations.removeLast() - launch { toDeactivate.deactivateIfNeeded() } - } - } - while (outputDeactivations.isNotEmpty()) { - val toDeactivate = outputDeactivations.remove() - launch { - toDeactivate.upstream?.removeDownstreamAndDeactivateIfNeeded( - downstream = toDeactivate.schedulable - ) - } - } + private fun evalDeactivations() { + while (deactivations.isNotEmpty()) { + // traverse in reverse order + // - deactivations are added in depth-order during the node traversal phase + // - perform deactivations in reverse order, in case later ones propagate to + // earlier ones + val toDeactivate = deactivations.removeLast() + toDeactivate.deactivateIfNeeded() + } + + while (outputDeactivations.isNotEmpty()) { + val toDeactivate = outputDeactivations.removeFirst() + toDeactivate.upstream?.removeDownstreamAndDeactivateIfNeeded( + downstream = toDeactivate.schedulable + ) } check(deactivations.isEmpty()) { "unexpected lingering deactivations" } check(outputDeactivations.isEmpty()) { "unexpected lingering output deactivations" } @@ -232,6 +267,11 @@ internal class ScheduledAction<T>( result = just(onStartTransaction(evalScope)) } + fun fail(ex: Exception) { + result = none + onResult?.completeExceptionally(ex) + } + fun completed() { if (onResult != null) { when (val result = result) { @@ -243,4 +283,39 @@ internal class ScheduledAction<T>( } } -internal typealias TransactionStore = HeteroMap +internal class TransactionStore private constructor(private val storage: HeteroMap) { + constructor(capacity: Int) : this(HeteroMap(capacity)) + + constructor() : this(HeteroMap()) + + operator fun <A> get(key: HeteroMap.Key<A>): A = + storage.getOrError(key) { "no value for $key in this transaction" } + + operator fun <A> set(key: HeteroMap.Key<A>, value: A) { + storage[key] = value + } + + fun clear() = storage.clear() +} + +internal class TransactionCache<A> { + private val key = object : HeteroMap.Key<A> {} + @Volatile + var epoch: Long = Long.MIN_VALUE + private set + + fun getOrPut(evalScope: EvalScope, block: () -> A): A = + if (epoch < evalScope.epoch) { + epoch = evalScope.epoch + block().also { evalScope.transactionStore[key] = it } + } else { + evalScope.transactionStore[key] + } + + fun put(evalScope: EvalScope, value: A) { + epoch = evalScope.epoch + evalScope.transactionStore[key] = value + } + + fun getCurrentValue(evalScope: EvalScope): A = evalScope.transactionStore[key] +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NoScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NoScope.kt index fbd9689eb1d0..f662f1907069 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NoScope.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NoScope.kt @@ -16,32 +16,6 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.FrpScope -import kotlin.coroutines.Continuation -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.coroutineContext -import kotlin.coroutines.startCoroutine -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.completeWith -import kotlinx.coroutines.job +import com.android.systemui.kairos.KairosScope -internal object NoScope { - private object FrpScopeImpl : FrpScope - - suspend fun <R> runInFrpScope(block: suspend FrpScope.() -> R): R { - val complete = CompletableDeferred<R>(coroutineContext.job) - block.startCoroutine( - FrpScopeImpl, - object : Continuation<R> { - override val context: CoroutineContext - get() = EmptyCoroutineContext - - override fun resumeWith(result: Result<R>) { - complete.completeWith(result) - } - }, - ) - return complete.await() - } -} +internal object NoScope : KairosScope diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NodeTypes.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NodeTypes.kt index 000240796a82..39b8bfe540d2 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NodeTypes.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NodeTypes.kt @@ -16,47 +16,45 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.util.Maybe - /* Dmux Muxes + Branch */ internal sealed interface SchedulableNode { /** schedule this node w/ given NodeEvalScope */ - suspend fun schedule(evalScope: EvalScope) + fun schedule(logIndent: Int, evalScope: EvalScope) - suspend fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) + fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) - suspend fun moveIndirectUpstreamToDirect( + fun moveIndirectUpstreamToDirect( scheduler: Scheduler, oldIndirectDepth: Int, - oldIndirectSet: Set<MuxDeferredNode<*, *>>, + oldIndirectSet: Set<MuxDeferredNode<*, *, *>>, newDirectDepth: Int, ) - suspend fun adjustIndirectUpstream( + fun adjustIndirectUpstream( scheduler: Scheduler, oldDepth: Int, newDepth: Int, - removals: Set<MuxDeferredNode<*, *>>, - additions: Set<MuxDeferredNode<*, *>>, + removals: Set<MuxDeferredNode<*, *, *>>, + additions: Set<MuxDeferredNode<*, *, *>>, ) - suspend fun moveDirectUpstreamToIndirect( + fun moveDirectUpstreamToIndirect( scheduler: Scheduler, oldDirectDepth: Int, newIndirectDepth: Int, - newIndirectSet: Set<MuxDeferredNode<*, *>>, + newIndirectSet: Set<MuxDeferredNode<*, *, *>>, ) - suspend fun removeIndirectUpstream( + fun removeIndirectUpstream( scheduler: Scheduler, depth: Int, - indirectSet: Set<MuxDeferredNode<*, *>>, + indirectSet: Set<MuxDeferredNode<*, *, *>>, ) - suspend fun removeDirectUpstream(scheduler: Scheduler, depth: Int) + fun removeDirectUpstream(scheduler: Scheduler, depth: Int) } /* @@ -68,7 +66,7 @@ internal sealed interface PullNode<out A> { * will read from the cache, otherwise it will perform a full evaluation, even if invoked * multiple times within a transaction. */ - suspend fun getPushEvent(evalScope: EvalScope): Maybe<A> + fun getPushEvent(logIndent: Int, evalScope: EvalScope): A } /* @@ -76,19 +74,19 @@ Muxes + DmuxBranch */ internal sealed interface PushNode<A> : PullNode<A> { - suspend fun hasCurrentValue(transactionStore: TransactionStore): Boolean + fun hasCurrentValue(logIndent: Int, evalScope: EvalScope): Boolean val depthTracker: DepthTracker - suspend fun removeDownstream(downstream: Schedulable) + fun removeDownstream(downstream: Schedulable) /** called during cleanup phase */ - suspend fun deactivateIfNeeded() + fun deactivateIfNeeded() /** called from mux nodes after severs */ - suspend fun scheduleDeactivationIfNeeded(evalScope: EvalScope) + fun scheduleDeactivationIfNeeded(evalScope: EvalScope) - suspend fun addDownstream(downstream: Schedulable) + fun addDownstream(downstream: Schedulable) - suspend fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) + fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Output.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Output.kt index a3af2d304f7f..38d8cf70b36e 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Output.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Output.kt @@ -16,14 +16,13 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.util.Just import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext internal class Output<A>( val context: CoroutineContext = EmptyCoroutineContext, - val onDeath: suspend () -> Unit = {}, - val onEmit: suspend EvalScope.(A) -> Unit, + val onDeath: () -> Unit = {}, + val onEmit: EvalScope.(A) -> Unit, ) { val schedulable = Schedulable.O(this) @@ -34,26 +33,24 @@ internal class Output<A>( private object NoResult // invoked by network - suspend fun visit(evalScope: EvalScope) { + fun visit(evalScope: EvalScope) { val upstreamResult = result check(upstreamResult !== NoResult) { "output visited with null upstream result" } - result = null + result = NoResult @Suppress("UNCHECKED_CAST") evalScope.onEmit(upstreamResult as A) } - suspend fun kill() { + fun kill() { onDeath() } - suspend fun schedule(evalScope: EvalScope) { - val upstreamResult = - checkNotNull(upstream) { "output scheduled with null upstream" }.getPushEvent(evalScope) - if (upstreamResult is Just) { - result = upstreamResult.value - evalScope.scheduleOutput(this) - } + fun schedule(logIndent: Int, evalScope: EvalScope) { + result = + checkNotNull(upstream) { "output scheduled with null upstream" } + .getPushEvent(logIndent, evalScope) + evalScope.scheduleOutput(this) } } -internal inline fun OneShot(crossinline onEmit: suspend EvalScope.() -> Unit): Output<Unit> = +internal inline fun OneShot(crossinline onEmit: EvalScope.() -> Unit): Output<Unit> = Output<Unit>(onEmit = { onEmit() }).apply { result = Unit } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/PullNodes.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/PullNodes.kt index dac98e0e807c..517e54f57833 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/PullNodes.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/PullNodes.kt @@ -16,24 +16,24 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.internal.util.Key -import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.map -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Deferred +import com.android.systemui.kairos.internal.util.logDuration -internal val neverImpl: TFlowImpl<Nothing> = TFlowCheap { null } +internal val neverImpl: EventsImpl<Nothing> = EventsImplCheap { null } -internal class MapNode<A, B>(val upstream: PullNode<A>, val transform: suspend EvalScope.(A) -> B) : +internal class MapNode<A, B>(val upstream: PullNode<A>, val transform: EvalScope.(A, Int) -> B) : PullNode<B> { - override suspend fun getPushEvent(evalScope: EvalScope): Maybe<B> = - upstream.getPushEvent(evalScope).map { evalScope.transform(it) } + override fun getPushEvent(logIndent: Int, evalScope: EvalScope): B = + logDuration(logIndent, "MapNode.getPushEvent") { + val upstream = + logDuration("upstream event") { upstream.getPushEvent(currentLogIndent, evalScope) } + logDuration("transform") { evalScope.transform(upstream, currentLogIndent) } + } } internal inline fun <A, B> mapImpl( - crossinline upstream: suspend EvalScope.() -> TFlowImpl<A>, - noinline transform: suspend EvalScope.(A) -> B, -): TFlowImpl<B> = TFlowCheap { downstream -> + crossinline upstream: EvalScope.() -> EventsImpl<A>, + noinline transform: EvalScope.(A, Int) -> B, +): EventsImpl<B> = EventsImplCheap { downstream -> upstream().activate(evalScope = this, downstream)?.let { (connection, needsEval) -> ActivationResult( connection = @@ -46,20 +46,29 @@ internal inline fun <A, B> mapImpl( } } -internal class CachedNode<A>(val key: Key<Deferred<Maybe<A>>>, val upstream: PullNode<A>) : - PullNode<A> { - override suspend fun getPushEvent(evalScope: EvalScope): Maybe<A> { - val deferred = - evalScope.transactionStore.getOrPut(key) { - evalScope.deferAsync(CoroutineStart.LAZY) { upstream.getPushEvent(evalScope) } - } - return deferred.await() - } +internal class CachedNode<A>( + private val transactionCache: TransactionCache<Lazy<A>>, + val upstream: PullNode<A>, +) : PullNode<A> { + override fun getPushEvent(logIndent: Int, evalScope: EvalScope): A = + logDuration(logIndent, "CachedNode.getPushEvent") { + val deferred = + logDuration("CachedNode.getOrPut", false) { + transactionCache.getOrPut(evalScope) { + evalScope.deferAsync { + logDuration("CachedNode.getUpstreamEvent") { + upstream.getPushEvent(currentLogIndent, evalScope) + } + } + } + } + logDuration("await") { deferred.value } + } } -internal fun <A> TFlowImpl<A>.cached(): TFlowImpl<A> { - val key = object : Key<Deferred<Maybe<A>>> {} - return TFlowCheap { +internal fun <A> EventsImpl<A>.cached(): EventsImpl<A> { + val key = TransactionCache<Lazy<A>>() + return EventsImplCheap { it -> activate(this, it)?.let { (connection, needsEval) -> ActivationResult( connection = diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Scheduler.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Scheduler.kt index c12ef6ae6a5d..0529bcb63c07 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Scheduler.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Scheduler.kt @@ -14,15 +14,10 @@ * limitations under the License. */ -@file:OptIn(ExperimentalCoroutinesApi::class) - package com.android.systemui.kairos.internal -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.PriorityBlockingQueue -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch +import com.android.systemui.kairos.internal.util.LogIndent +import java.util.PriorityQueue internal interface Scheduler { fun schedule(depth: Int, node: MuxNode<*, *, *>) @@ -30,12 +25,11 @@ internal interface Scheduler { fun scheduleIndirect(indirectDepth: Int, node: MuxNode<*, *, *>) } -internal class SchedulerImpl : Scheduler { - val enqueued = ConcurrentHashMap<MuxNode<*, *, *>, Any>() - val scheduledQ = PriorityBlockingQueue<Pair<Int, MuxNode<*, *, *>>>(16, compareBy { it.first }) +internal class SchedulerImpl(private val enqueue: (MuxNode<*, *, *>) -> Boolean) : Scheduler { + private val scheduledQ = PriorityQueue<Pair<Int, MuxNode<*, *, *>>>(compareBy { it.first }) override fun schedule(depth: Int, node: MuxNode<*, *, *>) { - if (enqueued.putIfAbsent(node, node) == null) { + if (enqueue(node)) { scheduledQ.add(Pair(depth, node)) } } @@ -44,33 +38,52 @@ internal class SchedulerImpl : Scheduler { schedule(Int.MIN_VALUE + indirectDepth, node) } - internal suspend fun drainEval(network: Network) { - drain { runStep -> - runStep { muxNode -> network.evalScope { muxNode.visit(this) } } + internal fun drainEval(logIndent: Int, network: Network): Int = + drain(logIndent) { runStep -> + runStep { muxNode -> + network.evalScope { + muxNode.markedForEvaluation = false + muxNode.visit(currentLogIndent, this) + } + } // If any visited MuxPromptNodes had their depths increased, eagerly propagate those // depth changes now before performing further network evaluation. - network.compactor.drainCompact() + val numNodes = network.compactor.drainCompact(currentLogIndent) + logLn("promptly compacted $numNodes nodes") } - } - internal suspend fun drainCompact() { - drain { runStep -> runStep { muxNode -> muxNode.visitCompact(scheduler = this) } } - } + internal fun drainCompact(logIndent: Int): Int = + drain(logIndent) { runStep -> + runStep { muxNode -> + muxNode.markedForCompaction = false + muxNode.visitCompact(scheduler = this@SchedulerImpl) + } + } - private suspend inline fun drain( + private inline fun drain( + logIndent: Int, crossinline onStep: - suspend (runStep: suspend (visit: suspend (MuxNode<*, *, *>) -> Unit) -> Unit) -> Unit - ): Unit = coroutineScope { + LogIndent.( + runStep: LogIndent.(visit: LogIndent.(MuxNode<*, *, *>) -> Unit) -> Unit + ) -> Unit, + ): Int { + var total = 0 while (scheduledQ.isNotEmpty()) { val maxDepth = scheduledQ.peek()?.first ?: error("Unexpected empty scheduler") - onStep { visit -> runStep(maxDepth, visit) } + LogIndent(logIndent).onStep { visit -> + logDuration("step $maxDepth") { + val subtotal = runStep(maxDepth) { visit(it) } + logLn("visited $subtotal nodes") + total += subtotal + } + } } + return total } - private suspend inline fun runStep( - maxDepth: Int, - crossinline visit: suspend (MuxNode<*, *, *>) -> Unit, - ) = coroutineScope { + private inline fun runStep(maxDepth: Int, crossinline visit: (MuxNode<*, *, *>) -> Unit): Int { + var total = 0 + val toVisit = mutableListOf<MuxNode<*, *, *>>() while (scheduledQ.peek()?.first?.let { it <= maxDepth } == true) { val (d, node) = scheduledQ.remove() if ( @@ -79,11 +92,15 @@ internal class SchedulerImpl : Scheduler { ) { scheduledQ.add(node.depthTracker.dirty_directDepth to node) } else { - launch { - enqueued.remove(node) - visit(node) - } + total++ + toVisit.add(node) } } + + for (node in toVisit) { + visit(node) + } + + return total } } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt new file mode 100644 index 000000000000..46127cb2276b --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt @@ -0,0 +1,467 @@ +/* + * Copyright (C) 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 com.android.systemui.kairos.internal + +import com.android.systemui.kairos.internal.store.ConcurrentHashMapK +import com.android.systemui.kairos.internal.store.MutableArrayMapK +import com.android.systemui.kairos.internal.store.MutableMapK +import com.android.systemui.kairos.internal.store.StoreEntry +import com.android.systemui.kairos.internal.util.hashString +import com.android.systemui.kairos.util.Maybe +import com.android.systemui.kairos.util.just +import com.android.systemui.kairos.util.none + +internal open class StateImpl<out A>( + val name: String?, + val operatorName: String, + val changes: EventsImpl<A>, + val store: StateStore<A>, +) { + fun getCurrentWithEpoch(evalScope: EvalScope): Pair<A, Long> = + store.getCurrentWithEpoch(evalScope) +} + +internal sealed class StateDerived<A> : StateStore<A>() { + + @Volatile + var invalidatedEpoch = Long.MIN_VALUE + private set + + @Volatile + protected var validatedEpoch = Long.MIN_VALUE + private set + + @Volatile + protected var cache: Any? = EmptyCache + private set + + private val transactionCache = TransactionCache<Lazy<Pair<A, Long>>>() + + override fun getCurrentWithEpoch(evalScope: EvalScope): Pair<A, Long> = + transactionCache.getOrPut(evalScope) { evalScope.deferAsync { pull(evalScope) } }.value + + fun pull(evalScope: EvalScope): Pair<A, Long> { + @Suppress("UNCHECKED_CAST") + val result = + recalc(evalScope)?.let { (newValue, epoch) -> + newValue.also { + if (epoch > validatedEpoch) { + validatedEpoch = epoch + if (cache != newValue) { + cache = newValue + invalidatedEpoch = epoch + } + } + } + } ?: (cache as A) + return result to invalidatedEpoch + } + + fun getCachedUnsafe(): Maybe<A> { + @Suppress("UNCHECKED_CAST") + return if (cache == EmptyCache) none else just(cache as A) + } + + protected abstract fun recalc(evalScope: EvalScope): Pair<A, Long>? + + fun setCacheFromPush(value: A, epoch: Long) { + cache = value + validatedEpoch = epoch + 1 + invalidatedEpoch = epoch + 1 + } + + private data object EmptyCache +} + +internal sealed class StateStore<out S> { + abstract fun getCurrentWithEpoch(evalScope: EvalScope): Pair<S, Long> +} + +internal class StateSource<S>(init: Lazy<S>) : StateStore<S>() { + constructor(init: S) : this(CompletableLazy(init)) + + lateinit var upstreamConnection: NodeConnection<S> + + // Note: Don't need to synchronize; we will never interleave reads and writes, since all writes + // are performed at the end of a network step, after any reads would have taken place. + + @Volatile private var _current: Lazy<S> = init + + @Volatile + var writeEpoch = 0L + private set + + override fun getCurrentWithEpoch(evalScope: EvalScope): Pair<S, Long> = + _current.value to writeEpoch + + /** called by network after eval phase has completed */ + fun updateState(logIndent: Int, evalScope: EvalScope) { + // write the latch + _current = CompletableLazy(upstreamConnection.getPushEvent(logIndent, evalScope)) + writeEpoch = evalScope.epoch + 1 + } + + override fun toString(): String = "StateImpl(current=$_current, writeEpoch=$writeEpoch)" + + fun getStorageUnsafe(): Maybe<S> = if (_current.isInitialized()) just(_current.value) else none +} + +internal fun <A> constState(name: String?, operatorName: String, init: A): StateImpl<A> = + StateImpl(name, operatorName, neverImpl, StateSource(init)) + +internal inline fun <A> activatedStateSource( + name: String?, + operatorName: String, + evalScope: EvalScope, + crossinline getChanges: EvalScope.() -> EventsImpl<A>, + init: Lazy<A>, +): StateImpl<A> { + val store = StateSource(init) + val calm: EventsImpl<A> = + filterImpl(getChanges) { new -> new != store.getCurrentWithEpoch(evalScope = this).first } + evalScope.scheduleOutput( + OneShot { + calm.activate(evalScope = this, downstream = Schedulable.S(store))?.let { + (connection, needsEval) -> + store.upstreamConnection = connection + if (needsEval) { + schedule(store) + } + } + } + ) + return StateImpl(name, operatorName, calm, store) +} + +private inline fun <A> EventsImpl<A>.calm(state: StateDerived<A>): EventsImpl<A> = + filterImpl({ this@calm }) { new -> + val (current, _) = state.getCurrentWithEpoch(evalScope = this) + if (new != current) { + state.setCacheFromPush(new, epoch) + true + } else { + false + } + } + .cached() + +internal fun <A, B> mapStateImplCheap( + stateImpl: Init<StateImpl<A>>, + name: String?, + operatorName: String, + transform: EvalScope.(A) -> B, +): StateImpl<B> = + StateImpl( + name = name, + operatorName = operatorName, + changes = mapImpl({ stateImpl.connect(this).changes }) { it, _ -> transform(it) }, + store = DerivedMapCheap(stateImpl, transform), + ) + +internal class DerivedMapCheap<A, B>( + val upstream: Init<StateImpl<A>>, + private val transform: EvalScope.(A) -> B, +) : StateStore<B>() { + + override fun getCurrentWithEpoch(evalScope: EvalScope): Pair<B, Long> { + val (a, epoch) = upstream.connect(evalScope).getCurrentWithEpoch(evalScope) + return evalScope.transform(a) to epoch + } + + override fun toString(): String = "${this::class.simpleName}@$hashString" +} + +internal fun <A, B> mapStateImpl( + stateImpl: InitScope.() -> StateImpl<A>, + name: String?, + operatorName: String, + transform: EvalScope.(A) -> B, +): StateImpl<B> { + val store = DerivedMap(stateImpl, transform) + val mappedChanges = + mapImpl({ stateImpl().changes }) { it, _ -> transform(it) }.cached().calm(store) + return StateImpl(name, operatorName, mappedChanges, store) +} + +internal class DerivedMap<A, B>( + val upstream: InitScope.() -> StateImpl<A>, + private val transform: EvalScope.(A) -> B, +) : StateDerived<B>() { + override fun toString(): String = "${this::class.simpleName}@$hashString" + + override fun recalc(evalScope: EvalScope): Pair<B, Long>? { + val (a, epoch) = evalScope.upstream().getCurrentWithEpoch(evalScope) + return if (epoch > validatedEpoch) { + evalScope.transform(a) to epoch + } else { + null + } + } +} + +internal fun <A> flattenStateImpl( + stateImpl: InitScope.() -> StateImpl<StateImpl<A>>, + name: String?, + operator: String, +): StateImpl<A> { + // emits the current value of the new inner state, when that state is emitted + val switchEvents = + mapImpl({ stateImpl().changes }) { newInner, _ -> newInner.getCurrentWithEpoch(this).first } + // emits the new value of the new inner state when that state is emitted, or + // falls back to the current value if a new state is *not* being emitted this + // transaction + val innerChanges = + mapImpl({ stateImpl().changes }) { newInner, _ -> + mergeNodes({ switchEvents }, { newInner.changes }) { _, new -> new } + } + val switchedChanges: EventsImpl<A> = + switchPromptImplSingle( + getStorage = { stateImpl().getCurrentWithEpoch(evalScope = this).first.changes }, + getPatches = { innerChanges }, + ) + val store: DerivedFlatten<A> = DerivedFlatten(stateImpl) + return StateImpl(name, operator, switchedChanges.calm(store), store) +} + +internal class DerivedFlatten<A>(val upstream: InitScope.() -> StateImpl<StateImpl<A>>) : + StateDerived<A>() { + override fun recalc(evalScope: EvalScope): Pair<A, Long> { + val (inner, epoch0) = evalScope.upstream().getCurrentWithEpoch(evalScope) + val (a, epoch1) = inner.getCurrentWithEpoch(evalScope) + return a to maxOf(epoch0, epoch1) + } + + override fun toString(): String = "${this::class.simpleName}@$hashString" +} + +@Suppress("NOTHING_TO_INLINE") +internal inline fun <A, B> flatMapStateImpl( + noinline stateImpl: InitScope.() -> StateImpl<A>, + name: String?, + operatorName: String, + noinline transform: EvalScope.(A) -> StateImpl<B>, +): StateImpl<B> { + val mapped = mapStateImpl(stateImpl, null, operatorName, transform) + return flattenStateImpl({ mapped }, name, operatorName) +} + +internal fun <A, B, Z> zipStates( + name: String?, + operatorName: String, + l1: Init<StateImpl<A>>, + l2: Init<StateImpl<B>>, + transform: EvalScope.(A, B) -> Z, +): StateImpl<Z> { + val zipped = + zipStateList( + null, + operatorName, + 2, + init(null) { listOf(l1.connect(this), l2.connect(this)) }, + ) + return mapStateImpl({ zipped }, name, operatorName) { + @Suppress("UNCHECKED_CAST") transform(it[0] as A, it[1] as B) + } +} + +internal fun <A, B, C, Z> zipStates( + name: String?, + operatorName: String, + l1: Init<StateImpl<A>>, + l2: Init<StateImpl<B>>, + l3: Init<StateImpl<C>>, + transform: EvalScope.(A, B, C) -> Z, +): StateImpl<Z> { + val zipped = + zipStateList( + null, + operatorName, + 3, + init(null) { listOf(l1.connect(this), l2.connect(this), l3.connect(this)) }, + ) + return mapStateImpl({ zipped }, name, operatorName) { + @Suppress("UNCHECKED_CAST") transform(it[0] as A, it[1] as B, it[2] as C) + } +} + +internal fun <A, B, C, D, Z> zipStates( + name: String?, + operatorName: String, + l1: Init<StateImpl<A>>, + l2: Init<StateImpl<B>>, + l3: Init<StateImpl<C>>, + l4: Init<StateImpl<D>>, + transform: EvalScope.(A, B, C, D) -> Z, +): StateImpl<Z> { + val zipped = + zipStateList( + null, + operatorName, + 4, + init(null) { + listOf(l1.connect(this), l2.connect(this), l3.connect(this), l4.connect(this)) + }, + ) + return mapStateImpl({ zipped }, name, operatorName) { + @Suppress("UNCHECKED_CAST") transform(it[0] as A, it[1] as B, it[2] as C, it[3] as D) + } +} + +internal fun <A, B, C, D, E, Z> zipStates( + name: String?, + operatorName: String, + l1: Init<StateImpl<A>>, + l2: Init<StateImpl<B>>, + l3: Init<StateImpl<C>>, + l4: Init<StateImpl<D>>, + l5: Init<StateImpl<E>>, + transform: EvalScope.(A, B, C, D, E) -> Z, +): StateImpl<Z> { + val zipped = + zipStateList( + null, + operatorName, + 5, + init(null) { + listOf( + l1.connect(this), + l2.connect(this), + l3.connect(this), + l4.connect(this), + l5.connect(this), + ) + }, + ) + return mapStateImpl({ zipped }, name, operatorName) { + @Suppress("UNCHECKED_CAST") + transform(it[0] as A, it[1] as B, it[2] as C, it[3] as D, it[4] as E) + } +} + +internal fun <K, V> zipStateMap( + name: String?, + operatorName: String, + numStates: Int, + states: Init<Map<K, StateImpl<V>>>, +): StateImpl<Map<K, V>> = + zipStates( + name = name, + operatorName = operatorName, + numStates = numStates, + states = init(null) { states.connect(this).asIterable() }, + storeFactory = ConcurrentHashMapK.Factory(), + ) + +internal fun <V> zipStateList( + name: String?, + operatorName: String, + numStates: Int, + states: Init<List<StateImpl<V>>>, +): StateImpl<List<V>> { + val zipped = + zipStates( + name = name, + operatorName = operatorName, + numStates = numStates, + states = init(name) { states.connect(this).asIterableWithIndex() }, + storeFactory = MutableArrayMapK.Factory(), + ) + // Like mapCheap, but with caching (or like map, but without the calm changes, as they are not + // necessary). + return StateImpl( + name = name, + operatorName = operatorName, + changes = mapImpl({ zipped.changes }) { arrayStore, _ -> arrayStore.values.toList() }, + DerivedMap(upstream = { zipped }, transform = { arrayStore -> arrayStore.values.toList() }), + ) +} + +internal fun <W, K, A> zipStates( + name: String?, + operatorName: String, + numStates: Int, + states: Init<Iterable<Map.Entry<K, StateImpl<A>>>>, + storeFactory: MutableMapK.Factory<W, K>, +): StateImpl<MutableMapK<W, K, A>> { + if (numStates == 0) { + return constState(name, operatorName, storeFactory.create(0)) + } + val stateStore = DerivedZipped(numStates, states, storeFactory) + // No need for calm; invariant ensures that changes will only emit when there's a difference + val switchDeferredImpl = + switchDeferredImpl( + getStorage = { + states + .connect(this) + .asSequence() + .map { (k, v) -> StoreEntry(k, v.changes) } + .asIterable() + }, + getPatches = { neverImpl }, + storeFactory = storeFactory, + ) + val changes = + mapImpl({ switchDeferredImpl }) { patch, logIndent -> + val muxStore = storeFactory.create<A>(numStates) + states.connect(this).forEach { (k, state) -> + muxStore[k] = + if (patch.contains(k)) { + patch.getValue(k).getPushEvent(logIndent, evalScope = this@mapImpl) + } else { + state.getCurrentWithEpoch(evalScope = this@mapImpl).first + } + } + // Read the current value so that it is cached in this transaction and won't be + // clobbered by the cache write + stateStore.getCurrentWithEpoch(evalScope = this) + muxStore.also { stateStore.setCacheFromPush(it, epoch) } + } + .cached() + return StateImpl(name, operatorName, changes, stateStore) +} + +internal class DerivedZipped<W, K, A>( + private val upstreamSize: Int, + val upstream: Init<Iterable<Map.Entry<K, StateImpl<A>>>>, + private val storeFactory: MutableMapK.Factory<W, K>, +) : StateDerived<MutableMapK<W, K, A>>() { + override fun recalc(evalScope: EvalScope): Pair<MutableMapK<W, K, A>, Long> { + var newEpoch = 0L + val store = storeFactory.create<A>(upstreamSize) + for ((key, value) in upstream.connect(evalScope)) { + val (a, epoch) = value.getCurrentWithEpoch(evalScope) + newEpoch = maxOf(newEpoch, epoch) + store[key] = a + } + return store to newEpoch + } + + override fun toString(): String = "${this::class.simpleName}@$hashString" +} + +@Suppress("NOTHING_TO_INLINE") +internal inline fun <A> zipStates( + name: String?, + operatorName: String, + numStates: Int, + states: Init<List<StateImpl<A>>>, +): StateImpl<List<A>> = + if (numStates <= 0) { + constState(name, operatorName, emptyList()) + } else { + zipStateList(null, operatorName, numStates, states) + } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt index baf4101d52ef..bd1f94fca22f 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt @@ -16,241 +16,152 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.FrpDeferredValue -import com.android.systemui.kairos.FrpStateScope -import com.android.systemui.kairos.FrpStateful -import com.android.systemui.kairos.FrpTransactionScope -import com.android.systemui.kairos.GroupedTFlow -import com.android.systemui.kairos.TFlow -import com.android.systemui.kairos.TFlowInit -import com.android.systemui.kairos.TFlowLoop -import com.android.systemui.kairos.TState -import com.android.systemui.kairos.TStateInit -import com.android.systemui.kairos.emptyTFlow +import com.android.systemui.kairos.DeferredValue +import com.android.systemui.kairos.Events +import com.android.systemui.kairos.EventsInit +import com.android.systemui.kairos.EventsLoop +import com.android.systemui.kairos.GroupedEvents +import com.android.systemui.kairos.Incremental +import com.android.systemui.kairos.IncrementalInit +import com.android.systemui.kairos.State +import com.android.systemui.kairos.StateInit +import com.android.systemui.kairos.StateScope +import com.android.systemui.kairos.Stateful +import com.android.systemui.kairos.emptyEvents import com.android.systemui.kairos.groupByKey import com.android.systemui.kairos.init -import com.android.systemui.kairos.internal.util.mapValuesParallel import com.android.systemui.kairos.mapCheap import com.android.systemui.kairos.merge -import com.android.systemui.kairos.switch +import com.android.systemui.kairos.switchEvents import com.android.systemui.kairos.util.Maybe import com.android.systemui.kairos.util.map -import kotlin.coroutines.Continuation -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.startCoroutine -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.completeWith -import kotlinx.coroutines.job -internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal: TFlow<Any>) : - StateScope, EvalScope by evalScope { +internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal: Events<Any>) : + InternalStateScope, EvalScope by evalScope { - private val endSignalOnce: TFlow<Any> = endSignal.nextOnlyInternal("StateScope.endSignal") + override val endSignalOnce: Events<Any> = endSignal.nextOnlyInternal("StateScope.endSignal") - private fun <A> TFlow<A>.truncateToScope(operatorName: String): TFlow<A> = - if (endSignalOnce === emptyTFlow) { - this - } else { - endSignalOnce.mapCheap { emptyTFlow }.toTStateInternal(operatorName, this).switch() - } + override fun <A> deferredStateScope(block: StateScope.() -> A): DeferredValue<A> = + DeferredValue(deferAsync { block() }) - private fun <A> TFlow<A>.nextOnlyInternal(operatorName: String): TFlow<A> = - if (this === emptyTFlow) { - this - } else { - TFlowLoop<A>().apply { - loopback = - mapCheap { emptyTFlow } - .toTStateInternal(operatorName, this@nextOnlyInternal) - .switch() - } - } - - private fun <A> TFlow<A>.toTStateInternal(operatorName: String, init: A): TState<A> = - toTStateInternalDeferred(operatorName, CompletableDeferred(init)) - - private fun <A> TFlow<A>.toTStateInternalDeferred( - operatorName: String, - init: Deferred<A>, - ): TState<A> { - val changes = this@toTStateInternalDeferred - val name = operatorName - val impl = - mkState(name, operatorName, evalScope, { changes.init.connect(evalScope = this) }, init) - return TStateInit(constInit(name, impl)) - } - - private fun <R> deferredInternal(block: suspend FrpStateScope.() -> R): FrpDeferredValue<R> = - FrpDeferredValue(deferAsync { runInStateScope(block) }) - - private fun <A> TFlow<A>.toTStateDeferredInternal( - initialValue: FrpDeferredValue<A> - ): TState<A> { - val operatorName = "toTStateDeferred" + override fun <A> Events<A>.holdStateDeferred(initialValue: DeferredValue<A>): State<A> { + val operatorName = "holdStateDeferred" // Ensure state is only collected until the end of this scope return truncateToScope(operatorName) - .toTStateInternalDeferred(operatorName, initialValue.unwrapped) + .holdStateInternalDeferred(operatorName, initialValue.unwrapped) } - private fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyInternal( - storage: TState<Map<K, TFlow<V>>> - ): TFlow<Map<K, V>> { - val name = "mergeIncrementally" - return TFlowInit( + override fun <K, V> Events<Map<K, Maybe<V>>>.foldStateMapIncrementally( + initialValues: DeferredValue<Map<K, V>> + ): Incremental<K, V> { + val operatorName = "foldStateMapIncrementally" + val name = operatorName + return IncrementalInit( constInit( - name, - switchDeferredImpl( - getStorage = { - storage.init - .connect(this) - .getCurrentWithEpoch(this) - .first - .mapValuesParallel { (_, flow) -> flow.init.connect(this) } - }, - getPatches = { - mapImpl({ init.connect(this) }) { patch -> - patch.mapValuesParallel { (_, m) -> - m.map { flow -> flow.init.connect(this) } - } - } - }, + operatorName, + activatedIncremental( + name, + operatorName, + evalScope, + { init.connect(this) }, + initialValues.unwrapped, ), ) ) } - private fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPromptInternal( - storage: TState<Map<K, TFlow<V>>> - ): TFlow<Map<K, V>> { - val name = "mergeIncrementallyPrompt" - return TFlowInit( - constInit( - name, - switchPromptImpl( - getStorage = { - storage.init - .connect(this) - .getCurrentWithEpoch(this) - .first - .mapValuesParallel { (_, flow) -> flow.init.connect(this) } - }, - getPatches = { - mapImpl({ init.connect(this) }) { patch -> - patch.mapValuesParallel { (_, m) -> - m.map { flow -> flow.init.connect(this) } - } - } - }, - ), - ) - ) - } - - private fun <K, A, B> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKeyInternal( - init: FrpDeferredValue<Map<K, FrpStateful<B>>>, + override fun <K, A, B> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey( + init: DeferredValue<Map<K, Stateful<B>>>, numKeys: Int?, - ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> { - val eventsByKey: GroupedTFlow<K, Maybe<FrpStateful<A>>> = groupByKey(numKeys) - val initOut: Deferred<Map<K, B>> = deferAsync { - init.unwrapped.await().mapValuesParallel { (k, stateful) -> - val newEnd = with(frpScope) { eventsByKey[k].skipNext() } + ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> { + val eventsByKey: GroupedEvents<K, Maybe<Stateful<A>>> = groupByKey(numKeys) + val initOut: Lazy<Map<K, B>> = deferAsync { + init.unwrapped.value.mapValues { (k, stateful) -> + val newEnd = eventsByKey[k] val newScope = childStateScope(newEnd) - newScope.runInStateScope(stateful) + newScope.stateful() } } - val changesNode: TFlowImpl<Map<K, Maybe<A>>> = - mapImpl( - upstream = { this@applyLatestStatefulForKeyInternal.init.connect(evalScope = this) } - ) { upstreamMap -> - upstreamMap.mapValuesParallel { (k: K, ma: Maybe<FrpStateful<A>>) -> + val changesNode: EventsImpl<Map<K, Maybe<A>>> = + mapImpl(upstream = { this@applyLatestStatefulForKey.init.connect(evalScope = this) }) { + upstreamMap, + _ -> + upstreamMap.mapValues { (k: K, ma: Maybe<Stateful<A>>) -> reenterStateScope(this@StateScopeImpl).run { ma.map { stateful -> - val newEnd = with(frpScope) { eventsByKey[k].skipNext() } + val newEnd = eventsByKey[k].skipNext() val newScope = childStateScope(newEnd) - newScope.runInStateScope(stateful) + newScope.stateful() } } } } val operatorName = "applyLatestStatefulForKey" val name = operatorName - val changes: TFlow<Map<K, Maybe<A>>> = TFlowInit(constInit(name, changesNode.cached())) - return changes to FrpDeferredValue(initOut) + val changes: Events<Map<K, Maybe<A>>> = EventsInit(constInit(name, changesNode.cached())) + return changes to DeferredValue(initOut) } - private fun <A> TFlow<FrpStateful<A>>.observeStatefulsInternal(): TFlow<A> { - val operatorName = "observeStatefuls" + override fun <A> Events<Stateful<A>>.applyStatefuls(): Events<A> { + val operatorName = "applyStatefuls" val name = operatorName - return TFlowInit( + return EventsInit( constInit( name, - mapImpl( - upstream = { this@observeStatefulsInternal.init.connect(evalScope = this) } - ) { stateful -> - reenterStateScope(outerScope = this@StateScopeImpl) - .runInStateScope(stateful) + mapImpl(upstream = { this@applyStatefuls.init.connect(evalScope = this) }) { + stateful, + _ -> + reenterStateScope(outerScope = this@StateScopeImpl).stateful() } .cached(), ) ) } - override val frpScope: FrpStateScope = FrpStateScopeImpl() - - private inner class FrpStateScopeImpl : - FrpStateScope, FrpTransactionScope by evalScope.frpScope { - - override fun <A> deferredStateScope( - block: suspend FrpStateScope.() -> A - ): FrpDeferredValue<A> = deferredInternal(block) - - override fun <A> TFlow<A>.holdDeferred(initialValue: FrpDeferredValue<A>): TState<A> = - toTStateDeferredInternal(initialValue) + override fun childStateScope(newEnd: Events<Any>) = + StateScopeImpl(evalScope, merge(newEnd, endSignal)) - override fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally( - initialTFlows: FrpDeferredValue<Map<K, TFlow<V>>> - ): TFlow<Map<K, V>> { - val storage: TState<Map<K, TFlow<V>>> = foldMapIncrementally(initialTFlows) - return mergeIncrementallyInternal(storage) + private fun <A> Events<A>.truncateToScope(operatorName: String): Events<A> = + if (endSignalOnce === emptyEvents) { + this + } else { + endSignalOnce + .mapCheap { emptyEvents } + .holdStateInternal(operatorName, this) + .switchEvents() } - override fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPromptly( - initialTFlows: FrpDeferredValue<Map<K, TFlow<V>>> - ): TFlow<Map<K, V>> { - val storage: TState<Map<K, TFlow<V>>> = foldMapIncrementally(initialTFlows) - return mergeIncrementallyPromptInternal(storage) + private fun <A> Events<A>.nextOnlyInternal(operatorName: String): Events<A> = + if (this === emptyEvents) { + this + } else { + EventsLoop<A>().apply { + loopback = + mapCheap { emptyEvents } + .holdStateInternal(operatorName, this@nextOnlyInternal) + .switchEvents() + } } - override fun <K, A, B> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKey( - init: FrpDeferredValue<Map<K, FrpStateful<B>>>, - numKeys: Int?, - ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> = - applyLatestStatefulForKeyInternal(init, numKeys) + private fun <A> Events<A>.holdStateInternal(operatorName: String, init: A): State<A> = + holdStateInternalDeferred(operatorName, CompletableLazy(init)) - override fun <A> TFlow<FrpStateful<A>>.applyStatefuls(): TFlow<A> = - observeStatefulsInternal() - } - - override suspend fun <R> runInStateScope(block: suspend FrpStateScope.() -> R): R { - val complete = CompletableDeferred<R>(parent = coroutineContext.job) - block.startCoroutine( - frpScope, - object : Continuation<R> { - override val context: CoroutineContext - get() = EmptyCoroutineContext - - override fun resumeWith(result: Result<R>) { - complete.completeWith(result) - } - }, - ) - return complete.await() + private fun <A> Events<A>.holdStateInternalDeferred( + operatorName: String, + init: Lazy<A>, + ): State<A> { + val changes = this@holdStateInternalDeferred + val name = operatorName + val impl = + activatedStateSource( + name, + operatorName, + evalScope, + { changes.init.connect(evalScope = this) }, + init, + ) + return StateInit(constInit(name, impl)) } - - override fun childStateScope(newEnd: TFlow<Any>) = - StateScopeImpl(evalScope, merge(newEnd, endSignal)) } private fun EvalScope.reenterStateScope(outerScope: StateScopeImpl) = diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TStateImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TStateImpl.kt deleted file mode 100644 index c68b4c366776..000000000000 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TStateImpl.kt +++ /dev/null @@ -1,399 +0,0 @@ -/* - * Copyright (C) 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 com.android.systemui.kairos.internal - -import com.android.systemui.kairos.internal.util.Key -import com.android.systemui.kairos.internal.util.associateByIndex -import com.android.systemui.kairos.internal.util.hashString -import com.android.systemui.kairos.internal.util.mapValuesParallel -import com.android.systemui.kairos.util.Just -import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.just -import com.android.systemui.kairos.util.none -import java.util.concurrent.atomic.AtomicLong -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.ExperimentalCoroutinesApi - -internal sealed interface TStateImpl<out A> { - val name: String? - val operatorName: String - val changes: TFlowImpl<A> - - suspend fun getCurrentWithEpoch(evalScope: EvalScope): Pair<A, Long> -} - -internal sealed class TStateDerived<A>(override val changes: TFlowImpl<A>) : - TStateImpl<A>, Key<Deferred<Pair<A, Long>>> { - - @Volatile - var invalidatedEpoch = Long.MIN_VALUE - private set - - @Volatile - protected var cache: Any? = EmptyCache - private set - - override suspend fun getCurrentWithEpoch(evalScope: EvalScope): Pair<A, Long> = - evalScope.transactionStore - .getOrPut(this) { evalScope.deferAsync(CoroutineStart.LAZY) { pull(evalScope) } } - .await() - - suspend fun pull(evalScope: EvalScope): Pair<A, Long> { - @Suppress("UNCHECKED_CAST") - return recalc(evalScope)?.also { (a, epoch) -> setCache(a, epoch) } - ?: ((cache as A) to invalidatedEpoch) - } - - fun setCache(value: A, epoch: Long) { - if (epoch > invalidatedEpoch) { - cache = value - invalidatedEpoch = epoch - } - } - - fun getCachedUnsafe(): Maybe<A> { - @Suppress("UNCHECKED_CAST") - return if (cache == EmptyCache) none else just(cache as A) - } - - protected abstract suspend fun recalc(evalScope: EvalScope): Pair<A, Long>? - - private data object EmptyCache -} - -internal class TStateSource<A>( - override val name: String?, - override val operatorName: String, - init: Deferred<A>, - override val changes: TFlowImpl<A>, -) : TStateImpl<A> { - constructor( - name: String?, - operatorName: String, - init: A, - changes: TFlowImpl<A>, - ) : this(name, operatorName, CompletableDeferred(init), changes) - - lateinit var upstreamConnection: NodeConnection<A> - - // Note: Don't need to synchronize; we will never interleave reads and writes, since all writes - // are performed at the end of a network step, after any reads would have taken place. - - @Volatile private var _current: Deferred<A> = init - @Volatile - var writeEpoch = 0L - private set - - override suspend fun getCurrentWithEpoch(evalScope: EvalScope): Pair<A, Long> = - _current.await() to writeEpoch - - /** called by network after eval phase has completed */ - suspend fun updateState(evalScope: EvalScope) { - // write the latch - val eventResult = upstreamConnection.getPushEvent(evalScope) - if (eventResult is Just) { - _current = CompletableDeferred(eventResult.value) - writeEpoch = evalScope.epoch - } - } - - override fun toString(): String = "TStateImpl(changes=$changes, current=$_current)" - - @OptIn(ExperimentalCoroutinesApi::class) - fun getStorageUnsafe(): Maybe<A> = - if (_current.isCompleted) just(_current.getCompleted()) else none -} - -internal fun <A> constS(name: String?, operatorName: String, init: A): TStateImpl<A> = - TStateSource(name, operatorName, init, neverImpl) - -internal inline fun <A> mkState( - name: String?, - operatorName: String, - evalScope: EvalScope, - crossinline getChanges: suspend EvalScope.() -> TFlowImpl<A>, - init: Deferred<A>, -): TStateImpl<A> { - lateinit var state: TStateSource<A> - val calm: TFlowImpl<A> = - filterNode(getChanges) { new -> new != state.getCurrentWithEpoch(evalScope = this).first } - .cached() - return TStateSource(name, operatorName, init, calm).also { - state = it - evalScope.scheduleOutput( - OneShot { - calm.activate(evalScope = this, downstream = Schedulable.S(state))?.let { - (connection, needsEval) -> - state.upstreamConnection = connection - if (needsEval) { - schedule(state) - } - } - } - ) - } -} - -private inline fun <A> TFlowImpl<A>.calm( - crossinline getState: () -> TStateDerived<A> -): TFlowImpl<A> = - filterNode({ this@calm }) { new -> - val state = getState() - val (current, _) = state.getCurrentWithEpoch(evalScope = this) - if (new != current) { - state.setCache(new, epoch) - true - } else { - false - } - } - .cached() - -internal fun <A, B> TStateImpl<A>.mapCheap( - name: String?, - operatorName: String, - transform: suspend EvalScope.(A) -> B, -): TStateImpl<B> = - DerivedMapCheap(name, operatorName, this, mapImpl({ changes }) { transform(it) }, transform) - -internal class DerivedMapCheap<A, B>( - override val name: String?, - override val operatorName: String, - val upstream: TStateImpl<A>, - override val changes: TFlowImpl<B>, - private val transform: suspend EvalScope.(A) -> B, -) : TStateImpl<B> { - - override suspend fun getCurrentWithEpoch(evalScope: EvalScope): Pair<B, Long> { - val (a, epoch) = upstream.getCurrentWithEpoch(evalScope) - return evalScope.transform(a) to epoch - } - - override fun toString(): String = "${this::class.simpleName}@$hashString" -} - -internal fun <A, B> TStateImpl<A>.map( - name: String?, - operatorName: String, - transform: suspend EvalScope.(A) -> B, -): TStateImpl<B> { - lateinit var state: TStateDerived<B> - val mappedChanges = mapImpl({ changes }) { transform(it) }.cached().calm { state } - state = DerivedMap(name, operatorName, transform, this, mappedChanges) - return state -} - -internal class DerivedMap<A, B>( - override val name: String?, - override val operatorName: String, - private val transform: suspend EvalScope.(A) -> B, - val upstream: TStateImpl<A>, - changes: TFlowImpl<B>, -) : TStateDerived<B>(changes) { - override fun toString(): String = "${this::class.simpleName}@$hashString" - - override suspend fun recalc(evalScope: EvalScope): Pair<B, Long>? { - val (a, epoch) = upstream.getCurrentWithEpoch(evalScope) - return if (epoch > invalidatedEpoch) { - evalScope.transform(a) to epoch - } else { - null - } - } -} - -internal fun <A> TStateImpl<TStateImpl<A>>.flatten(name: String?, operator: String): TStateImpl<A> { - // emits the current value of the new inner state, when that state is emitted - val switchEvents = mapImpl({ changes }) { newInner -> newInner.getCurrentWithEpoch(this).first } - // emits the new value of the new inner state when that state is emitted, or - // falls back to the current value if a new state is *not* being emitted this - // transaction - val innerChanges = - mapImpl({ changes }) { newInner -> - mergeNodes({ switchEvents }, { newInner.changes }) { _, new -> new } - } - val switchedChanges: TFlowImpl<A> = - mapImpl({ - switchPromptImpl( - getStorage = { - mapOf(Unit to this@flatten.getCurrentWithEpoch(evalScope = this).first.changes) - }, - getPatches = { mapImpl({ innerChanges }) { new -> mapOf(Unit to just(new)) } }, - ) - }) { map -> - map.getValue(Unit) - } - lateinit var state: DerivedFlatten<A> - state = DerivedFlatten(name, operator, this, switchedChanges.calm { state }) - return state -} - -internal class DerivedFlatten<A>( - override val name: String?, - override val operatorName: String, - val upstream: TStateImpl<TStateImpl<A>>, - changes: TFlowImpl<A>, -) : TStateDerived<A>(changes) { - override suspend fun recalc(evalScope: EvalScope): Pair<A, Long> { - val (inner, epoch0) = upstream.getCurrentWithEpoch(evalScope) - val (a, epoch1) = inner.getCurrentWithEpoch(evalScope) - return a to maxOf(epoch0, epoch1) - } - - override fun toString(): String = "${this::class.simpleName}@$hashString" -} - -@Suppress("NOTHING_TO_INLINE") -internal inline fun <A, B> TStateImpl<A>.flatMap( - name: String?, - operatorName: String, - noinline transform: suspend EvalScope.(A) -> TStateImpl<B>, -): TStateImpl<B> = map(null, operatorName, transform).flatten(name, operatorName) - -internal fun <A, B, Z> zipStates( - name: String?, - operatorName: String, - l1: TStateImpl<A>, - l2: TStateImpl<B>, - transform: suspend EvalScope.(A, B) -> Z, -): TStateImpl<Z> = - zipStates(null, operatorName, mapOf(0 to l1, 1 to l2)).map(name, operatorName) { - val a = it.getValue(0) - val b = it.getValue(1) - @Suppress("UNCHECKED_CAST") transform(a as A, b as B) - } - -internal fun <A, B, C, Z> zipStates( - name: String?, - operatorName: String, - l1: TStateImpl<A>, - l2: TStateImpl<B>, - l3: TStateImpl<C>, - transform: suspend EvalScope.(A, B, C) -> Z, -): TStateImpl<Z> = - zipStates(null, operatorName, mapOf(0 to l1, 1 to l2, 2 to l3)).map(name, operatorName) { - val a = it.getValue(0) - val b = it.getValue(1) - val c = it.getValue(2) - @Suppress("UNCHECKED_CAST") transform(a as A, b as B, c as C) - } - -internal fun <A, B, C, D, Z> zipStates( - name: String?, - operatorName: String, - l1: TStateImpl<A>, - l2: TStateImpl<B>, - l3: TStateImpl<C>, - l4: TStateImpl<D>, - transform: suspend EvalScope.(A, B, C, D) -> Z, -): TStateImpl<Z> = - zipStates(null, operatorName, mapOf(0 to l1, 1 to l2, 2 to l3, 3 to l4)).map( - name, - operatorName, - ) { - val a = it.getValue(0) - val b = it.getValue(1) - val c = it.getValue(2) - val d = it.getValue(3) - @Suppress("UNCHECKED_CAST") transform(a as A, b as B, c as C, d as D) - } - -internal fun <A, B, C, D, E, Z> zipStates( - name: String?, - operatorName: String, - l1: TStateImpl<A>, - l2: TStateImpl<B>, - l3: TStateImpl<C>, - l4: TStateImpl<D>, - l5: TStateImpl<E>, - transform: suspend EvalScope.(A, B, C, D, E) -> Z, -): TStateImpl<Z> = - zipStates(null, operatorName, mapOf(0 to l1, 1 to l2, 2 to l3, 3 to l4, 4 to l5)).map( - name, - operatorName, - ) { - val a = it.getValue(0) - val b = it.getValue(1) - val c = it.getValue(2) - val d = it.getValue(3) - val e = it.getValue(4) - @Suppress("UNCHECKED_CAST") transform(a as A, b as B, c as C, d as D, e as E) - } - -internal fun <K : Any, A> zipStates( - name: String?, - operatorName: String, - states: Map<K, TStateImpl<A>>, -): TStateImpl<Map<K, A>> { - if (states.isEmpty()) return constS(name, operatorName, emptyMap()) - val stateChanges: Map<K, TFlowImpl<A>> = states.mapValues { it.value.changes } - lateinit var state: DerivedZipped<K, A> - // No need for calm; invariant ensures that changes will only emit when there's a difference - val changes: TFlowImpl<Map<K, A>> = - mapImpl({ - switchDeferredImpl(getStorage = { stateChanges }, getPatches = { neverImpl }) - }) { patch -> - states - .mapValues { (k, v) -> - if (k in patch) { - patch.getValue(k) - } else { - v.getCurrentWithEpoch(evalScope = this).first - } - } - .also { state.setCache(it, epoch) } - } - state = DerivedZipped(name, operatorName, states, changes) - return state -} - -internal class DerivedZipped<K : Any, A>( - override val name: String?, - override val operatorName: String, - val upstream: Map<K, TStateImpl<A>>, - changes: TFlowImpl<Map<K, A>>, -) : TStateDerived<Map<K, A>>(changes) { - override suspend fun recalc(evalScope: EvalScope): Pair<Map<K, A>, Long> { - val newEpoch = AtomicLong() - return upstream.mapValuesParallel { - val (a, epoch) = it.value.getCurrentWithEpoch(evalScope) - newEpoch.accumulateAndGet(epoch, ::maxOf) - a - } to newEpoch.get() - } - - override fun toString(): String = "${this::class.simpleName}@$hashString" -} - -@Suppress("NOTHING_TO_INLINE") -internal inline fun <A> zipStates( - name: String?, - operatorName: String, - states: List<TStateImpl<A>>, -): TStateImpl<List<A>> = - if (states.isEmpty()) { - constS(name, operatorName, emptyList()) - } else { - zipStates(null, operatorName, states.asIterable().associateByIndex()).mapCheap( - name, - operatorName, - ) { - it.values.toList() - } - } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TransactionalImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TransactionalImpl.kt index 8647bdd5b7b1..13bd3b005871 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TransactionalImpl.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TransactionalImpl.kt @@ -16,31 +16,25 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.internal.util.Key import com.android.systemui.kairos.internal.util.hashString -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Deferred internal sealed class TransactionalImpl<out A> { - data class Const<out A>(val value: Deferred<A>) : TransactionalImpl<A>() + data class Const<out A>(val value: Lazy<A>) : TransactionalImpl<A>() + + class Impl<A>(val block: EvalScope.() -> A) : TransactionalImpl<A>() { + val cache = TransactionCache<Lazy<A>>() - class Impl<A>(val block: suspend EvalScope.() -> A) : TransactionalImpl<A>(), Key<Deferred<A>> { override fun toString(): String = "${this::class.simpleName}@$hashString" } } @Suppress("NOTHING_TO_INLINE") -internal inline fun <A> transactionalImpl( - noinline block: suspend EvalScope.() -> A -): TransactionalImpl<A> = TransactionalImpl.Impl(block) +internal inline fun <A> transactionalImpl(noinline block: EvalScope.() -> A): TransactionalImpl<A> = + TransactionalImpl.Impl(block) -internal fun <A> TransactionalImpl<A>.sample(evalScope: EvalScope): Deferred<A> = +internal fun <A> TransactionalImpl<A>.sample(evalScope: EvalScope): Lazy<A> = when (this) { is TransactionalImpl.Const -> value is TransactionalImpl.Impl -> - evalScope.transactionStore - .getOrPut(this) { - evalScope.deferAsync(start = CoroutineStart.LAZY) { evalScope.block() } - } - .also { it.start() } + cache.getOrPut(evalScope) { evalScope.deferAsync { evalScope.block() } } } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/store/ArrayMapK.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/store/ArrayMapK.kt new file mode 100644 index 000000000000..f0c2f346e9b7 --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/store/ArrayMapK.kt @@ -0,0 +1,129 @@ +/* + * Copyright (C) 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 com.android.systemui.kairos.internal.store + +import java.util.concurrent.atomic.AtomicReferenceArray + +/** A [Map] backed by a flat array. */ +internal class ArrayMapK<V>( + val unwrapped: List<MutableMap.MutableEntry<Int, V>>, + val originalCapacity: Int, +) : MapK<ArrayMapK.W, Int, V>, AbstractMap<Int, V>() { + object W + + override val entries: Set<Map.Entry<Int, V>> = + object : AbstractSet<Map.Entry<Int, V>>() { + override val size: Int + get() = unwrapped.size + + override fun iterator(): Iterator<Map.Entry<Int, V>> = unwrapped.iterator() + } +} + +@Suppress("NOTHING_TO_INLINE") +internal inline fun <V> MapK<ArrayMapK.W, Int, V>.asArrayHolder(): ArrayMapK<V> = + this as ArrayMapK<V> + +internal class MutableArrayMapK<V> +private constructor(private val storage: AtomicReferenceArray<MutableMap.MutableEntry<Int, V>?>) : + MutableMapK<ArrayMapK.W, Int, V>, AbstractMutableMap<Int, V>() { + + constructor(length: Int) : this(AtomicReferenceArray<MutableMap.MutableEntry<Int, V>?>(length)) + + override fun readOnlyCopy(): ArrayMapK<V> { + val size1 = storage.length() + return ArrayMapK( + buildList { + for (i in 0 until size1) { + storage.get(i)?.let { entry -> add(StoreEntry(entry.key, entry.value)) } + } + }, + size1, + ) + } + + override fun asReadOnly(): MapK<ArrayMapK.W, Int, V> = readOnlyCopy() + + private fun getNumEntries(): Int { + val capacity = storage.length() + var total = 0 + for (i in 0 until capacity) { + storage.get(i)?.let { total++ } + } + return total + } + + override fun put(key: Int, value: V): V? = + storage.get(key)?.value.also { storage.set(key, StoreEntry(key, value)) } + + override val entries: MutableSet<MutableMap.MutableEntry<Int, V>> = + object : AbstractMutableSet<MutableMap.MutableEntry<Int, V>>() { + override val size: Int + get() = getNumEntries() + + override fun add(element: MutableMap.MutableEntry<Int, V>): Boolean = + (storage.get(element.key) is MutableMap.MutableEntry<*, *>).also { + storage.set(element.key, element) + } + + override fun iterator(): MutableIterator<MutableMap.MutableEntry<Int, V>> = + object : MutableIterator<MutableMap.MutableEntry<Int, V>> { + + var cursor = -1 + var nextIndex = -1 + + override fun hasNext(): Boolean { + val capacity = storage.length() + if (nextIndex >= capacity) return false + if (nextIndex != cursor) return true + while (++nextIndex < capacity) { + if (storage.get(nextIndex) != null) { + return true + } + } + return false + } + + override fun next(): MutableMap.MutableEntry<Int, V> { + if (!hasNext()) throw NoSuchElementException() + cursor = nextIndex + return storage.get(cursor)!! + } + + override fun remove() { + check( + cursor >= 0 && + cursor < storage.length() && + storage.getAndSet(cursor, null) != null + ) + } + } + } + + class Factory : MutableMapK.Factory<ArrayMapK.W, Int> { + override fun <V> create(capacity: Int?) = + MutableArrayMapK<V>(checkNotNull(capacity) { "Cannot use ArrayMapK with null capacity." }) + + override fun <V> create(input: MapK<ArrayMapK.W, Int, V>): MutableArrayMapK<V> { + val holder = input.asArrayHolder() + return MutableArrayMapK( + AtomicReferenceArray<MutableMap.MutableEntry<Int, V>?>(holder.originalCapacity) + .apply { holder.unwrapped.forEach { set(it.key, it) } } + ) + } + } +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/store/MapHolder.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/store/MapHolder.kt new file mode 100644 index 000000000000..db2dde00f17a --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/store/MapHolder.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 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 com.android.systemui.kairos.internal.store + +import com.android.systemui.kairos.internal.util.ConcurrentNullableHashMap + +@JvmInline +internal value class MapHolder<K, V>(val unwrapped: Map<K, V>) : + MapK<MapHolder.W, K, V>, Map<K, V> by unwrapped { + object W +} + +@Suppress("NOTHING_TO_INLINE") +internal inline fun <K, V> MapK<MapHolder.W, K, V>.asMapHolder(): MapHolder<K, V> = + this as MapHolder<K, V> + +// TODO: preserve insertion order? +internal class ConcurrentHashMapK<K, V>(private val storage: ConcurrentNullableHashMap<K, V>) : + MutableMapK<MapHolder.W, K, V>, MutableMap<K, V> by storage { + + override fun readOnlyCopy() = MapHolder(storage.toMap()) + + override fun asReadOnly(): MapK<MapHolder.W, K, V> = MapHolder(storage) + + class Factory<K> : MutableMapK.Factory<MapHolder.W, K> { + override fun <V> create(capacity: Int?) = + ConcurrentHashMapK<K, V>( + capacity?.let { ConcurrentNullableHashMap(capacity) } ?: ConcurrentNullableHashMap() + ) + + override fun <V> create(input: MapK<MapHolder.W, K, V>) = + ConcurrentHashMapK( + ConcurrentNullableHashMap<K, V>().apply { + input.asMapHolder().unwrapped.forEach { (k, v) -> set(k, v) } + } + ) + } +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/store/MapK.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/store/MapK.kt new file mode 100644 index 000000000000..e193a4957bd0 --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/store/MapK.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 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 com.android.systemui.kairos.internal.store + +/** + * Higher-kinded encoding for [Map]. + * + * Let's say you want to write a class that is generic over both a map, and the type of data within + * the map: + * ``` kotlin + * class Foo<TMap, TKey, TValue> { + * val container: TMap<TKey, TElement> // disallowed! + * } + * ``` + * + * You can use `MapK` to represent the "higher-kinded" type variable `TMap`: + * ``` kotlin + * class Foo<TMap, TKey, TValue> { + * val container: MapK<TMap, TKey, TValue> // OK! + * } + * ``` + * + * Note that Kotlin will not let you use the generic type without parameters as `TMap`: + * ``` kotlin + * val fooHk: MapK<HashMap, Int, String> // not allowed: HashMap requires two type parameters + * ``` + * + * To work around this, you need to declare a special type-witness object. This object is only used + * at compile time and can be stripped out by a minifier because it's never used at runtime. + * + * ``` kotlin + * class Foo<A, B> : MapK<FooWitness, A, B> { ... } + * object FooWitness + * + * // safe, as long as Foo is the only implementor of MapK<FooWitness, *, *> + * fun <A, B> MapK<FooWitness, A, B>.asFoo(): Foo<A, B> = this as Foo<A, B> + * + * val fooStore: MapK<FooWitness, Int, String> = Foo() + * val foo: Foo<Int, String> = fooStore.asFoo() + * ``` + */ +internal interface MapK<W, K, V> : Map<K, V> + +internal interface MutableMapK<W, K, V> : MutableMap<K, V> { + + fun readOnlyCopy(): MapK<W, K, V> + + fun asReadOnly(): MapK<W, K, V> + + interface Factory<W, K> { + fun <V> create(capacity: Int?): MutableMapK<W, K, V> + + fun <V> create(input: MapK<W, K, V>): MutableMapK<W, K, V> + } +} + +internal object NoValue + +internal data class StoreEntry<K, V>(override var key: K, override var value: V) : + MutableMap.MutableEntry<K, V> { + override fun setValue(newValue: V): V = value.also { value = newValue } +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/store/Single.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/store/Single.kt new file mode 100644 index 000000000000..2d0894884a4c --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/store/Single.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 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 com.android.systemui.kairos.internal.store + +@Suppress("NOTHING_TO_INLINE") internal inline fun <V> singleOf(value: V) = Single<V>(value) + +/** A [Map] with a single element that has key [Unit]. */ +internal class Single<V>(val unwrapped: Any?) : MapK<Single.W, Unit, V>, AbstractMap<Unit, V>() { + + constructor() : this(NoValue) + + @Suppress("UNCHECKED_CAST") + override val entries: Set<Map.Entry<Unit, V>> = + if (unwrapped === NoValue) emptySet() else setOf(StoreEntry(Unit, unwrapped as V)) + + object W +} + +@Suppress("NOTHING_TO_INLINE") +internal inline fun <V> MapK<Single.W, Unit, V>.asSingle(): Single<V> = this as Single<V> + +internal class SingletonMapK<V>(@Volatile private var value: Any?) : + MutableMapK<Single.W, Unit, V>, AbstractMutableMap<Unit, V>() { + + constructor() : this(NoValue) + + override fun readOnlyCopy() = + Single<V>(if (value === NoValue) value else (value as MutableMap.MutableEntry<*, *>).value) + + override fun asReadOnly(): MapK<Single.W, Unit, V> = readOnlyCopy() + + @Suppress("UNCHECKED_CAST") + override fun put(key: Unit, value: V): V? = + (this.value as? MutableMap.MutableEntry<Unit, V>)?.value.also { + this.value = StoreEntry(Unit, value) + } + + override val entries: MutableSet<MutableMap.MutableEntry<Unit, V>> = + object : AbstractMutableSet<MutableMap.MutableEntry<Unit, V>>() { + override fun add(element: MutableMap.MutableEntry<Unit, V>): Boolean = + (value !== NoValue).also { value = element } + + override val size: Int + get() = if (value === NoValue) 0 else 1 + + override fun iterator(): MutableIterator<MutableMap.MutableEntry<Unit, V>> { + return object : MutableIterator<MutableMap.MutableEntry<Unit, V>> { + + var done = false + + override fun hasNext(): Boolean = value !== NoValue && !done + + override fun next(): MutableMap.MutableEntry<Unit, V> { + if (!hasNext()) throw NoSuchElementException() + done = true + @Suppress("UNCHECKED_CAST") + return value as MutableMap.MutableEntry<Unit, V> + } + + override fun remove() { + if (!done || value === NoValue) throw IllegalStateException() + value = NoValue + } + } + } + } + + internal class Factory : MutableMapK.Factory<Single.W, Unit> { + override fun <V> create(capacity: Int?): SingletonMapK<V> { + check(capacity == null || capacity == 0 || capacity == 1) { + "Can't use singleton store with capacity > 1. Got: $capacity" + } + return SingletonMapK() + } + + override fun <V> create(input: MapK<Single.W, Unit, V>): SingletonMapK<V> = + SingletonMapK(input.asSingle().unwrapped) + } +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/ConcurrentNullableHashMap.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/ConcurrentNullableHashMap.kt index 6c8ae7cf6436..afeb0679fe12 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/ConcurrentNullableHashMap.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/ConcurrentNullableHashMap.kt @@ -16,31 +16,114 @@ package com.android.systemui.kairos.internal.util +import com.android.systemui.kairos.internal.store.NoValue import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap + +internal class ConcurrentNullableHashMap<K, V> +private constructor(private val inner: ConcurrentHashMap<Any, Any>) : + ConcurrentMap<K, V>, AbstractMutableMap<K, V>() { -internal class ConcurrentNullableHashMap<K : Any, V> -private constructor(private val inner: ConcurrentHashMap<K, Any>) { constructor() : this(ConcurrentHashMap()) - @Suppress("UNCHECKED_CAST") - operator fun get(key: K): V? = inner[key]?.takeIf { it !== NullValue } as V? + constructor(capacity: Int) : this(ConcurrentHashMap(capacity)) + + override fun get(key: K): V? = inner[key ?: NullValue]?.let { toNullable<V>(it) } + + fun getValue(key: K): V = toNullable(inner.getValue(key ?: NullValue)) @Suppress("UNCHECKED_CAST") - fun put(key: K, value: V?): V? = - inner.put(key, value ?: NullValue)?.takeIf { it !== NullValue } as V? + override fun put(key: K, value: V): V? = + inner.put(key ?: NullValue, value ?: NullValue)?.takeIf { it !== NullValue } as V? - operator fun set(key: K, value: V?) { + operator fun set(key: K, value: V) { put(key, value) } - @Suppress("UNCHECKED_CAST") - fun toMap(): Map<K, V> = inner.mapValues { (_, v) -> v.takeIf { it !== NullValue } as V } + fun toMap(): Map<K, V> = + inner.asSequence().associate { (k, v) -> toNullable<K>(k) to toNullable(v) } - fun clear() { + override fun clear() { inner.clear() } + override fun remove(key: K, value: V): Boolean = inner.remove(key ?: NoValue, value ?: NoValue) + + override val entries: MutableSet<MutableMap.MutableEntry<K, V>> = + object : AbstractMutableSet<MutableMap.MutableEntry<K, V>>() { + val wrapped = inner.entries + + override fun add(element: MutableMap.MutableEntry<K, V>): Boolean { + val e = + object : MutableMap.MutableEntry<Any, Any> { + override val key: Any + get() = element.key ?: NullValue + + override val value: Any + get() = element.value ?: NullValue + + override fun setValue(newValue: Any): Any = + element.setValue(toNullable(newValue)) ?: NullValue + } + return wrapped.add(e) + } + + override val size: Int + get() = wrapped.size + + override fun iterator(): MutableIterator<MutableMap.MutableEntry<K, V>> { + val iter = wrapped.iterator() + return object : MutableIterator<MutableMap.MutableEntry<K, V>> { + override fun hasNext(): Boolean = iter.hasNext() + + override fun next(): MutableMap.MutableEntry<K, V> { + val element = iter.next() + return object : MutableMap.MutableEntry<K, V> { + override val key: K + get() = toNullable(element.key) + + override val value: V + get() = toNullable(element.value) + + override fun setValue(newValue: V): V = + toNullable(element.setValue(newValue ?: NullValue)) + } + } + + override fun remove() { + iter.remove() + } + } + } + } + + override fun replace(key: K, oldValue: V, newValue: V): Boolean = + inner.replace(key ?: NullValue, oldValue ?: NullValue, newValue ?: NullValue) + + override fun replace(key: K, value: V): V? = + inner.replace(key ?: NullValue, value ?: NullValue)?.let { toNullable<V>(it) } + + override fun putIfAbsent(key: K, value: V): V? = + inner.putIfAbsent(key ?: NullValue, value ?: NullValue)?.let { toNullable<V>(it) } + + @Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE") + private inline fun <T> toNullable(value: Any): T = value.takeIf { it !== NullValue } as T + fun isNotEmpty(): Boolean = inner.isNotEmpty() + + @Suppress("UNCHECKED_CAST") + override fun remove(key: K): V? = + inner.remove(key ?: NullValue)?.takeIf { it !== NullValue } as V? + + fun asSequence(): Sequence<Pair<K, V>> = + inner.asSequence().map { (key, value) -> toNullable<K>(key) to toNullable(value) } + + override fun isEmpty(): Boolean = inner.isEmpty() + + override fun containsKey(key: K): Boolean = inner.containsKey(key ?: NullValue) + + fun getOrPut(key: K, defaultValue: () -> V): V = + toNullable(inner.getOrPut(key) { defaultValue() ?: NullValue }) } private object NullValue diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt index 5cee2dd5880a..9b6940d03270 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt @@ -17,17 +17,18 @@ package com.android.systemui.kairos.internal.util import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.None +import com.android.systemui.kairos.util.Maybe.None import com.android.systemui.kairos.util.just import java.util.concurrent.ConcurrentHashMap -internal interface Key<A> - private object NULL -internal class HeteroMap { +internal class HeteroMap private constructor(private val store: ConcurrentHashMap<Key<*>, Any>) { + interface Key<A> {} + + constructor() : this(ConcurrentHashMap()) - private val store = ConcurrentHashMap<Key<*>, Any>() + constructor(capacity: Int) : this(ConcurrentHashMap(capacity)) @Suppress("UNCHECKED_CAST") operator fun <A> get(key: Key<A>): Maybe<A> = @@ -37,6 +38,17 @@ internal class HeteroMap { store[key] = value ?: NULL } + @Suppress("UNCHECKED_CAST") + fun <A : Any> getOrNull(key: Key<A>): A? = + store[key]?.let { (if (it === NULL) null else it) as A } + + @Suppress("UNCHECKED_CAST") + fun <A> getOrError(key: Key<A>, block: () -> String): A { + store[key]?.let { + return (if (it === NULL) null else it) as A + } ?: error(block()) + } + operator fun contains(key: Key<*>): Boolean = store.containsKey(key) fun clear() { diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/MapUtils.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/MapUtils.kt index ebf9a66be0ae..67862593b80a 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/MapUtils.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/MapUtils.kt @@ -16,8 +16,6 @@ package com.android.systemui.kairos.internal.util -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.yield @@ -32,7 +30,7 @@ internal suspend inline fun <K, A, B : Any, M : MutableMap<K, B>> Map<K, A> destination.also { coroutineScope { mapValues { - async { + asyncImmediate { yield() block(it) } @@ -41,7 +39,7 @@ internal suspend inline fun <K, A, B : Any, M : MutableMap<K, B>> Map<K, A> .mapValuesNotNullTo(it) { (_, deferred) -> deferred.await() } } -internal inline fun <K, A, B : Any, M : MutableMap<K, B>> Map<K, A>.mapValuesNotNullTo( +internal inline fun <K, A, B, M : MutableMap<K, B>> Map<K, A>.mapValuesNotNullTo( destination: M, block: (Map.Entry<K, A>) -> B?, ): M = @@ -51,9 +49,13 @@ internal inline fun <K, A, B : Any, M : MutableMap<K, B>> Map<K, A>.mapValuesNot } } +internal inline fun <K, A, B> Map<K, A>.mapValuesNotNull( + block: (Map.Entry<K, A>) -> B? +): Map<K, B> = mapValuesNotNullTo(mutableMapOf(), block) + internal suspend fun <A, B> Iterable<A>.mapParallel(transform: suspend (A) -> B): List<B> = coroutineScope { - map { async(start = CoroutineStart.LAZY) { transform(it) } }.awaitAll() + map { asyncImmediate { transform(it) } }.awaitAll() } internal suspend fun <K, A, B, M : MutableMap<K, B>> Map<K, A>.mapValuesParallelTo( diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt index 6bb7f9f593aa..466a9f83b91f 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt @@ -18,8 +18,13 @@ package com.android.systemui.kairos.internal.util +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext +import kotlin.time.DurationUnit +import kotlin.time.measureTimedValue import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Deferred @@ -31,6 +36,62 @@ import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.launch import kotlinx.coroutines.newCoroutineContext +private const val LogEnabled = false + +@Suppress("NOTHING_TO_INLINE") +internal inline fun logLn(indent: Int = 0, message: Any?) { + if (!LogEnabled) return + log(indent, message) + println() +} + +@Suppress("NOTHING_TO_INLINE") +internal inline fun log(indent: Int = 0, message: Any?) { + if (!LogEnabled) return + printIndent(indent) + print(message) +} + +@JvmInline +internal value class LogIndent(val currentLogIndent: Int) { + @OptIn(ExperimentalContracts::class) + inline fun <R> logDuration(prefix: String, start: Boolean = true, block: LogIndent.() -> R): R { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return logDuration(currentLogIndent, prefix, start, block) + } + + @Suppress("NOTHING_TO_INLINE") + inline fun logLn(message: Any?) = logLn(currentLogIndent, message) +} + +@OptIn(ExperimentalContracts::class) +internal inline fun <R> logDuration( + indent: Int, + prefix: String, + start: Boolean = true, + block: LogIndent.() -> R, +): R { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + if (!LogEnabled) return LogIndent(0).block() + if (start) { + logLn(indent, prefix) + } + val (result, duration) = measureTimedValue { LogIndent(indent + 1).block() } + + printIndent(indent) + print(prefix) + print(": ") + println(duration.toString(DurationUnit.MICROSECONDS)) + return result +} + +@Suppress("NOTHING_TO_INLINE") +private inline fun printIndent(indent: Int) { + for (i in 0 until indent) { + print(" ") + } +} + internal fun <A> CoroutineScope.asyncImmediate( start: CoroutineStart = CoroutineStart.UNDISPATCHED, context: CoroutineContext = EmptyCoroutineContext, diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Either.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Either.kt index ad9f7d715156..957d46ff1ecd 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Either.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Either.kt @@ -18,6 +18,9 @@ package com.android.systemui.kairos.util +import com.android.systemui.kairos.util.Either.Left +import com.android.systemui.kairos.util.Either.Right + /** * Contains a value of two possibilities: `Left<A>` or `Right<B>` * @@ -25,13 +28,13 @@ package com.android.systemui.kairos.util * [Pair] is effectively an anonymous grouping of two instances, then an [Either] is an anonymous * set of two options. */ -sealed class Either<out A, out B> - -/** An [Either] that contains a [Left] value. */ -data class Left<out A>(val value: A) : Either<A, Nothing>() +sealed interface Either<out A, out B> { + /** An [Either] that contains a [Left] value. */ + @JvmInline value class Left<out A>(val value: A) : Either<A, Nothing> -/** An [Either] that contains a [Right] value. */ -data class Right<out B>(val value: B) : Either<Nothing, B>() + /** An [Either] that contains a [Right] value. */ + @JvmInline value class Right<out B>(val value: B) : Either<Nothing, B> +} /** * Returns an [Either] containing the result of applying [transform] to the [Left] value, or the @@ -57,7 +60,7 @@ inline fun <A, B, C> Either<A, B>.mapRight(transform: (B) -> C): Either<A, C> = inline fun <A> Either<A, *>.leftMaybe(): Maybe<A> = when (this) { is Left -> just(value) - else -> None + else -> none } /** Returns the [Left] value held by this [Either], or `null` if this is a [Right] value. */ @@ -71,7 +74,7 @@ inline fun <A> Either<A, *>.leftOrNull(): A? = inline fun <B> Either<*, B>.rightMaybe(): Maybe<B> = when (this) { is Right -> just(value) - else -> None + else -> none } /** Returns the [Right] value held by this [Either], or `null` if this is a [Left] value. */ diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt new file mode 100644 index 000000000000..f368cbf8f124 --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt @@ -0,0 +1,98 @@ +/* + * Copyright (C) 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 com.android.systemui.kairos.util + +import com.android.systemui.kairos.util.Either.Left +import com.android.systemui.kairos.util.Either.Right +import com.android.systemui.kairos.util.Maybe.Just + +/** A "patch" that can be used to batch-update a [Map], via [applyPatch]. */ +typealias MapPatch<K, V> = Map<K, Maybe<V>> + +/** + * Returns a new [Map] that has [patch] applied to the original map. + * + * For each entry in [patch]: + * * a [Just] value will be included in the new map, replacing the entry in the original map with + * the same key, if present. + * * a [Maybe.None] value will be omitted from the new map, excluding the entry in the original map + * with the same key, if present. + */ +fun <K, V> Map<K, V>.applyPatch(patch: MapPatch<K, V>): Map<K, V> { + val (adds: List<Pair<K, V>>, removes: List<K>) = + patch + .asSequence() + .map { (k, v) -> if (v is Just) Left(k to v.value) else Right(k) } + .partitionEithers() + val removed: Map<K, V> = this - removes.toSet() + val updated: Map<K, V> = removed + adds + return updated +} + +/** + * Returns a [MapPatch] that, when applied, includes all of the values from the original [Map]. + * + * Shorthand for: + * ```kotlin + * mapValues { just(it.value) } + * ``` + */ +fun <K, V> Map<K, V>.toMapPatch(): MapPatch<K, V> = mapValues { just(it.value) } + +/** + * Returns a [MapPatch] that, when applied, includes all of the entries from [new] whose keys are + * not present in [old], and excludes all entries with keys present in [old] that are not also + * present in [new]. + * + * Note that, unlike [mapPatchFromFullDiff], only keys are taken into account. If the same key is + * present in both [old] and [new], but the associated values are not equal, then the returned + * [MapPatch] will *not* include any update to that key. + */ +fun <K, V> mapPatchFromKeyDiff(old: Map<K, V>, new: Map<K, V>): MapPatch<K, V> { + val removes = old.keys - new.keys + val adds = new - old.keys + return buildMap { + for (removed in removes) { + put(removed, none) + } + for ((newKey, newValue) in adds) { + put(newKey, just(newValue)) + } + } +} + +/** + * Returns a [MapPatch] that, when applied, includes all of the entries from [new] that are not + * present in [old], and excludes all entries with keys present in [old] that are not also present + * in [new]. + * + * Note that, unlike [mapPatchFromKeyDiff], both keys and values are taken into account. If the same + * key is present in both [old] and [new], but the associated values are not equal, then the + * returned [MapPatch] will include the entry from [new]. + */ +fun <K, V> mapPatchFromFullDiff(old: Map<K, V>, new: Map<K, V>): MapPatch<K, V> { + val removes = old.keys - new.keys + val adds = new.mapMaybeValues { (k, v) -> if (k in old && v == old[k]) none else just(v) } + return hashMapOf<K, Maybe<V>>().apply { + for (removed in removes) { + put(removed, none) + } + for ((newKey, newValue) in adds) { + put(newKey, just(newValue)) + } + } +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt index c3cae3885bd3..681218399d93 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt @@ -18,6 +18,8 @@ package com.android.systemui.kairos.util +import com.android.systemui.kairos.util.Maybe.Just +import com.android.systemui.kairos.util.Maybe.None import kotlin.coroutines.Continuation import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext @@ -27,13 +29,13 @@ import kotlin.coroutines.startCoroutine import kotlin.coroutines.suspendCoroutine /** Represents a value that may or may not be present. */ -sealed class Maybe<out A> +sealed interface Maybe<out A> { + /** A [Maybe] value that is present. */ + @JvmInline value class Just<out A> internal constructor(val value: A) : Maybe<A> -/** A [Maybe] value that is present. */ -data class Just<out A> internal constructor(val value: A) : Maybe<A>() - -/** A [Maybe] value that is not present. */ -data object None : Maybe<Nothing>() + /** A [Maybe] value that is not present. */ + data object None : Maybe<Nothing> +} /** Utilities to query [Maybe] instances from within a [maybe] block. */ @RestrictsSuspension @@ -230,8 +232,14 @@ fun <A> Maybe<A>.mergeWith(other: Maybe<A>, transform: (A, A) -> A): Maybe<A> = * Returns a list containing only the present results of applying [transform] to each element in the * original iterable. */ -fun <A, B> Iterable<A>.mapMaybe(transform: (A) -> Maybe<B>): List<B> = - asSequence().mapMaybe(transform).toList() +inline fun <A, B> Iterable<A>.mapMaybe(transform: (A) -> Maybe<B>): List<B> = buildList { + for (a in this@mapMaybe) { + val result = transform(a) + if (result is Just) { + add(result.value) + } + } +} /** * Returns a sequence containing only the present results of applying [transform] to each element in @@ -244,9 +252,15 @@ fun <A, B> Sequence<A>.mapMaybe(transform: (A) -> Maybe<B>): Sequence<B> = * Returns a map with values of only the present results of applying [transform] to each entry in * the original map. */ -inline fun <K, A, B> Map<K, A>.mapMaybeValues( - crossinline p: (Map.Entry<K, A>) -> Maybe<B> -): Map<K, B> = asSequence().mapMaybe { entry -> p(entry).map { entry.key to it } }.toMap() +inline fun <K, A, B> Map<K, A>.mapMaybeValues(transform: (Map.Entry<K, A>) -> Maybe<B>): Map<K, B> = + buildMap { + for (entry in this@mapMaybeValues) { + val result = transform(entry) + if (result is Just) { + put(entry.key, result.value) + } + } + } /** Returns a map with all non-present values filtered out. */ fun <K, A> Map<K, Maybe<A>>.filterJustValues(): Map<K, A> = diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/These.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/These.kt index aa95e0d2dc1b..092dca4d2f1d 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/These.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/These.kt @@ -16,8 +16,10 @@ package com.android.systemui.kairos.util +import com.android.systemui.kairos.util.Maybe.Just + /** Contains at least one of two potential values. */ -sealed class These<A, B> { +sealed class These<out A, out B> { /** Contains a single potential value. */ class This<A, B> internal constructor(val thiz: A) : These<A, B>() @@ -29,10 +31,10 @@ sealed class These<A, B> { companion object { /** Constructs a [These] containing only [thiz]. */ - fun <A, B> thiz(thiz: A): These<A, B> = This(thiz) + fun <A> thiz(thiz: A): These<A, Nothing> = This(thiz) /** Constructs a [These] containing only [that]. */ - fun <A, B> that(that: B): These<A, B> = That(that) + fun <B> that(that: B): These<Nothing, B> = That(that) /** Constructs a [These] containing both [thiz] and [that]. */ fun <A, B> both(thiz: A, that: B): These<A, B> = Both(thiz, that) @@ -54,7 +56,7 @@ inline fun <A> These<A, A>.merge(f: (A, A) -> A): A = fun <A> These<A, *>.maybeThis(): Maybe<A> = when (this) { is These.Both -> just(thiz) - is These.That -> None + is These.That -> none is These.This -> just(thiz) } @@ -74,7 +76,7 @@ fun <A> These<*, A>.maybeThat(): Maybe<A> = when (this) { is These.Both -> just(that) is These.That -> just(that) - is These.This -> None + is These.This -> none } /** @@ -92,7 +94,7 @@ fun <A : Any> These<*, A>.thatOrNull(): A? = fun <A, B> These<A, B>.maybeBoth(): Maybe<Pair<A, B>> = when (this) { is These.Both -> just(thiz to that) - else -> None + else -> none } /** Returns a [These] containing [thiz] and/or [that] if they are present. */ diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/WithPrev.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/WithPrev.kt index 5cfaa3ea2801..1bb97acc165d 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/WithPrev.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/WithPrev.kt @@ -16,5 +16,5 @@ package com.android.systemui.kairos.util -/** Holds a [newValue] emitted from a `TFlow`, along with the [previousValue] emitted value. */ +/** Holds a [newValue] emitted from a `Events`, along with the [previousValue] emitted value. */ data class WithPrev<out S, out T : S>(val previousValue: S, val newValue: T) diff --git a/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt b/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt index 688adae8fcae..150b462df655 100644 --- a/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt +++ b/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt @@ -1,28 +1,10 @@ -/* - * Copyright (C) 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:OptIn(ExperimentalCoroutinesApi::class, ExperimentalFrpApi::class) - package com.android.systemui.kairos import com.android.systemui.kairos.util.Either -import com.android.systemui.kairos.util.Left +import com.android.systemui.kairos.util.Either.Left +import com.android.systemui.kairos.util.Either.Right import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.None -import com.android.systemui.kairos.util.Right +import com.android.systemui.kairos.util.Maybe.None import com.android.systemui.kairos.util.just import com.android.systemui.kairos.util.map import com.android.systemui.kairos.util.maybe @@ -53,13 +35,15 @@ import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Assert.assertNull import org.junit.Assert.assertTrue +import org.junit.Assert.fail import org.junit.Test +@OptIn(ExperimentalCoroutinesApi::class) class KairosTests { @Test fun basic() = runFrpTest { network -> - val emitter = network.mutableTFlow<Int>() + val emitter = network.mutableEvents<Int>() var result: Int? = null activateSpec(network) { emitter.observe { result = it } } runCurrent() @@ -70,8 +54,8 @@ class KairosTests { } @Test - fun basicTFlow() = runFrpTest { network -> - val emitter = network.mutableTFlow<Int>() + fun basicEvents() = runFrpTest { network -> + val emitter = network.mutableEvents<Int>() println("starting network") val result = activateSpecWithResult(network) { emitter.nextDeferred() } runCurrent() @@ -84,9 +68,9 @@ class KairosTests { } @Test - fun basicTState() = runFrpTest { network -> - val emitter = network.mutableTFlow<Int>() - val result = activateSpecWithResult(network) { emitter.hold(0).stateChanges.nextDeferred() } + fun basicState() = runFrpTest { network -> + val emitter = network.mutableEvents<Int>() + val result = activateSpecWithResult(network) { emitter.holdState(0).changes.nextDeferred() } runCurrent() emitter.emit(3) @@ -110,7 +94,7 @@ class KairosTests { fun basicTransactional() = runFrpTest { network -> var value: Int? = null var bSource = 1 - val emitter = network.mutableTFlow<Unit>() + val emitter = network.mutableEvents<Unit>() // Sampling this transactional will increment the source count. val transactional = transactionally { bSource++ } measureTime { @@ -149,24 +133,26 @@ class KairosTests { @Test fun diamondGraph() = runFrpTest { network -> - val flow = network.mutableTFlow<Int>() - val outFlow = + val flow = network.mutableEvents<Int>() + val ouevents = activateSpecWithResult(network) { - // map TFlow like we map Flow + // map Events like we map Flow val left = flow.map { "left" to it }.onEach { println("left: $it") } val right = flow.map { "right" to it }.onEach { println("right: $it") } - // convert TFlows to TStates so that they can be combined + // convert Eventss to States so that they can be combined val combined = - left.hold("left" to 0).combineWith(right.hold("right" to 0)) { l, r -> l to r } - combined.stateChanges // get TState changes + left.holdState("left" to 0).combineWith(right.holdState("right" to 0)) { l, r -> + l to r + } + combined.changes // get State changes .onEach { println("merged: $it") } .toSharedFlow() // convert back to Flow } runCurrent() val results = mutableListOf<Pair<Pair<String, Int>, Pair<String, Int>>>() - backgroundScope.launch { outFlow.toCollection(results) } + backgroundScope.launch { ouevents.toCollection(results) } runCurrent() flow.emit(1) @@ -185,19 +171,19 @@ class KairosTests { fun staticNetwork() = runFrpTest { network -> var finalSum: Int? = null - val intEmitter = network.mutableTFlow<Int>() - val sampleEmitter = network.mutableTFlow<Unit>() + val intEmitter = network.mutableEvents<Int>() + val sampleEmitter = network.mutableEvents<Unit>() activateSpecWithResult(network) { val updates = intEmitter.map { a -> { b: Int -> a + b } } val sumD = - TStateLoop<Int>().apply { + StateLoop<Int>().apply { loopback = updates .sample(this) { f, sum -> f(sum) } .onEach { println("sum update: $it") } - .hold(0) + .holdState(0) } sampleEmitter .onEach { println("sampleEmitter emitted") } @@ -227,10 +213,10 @@ class KairosTests { var wasSold = false var currentAmt: Int? = null - val coin = network.mutableTFlow<Unit>() + val coin = network.mutableEvents<Unit>() val price = 50 - val frpSpec = frpSpec { - val eSold = TFlowLoop<Unit>() + val buildSpec = buildSpec { + val eSold = EventsLoop<Unit>() val eInsert = coin.map { @@ -250,10 +236,10 @@ class KairosTests { val eUpdate = eInsert.mergeWith(eReset) { f, g -> { a -> g(f(a)) } } - val dTotal = TStateLoop<Int>() - dTotal.loopback = eUpdate.sample(dTotal) { f, total -> f(total) }.hold(price) + val dTotal = StateLoop<Int>() + dTotal.loopback = eUpdate.sample(dTotal) { f, total -> f(total) }.holdState(price) - val eAmt = dTotal.stateChanges + val eAmt = dTotal.changes val bAmt = transactionally { dTotal.sample() } eSold.loopback = coin @@ -268,7 +254,7 @@ class KairosTests { eSold.nextDeferred() } - activateSpec(network) { frpSpec.applySpec() } + activateSpec(network) { buildSpec.applySpec() } runCurrent() @@ -312,8 +298,8 @@ class KairosTests { @Test fun promptCleanup() = runFrpTest { network -> - val emitter = network.mutableTFlow<Int>() - val stopper = network.mutableTFlow<Unit>() + val emitter = network.mutableEvents<Int>() + val stopper = network.mutableEvents<Unit>() var result: Int? = null @@ -331,19 +317,19 @@ class KairosTests { } @Test - fun switchTFlow() = runFrpTest { network -> + fun switchEvents() = runFrpTest { network -> var currentSum: Int? = null - val switchHandler = network.mutableTFlow<Pair<TFlow<Int>, String>>() - val aHandler = network.mutableTFlow<Int>() - val stopHandler = network.mutableTFlow<Unit>() - val bHandler = network.mutableTFlow<Int>() + val switchHandler = network.mutableEvents<Pair<Events<Int>, String>>() + val aHandler = network.mutableEvents<Int>() + val stopHandler = network.mutableEvents<Unit>() + val bHandler = network.mutableEvents<Int>() val sumFlow = activateSpecWithResult(network) { - val switchE = TFlowLoop<TFlow<Int>>() + val switchE = EventsLoop<Events<Int>>() switchE.loopback = - switchHandler.mapStateful { (intFlow, name) -> + switchHandler.mapStateful { (inevents, name) -> println("[onEach] Switching to: $name") val nextSwitch = switchE.skipNext().onEach { println("[onEach] switched-out") } @@ -351,11 +337,11 @@ class KairosTests { stopHandler .onEach { println("[onEach] stopped") } .mergeWith(nextSwitch) { _, b -> b } - intFlow.takeUntil(stopEvent) + inevents.takeUntil(stopEvent) } - val adderE: TFlow<(Int) -> Int> = - switchE.hold(emptyTFlow).switch().map { a -> + val adderE: Events<(Int) -> Int> = + switchE.holdState(emptyEvents).switchEvents().map { a -> println("[onEach] new number $a") ({ sum: Int -> println("$a+$sum=${a + sum}") @@ -363,13 +349,13 @@ class KairosTests { }) } - val sumD = TStateLoop<Int>() + val sumD = StateLoop<Int>() sumD.loopback = adderE .sample(sumD) { f, sum -> f(sum) } .onEach { println("[onEach] writing sum: $it") } - .hold(0) - val sumE = sumD.stateChanges + .holdState(0) + val sumE = sumD.changes sumE.toSharedFlow() } @@ -493,16 +479,16 @@ class KairosTests { @Test fun switchIndirect() = runFrpTest { network -> - val emitter = network.mutableTFlow<Unit>() + val emitter = network.mutableEvents<Unit>() activateSpec(network) { - emptyTFlow.map { emitter.map { 1 } }.flatten().map { "$it" }.observe() + emptyEvents.map { emitter.map { 1 } }.flatten().map { "$it" }.observe() } runCurrent() } @Test fun switchInWithResult() = runFrpTest { network -> - val emitter = network.mutableTFlow<Unit>() + val emitter = network.mutableEvents<Unit>() val out = activateSpecWithResult(network) { emitter.map { emitter.map { 1 } }.flatten().toSharedFlow() @@ -518,11 +504,11 @@ class KairosTests { fun switchInCompleted() = runFrpTest { network -> val outputs = mutableListOf<Int>() - val switchAH = network.mutableTFlow<Unit>() - val intAH = network.mutableTFlow<Int>() - val stopEmitter = network.mutableTFlow<Unit>() + val switchAH = network.mutableEvents<Unit>() + val intAH = network.mutableEvents<Int>() + val stopEmitter = network.mutableEvents<Unit>() - val top = frpSpec { + val top = buildSpec { val intS = intAH.takeUntil(stopEmitter) val switched = switchAH.map { intS }.flatten() switched.toSharedFlow() @@ -554,13 +540,13 @@ class KairosTests { } @Test - fun switchTFlow_outerCompletesFirst() = runFrpTest { network -> + fun switchEvents_outerCompletesFirst() = runFrpTest { network -> var stepResult: Int? = null - val switchAH = network.mutableTFlow<Unit>() - val switchStopEmitter = network.mutableTFlow<Unit>() - val intStopEmitter = network.mutableTFlow<Unit>() - val intAH = network.mutableTFlow<Int>() + val switchAH = network.mutableEvents<Unit>() + val switchStopEmitter = network.mutableEvents<Unit>() + val intStopEmitter = network.mutableEvents<Unit>() + val intAH = network.mutableEvents<Int>() val flow = activateSpecWithResult(network) { val intS = intAH.takeUntil(intStopEmitter) @@ -608,8 +594,8 @@ class KairosTests { } @Test - fun mapTFlow() = runFrpTest { network -> - val emitter = network.mutableTFlow<Int>() + fun mapEvents() = runFrpTest { network -> + val emitter = network.mutableEvents<Int>() var stepResult: Int? = null val flow = @@ -643,7 +629,7 @@ class KairosTests { var pullValue = 0 val a = transactionally { pullValue } val b = transactionally { a.sample() * 2 } - val emitter = network.mutableTFlow<Unit>() + val emitter = network.mutableEvents<Unit>() val flow = activateSpecWithResult(network) { val sampleB = emitter.sample(b) { _, b -> b } @@ -667,14 +653,14 @@ class KairosTests { } @Test - fun mapTState() = runFrpTest { network -> - val emitter = network.mutableTFlow<Int>() + fun mapState() = runFrpTest { network -> + val emitter = network.mutableEvents<Int>() var stepResult: Int? = null val flow = activateSpecWithResult(network) { - val state = emitter.hold(0).map { it + 2 } + val state = emitter.holdState(0).map { it + 2 } val stateCurrent = transactionally { state.sample() } - val stateChanges = state.stateChanges + val stateChanges = state.changes val sampleState = emitter.sample(stateCurrent) { _, b -> b } val merge = stateChanges.mergeWith(sampleState) { a, b -> a + b } merge.toSharedFlow() @@ -695,14 +681,14 @@ class KairosTests { @Test fun partitionEither() = runFrpTest { network -> - val emitter = network.mutableTFlow<Either<Int, Int>>() + val emitter = network.mutableEvents<Either<Int, Int>>() val result = activateSpecWithResult(network) { val (l, r) = emitter.partitionEither() val pDiamond = l.map { it * 2 } .mergeWith(r.map { it * -1 }) { _, _ -> error("unexpected coincidence") } - pDiamond.hold(null).toStateFlow() + pDiamond.holdState(null).toStateFlow() } runCurrent() @@ -718,15 +704,16 @@ class KairosTests { } @Test - fun accumTState() = runFrpTest { network -> - val emitter = network.mutableTFlow<Int>() - val sampler = network.mutableTFlow<Unit>() + fun accumState() = runFrpTest { network -> + val emitter = network.mutableEvents<Int>() + val sampler = network.mutableEvents<Unit>() var stepResult: Int? = null val flow = activateSpecWithResult(network) { - val sumState = emitter.map { a -> { b: Int -> a + b } }.fold(0) { f, a -> f(a) } + val sumState = + emitter.map { a -> { b: Int -> a + b } }.foldState(0) { f, a -> f(a) } - sumState.stateChanges + sumState.changes .mergeWith(sampler.sample(sumState) { _, sum -> sum }) { _, _ -> error("Unexpected coincidence") } @@ -750,11 +737,11 @@ class KairosTests { } @Test - fun mergeTFlows() = runFrpTest { network -> - val first = network.mutableTFlow<Int>() - val stopFirst = network.mutableTFlow<Unit>() - val second = network.mutableTFlow<Int>() - val stopSecond = network.mutableTFlow<Unit>() + fun mergeEventss() = runFrpTest { network -> + val first = network.mutableEvents<Int>() + val stopFirst = network.mutableEvents<Unit>() + val second = network.mutableEvents<Int>() + val stopSecond = network.mutableEvents<Unit>() var stepResult: Int? = null val flow: SharedFlow<Int> @@ -831,19 +818,19 @@ class KairosTests { secondEmitDuration: ${secondEmitDuration.toString(DurationUnit.MILLISECONDS, 2)} stopFirstDuration: ${stopFirstDuration.toString(DurationUnit.MILLISECONDS, 2)} testDeadEmitFirstDuration: ${ - testDeadEmitFirstDuration.toString( - DurationUnit.MILLISECONDS, - 2, - ) - } + testDeadEmitFirstDuration.toString( + DurationUnit.MILLISECONDS, + 2, + ) + } secondEmitDuration2: ${secondEmitDuration2.toString(DurationUnit.MILLISECONDS, 2)} stopSecondDuration: ${stopSecondDuration.toString(DurationUnit.MILLISECONDS, 2)} testDeadEmitSecondDuration: ${ - testDeadEmitSecondDuration.toString( - DurationUnit.MILLISECONDS, - 2, - ) - } + testDeadEmitSecondDuration.toString( + DurationUnit.MILLISECONDS, + 2, + ) + } """ .trimIndent() ) @@ -851,10 +838,10 @@ class KairosTests { @Test fun sampleCancel() = runFrpTest { network -> - val updater = network.mutableTFlow<Int>() - val stopUpdater = network.mutableTFlow<Unit>() - val sampler = network.mutableTFlow<Unit>() - val stopSampler = network.mutableTFlow<Unit>() + val updater = network.mutableEvents<Int>() + val stopUpdater = network.mutableEvents<Unit>() + val sampler = network.mutableEvents<Unit>() + val stopSampler = network.mutableEvents<Unit>() var stepResult: Int? = null val flow = activateSpecWithResult(network) { @@ -862,7 +849,7 @@ class KairosTests { val samplerS = sampler.takeUntil(stopSamplerFirst) val stopUpdaterFirst = stopUpdater val updaterS = updater.takeUntil(stopUpdaterFirst) - val sampledS = samplerS.sample(updaterS.hold(0)) { _, b -> b } + val sampledS = samplerS.sample(updaterS.holdState(0)) { _, b -> b } sampledS.toSharedFlow() } @@ -893,35 +880,35 @@ class KairosTests { @Test fun combineStates_differentUpstreams() = runFrpTest { network -> - val a = network.mutableTFlow<Int>() - val b = network.mutableTFlow<Int>() + val a = network.mutableEvents<Int>() + val b = network.mutableEvents<Int>() var observed: Pair<Int, Int>? = null - val tState = + val state = activateSpecWithResult(network) { - val state = combine(a.hold(0), b.hold(0)) { a, b -> Pair(a, b) } - state.stateChanges.observe { observed = it } + val state = combine(a.holdState(0), b.holdState(0)) { a, b -> Pair(a, b) } + state.changes.observe { observed = it } state } - assertEquals(0 to 0, network.transact { tState.sample() }) + assertEquals(0 to 0, network.transact { state.sample() }) assertEquals(null, observed) a.emit(5) assertEquals(5 to 0, observed) - assertEquals(5 to 0, network.transact { tState.sample() }) + assertEquals(5 to 0, network.transact { state.sample() }) b.emit(3) assertEquals(5 to 3, observed) - assertEquals(5 to 3, network.transact { tState.sample() }) + assertEquals(5 to 3, network.transact { state.sample() }) } @Test fun sampleCombinedStates() = runFrpTest { network -> - val updater = network.mutableTFlow<Int>() - val emitter = network.mutableTFlow<Unit>() + val updater = network.mutableEvents<Int>() + val emitter = network.mutableEvents<Unit>() val result = activateSpecWithResult(network) { - val bA = updater.map { it * 2 }.hold(0) - val bB = updater.hold(0) - val combineD: TState<Pair<Int, Int>> = bA.combineWith(bB) { a, b -> a to b } + val bA = updater.map { it * 2 }.holdState(0) + val bB = updater.holdState(0) + val combineD: State<Pair<Int, Int>> = bA.combineWith(bB) { a, b -> a to b } val sampleS = emitter.sample(combineD) { _, b -> b } sampleS.nextDeferred() } @@ -942,13 +929,13 @@ class KairosTests { @Test fun switchMapPromptly() = runFrpTest { network -> - val emitter = network.mutableTFlow<Unit>() + val emitter = network.mutableEvents<Unit>() val result = activateSpecWithResult(network) { emitter .map { emitter.map { 1 }.map { it + 1 }.map { it * 2 } } - .hold(emptyTFlow) - .switchPromptly() + .holdState(emptyEvents) + .switchEventsPromptly() .nextDeferred() } runCurrent() @@ -962,8 +949,8 @@ class KairosTests { @Test fun switchDeeper() = runFrpTest { network -> - val emitter = network.mutableTFlow<Unit>() - val e2 = network.mutableTFlow<Unit>() + val emitter = network.mutableEvents<Unit>() + val e2 = network.mutableEvents<Unit>() val result = activateSpecWithResult(network) { val tres = @@ -988,14 +975,14 @@ class KairosTests { @Test fun recursionBasic() = runFrpTest { network -> - val add1 = network.mutableTFlow<Unit>() - val sub1 = network.mutableTFlow<Unit>() + val add1 = network.mutableEvents<Unit>() + val sub1 = network.mutableEvents<Unit>() val stepResult: StateFlow<Int> = activateSpecWithResult(network) { - val dSum = TStateLoop<Int>() + val dSum = StateLoop<Int>() val sAdd1 = add1.sample(dSum) { _, sum -> sum + 1 } val sMinus1 = sub1.sample(dSum) { _, sum -> sum - 1 } - dSum.loopback = sAdd1.mergeWith(sMinus1) { a, _ -> a }.hold(0) + dSum.loopback = sAdd1.mergeWith(sMinus1) { a, _ -> a }.holdState(0) dSum.toStateFlow() } runCurrent() @@ -1017,16 +1004,17 @@ class KairosTests { } @Test - fun recursiveTState() = runFrpTest { network -> - val e = network.mutableTFlow<Unit>() + fun recursiveState() = runFrpTest { network -> + val e = network.mutableEvents<Unit>() var changes = 0 val state = activateSpecWithResult(network) { - val s = TFlowLoop<Unit>() - val deferred = s.map { tStateOf(null) } - val e3 = e.map { tStateOf(Unit) } - val flattened = e3.mergeWith(deferred) { a, _ -> a }.hold(tStateOf(null)).flatten() - s.loopback = emptyTFlow + val s = EventsLoop<Unit>() + val deferred = s.map { stateOf(null) } + val e3 = e.map { stateOf(Unit) } + val flattened = + e3.mergeWith(deferred) { a, _ -> a }.holdState(stateOf(null)).flatten() + s.loopback = emptyEvents flattened.toStateFlow() } @@ -1036,7 +1024,7 @@ class KairosTests { @Test fun fanOut() = runFrpTest { network -> - val e = network.mutableTFlow<Map<String, Int>>() + val e = network.mutableEvents<Map<String, Int>>() val (fooFlow, barFlow) = activateSpecWithResult(network) { val selector = e.groupByKey() @@ -1057,16 +1045,30 @@ class KairosTests { } @Test + fun propagateError() { + try { + runFrpTest { network -> + runCurrent() + try { + network.transact<Unit> { error("message") } + fail("caller did not throw exception") + } catch (_: IllegalStateException) {} + } + fail("scheduler did not throw exception") + } catch (_: IllegalStateException) {} + } + + @Test fun fanOutLateSubscribe() = runFrpTest { network -> - val e = network.mutableTFlow<Map<String, Int>>() + val e = network.mutableEvents<Map<String, Int>>() val barFlow = activateSpecWithResult(network) { val selector = e.groupByKey() selector .eventsForKey("foo") .map { selector.eventsForKey("bar") } - .hold(emptyTFlow) - .switchPromptly() + .holdState(emptyEvents) + .switchEventsPromptly() .toSharedFlow() } val stateFlow = barFlow.stateIn(backgroundScope, SharingStarted.Eagerly, null) @@ -1081,9 +1083,9 @@ class KairosTests { } @Test - fun inputFlowCompleted() = runFrpTest { network -> + fun inputEventsCompleted() = runFrpTest { network -> val results = mutableListOf<Int>() - val e = network.mutableTFlow<Int>() + val e = network.mutableEvents<Int>() activateSpec(network) { e.nextOnly().observe { results.add(it) } } runCurrent() @@ -1099,49 +1101,59 @@ class KairosTests { @Test fun fanOutThenMergeIncrementally() = runFrpTest { network -> - // A tflow of group updates, where a group is a tflow of child updates, where a child is a + // A events of group updates, where a group is a events of child updates, where a child is a // stateflow - val e = network.mutableTFlow<Map<Int, Maybe<TFlow<Map<Int, Maybe<StateFlow<String>>>>>>>() + val e = network.mutableEvents<Map<Int, Maybe<Events<Map<Int, Maybe<StateFlow<String>>>>>>>() println("fanOutMergeInc START") val state = activateSpecWithResult(network) { - // Convert nested Flows to nested TFlow/TState - val emitter: TFlow<Map<Int, Maybe<TFlow<Map<Int, Maybe<TState<String>>>>>>> = + // Convert nested Flows to nested Events/State + val emitter: Events<Map<Int, Maybe<Events<Map<Int, Maybe<State<String>>>>>>> = e.mapBuild { m -> m.mapValues { (_, mFlow) -> mFlow.map { it.mapBuild { m2 -> + println("m2: $m2") m2.mapValues { (_, mState) -> - mState.map { stateFlow -> stateFlow.toTState() } + mState.map { stateFlow -> stateFlow.toState() } } } } } } - // Accumulate all of our updates into a single TState - val accState: TState<Map<Int, Map<Int, String>>> = + // Accumulate all of our updates into a single State + val accState: State<Map<Int, Map<Int, String>>> = emitter .mapStateful { - changeMap: Map<Int, Maybe<TFlow<Map<Int, Maybe<TState<String>>>>>> -> + changeMap: Map<Int, Maybe<Events<Map<Int, Maybe<State<String>>>>>> -> changeMap.mapValues { (groupId, mGroupChanges) -> mGroupChanges.map { - groupChanges: TFlow<Map<Int, Maybe<TState<String>>>> -> + groupChanges: Events<Map<Int, Maybe<State<String>>>> -> // New group val childChangeById = groupChanges.groupByKey() - val map: TFlow<Map<Int, Maybe<TFlow<Maybe<TState<String>>>>>> = + val map: Events<Map<Int, Maybe<Events<Maybe<State<String>>>>>> = groupChanges.mapStateful { - gChangeMap: Map<Int, Maybe<TState<String>>> -> + gChangeMap: Map<Int, Maybe<State<String>>> -> + println("gChangeMap: $gChangeMap") gChangeMap.mapValues { (childId, mChild) -> - mChild.map { child: TState<String> -> + mChild.map { child: State<String> -> println("new child $childId in the house") // New child val eRemoved = childChangeById .eventsForKey(childId) .filter { it === None } - .nextOnly() + .onEach { + println( + "removing? (groupId=$groupId, childId=$childId)" + ) + } + .nextOnly( + name = + "eRemoved(groupId=$groupId, childId=$childId)" + ) - val addChild: TFlow<Maybe<TState<String>>> = + val addChild: Events<Maybe<State<String>>> = now.map { mChild } .onEach { println( @@ -1149,7 +1161,7 @@ class KairosTests { ) } - val removeChild: TFlow<Maybe<TState<String>>> = + val removeChild: Events<Maybe<State<String>>> = eRemoved .onEach { println( @@ -1158,23 +1170,28 @@ class KairosTests { } .map { none() } - addChild.mergeWith(removeChild) { _, _ -> + addChild.mergeWith( + removeChild, + name = + "childUpdatesMerged(groupId=$groupId, childId=$childId)", + ) { _, _ -> error("unexpected coincidence") } } } } - val mergeIncrementally: TFlow<Map<Int, Maybe<TState<String>>>> = + val mergeIncrementally: Events<Map<Int, Maybe<State<String>>>> = map.onEach { println("merge patch: $it") } - .mergeIncrementallyPromptly() + .mergeIncrementallyPromptly(name = "mergeIncrementally") mergeIncrementally - .onEach { println("patch: $it") } - .foldMapIncrementally() + .onEach { println("foldmap patch: $it") } + .foldStateMapIncrementally() .flatMap { it.combine() } } } } - .foldMapIncrementally() + .onEach { println("fold patch: $it") } + .foldStateMapIncrementally() .flatMap { it.combine() } accState.toStateFlow() @@ -1183,7 +1200,7 @@ class KairosTests { assertEquals(emptyMap(), state.value) - val emitter2 = network.mutableTFlow<Map<Int, Maybe<StateFlow<String>>>>() + val emitter2 = network.mutableEvents<Map<Int, Maybe<StateFlow<String>>>>() println() println("init outer 0") e.emit(mapOf(0 to just(emitter2.onEach { println("emitter2 emit: $it") }))) @@ -1218,6 +1235,10 @@ class KairosTests { assertEquals(mapOf(0 to mapOf(10 to "(2, 10)")), state.value) + // LogEnabled = true + + println("batch update") + // batch update emitter2.emit( mapOf( @@ -1233,13 +1254,13 @@ class KairosTests { @Test fun applyLatestNetworkChanges() = runFrpTest { network -> - val newCount = network.mutableTFlow<FrpSpec<Flow<Int>>>() + val newCount = network.mutableEvents<BuildSpec<Flow<Int>>>() val flowOfFlows: Flow<Flow<Int>> = activateSpecWithResult(network) { newCount.applyLatestSpec().toSharedFlow() } runCurrent() - val incCount = network.mutableTFlow<Unit>() - fun newFlow(): FrpSpec<SharedFlow<Int>> = frpSpec { + val incCount = network.mutableEvents<Unit>() + fun newFlow(): BuildSpec<SharedFlow<Int>> = buildSpec { launchEffect { try { println("new flow!") @@ -1248,16 +1269,16 @@ class KairosTests { println("cancelling old flow") } } - lateinit var count: TState<Int> + lateinit var count: State<Int> count = incCount .onEach { println("incrementing ${count.sample()}") } - .fold(0) { _, c -> c + 1 } - count.stateChanges.toSharedFlow() + .foldState(0) { _, c -> c + 1 } + count.changes.toSharedFlow() } var outerCount = 0 - val lastFlows: StateFlow<Pair<StateFlow<Int?>, StateFlow<Int?>>> = + val laseventss: StateFlow<Pair<StateFlow<Int?>, StateFlow<Int?>>> = flowOfFlows .map { it.stateIn(backgroundScope, SharingStarted.Eagerly, null) } .pairwise(MutableStateFlow(null)) @@ -1275,18 +1296,18 @@ class KairosTests { assertEquals(1, outerCount) // assertEquals(1, incCount.subscriptionCount) - assertNull(lastFlows.value.second.value) + assertNull(laseventss.value.second.value) incCount.emit(Unit) runCurrent() println("checking") - assertEquals(1, lastFlows.value.second.value) + assertEquals(1, laseventss.value.second.value) incCount.emit(Unit) runCurrent() - assertEquals(2, lastFlows.value.second.value) + assertEquals(2, laseventss.value.second.value) newCount.emit(newFlow()) runCurrent() @@ -1294,17 +1315,17 @@ class KairosTests { runCurrent() // verify old flow is not getting updates - assertEquals(2, lastFlows.value.first.value) + assertEquals(2, laseventss.value.first.value) // but the new one is - assertEquals(1, lastFlows.value.second.value) + assertEquals(1, laseventss.value.second.value) } @Test fun buildScope_stateAccumulation() = runFrpTest { network -> - val input = network.mutableTFlow<Unit>() + val input = network.mutableEvents<Unit>() var observedCount: Int? = null activateSpec(network) { - val (c, j) = asyncScope { input.fold(0) { _, x -> x + 1 } } + val (c, j) = asyncScope { input.foldState(0) { _, x -> x + 1 } } deferredBuildScopeAction { c.get().observe { observedCount = it } } } runCurrent() @@ -1321,7 +1342,7 @@ class KairosTests { @Test fun effect() = runFrpTest { network -> - val input = network.mutableTFlow<Unit>() + val input = network.mutableEvents<Unit>() var effectRunning = false var count = 0 activateSpec(network) { @@ -1333,7 +1354,7 @@ class KairosTests { effectRunning = false } } - merge(emptyTFlow, input.nextOnly()).observe { + merge(emptyEvents, input.nextOnly()).observe { count++ j.cancel() } @@ -1355,23 +1376,93 @@ class KairosTests { assertEquals(1, count) } + @Test + fun observeEffect_disposeHandle() = runFrpTest { network -> + val input = network.mutableEvents<Unit>() + val stopper = network.mutableEvents<Unit>() + var runningCount = 0 + val specJob = + activateSpec(network) { + val handle = + input.observe { + effectCoroutineScope.launch { + runningCount++ + awaitClose { runningCount-- } + } + } + stopper.nextOnly().observe { handle.dispose() } + } + runCurrent() + assertEquals(0, runningCount) + + input.emit(Unit) + assertEquals(1, runningCount) + + input.emit(Unit) + assertEquals(2, runningCount) + + stopper.emit(Unit) + assertEquals(2, runningCount) + + input.emit(Unit) + assertEquals(2, runningCount) + + specJob.cancel() + runCurrent() + assertEquals(0, runningCount) + } + + @Test + fun observeEffect_takeUntil() = runFrpTest { network -> + val input = network.mutableEvents<Unit>() + val stopper = network.mutableEvents<Unit>() + var runningCount = 0 + val specJob = + activateSpec(network) { + input.takeUntil(stopper).observe { + effectCoroutineScope.launch { + runningCount++ + awaitClose { runningCount-- } + } + } + } + runCurrent() + assertEquals(0, runningCount) + + input.emit(Unit) + assertEquals(1, runningCount) + + input.emit(Unit) + assertEquals(2, runningCount) + + stopper.emit(Unit) + assertEquals(2, runningCount) + + input.emit(Unit) + assertEquals(2, runningCount) + + specJob.cancel() + runCurrent() + assertEquals(0, runningCount) + } + private fun runFrpTest( timeout: Duration = 3.seconds, - block: suspend TestScope.(FrpNetwork) -> Unit, + block: suspend TestScope.(KairosNetwork) -> Unit, ) { runTest(timeout = timeout) { - val network = backgroundScope.newFrpNetwork() + val network = backgroundScope.launchKairosNetwork() runCurrent() block(network) } } - private fun TestScope.activateSpec(network: FrpNetwork, spec: FrpSpec<*>) = + private fun TestScope.activateSpec(network: KairosNetwork, spec: BuildSpec<*>) = backgroundScope.launch { network.activateSpec(spec) } private suspend fun <R> TestScope.activateSpecWithResult( - network: FrpNetwork, - spec: FrpSpec<R>, + network: KairosNetwork, + spec: BuildSpec<R>, ): R = CompletableDeferred<R>() .apply { activateSpec(network) { complete(spec.applySpec()) } } diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index 38617621bf89..b441e9dd561d 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -5386,7 +5386,7 @@ public class ComputerEngine implements Computer { + ", uid:" + callingUid); throw new IllegalArgumentException("Unknown package: " + packageName); } - if (pkg.getUid() != callingUid + if (!UserHandle.isSameApp(callingUid, pkg.getUid()) && Process.SYSTEM_UID != callingUid) { throw new SecurityException("May not access signing KeySet of other apps."); } diff --git a/telephony/java/android/telephony/satellite/ISatelliteCommunicationAllowedStateCallback.aidl b/telephony/java/android/telephony/satellite/ISatelliteCommunicationAccessStateCallback.aidl index 2730f90c4e5e..a3c66a03fd7f 100644 --- a/telephony/java/android/telephony/satellite/ISatelliteCommunicationAllowedStateCallback.aidl +++ b/telephony/java/android/telephony/satellite/ISatelliteCommunicationAccessStateCallback.aidl @@ -19,18 +19,18 @@ package android.telephony.satellite; import android.telephony.satellite.SatelliteAccessConfiguration; /** - * Interface for satellite communication allowed state callback. + * Interface for satellite communication access state callback. * @hide */ -oneway interface ISatelliteCommunicationAllowedStateCallback { +oneway interface ISatelliteCommunicationAccessStateCallback { /** * Telephony does not guarantee that whenever there is a change in communication allowed * state, this API will be called. Telephony does its best to detect the changes and notify - * its listners accordingly. + * its listeners accordingly. * * @param allowed whether satellite communication state or not */ - void onSatelliteCommunicationAllowedStateChanged(in boolean isAllowed); + void onAccessAllowedStateChanged(in boolean isAllowed); /** * Callback method invoked when the satellite access configuration changes @@ -39,6 +39,6 @@ oneway interface ISatelliteCommunicationAllowedStateCallback { * When satellite is not allowed at the current location, * {@code satelliteRegionalConfiguration} will be null. */ - void onSatelliteAccessConfigurationChanged(in SatelliteAccessConfiguration + void onAccessConfigurationChanged(in SatelliteAccessConfiguration satelliteAccessConfiguration); } diff --git a/telephony/java/android/telephony/satellite/SatelliteCommunicationAllowedStateCallback.java b/telephony/java/android/telephony/satellite/SatelliteCommunicationAccessStateCallback.java index 6291102cd6e3..7fb8a968a6e3 100644 --- a/telephony/java/android/telephony/satellite/SatelliteCommunicationAllowedStateCallback.java +++ b/telephony/java/android/telephony/satellite/SatelliteCommunicationAccessStateCallback.java @@ -25,24 +25,24 @@ import com.android.internal.telephony.flags.Flags; /** - * A callback class for monitoring satellite communication allowed state changed events. + * A callback class for monitoring satellite communication access state changed events. * * @hide */ @SystemApi @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) -public interface SatelliteCommunicationAllowedStateCallback { +public interface SatelliteCommunicationAccessStateCallback { /** * Telephony does not guarantee that whenever there is a change in communication allowed state, * this API will be called. Telephony does its best to detect the changes and notify its - * listeners accordingly. Satellite communication is allowed at a location when it is legally - * allowed by the local authority and satellite signal coverage is available. + * listeners accordingly. Satellite communication access is allowed at a location when it is + * legally allowed by the local authority and satellite signal coverage is available. * * @param isAllowed {@code true} means satellite is allowed, * {@code false} satellite is not allowed. */ - void onSatelliteCommunicationAllowedStateChanged(boolean isAllowed); + void onAccessAllowedStateChanged(boolean isAllowed); /** * Callback method invoked when the satellite access configuration changes @@ -52,6 +52,6 @@ public interface SatelliteCommunicationAllowedStateCallback { * the current location, * {@code satelliteRegionalConfiguration} will be null. */ - default void onSatelliteAccessConfigurationChanged( + default void onAccessConfigurationChanged( @Nullable SatelliteAccessConfiguration satelliteAccessConfiguration) {}; } diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index b885b30d17d7..63a12816f783 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -100,9 +100,9 @@ public final class SatelliteManager { private static final ConcurrentHashMap<Consumer<Boolean>, IBooleanConsumer> sSatelliteSupportedStateCallbackMap = new ConcurrentHashMap<>(); - private static final ConcurrentHashMap<SatelliteCommunicationAllowedStateCallback, - ISatelliteCommunicationAllowedStateCallback> - sSatelliteCommunicationAllowedStateCallbackMap = + private static final ConcurrentHashMap<SatelliteCommunicationAccessStateCallback, + ISatelliteCommunicationAccessStateCallback> + sSatelliteCommunicationAccessStateCallbackMap = new ConcurrentHashMap<>(); private static final ConcurrentHashMap<SatelliteDisallowedReasonsCallback, ISatelliteDisallowedReasonsCallback> @@ -3398,10 +3398,10 @@ public final class SatelliteManager { } /** - * Registers for the satellite communication allowed state changed. + * Registers for the satellite communication access state changed event. * * @param executor The executor on which the callback will be called. - * @param callback The callback to handle satellite communication allowed state changed event. + * @param callback The callback to handle satellite communication access state changed event. * @return The {@link SatelliteResult} result of the operation. * @throws SecurityException if the caller doesn't have required permission. * @throws IllegalStateException if the Telephony process is not currently available. @@ -3411,54 +3411,54 @@ public final class SatelliteManager { @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @SatelliteResult - public int registerForCommunicationAllowedStateChanged( + public int registerForCommunicationAccessStateChanged( @NonNull @CallbackExecutor Executor executor, - @NonNull SatelliteCommunicationAllowedStateCallback callback) { + @NonNull SatelliteCommunicationAccessStateCallback callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { ITelephony telephony = getITelephony(); if (telephony != null) { - ISatelliteCommunicationAllowedStateCallback internalCallback = - new ISatelliteCommunicationAllowedStateCallback.Stub() { + ISatelliteCommunicationAccessStateCallback internalCallback = + new ISatelliteCommunicationAccessStateCallback.Stub() { @Override - public void onSatelliteCommunicationAllowedStateChanged( + public void onAccessAllowedStateChanged( boolean isAllowed) { executor.execute(() -> Binder.withCleanCallingIdentity( - () -> callback.onSatelliteCommunicationAllowedStateChanged( + () -> callback.onAccessAllowedStateChanged( isAllowed))); } @Override - public void onSatelliteAccessConfigurationChanged( + public void onAccessConfigurationChanged( @Nullable SatelliteAccessConfiguration satelliteAccessConfiguration) { executor.execute(() -> Binder.withCleanCallingIdentity( - () -> callback.onSatelliteAccessConfigurationChanged( + () -> callback.onAccessConfigurationChanged( satelliteAccessConfiguration))); } }; - sSatelliteCommunicationAllowedStateCallbackMap.put(callback, internalCallback); - return telephony.registerForCommunicationAllowedStateChanged( + sSatelliteCommunicationAccessStateCallbackMap.put(callback, internalCallback); + return telephony.registerForCommunicationAccessStateChanged( mSubId, internalCallback); } else { throw new IllegalStateException("telephony service is null."); } } catch (RemoteException ex) { - loge("registerForCommunicationAllowedStateChanged() RemoteException: " + ex); + loge("registerForCommunicationAccessStateChanged() RemoteException: " + ex); ex.rethrowAsRuntimeException(); } return SATELLITE_RESULT_REQUEST_FAILED; } /** - * Unregisters for the satellite communication allowed state changed. + * Unregisters for the satellite communication access state changed event. * If callback was not registered before, the request will be ignored. * * @param callback The callback that was passed to - * {@link #registerForCommunicationAllowedStateChanged(Executor, - * SatelliteCommunicationAllowedStateCallback)} + * {@link #registerForCommunicationAccessStateChanged(Executor, + * SatelliteCommunicationAccessStateCallback)} * @throws SecurityException if the caller doesn't have required permission. * @throws IllegalStateException if the Telephony process is not currently available. * @hide @@ -3466,26 +3466,26 @@ public final class SatelliteManager { @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) - public void unregisterForCommunicationAllowedStateChanged( - @NonNull SatelliteCommunicationAllowedStateCallback callback) { + public void unregisterForCommunicationAccessStateChanged( + @NonNull SatelliteCommunicationAccessStateCallback callback) { Objects.requireNonNull(callback); - ISatelliteCommunicationAllowedStateCallback internalCallback = - sSatelliteCommunicationAllowedStateCallbackMap.remove(callback); + ISatelliteCommunicationAccessStateCallback internalCallback = + sSatelliteCommunicationAccessStateCallbackMap.remove(callback); try { ITelephony telephony = getITelephony(); if (telephony != null) { if (internalCallback != null) { - telephony.unregisterForCommunicationAllowedStateChanged(mSubId, + telephony.unregisterForCommunicationAccessStateChanged(mSubId, internalCallback); } else { - loge("unregisterForCommunicationAllowedStateChanged: No internal callback."); + loge("unregisterForCommunicationAccessStateChanged: No internal callback."); } } else { throw new IllegalStateException("telephony service is null."); } } catch (RemoteException ex) { - loge("unregisterForCommunicationAllowedStateChanged() RemoteException: " + ex); + loge("unregisterForCommunicationAccessStateChanged() RemoteException: " + ex); ex.rethrowAsRuntimeException(); } } @@ -3690,6 +3690,11 @@ public final class SatelliteManager { * @param list The list of provisioned satellite subscriber infos. * @param executor The executor on which the callback will be called. * @param callback The callback object to which the result will be delivered. + * If the request is successful, {@link OutcomeReceiver#onResult(Object)} + * will return {@code true}. + * If the request is not successful, + * {@link OutcomeReceiver#onError(Throwable)} will return an error with + * a SatelliteException. * * @throws SecurityException if the caller doesn't have required permission. * @hide @@ -3746,6 +3751,11 @@ public final class SatelliteManager { * @param list The list of deprovisioned satellite subscriber infos. * @param executor The executor on which the callback will be called. * @param callback The callback object to which the result will be delivered. + * If the request is successful, {@link OutcomeReceiver#onResult(Object)} + * will return {@code true}. + * If the request is not successful, + * {@link OutcomeReceiver#onError(Throwable)} will return an error with + * a SatelliteException. * * @throws SecurityException if the caller doesn't have required permission. * @hide diff --git a/telephony/java/android/telephony/satellite/SatellitePosition.java b/telephony/java/android/telephony/satellite/SatellitePosition.java index 46af5c827571..354b729f3854 100644 --- a/telephony/java/android/telephony/satellite/SatellitePosition.java +++ b/telephony/java/android/telephony/satellite/SatellitePosition.java @@ -33,6 +33,7 @@ import java.util.Objects; * Longitude is the angular distance, measured in degrees, east or west of the prime longitude line * ranging from -180 to 180 degrees * Altitude is the distance from the center of the Earth to the satellite, measured in kilometers + * Latitude is not added as only geo stationary satellite are handled for now. * * @hide */ diff --git a/telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.java b/telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.java index 6e33995f1a3c..9d9cac9702bb 100644 --- a/telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.java +++ b/telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.java @@ -47,7 +47,7 @@ public final class SatelliteSubscriberInfo implements Parcelable { /** apn */ private String mNiddApn; - private int mSubId; + private int mSubscriptionId; /** SubscriberId format is the ICCID. */ public static final int SUBSCRIBER_ID_TYPE_ICCID = 0; @@ -75,7 +75,7 @@ public final class SatelliteSubscriberInfo implements Parcelable { this.mSubscriberId = builder.mSubscriberId; this.mCarrierId = builder.mCarrierId; this.mNiddApn = builder.mNiddApn; - this.mSubId = builder.mSubId; + this.mSubscriptionId = builder.mSubscriptionId; this.mSubscriberIdType = builder.mSubscriberIdType; } @@ -87,7 +87,7 @@ public final class SatelliteSubscriberInfo implements Parcelable { private int mCarrierId; @NonNull private String mNiddApn; - private int mSubId; + private int mSubscriptionId; @SubscriberIdType private int mSubscriberIdType; @@ -125,8 +125,8 @@ public final class SatelliteSubscriberInfo implements Parcelable { * Set the subId and returns the Builder class. */ @NonNull - public Builder setSubId(int subId) { - mSubId = subId; + public Builder setSubscriptionId(int subId) { + mSubscriptionId = subId; return this; } @@ -153,7 +153,7 @@ public final class SatelliteSubscriberInfo implements Parcelable { out.writeString(mSubscriberId); out.writeInt(mCarrierId); out.writeString(mNiddApn); - out.writeInt(mSubId); + out.writeInt(mSubscriptionId); out.writeInt(mSubscriberIdType); } @@ -203,8 +203,8 @@ public final class SatelliteSubscriberInfo implements Parcelable { /** * Return the subscriptionId of the subscription which is used for satellite attachment. */ - public int getSubId() { - return mSubId; + public int getSubscriptionId() { + return mSubscriptionId; } /** @@ -231,8 +231,8 @@ public final class SatelliteSubscriberInfo implements Parcelable { sb.append(mNiddApn); sb.append(","); - sb.append("SubId:"); - sb.append(mSubId); + sb.append("SubscriptionId:"); + sb.append(mSubscriptionId); sb.append(","); sb.append("SubscriberIdType:"); @@ -242,7 +242,8 @@ public final class SatelliteSubscriberInfo implements Parcelable { @Override public int hashCode() { - return Objects.hash(mSubscriberId, mCarrierId, mNiddApn, mSubId, mSubscriberIdType); + return Objects.hash( + mSubscriberId, mCarrierId, mNiddApn, mSubscriptionId, mSubscriberIdType); } @Override @@ -251,7 +252,8 @@ public final class SatelliteSubscriberInfo implements Parcelable { if (!(o instanceof SatelliteSubscriberInfo)) return false; SatelliteSubscriberInfo that = (SatelliteSubscriberInfo) o; return Objects.equals(mSubscriberId, that.mSubscriberId) && mCarrierId == that.mCarrierId - && Objects.equals(mNiddApn, that.mNiddApn) && mSubId == that.mSubId + && Objects.equals(mNiddApn, that.mNiddApn) + && mSubscriptionId == that.mSubscriptionId && mSubscriberIdType == that.mSubscriberIdType; } @@ -259,7 +261,7 @@ public final class SatelliteSubscriberInfo implements Parcelable { mSubscriberId = in.readString(); mCarrierId = in.readInt(); mNiddApn = in.readString(); - mSubId = in.readInt(); + mSubscriptionId = in.readInt(); mSubscriberIdType = in.readInt(); } } diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index aa577304c564..08c003027c5b 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -69,7 +69,7 @@ import android.telephony.ims.aidl.IImsRegistrationCallback; import android.telephony.ims.aidl.IRcsConfigCallback; import android.telephony.satellite.INtnSignalStrengthCallback; import android.telephony.satellite.ISatelliteCapabilitiesCallback; -import android.telephony.satellite.ISatelliteCommunicationAllowedStateCallback; +import android.telephony.satellite.ISatelliteCommunicationAccessStateCallback; import android.telephony.satellite.ISatelliteDatagramCallback; import android.telephony.satellite.ISatelliteDisallowedReasonsCallback; import android.telephony.satellite.ISatelliteTransmissionUpdateCallback; @@ -3446,20 +3446,20 @@ interface ITelephony { */ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(" + "android.Manifest.permission.SATELLITE_COMMUNICATION)") - int registerForCommunicationAllowedStateChanged(int subId, - in ISatelliteCommunicationAllowedStateCallback callback); + int registerForCommunicationAccessStateChanged(int subId, + in ISatelliteCommunicationAccessStateCallback callback); /** * Unregisters for satellite communication allowed state. * If callback was not registered before, the request will be ignored. * * @param subId The subId of the subscription to unregister for supported state changed. - * @param callback The callback that was passed to registerForCommunicationAllowedStateChanged. + * @param callback The callback that was passed to registerForCommunicationAccessStateChanged. */ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(" + "android.Manifest.permission.SATELLITE_COMMUNICATION)") - void unregisterForCommunicationAllowedStateChanged(int subId, - in ISatelliteCommunicationAllowedStateCallback callback); + void unregisterForCommunicationAccessStateChanged(int subId, + in ISatelliteCommunicationAccessStateCallback callback); /** * This API can be used by only CTS to override the boolean configs used by the |