diff --git a/Assets/Samples/Visualizers/InputActionVisualizer.cs b/Assets/Samples/Visualizers/InputActionVisualizer.cs index b282a568c4..4f41c8a9b6 100644 --- a/Assets/Samples/Visualizers/InputActionVisualizer.cs +++ b/Assets/Samples/Visualizers/InputActionVisualizer.cs @@ -63,9 +63,12 @@ protected void Update() { base.OnDisable(); - s_EnabledInstances.Remove(this); - if (s_EnabledInstances.Count == 0) - InputSystem.onActionChange -= OnActionChange; + if (s_EnabledInstances != null) + { + s_EnabledInstances.Remove(this); + if (s_EnabledInstances.Count == 0) + InputSystem.onActionChange -= OnActionChange; + } if (m_Visualization == Visualization.Interaction && m_Action != null) { diff --git a/Assets/Samples/Visualizers/InputControlVisualizer.cs b/Assets/Samples/Visualizers/InputControlVisualizer.cs index fd9b70ccaf..9a2950c7ab 100644 --- a/Assets/Samples/Visualizers/InputControlVisualizer.cs +++ b/Assets/Samples/Visualizers/InputControlVisualizer.cs @@ -102,11 +102,14 @@ public int controlIndex if (m_Visualization == Mode.None) return; - s_EnabledInstances.Remove(this); - if (s_EnabledInstances.Count == 0) + if (s_EnabledInstances != null) { - InputSystem.onDeviceChange -= OnDeviceChange; - InputSystem.onEvent -= OnEvent; + s_EnabledInstances.Remove(this); + if (s_EnabledInstances.Count == 0) + { + InputSystem.onDeviceChange -= OnDeviceChange; + InputSystem.onEvent -= OnEvent; + } } m_Control = null; diff --git a/Assets/Tests/InputSystem.Editor/TestData.cs b/Assets/Tests/InputSystem.Editor/TestData.cs index 4621e529a2..9571632ef4 100644 --- a/Assets/Tests/InputSystem.Editor/TestData.cs +++ b/Assets/Tests/InputSystem.Editor/TestData.cs @@ -4,6 +4,7 @@ using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Editor; +using InputAnalytics = UnityEngine.InputSystem.InputAnalytics; using Random = UnityEngine.Random; public static class TestData @@ -33,11 +34,13 @@ public static class TestData }); internal static Generator editorState = - new(() => new InputActionsEditorState(new SerializedObject(ScriptableObject.CreateInstance()))); + new(() => new InputActionsEditorState( + new InputActionsEditorSessionAnalytic(InputActionsEditorSessionAnalytic.Data.Kind.EditorWindow), + new SerializedObject(ScriptableObject.CreateInstance()))); internal static Generator EditorStateWithAsset(ScriptableObject asset) { - return new Generator(() => new InputActionsEditorState(new SerializedObject(asset))); + return new Generator(() => new InputActionsEditorState(null, new SerializedObject(asset))); } public static Generator deviceRequirement = diff --git a/Assets/Tests/InputSystem/CoreTests_Analytics.cs b/Assets/Tests/InputSystem/CoreTests_Analytics.cs index 44893f1851..ef86b4fa64 100644 --- a/Assets/Tests/InputSystem/CoreTests_Analytics.cs +++ b/Assets/Tests/InputSystem/CoreTests_Analytics.cs @@ -1,12 +1,23 @@ // We always send analytics in the editor (though the actual sending may be disabled in Pro) but we // only send analytics in the player if enabled. #if UNITY_ANALYTICS || UNITY_EDITOR +using System; using System.Collections.Generic; using NUnit.Framework; +using Unity.PerformanceTesting.Data; +using UnityEditor; +using UnityEditor.Build.Reporting; using UnityEngine; +using UnityEngine.EventSystems; using UnityEngine.InputSystem; +using UnityEngine.InputSystem.EnhancedTouch; using UnityEngine.InputSystem.Layouts; using UnityEngine.InputSystem.LowLevel; +using UnityEngine.InputSystem.OnScreen; +using UnityEngine.InputSystem.UI; +using Editor = UnityEditor.Editor; +using InputAnalytics = UnityEngine.InputSystem.InputAnalytics; +using Object = UnityEngine.Object; #if UNITY_EDITOR using UnityEngine.InputSystem.Editor; @@ -32,7 +43,12 @@ public void Analytics_ReceivesStartupEventOnFirstUpdate() runtime.onSendAnalyticsEvent = (name, data) => { + #if UNITY_EDITOR && UNITY_2023_2_OR_NEWER + // Registration handled by framework + Assert.That(registeredNames.Count, Is.EqualTo(0)); + #else Assert.That(registeredNames.Contains(name)); + #endif Assert.That(receivedName, Is.Null); receivedName = name; receivedData = data; @@ -64,7 +80,7 @@ public void Analytics_ReceivesStartupEventOnFirstUpdate() InputSystem.Update(); - Assert.That(receivedName, Is.EqualTo(InputAnalytics.kEventStartup)); + Assert.That(receivedName, Is.EqualTo(InputAnalytics.StartupEventAnalytic.kEventName)); Assert.That(receivedData, Is.TypeOf()); var startupData = (InputAnalytics.StartupEventData)receivedData; @@ -174,7 +190,12 @@ public void Analytics_ReceivesEventOnShutdown() runtime.onSendAnalyticsEvent = (name, data) => { +#if UNITY_EDITOR && UNITY_2023_2_OR_NEWER + // Registration handled by framework + Assert.That(registeredNames.Count, Is.EqualTo(0)); +#else Assert.That(registeredNames.Contains(name)); +#endif Assert.That(receivedData, Is.Null); receivedName = name; receivedData = data; @@ -183,7 +204,7 @@ public void Analytics_ReceivesEventOnShutdown() // Simulate shutdown. runtime.onShutdown(); - Assert.That(receivedName, Is.EqualTo(InputAnalytics.kEventShutdown)); + Assert.That(receivedName, Is.EqualTo(InputAnalytics.ShutdownEventDataAnalytic.kEventName)); Assert.That(receivedData, Is.TypeOf()); var shutdownData = (InputAnalytics.ShutdownEventData)receivedData; @@ -205,5 +226,496 @@ public void TODO_Analytics_ReceivesEventOnFirstUserInteraction() { Assert.Fail(); } + + [Test] + [Category("Analytics")] + public void Analytics_ShouldReportEditorSessionAnalytics_IfAccordingToEditorSessionAnalyticsFiniteStateMachine() + { + CollectAnalytics(InputActionsEditorSessionAnalytic.kEventName); + + // Editor session analytics is stateful and instantiated + var session = new InputActionsEditorSessionAnalytic( + InputActionsEditorSessionAnalytic.Data.Kind.EmbeddedInProjectSettings); + + session.Begin(); // the user opens project settings and navigates to Input Actions + session.RegisterEditorFocusIn(); // when window opens, it receives edit focus directly + runtime.currentTime += 5; // the user is just grasping what is on the screen for 5 seconds + session.RegisterActionMapEdit(); // the user adds an action map or renames and action map or deletes one + session.RegisterActionEdit(); // the user adds an action, or renames it, or deletes one or add binding + session.RegisterBindingEdit(); // the user modifies a binding configuration + session.RegisterEditorFocusOut(); // the window looses focus due to user closing e.g. project settings + session.End(); // the window is destroyed and the session ends. + + // Assert: Registration +#if (UNITY_2023_2_OR_NEWER && UNITY_EDITOR) + // Registration is a responsibility of the framework + Assert.That(registeredAnalytics.Count, Is.EqualTo(0)); +#else + Assert.That(registeredAnalytics.Count, Is.EqualTo(1)); + Assert.That(registeredAnalytics[0].name, Is.EqualTo(InputActionsEditorSessionAnalytic.kEventName)); + Assert.That(registeredAnalytics[0].maxPerHour, Is.EqualTo(InputActionsEditorSessionAnalytic.kMaxEventsPerHour)); + Assert.That(registeredAnalytics[0].maxPropertiesPerEvent, Is.EqualTo(InputActionsEditorSessionAnalytic.kMaxNumberOfElements)); +#endif // (UNITY_2023_2_OR_NEWER && UNITY_EDITOR) + + // Assert: Data received + Assert.That(sentAnalyticsEvents.Count, Is.EqualTo(1)); + Assert.That(sentAnalyticsEvents[0].name, Is.EqualTo(InputActionsEditorSessionAnalytic.kEventName)); + Assert.That(sentAnalyticsEvents[0].data, Is.TypeOf()); + + // Assert: Data content + var data = (InputActionsEditorSessionAnalytic.Data)sentAnalyticsEvents[0].data; + Assert.That(data.kind, Is.EqualTo(InputActionsEditorSessionAnalytic.Data.Kind.EmbeddedInProjectSettings)); + Assert.That(data.explicit_save_count, Is.EqualTo(0)); + Assert.That(data.auto_save_count, Is.EqualTo(0)); + Assert.That(data.session_duration_seconds, Is.EqualTo(5.0)); + Assert.That(data.session_focus_duration_seconds, Is.EqualTo(5.0)); + Assert.That(data.session_focus_switch_count, Is.EqualTo(1)); // TODO Unclear name + Assert.That(data.action_map_modification_count, Is.EqualTo(1)); + Assert.That(data.action_modification_count, Is.EqualTo(1)); + Assert.That(data.binding_modification_count, Is.EqualTo(1)); + Assert.That(data.control_scheme_modification_count, Is.EqualTo(0)); + Assert.That(data.reset_count, Is.EqualTo(0)); + } + + private void TestMultipleEditorFocusSessions(InputActionsEditorSessionAnalytic session = null) + { + CollectAnalytics(InputActionsEditorSessionAnalytic.kEventName); + + session.Begin(); // the user opens project settings and navigates to Input Actions + session.RegisterEditorFocusIn(); // when window opens, it receives edit focus directly + runtime.currentTime += 5; // the user is just grasping what is on the screen for 5 seconds + session.RegisterActionMapEdit(); // the user adds an action map or renames and action map or deletes one + session.RegisterActionEdit(); // the user adds an action, or renames it, or deletes one or add binding + session.RegisterBindingEdit(); // the user modifies a binding configuration + session.RegisterControlSchemeEdit();// the user modifies control schemes + session.RegisterEditorFocusOut(); // the window looses focus due to user closing e.g. project settings + session.RegisterAutoSave(); // the asset is saved by automatic trigger + runtime.currentTime += 30; // the user has switched to something else but still has the window open. + session.RegisterEditorFocusIn(); // the user switches back to the window + runtime.currentTime += 2; // the user spends some time in edit focus + session.RegisterBindingEdit(); // the user is editing a binding. + session.RegisterEditorFocusOut(); // the user is dismissing the window and loosing focus + session.RegisterAutoSave(); // the asset is saved by automatic trigger + session.End(); // the window is destroyed and the session ends. + + // Assert: Registration +#if (UNITY_2023_2_OR_NEWER && UNITY_EDITOR) + // Registration is a responsibility of the framework + Assert.That(registeredAnalytics.Count, Is.EqualTo(0)); +#else + Assert.That(registeredAnalytics.Count, Is.EqualTo(1)); + Assert.That(registeredAnalytics[0].name, Is.EqualTo(InputActionsEditorSessionAnalytic.kEventName)); + Assert.That(registeredAnalytics[0].maxPerHour, Is.EqualTo(InputActionsEditorSessionAnalytic.kMaxEventsPerHour)); + Assert.That(registeredAnalytics[0].maxPropertiesPerEvent, Is.EqualTo(InputActionsEditorSessionAnalytic.kMaxNumberOfElements)); +#endif // (UNITY_2023_2_OR_NEWER && UNITY_EDITOR) + + // Assert: Data received + Assert.That(sentAnalyticsEvents.Count, Is.EqualTo(1)); + Assert.That(sentAnalyticsEvents[0].name, Is.EqualTo(InputActionsEditorSessionAnalytic.kEventName)); + Assert.That(sentAnalyticsEvents[0].data, Is.TypeOf()); + + // Assert: Data content + var data = (InputActionsEditorSessionAnalytic.Data)sentAnalyticsEvents[0].data; + Assert.That(data.kind, Is.EqualTo(InputActionsEditorSessionAnalytic.Data.Kind.EmbeddedInProjectSettings)); + Assert.That(data.explicit_save_count, Is.EqualTo(0)); + Assert.That(data.auto_save_count, Is.EqualTo(2)); + Assert.That(data.session_duration_seconds, Is.EqualTo(37.0)); + Assert.That(data.session_focus_duration_seconds, Is.EqualTo(7.0)); + Assert.That(data.session_focus_switch_count, Is.EqualTo(2)); // TODO Unclear name + Assert.That(data.action_map_modification_count, Is.EqualTo(1)); + Assert.That(data.action_modification_count, Is.EqualTo(1)); + Assert.That(data.binding_modification_count, Is.EqualTo(2)); + Assert.That(data.control_scheme_modification_count, Is.EqualTo(1)); + Assert.That(data.reset_count, Is.EqualTo(0)); + } + + [Test] + [Category("Analytics")] + public void Analytics_ShouldReportEditorSessionAnalyticsWithFocusTime_IfHavingMultipleFocusSessionsWithinSession() + { + TestMultipleEditorFocusSessions( + new InputActionsEditorSessionAnalytic(InputActionsEditorSessionAnalytic.Data.Kind.EmbeddedInProjectSettings)); + } + + [Test] + [Category("Analytics")] + public void Analytics_ShouldReportEditorSessionAnalyticsWithFocusTime_WhenActionsDriveImplicitConditions() + { + CollectAnalytics(InputActionsEditorSessionAnalytic.kEventName); + + // Editor session analytics is stateful and instantiated + var session = new InputActionsEditorSessionAnalytic( + InputActionsEditorSessionAnalytic.Data.Kind.EmbeddedInProjectSettings); + + session.Begin(); // the user opens project settings and navigates to Input Actions + // session.RegisterEditorFocusIn(); // assumes we fail to capture focus-in event due to UI framework malfunction + runtime.currentTime += 5; // the user is just grasping what is on the screen for 5 seconds + session.RegisterActionMapEdit(); // the user adds an action map or renames and action map or deletes one + session.RegisterActionMapEdit(); // the user adds an action map or renames and action map or deletes one + session.RegisterBindingEdit(); // the user modifies a binding configuration + runtime.currentTime += 25; // the user spends some time in edit focus + // session.RegisterEditorFocusOut();// assumes we fail to detect focus out event due to UI framework malfunction + session.RegisterExplicitSave(); // the user presses a save button + session.End(); // the window is destroyed and the session ends. + + // Assert: Registration + #if (UNITY_2023_2_OR_NEWER && UNITY_EDITOR) + // Registration is a responsibility of the framework + Assert.That(registeredAnalytics.Count, Is.EqualTo(0)); + #else + Assert.That(registeredAnalytics.Count, Is.EqualTo(1)); + Assert.That(registeredAnalytics[0].name, Is.EqualTo(InputActionsEditorSessionAnalytic.kEventName)); + Assert.That(registeredAnalytics[0].maxPerHour, Is.EqualTo(InputActionsEditorSessionAnalytic.kMaxEventsPerHour)); + Assert.That(registeredAnalytics[0].maxPropertiesPerEvent, Is.EqualTo(InputActionsEditorSessionAnalytic.kMaxNumberOfElements)); + #endif // (UNITY_2023_2_OR_NEWER && UNITY_EDITOR) + + // Assert: Data received + Assert.That(sentAnalyticsEvents.Count, Is.EqualTo(1)); + Assert.That(sentAnalyticsEvents[0].name, Is.EqualTo(InputActionsEditorSessionAnalytic.kEventName)); + Assert.That(sentAnalyticsEvents[0].data, Is.TypeOf()); + + // Assert: Data content + var data = (InputActionsEditorSessionAnalytic.Data)sentAnalyticsEvents[0].data; + Assert.That(data.kind, Is.EqualTo(InputActionsEditorSessionAnalytic.Data.Kind.EmbeddedInProjectSettings)); + Assert.That(data.explicit_save_count, Is.EqualTo(1)); + Assert.That(data.auto_save_count, Is.EqualTo(0)); + Assert.That(data.session_duration_seconds, Is.EqualTo(30.0)); + Assert.That(data.session_focus_duration_seconds, Is.EqualTo(25.0)); + Assert.That(data.session_focus_switch_count, Is.EqualTo(1)); // TODO Unclear name + Assert.That(data.action_map_modification_count, Is.EqualTo(2)); + Assert.That(data.action_modification_count, Is.EqualTo(0)); + Assert.That(data.binding_modification_count, Is.EqualTo(1)); + Assert.That(data.control_scheme_modification_count, Is.EqualTo(0)); + Assert.That(data.reset_count, Is.EqualTo(0)); + } + + [Test] + [Category("Analytics")] + public void Analytics_ShouldReportEditorSessionAnalytics_IfMultipleSessionsAreReportedUsingTheSameInstance() + { + // We reuse an existing test case to prove that the object is reset properly and can be reused after + // ending the session. We currently let CollectAnalytics reset test harness state which is fine for + // the targeted verification aspect since only affecting test harness data. + var session = new InputActionsEditorSessionAnalytic( + InputActionsEditorSessionAnalytic.Data.Kind.EmbeddedInProjectSettings); + + TestMultipleEditorFocusSessions(session); + TestMultipleEditorFocusSessions(session); + } + + [Test] + [Category("Analytics")] + public void Analytics_ShouldReportBuildAnalytics_WhenNotHavingSettingsAsset() + { + CollectAnalytics(InputBuildAnalytic.kEventName); + + var storedSettings = InputSystem.s_Manager.settings; + InputSettings defaultSettings = null; + + try + { + defaultSettings = ScriptableObject.CreateInstance(); + InputSystem.settings = defaultSettings; + + // Simulate a build (note that we cannot create a proper build report) + var processor = new InputBuildAnalytic.ReportProcessor(); + processor.OnPostprocessBuild(null); // Note that we cannot create a report + + // Assert: Data received + Assert.That(sentAnalyticsEvents.Count, Is.EqualTo(1)); + Assert.That(sentAnalyticsEvents[0].name, Is.EqualTo(InputBuildAnalytic.kEventName)); + Assert.That(sentAnalyticsEvents[0].data, Is.TypeOf()); + + // Assert: Data content + var data = (InputBuildAnalytic.InputBuildAnalyticData)sentAnalyticsEvents[0].data; + Assert.That(data.build_guid, Is.EqualTo(string.Empty)); +#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS + Assert.That(data.has_projectwide_input_action_asset, Is.EqualTo(InputSystem.actions != null)); +#else + Assert.That(data.has_projectwide_input_action_asset, Is.False); +#endif + Assert.That(data.has_settings_asset, Is.False); + Assert.That(data.has_default_settings, Is.True); + + Assert.That(data.update_mode, Is.EqualTo(InputBuildAnalytic.InputBuildAnalyticData.UpdateMode.ProcessEventsInDynamicUpdate)); + Assert.That(data.compensate_for_screen_orientation, Is.EqualTo(defaultSettings.compensateForScreenOrientation)); + Assert.That(data.default_deadzone_min, Is.EqualTo(defaultSettings.defaultDeadzoneMin)); + Assert.That(data.default_deadzone_max, Is.EqualTo(defaultSettings.defaultDeadzoneMax)); + Assert.That(data.default_button_press_point, Is.EqualTo(defaultSettings.defaultButtonPressPoint)); + Assert.That(data.button_release_threshold, Is.EqualTo(defaultSettings.buttonReleaseThreshold)); + Assert.That(data.default_tap_time, Is.EqualTo(defaultSettings.defaultTapTime)); + Assert.That(data.default_slow_tap_time, Is.EqualTo(defaultSettings.defaultSlowTapTime)); + Assert.That(data.default_hold_time, Is.EqualTo(defaultSettings.defaultHoldTime)); + Assert.That(data.tap_radius, Is.EqualTo(defaultSettings.tapRadius)); + Assert.That(data.multi_tap_delay_time, Is.EqualTo(defaultSettings.multiTapDelayTime)); + Assert.That(data.background_behavior, Is.EqualTo(InputBuildAnalytic.InputBuildAnalyticData.BackgroundBehavior.ResetAndDisableNonBackgroundDevices)); + Assert.That(data.editor_input_behavior_in_playmode, Is.EqualTo(InputBuildAnalytic.InputBuildAnalyticData.EditorInputBehaviorInPlayMode.PointersAndKeyboardsRespectGameViewFocus)); + Assert.That(data.input_action_property_drawer_mode, Is.EqualTo(InputBuildAnalytic.InputBuildAnalyticData.InputActionPropertyDrawerMode.Compact)); + Assert.That(data.max_event_bytes_per_update, Is.EqualTo(defaultSettings.maxEventBytesPerUpdate)); + Assert.That(data.max_queued_events_per_update, Is.EqualTo(defaultSettings.maxQueuedEventsPerUpdate)); + Assert.That(data.supported_devices, Is.EqualTo(defaultSettings.supportedDevices)); + Assert.That(data.disable_redundant_events_merging, Is.EqualTo(defaultSettings.disableRedundantEventsMerging)); + Assert.That(data.shortcut_keys_consume_input, Is.EqualTo(defaultSettings.shortcutKeysConsumeInput)); + + Assert.That(data.feature_optimized_controls_enabled, Is.EqualTo(defaultSettings.IsFeatureEnabled(InputFeatureNames.kUseOptimizedControls))); + Assert.That(data.feature_read_value_caching_enabled, Is.EqualTo(defaultSettings.IsFeatureEnabled(InputFeatureNames.kUseReadValueCaching))); + Assert.That(data.feature_paranoid_read_value_caching_checks_enabled, Is.EqualTo(defaultSettings.IsFeatureEnabled(InputFeatureNames.kParanoidReadValueCachingChecks))); + Assert.That(data.feature_disable_unity_remote_support, Is.EqualTo(defaultSettings.IsFeatureEnabled(InputFeatureNames.kDisableUnityRemoteSupport))); + Assert.That(data.feature_run_player_updates_in_editmode, Is.EqualTo(defaultSettings.IsFeatureEnabled(InputFeatureNames.kRunPlayerUpdatesInEditMode))); +#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS + Assert.That(data.feature_use_imgui_editor_for_assets, Is.EqualTo(defaultSettings.IsFeatureEnabled(InputFeatureNames.kUseIMGUIEditorForAssets))); +#else + Assert.That(data.feature_use_imgui_editor_for_assets, Is.False); +#endif + } + finally + { + InputSystem.s_Manager.settings = storedSettings; + if (defaultSettings != null) + Object.DestroyImmediate(defaultSettings); + } + } + + [Test] + [Category("Analytics")] + public void Analytics_ShouldReportBuildAnalytics_WhenHavingSettingsAssetWithCustomSettings() + { + CollectAnalytics(InputBuildAnalytic.kEventName); + + var storedSettings = InputSystem.s_Manager.settings; + InputSettings customSettings = null; + + try + { + customSettings = ScriptableObject.CreateInstance(); + customSettings.updateMode = InputSettings.UpdateMode.ProcessEventsInFixedUpdate; + customSettings.compensateForScreenOrientation = true; + customSettings.defaultDeadzoneMin = 0.4f; + customSettings.defaultDeadzoneMax = 0.6f; + customSettings.defaultButtonPressPoint = 0.1f; + customSettings.buttonReleaseThreshold = 0.7f; + customSettings.defaultTapTime = 1.3f; + customSettings.defaultSlowTapTime = 2.3f; + customSettings.defaultHoldTime = 3.3f; + customSettings.tapRadius = 0.1f; + customSettings.multiTapDelayTime = 1.2f; + customSettings.backgroundBehavior = InputSettings.BackgroundBehavior.IgnoreFocus; + customSettings.editorInputBehaviorInPlayMode = + InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView; + + customSettings.inputActionPropertyDrawerMode = + InputSettings.InputActionPropertyDrawerMode.MultilineEffective; + customSettings.maxEventBytesPerUpdate = 11; + customSettings.maxQueuedEventsPerUpdate = 12; + customSettings.supportedDevices = Array.Empty(); + customSettings.disableRedundantEventsMerging = true; + customSettings.shortcutKeysConsumeInput = true; + + customSettings.SetInternalFeatureFlag(InputFeatureNames.kUseOptimizedControls, true); + customSettings.SetInternalFeatureFlag(InputFeatureNames.kParanoidReadValueCachingChecks, true); + customSettings.SetInternalFeatureFlag(InputFeatureNames.kDisableUnityRemoteSupport, true); + customSettings.SetInternalFeatureFlag(InputFeatureNames.kRunPlayerUpdatesInEditMode, true); +#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS + customSettings.SetInternalFeatureFlag(InputFeatureNames.kUseIMGUIEditorForAssets, true); +#endif + customSettings.SetInternalFeatureFlag(InputFeatureNames.kUseReadValueCaching, true); + + InputSystem.settings = customSettings; + + // Simulate a build (note that we cannot create a proper build report) + var processor = new InputBuildAnalytic.ReportProcessor(); + processor.OnPostprocessBuild(null); // Note that we cannot create a report + + // Assert: Data received + Assert.That(sentAnalyticsEvents.Count, Is.EqualTo(1)); + Assert.That(sentAnalyticsEvents[0].name, Is.EqualTo(InputBuildAnalytic.kEventName)); + Assert.That(sentAnalyticsEvents[0].data, Is.TypeOf()); + + // Assert: Data content + var data = (InputBuildAnalytic.InputBuildAnalyticData)sentAnalyticsEvents[0].data; + + Assert.That(data.build_guid, Is.EqualTo(string.Empty)); +#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS + Assert.That(data.has_projectwide_input_action_asset, Is.EqualTo(InputSystem.actions != null)); +#else + Assert.That(data.has_projectwide_input_action_asset, Is.False); +#endif + Assert.That(data.has_settings_asset, Is.False); // Note: We just don't write any file in this test, hence false + Assert.That(data.has_default_settings, Is.False); + + Assert.That(data.update_mode, Is.EqualTo(InputBuildAnalytic.InputBuildAnalyticData.UpdateMode.ProcessEventsInFixedUpdate)); + Assert.That(data.compensate_for_screen_orientation, Is.EqualTo(true)); + Assert.That(data.default_deadzone_min, Is.EqualTo(0.4f)); + Assert.That(data.default_deadzone_max, Is.EqualTo(0.6f)); + Assert.That(data.default_button_press_point, Is.EqualTo(0.1f)); + Assert.That(data.button_release_threshold, Is.EqualTo(0.7f)); + Assert.That(data.default_tap_time, Is.EqualTo(1.3f)); + Assert.That(data.default_slow_tap_time, Is.EqualTo(2.3f)); + Assert.That(data.default_hold_time, Is.EqualTo(3.3f)); + Assert.That(data.tap_radius, Is.EqualTo(customSettings.tapRadius)); + Assert.That(data.multi_tap_delay_time, Is.EqualTo(customSettings.multiTapDelayTime)); + Assert.That(data.background_behavior, Is.EqualTo(InputBuildAnalytic.InputBuildAnalyticData.BackgroundBehavior.IgnoreFocus)); + Assert.That(data.editor_input_behavior_in_playmode, Is.EqualTo(InputBuildAnalytic.InputBuildAnalyticData.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView)); + Assert.That(data.input_action_property_drawer_mode, Is.EqualTo(InputBuildAnalytic.InputBuildAnalyticData.InputActionPropertyDrawerMode.MultilineEffective)); + Assert.That(data.max_event_bytes_per_update, Is.EqualTo(customSettings.maxEventBytesPerUpdate)); + Assert.That(data.max_queued_events_per_update, Is.EqualTo(customSettings.maxQueuedEventsPerUpdate)); + Assert.That(data.supported_devices, Is.EqualTo(customSettings.supportedDevices)); + Assert.That(data.disable_redundant_events_merging, Is.EqualTo(customSettings.disableRedundantEventsMerging)); + Assert.That(data.shortcut_keys_consume_input, Is.EqualTo(customSettings.shortcutKeysConsumeInput)); + + Assert.That(data.feature_optimized_controls_enabled, Is.True); + Assert.That(data.feature_read_value_caching_enabled, Is.True); + Assert.That(data.feature_paranoid_read_value_caching_checks_enabled, Is.True); + Assert.That(data.feature_disable_unity_remote_support, Is.True); + Assert.That(data.feature_run_player_updates_in_editmode, Is.True); +#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS + Assert.That(data.feature_use_imgui_editor_for_assets, Is.True); +#else + Assert.That(data.feature_use_imgui_editor_for_assets, Is.False); // No impact +#endif + } + finally + { + InputSystem.s_Manager.settings = storedSettings; + if (customSettings != null) + Object.DestroyImmediate(customSettings); + } + } + + [TestCase(InputSystemComponent.PlayerInput, typeof(PlayerInput))] + [TestCase(InputSystemComponent.PlayerInputManager, typeof(PlayerInputManager))] + [TestCase(InputSystemComponent.InputSystemUIInputModule, typeof(InputSystemUIInputModule))] + [TestCase(InputSystemComponent.StandaloneInputModule, typeof(StandaloneInputModule))] + [TestCase(InputSystemComponent.VirtualMouseInput, typeof(VirtualMouseInput))] + [TestCase(InputSystemComponent.TouchSimulation, typeof(TouchSimulation))] + [TestCase(InputSystemComponent.OnScreenButton, typeof(OnScreenButton))] + [TestCase(InputSystemComponent.OnScreenStick, typeof(OnScreenStick))] + [Category("Analytics")] + public void Analytics_ShouldReportComponentAnalytics_WhenEditorIsCreatedAndDestroyed( + InputSystemComponent componentEnum, Type componentType) + { + CollectAnalytics(InputComponentEditorAnalytic.kEventName); + + using (var gameObject = Scoped.Object(new GameObject())) + { + var component = gameObject.value.AddComponent(componentType); + Object.DestroyImmediate(Editor.CreateEditor(component)); + + // Assert: Data received + Assert.That(sentAnalyticsEvents.Count, Is.EqualTo(1)); + Assert.That(sentAnalyticsEvents[0].name, Is.EqualTo(InputComponentEditorAnalytic.kEventName)); + Assert.That(sentAnalyticsEvents[0].data, Is.TypeOf()); + + // Assert: Data content + var data = (InputComponentEditorAnalytic.Data)sentAnalyticsEvents[0].data; + Assert.That(data.component, Is.EqualTo(componentEnum)); + } + } + + [Test] + [Category("Analytics")] + public void Analytics_ShouldReportPlayerInputData() + { + CollectAnalytics(PlayerInputEditorAnalytic.kEventName); + + using (var gameObject = Scoped.Object(new GameObject())) + { + var playerInput = gameObject.value.AddComponent(); + Object.DestroyImmediate(Editor.CreateEditor(playerInput)); + + // Assert: Data received + Assert.That(sentAnalyticsEvents.Count, Is.EqualTo(1)); + Assert.That(sentAnalyticsEvents[0].name, Is.EqualTo(PlayerInputEditorAnalytic.kEventName)); + Assert.That(sentAnalyticsEvents[0].data, Is.TypeOf()); + + // Assert: Data content + var data = (PlayerInputEditorAnalytic.Data)sentAnalyticsEvents[0].data; + Assert.That(data.behavior, Is.EqualTo(InputEditorAnalytics.PlayerNotificationBehavior.SendMessages)); + Assert.That(data.has_actions, Is.False); + Assert.That(data.has_default_map, Is.False); + Assert.That(data.has_ui_input_module, Is.False); + Assert.That(data.has_camera, Is.False); + } + } + + [Test] + [Category("Analytics")] + public void Analytics_ShouldReportPlayerInputManagerData() + { + CollectAnalytics(PlayerInputManagerEditorAnalytic.kEventName); + + using (var gameObject = Scoped.Object(new GameObject())) + { + var playerInputManager = gameObject.value.AddComponent(); + Object.DestroyImmediate(Editor.CreateEditor(playerInputManager)); + + // Assert: Data received + Assert.That(sentAnalyticsEvents.Count, Is.EqualTo(1)); + Assert.That(sentAnalyticsEvents[0].name, Is.EqualTo(PlayerInputManagerEditorAnalytic.kEventName)); + Assert.That(sentAnalyticsEvents[0].data, Is.TypeOf()); + + // Assert: Data content + var data = (PlayerInputManagerEditorAnalytic.Data)sentAnalyticsEvents[0].data; + Assert.That(data.behavior, Is.EqualTo(InputEditorAnalytics.PlayerNotificationBehavior.SendMessages)); + Assert.That(data.join_behavior, Is.EqualTo(PlayerInputManagerEditorAnalytic.Data.PlayerJoinBehavior.JoinPlayersWhenButtonIsPressed)); + Assert.That(data.joining_enabled_by_default, Is.True); + Assert.That(data.max_player_count, Is.EqualTo(-1)); + } + } + +#if UNITY_INPUT_SYSTEM_ENABLE_UI + [Test] + [Category("Analytics")] + public void Analytics_ShouldReportOnScreenStickData() + { + CollectAnalytics(OnScreenStickEditorAnalytic.kEventName); + + using (var gameObject = Scoped.Object(new GameObject())) + { + var onScreenStick = gameObject.value.AddComponent(); + Object.DestroyImmediate(Editor.CreateEditor(onScreenStick)); + + // Assert: Data received + Assert.That(sentAnalyticsEvents.Count, Is.EqualTo(1)); + Assert.That(sentAnalyticsEvents[0].name, Is.EqualTo(OnScreenStickEditorAnalytic.kEventName)); + Assert.That(sentAnalyticsEvents[0].data, Is.TypeOf()); + + // Assert: Data content + var data = (OnScreenStickEditorAnalytic.Data)sentAnalyticsEvents[0].data; + Assert.That(data.behavior, Is.EqualTo(OnScreenStickEditorAnalytic.Data.OnScreenStickBehaviour.RelativePositionWithStaticOrigin)); + Assert.That(data.movement_range, Is.EqualTo(50.0f)); + Assert.That(data.dynamic_origin_range, Is.EqualTo(100.0f)); + Assert.That(data.use_isolated_input_actions, Is.False); + } + } + + [Test] + [Category("Analytics")] + public void Analytics_ShouldReportVirtualMouseInputData() + { + CollectAnalytics(VirtualMouseInputEditorAnalytic.kEventName); + + using (var gameObject = Scoped.Object(new GameObject())) + { + var virtualMouseInput = gameObject.value.AddComponent(); + Object.DestroyImmediate(Editor.CreateEditor(virtualMouseInput)); + + // Assert: Data received + Assert.That(sentAnalyticsEvents.Count, Is.EqualTo(1)); + Assert.That(sentAnalyticsEvents[0].name, Is.EqualTo(VirtualMouseInputEditorAnalytic.kEventName)); + Assert.That(sentAnalyticsEvents[0].data, Is.TypeOf()); + + // Assert: Data content + var data = (VirtualMouseInputEditorAnalytic.Data)sentAnalyticsEvents[0].data; + Assert.That(data.cursor_mode, Is.EqualTo(VirtualMouseInputEditorAnalytic.Data.CursorMode.SoftwareCursor)); + Assert.That(data.cursor_speed, Is.EqualTo(400.0f)); + Assert.That(data.scroll_speed, Is.EqualTo(45.0f)); + } + } + +#endif // #if UNITY_INPUT_SYSTEM_ENABLE_UI + + // Note: Currently not testing proper analytics reporting when editor is enabled/disabled since unclear how + // to achieve this with test framework. This would be a good future improvement. } #endif // UNITY_ANALYTICS || UNITY_EDITOR diff --git a/Packages/com.unity.inputsystem/CHANGELOG.md b/Packages/com.unity.inputsystem/CHANGELOG.md index e3a7d8321b..e88018cd0f 100644 --- a/Packages/com.unity.inputsystem/CHANGELOG.md +++ b/Packages/com.unity.inputsystem/CHANGELOG.md @@ -35,11 +35,17 @@ however, it has to be formatted properly to pass verification tests. - Fixed the UI generation of enum fields when editing interactions of action properties. The new selected value was lost when saving. - Fixed the UI generation of custom interactions of action properties when it rely on OnGUI callback. [ISXB-886](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-886). - Fixed deletion of last composite part raising an exception. [ISXB-804](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-804) +- Fixed an issue related to Visualizers sample where exceptions would be thrown by InputActionVisualizer and InputControlVisualizer when entering play-mode if added as components to a new `GameObject`. ### Added - Added additional device information when logging the error due to exceeding the maximum number of events processed set by `InputSystem.settings.maxEventsBytesPerUpdate`. This additional information is available in development builds only. +- Fixed deletion of last composite part raising an exception. [ISXB-804](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-804) +- Expanded editor and build insight analytics to cover ``.inputactions` asset editor usage, `InputSettings` and common component configurations. + +### Changed +- Changed `DualSenseHIDInputReport` from internal to public visibility - Added Input Setting option allowing to keep platform-specific scroll wheel input values instead of automatically converting them to a normalized range. ## [1.8.2] - 2024-04-29 diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics.meta b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics.meta new file mode 100644 index 0000000000..cd23c10482 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d7fcb545eb7743c8bd27f341b51151dc +timeCreated: 1719232353 \ No newline at end of file diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputActionsEditorSessionAnalytic.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputActionsEditorSessionAnalytic.cs new file mode 100644 index 0000000000..2733dd0fca --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputActionsEditorSessionAnalytic.cs @@ -0,0 +1,328 @@ +#if UNITY_EDITOR +using System; +using UnityEditor; +using UnityEngine.InputSystem.LowLevel; +using UnityEngine.Serialization; + +namespace UnityEngine.InputSystem.Editor +{ + /// + /// Analytics record for tracking engagement with Input Action Asset editor(s). + /// +#if UNITY_2023_2_OR_NEWER + [UnityEngine.Analytics.AnalyticInfo(eventName: kEventName, maxEventsPerHour: kMaxEventsPerHour, + maxNumberOfElements: kMaxNumberOfElements, vendorKey: UnityEngine.InputSystem.InputAnalytics.kVendorKey)] +#endif // UNITY_2023_2_OR_NEWER + internal class InputActionsEditorSessionAnalytic : UnityEngine.InputSystem.InputAnalytics.IInputAnalytic + { + public const string kEventName = "input_actionasset_editor_closed"; + public const int kMaxEventsPerHour = 100; // default: 1000 + public const int kMaxNumberOfElements = 100; // default: 1000 + + /// + /// Construct a new InputActionsEditorSession record of the given type. + /// + /// The editor type for which this record is valid. + public InputActionsEditorSessionAnalytic(Data.Kind kind) + { + if (kind == Data.Kind.Invalid) + throw new ArgumentException(nameof(kind)); + + Initialize(kind); + } + + /// + /// Register that an action map edit has occurred. + /// + public void RegisterActionMapEdit() + { + if (ImplicitFocus()) + ++m_Data.action_map_modification_count; + } + + /// + /// Register that an action edit has occurred. + /// + public void RegisterActionEdit() + { + if (ImplicitFocus() && ComputeDuration() > 0.5) // Avoid logging actions triggered via UI initialization + ++m_Data.action_modification_count; + } + + /// + /// Register than a binding edit has occurred. + /// + public void RegisterBindingEdit() + { + if (ImplicitFocus()) + ++m_Data.binding_modification_count; + } + + /// + /// Register that a control scheme edit has occurred. + /// + public void RegisterControlSchemeEdit() + { + if (ImplicitFocus()) + ++m_Data.control_scheme_modification_count; + } + + /// + /// Register that the editor has received focus which is expected to reflect that the user + /// is currently exploring or editing it. + /// + public void RegisterEditorFocusIn() + { + if (!hasSession || hasFocus) + return; + + m_FocusStart = currentTime; + } + + /// + /// Register that the editor has lost focus which is expected to reflect that the user currently + /// has the attention elsewhere. + /// + /// + /// Calling this method without having an ongoing session and having focus will not have any effect. + /// + public void RegisterEditorFocusOut() + { + if (!hasSession || !hasFocus) + return; + + var duration = currentTime - m_FocusStart; + m_FocusStart = float.NaN; + m_Data.session_focus_duration_seconds += (float)duration; + ++m_Data.session_focus_switch_count; + } + + /// + /// Register a user-event related to explicitly saving in the editor, e.g. + /// using a button, menu or short-cut to trigger the save command. + /// + public void RegisterExplicitSave() + { + if (!hasSession) + return; // No pending session + + ++m_Data.explicit_save_count; + } + + /// + /// Register a user-event related to implicitly saving in the editor, e.g. + /// by having auto-save enabled and indirectly saving the associated asset. + /// + public void RegisterAutoSave() + { + if (!hasSession) + return; // No pending session + + ++m_Data.auto_save_count; + } + + /// + /// Register a user-event related to resetting the editor action configuration to defaults. + /// + public void RegisterReset() + { + if (!hasSession) + return; // No pending session + + ++m_Data.reset_count; + } + + /// + /// Begins a new session if the session has not already been started. + /// + /// + /// If the session has already been started due to a previous call to without + /// a call to this method has no effect. + /// + public void Begin() + { + if (hasSession) + return; // Session already started. + + m_SessionStart = currentTime; + } + + /// + /// Ends the current session. + /// + /// + /// If the session has not previously been started via a call to calling this + /// method has no effect. + /// + public void End() + { + if (!hasSession) + return; // No pending session + + // Make sure we register focus out if failed to capture or not invoked + if (hasFocus) + RegisterEditorFocusOut(); + + // Compute and record total session duration + var duration = ComputeDuration(); + m_Data.session_duration_seconds += duration; + + // Sanity check data, if less than a second its likely a glitch so avoid sending incorrect data + // Send analytics event + if (duration >= 1.0) + runtime.SendAnalytic(this); + + // Reset to allow instance to be reused + Initialize(m_Data.kind); + } + + #region IInputAnalytic Interface + +#if UNITY_EDITOR && UNITY_2023_2_OR_NEWER + public bool TryGatherData(out UnityEngine.Analytics.IAnalytic.IData data, out Exception error) +#else + public bool TryGatherData(out InputAnalytics.IInputAnalyticData data, out Exception error) +#endif + { + if (!isValid) + { + data = null; + error = new Exception("Unable to gather data without a valid session"); + return false; + } + + data = this.m_Data; + error = null; + return true; + } + + public InputAnalytics.InputAnalyticInfo info => new InputAnalytics.InputAnalyticInfo(kEventName, kMaxEventsPerHour, kMaxNumberOfElements); + + #endregion + + private double ComputeDuration() => hasSession ? currentTime - m_SessionStart : 0.0; + + private void Initialize(Data.Kind kind) + { + m_FocusStart = float.NaN; + m_SessionStart = float.NaN; + + m_Data = new Data(kind); + } + + private bool ImplicitFocus() + { + if (!hasSession) + return false; + if (!hasFocus) + RegisterEditorFocusIn(); + return true; + } + + private Data m_Data; + private double m_FocusStart; + private double m_SessionStart; + + private static IInputRuntime runtime => InputSystem.s_Manager.m_Runtime; + private bool hasFocus => !double.IsNaN(m_FocusStart); + private bool hasSession => !double.IsNaN(m_SessionStart); + // Returns current time since startup. Note that IInputRuntime explicitly defines in interface that + // IInputRuntime.currentTime corresponds to EditorApplication.timeSinceStartup in editor. + private double currentTime => runtime.currentTime; + private bool isValid => m_Data.session_duration_seconds >= 0; + + [Serializable] + public struct Data : UnityEngine.InputSystem.InputAnalytics.IInputAnalyticData + { + /// + /// Represents an editor type. + /// + /// + /// This may be added to in the future but items may never be removed. + /// + [Serializable] + public enum Kind + { + Invalid = 0, + EditorWindow = 1, + EmbeddedInProjectSettings = 2 + } + + /// + /// Constructs a InputActionsEditorSessionData. + /// + /// Specifies the kind of editor metrics is being collected for. + public Data(Kind kind) + { + this.kind = kind; + session_duration_seconds = 0; + session_focus_duration_seconds = 0; + session_focus_switch_count = 0; + action_map_modification_count = 0; + action_modification_count = 0; + binding_modification_count = 0; + explicit_save_count = 0; + auto_save_count = 0; + reset_count = 0; + control_scheme_modification_count = 0; + } + + /// + /// Specifies what kind of Input Actions editor this event represents. + /// + public Kind kind; + + /// + /// The total duration for the session, i.e. the duration during which the editor window was open. + /// + public double session_duration_seconds; + + /// + /// The total duration for which the editor window was open and had focus. + /// + public double session_focus_duration_seconds; + + /// + /// Specifies the number of times the window has transitioned from not having focus to having focus in a single session. + /// + public int session_focus_switch_count; + + /// + /// The total number of action map modifications during the session. + /// + public int action_map_modification_count; + + /// + /// The total number of action modifications during the session. + /// + public int action_modification_count; + + /// The total number of binding modifications during the session. + /// + public int binding_modification_count; + + /// + /// The total number of controls scheme modifications during the session. + /// + public int control_scheme_modification_count; + + /// + /// The total number of explicit saves during the session, i.e. as in user-initiated save. + /// + public int explicit_save_count; + + /// + /// The total number of automatic saves during the session, i.e. as in auto-save on close or focus-lost. + /// + public int auto_save_count; + + /// + /// The total number of user-initiated resets during the session, i.e. as in using Reset option in menu. + /// + public int reset_count; + + public bool isValid => kind != Kind.Invalid && session_duration_seconds >= 0; + } + } +} +#endif diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputActionsEditorSessionAnalytic.cs.meta b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputActionsEditorSessionAnalytic.cs.meta new file mode 100644 index 0000000000..6b12f5c217 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputActionsEditorSessionAnalytic.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d771fd88f0934b4dbe724b2690a9f330 +timeCreated: 1719312605 \ No newline at end of file diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputBuildAnalytic.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputBuildAnalytic.cs new file mode 100644 index 0000000000..e2860d398e --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputBuildAnalytic.cs @@ -0,0 +1,400 @@ +#if UNITY_EDITOR +using System; +using UnityEditor; +using UnityEditor.Build; +using UnityEditor.Build.Content; +using UnityEditor.Build.Reporting; +using UnityEngine.Serialization; + +namespace UnityEngine.InputSystem.Editor +{ + /// + /// Analytics for tracking Player Input component user engagement in the editor. + /// +#if UNITY_2023_2_OR_NEWER + [UnityEngine.Analytics.AnalyticInfo(eventName: kEventName, maxEventsPerHour: kMaxEventsPerHour, + maxNumberOfElements: kMaxNumberOfElements, vendorKey: UnityEngine.InputSystem.InputAnalytics.kVendorKey)] +#endif // UNITY_2023_2_OR_NEWER + internal class InputBuildAnalytic : UnityEngine.InputSystem.InputAnalytics.IInputAnalytic + { + public const string kEventName = "input_build_completed"; + public const int kMaxEventsPerHour = 100; // default: 1000 + public const int kMaxNumberOfElements = 100; // default: 1000 + + private readonly BuildReport m_BuildReport; + + public InputBuildAnalytic(BuildReport buildReport) + { + m_BuildReport = buildReport; + } + + public InputAnalytics.InputAnalyticInfo info => + new InputAnalytics.InputAnalyticInfo(kEventName, kMaxEventsPerHour, kMaxNumberOfElements); + +#if UNITY_EDITOR && UNITY_2023_2_OR_NEWER + public bool TryGatherData(out UnityEngine.Analytics.IAnalytic.IData data, out Exception error) +#else + public bool TryGatherData(out InputAnalytics.IInputAnalyticData data, out Exception error) +#endif + { + InputSettings defaultSettings = null; + try + { + defaultSettings = ScriptableObject.CreateInstance(); + data = new InputBuildAnalyticData(m_BuildReport, InputSystem.settings, defaultSettings); + error = null; + return true; + } + catch (Exception e) + { + data = null; + error = e; + return false; + } + finally + { + if (defaultSettings != null) + Object.DestroyImmediate(defaultSettings); + } + } + + /// + /// Input system build analytics data structure. + /// + [Serializable] + internal struct InputBuildAnalyticData : UnityEngine.InputSystem.InputAnalytics.IInputAnalyticData + { + #region InputSettings + + [Serializable] + public enum UpdateMode + { + ProcessEventsInBothFixedAndDynamicUpdate = 0, // Note: Deprecated + ProcessEventsInDynamicUpdate = 1, + ProcessEventsInFixedUpdate = 2, + ProcessEventsManually = 3, + } + + [Serializable] + public enum BackgroundBehavior + { + ResetAndDisableNonBackgroundDevices = 0, + ResetAndDisableAllDevices = 1, + IgnoreFocus = 2 + } + + [Serializable] + public enum EditorInputBehaviorInPlayMode + { + PointersAndKeyboardsRespectGameViewFocus = 0, + AllDevicesRespectGameViewFocus = 1, + AllDeviceInputAlwaysGoesToGameView = 2 + } + + [Serializable] + public enum InputActionPropertyDrawerMode + { + Compact = 0, + MultilineEffective = 1, + MultilineBoth = 2 + } + + public InputBuildAnalyticData(BuildReport report, InputSettings settings, InputSettings defaultSettings) + { + switch (settings.updateMode) + { + case 0: // ProcessEventsInBothFixedAndDynamicUpdate (deprecated/removed) + update_mode = UpdateMode.ProcessEventsInBothFixedAndDynamicUpdate; + break; + case InputSettings.UpdateMode.ProcessEventsManually: + update_mode = UpdateMode.ProcessEventsManually; + break; + case InputSettings.UpdateMode.ProcessEventsInDynamicUpdate: + update_mode = UpdateMode.ProcessEventsInDynamicUpdate; + break; + case InputSettings.UpdateMode.ProcessEventsInFixedUpdate: + update_mode = UpdateMode.ProcessEventsInFixedUpdate; + break; + default: + throw new Exception("Unsupported updateMode"); + } + + switch (settings.backgroundBehavior) + { + case InputSettings.BackgroundBehavior.IgnoreFocus: + background_behavior = BackgroundBehavior.IgnoreFocus; + break; + case InputSettings.BackgroundBehavior.ResetAndDisableAllDevices: + background_behavior = BackgroundBehavior.ResetAndDisableAllDevices; + break; + case InputSettings.BackgroundBehavior.ResetAndDisableNonBackgroundDevices: + background_behavior = BackgroundBehavior.ResetAndDisableNonBackgroundDevices; + break; + default: + throw new Exception("Unsupported background behavior"); + } + + switch (settings.editorInputBehaviorInPlayMode) + { + case InputSettings.EditorInputBehaviorInPlayMode.PointersAndKeyboardsRespectGameViewFocus: + editor_input_behavior_in_playmode = EditorInputBehaviorInPlayMode + .PointersAndKeyboardsRespectGameViewFocus; + break; + case InputSettings.EditorInputBehaviorInPlayMode.AllDevicesRespectGameViewFocus: + editor_input_behavior_in_playmode = EditorInputBehaviorInPlayMode + .AllDevicesRespectGameViewFocus; + break; + case InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView: + editor_input_behavior_in_playmode = EditorInputBehaviorInPlayMode + .AllDeviceInputAlwaysGoesToGameView; + break; + default: + throw new Exception("Unsupported editor background behavior"); + } + + switch (settings.inputActionPropertyDrawerMode) + { + case InputSettings.InputActionPropertyDrawerMode.Compact: + input_action_property_drawer_mode = InputActionPropertyDrawerMode.Compact; + break; + case InputSettings.InputActionPropertyDrawerMode.MultilineBoth: + input_action_property_drawer_mode = InputActionPropertyDrawerMode.MultilineBoth; + break; + case InputSettings.InputActionPropertyDrawerMode.MultilineEffective: + input_action_property_drawer_mode = InputActionPropertyDrawerMode.MultilineEffective; + break; + default: + throw new Exception("Unsupported editor property drawer mode"); + } + +#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS + var inputSystemActions = InputSystem.actions; + var actionsPath = inputSystemActions == null ? null : AssetDatabase.GetAssetPath(inputSystemActions); + has_projectwide_input_action_asset = !string.IsNullOrEmpty(actionsPath); +#else + has_projectwide_input_action_asset = false; +#endif + + var settingsPath = settings == null ? null : AssetDatabase.GetAssetPath(settings); + has_settings_asset = !string.IsNullOrEmpty(settingsPath); + + compensate_for_screen_orientation = settings.compensateForScreenOrientation; + default_deadzone_min = settings.defaultDeadzoneMin; + default_deadzone_max = settings.defaultDeadzoneMax; + default_button_press_point = settings.defaultButtonPressPoint; + button_release_threshold = settings.buttonReleaseThreshold; + default_tap_time = settings.defaultTapTime; + default_slow_tap_time = settings.defaultSlowTapTime; + default_hold_time = settings.defaultHoldTime; + tap_radius = settings.tapRadius; + multi_tap_delay_time = settings.multiTapDelayTime; + max_event_bytes_per_update = settings.maxEventBytesPerUpdate; + max_queued_events_per_update = settings.maxQueuedEventsPerUpdate; + supported_devices = settings.supportedDevices.ToArray(); + disable_redundant_events_merging = settings.disableRedundantEventsMerging; + shortcut_keys_consume_input = settings.shortcutKeysConsumeInput; + + feature_optimized_controls_enabled = settings.IsFeatureEnabled(InputFeatureNames.kUseOptimizedControls); + feature_read_value_caching_enabled = settings.IsFeatureEnabled(InputFeatureNames.kUseReadValueCaching); + feature_paranoid_read_value_caching_checks_enabled = + settings.IsFeatureEnabled(InputFeatureNames.kParanoidReadValueCachingChecks); + +#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS + feature_use_imgui_editor_for_assets = + settings.IsFeatureEnabled(InputFeatureNames.kUseIMGUIEditorForAssets); +#else + feature_use_imgui_editor_for_assets = false; +#endif + feature_disable_unity_remote_support = + settings.IsFeatureEnabled(InputFeatureNames.kDisableUnityRemoteSupport); + feature_run_player_updates_in_editmode = + settings.IsFeatureEnabled(InputFeatureNames.kRunPlayerUpdatesInEditMode); + + has_default_settings = InputSettings.AreEqual(settings, defaultSettings); + + build_guid = report != null ? report.summary.guid.ToString() : string.Empty; // Allows testing + } + + /// + /// Represents and indicates how the project handles updates. + /// + public UpdateMode update_mode; + + /// + /// Represents and if true automatically + /// adjust rotations when the screen orientation changes. + /// + public bool compensate_for_screen_orientation; + + /// + /// Represents which determines what happens when application + /// focus changes and how the system handle input while running in the background. + /// + public BackgroundBehavior background_behavior; + + // Note: InputSettings.filterNoiseOnCurrent not present since already deprecated when these analytics + // where added. + + /// + /// Represents + /// + public float default_deadzone_min; + + /// + /// Represents + /// + public float default_deadzone_max; + + /// + /// Represents + /// + public float default_button_press_point; + + /// + /// Represents + /// + public float button_release_threshold; + + /// + /// Represents + /// + public float default_tap_time; + + /// + /// Represents + /// + public float default_slow_tap_time; + + /// + /// Represents + /// + public float default_hold_time; + + /// + /// Represents + /// + public float tap_radius; + + /// + /// Represents + /// + public float multi_tap_delay_time; + + /// + /// Represents + /// + public EditorInputBehaviorInPlayMode editor_input_behavior_in_playmode; + + /// + /// Represents + /// + public InputActionPropertyDrawerMode input_action_property_drawer_mode; + + /// + /// Represents + /// + public int max_event_bytes_per_update; + + /// + /// Represents + /// + public int max_queued_events_per_update; + + /// + /// Represents + /// + public string[] supported_devices; + + /// + /// Represents + /// + public bool disable_redundant_events_merging; + + /// + /// Represents + /// + public bool shortcut_keys_consume_input; + + #endregion + + #region Feature flag settings + + /// + /// Represents internal feature flag as defined + /// in Input System 1.8.x. + /// + public bool feature_optimized_controls_enabled; + + /// + /// Represents internal feature flag as defined + /// in Input System 1.8.x. + /// + public bool feature_read_value_caching_enabled; + + /// + /// Represents internal feature flag + /// as defined in InputSystem 1.8.x. + /// + public bool feature_paranoid_read_value_caching_checks_enabled; + + /// + /// Represents internal feature flag + /// as defined in InputSystem 1.8.x. + /// + public bool feature_use_imgui_editor_for_assets; + + /// + /// Represents internal feature flag + /// as defined in InputSystem 1.8.x. + /// + public bool feature_disable_unity_remote_support; + + /// + /// Represents internal feature flag + /// as defined in InputSystem 1.8.x. + /// + public bool feature_run_player_updates_in_editmode; + + #endregion + + #region + + /// + /// Specifies whether the project is using a project-wide input actions asset or not. + /// + public bool has_projectwide_input_action_asset; + + /// + /// Specifies whether the project is using a user-provided settings asset or not. + /// + public bool has_settings_asset; + + /// + /// Specifies whether the settings asset (if present) of the built project is equal to default settings + /// or not. In case of no settings asset this is also true since implicitly using default settings. + /// + public bool has_default_settings; + + /// + /// A unique GUID identifying the build. + /// + public string build_guid; + + #endregion + } + + /// + /// Input System build analytics. + /// + internal class ReportProcessor : IPostprocessBuildWithReport + { + public int callbackOrder => int.MaxValue; + + public void OnPostprocessBuild(BuildReport report) + { + InputSystem.s_Manager?.m_Runtime?.SendAnalytic(new InputBuildAnalytic(report)); + } + } + } +} +#endif diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputBuildAnalytic.cs.meta b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputBuildAnalytic.cs.meta new file mode 100644 index 0000000000..ab45fb3bf8 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputBuildAnalytic.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f760afcbd6744a0e8c9d0b7039dda306 +timeCreated: 1719312637 \ No newline at end of file diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputComponentEditorAnalytic.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputComponentEditorAnalytic.cs new file mode 100644 index 0000000000..d559e7618c --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputComponentEditorAnalytic.cs @@ -0,0 +1,89 @@ +#if UNITY_EDITOR +using System; + +namespace UnityEngine.InputSystem.Editor +{ + /// + /// Enumeration type identifying a Input System MonoBehavior component. + /// + [Serializable] + internal enum InputSystemComponent + { + // Feature components + PlayerInput = 1, + PlayerInputManager = 2, + OnScreenStick = 3, + OnScreenButton = 4, + VirtualMouseInput = 5, + + // Debug components + TouchSimulation = 1000, + + // Integration components + StandaloneInputModule = 2000, + InputSystemUIInputModule = 2001, + } + + /// + /// Analytics record for tracking engagement with Input Component editor(s). + /// +#if UNITY_2023_2_OR_NEWER + [UnityEngine.Analytics.AnalyticInfo(eventName: kEventName, maxEventsPerHour: kMaxEventsPerHour, + maxNumberOfElements: kMaxNumberOfElements, vendorKey: UnityEngine.InputSystem.InputAnalytics.kVendorKey)] +#endif // UNITY_2023_2_OR_NEWER + internal class InputComponentEditorAnalytic : UnityEngine.InputSystem.InputAnalytics.IInputAnalytic + { + public const string kEventName = "input_component_editor_closed"; + public const int kMaxEventsPerHour = 100; // default: 1000 + public const int kMaxNumberOfElements = 100; // default: 1000 + + /// + /// The associated component type. + /// + private readonly InputSystemComponent m_Component; + + /// + /// Represents component inspector editor data. + /// + /// + /// Ideally this struct should be readonly but then Unity cannot serialize/deserialize it. + /// + [Serializable] + public struct Data : UnityEngine.InputSystem.InputAnalytics.IInputAnalyticData + { + /// + /// Creates a new ComponentEditorData instance. + /// + /// The associated component. + public Data(InputSystemComponent component) + { + this.component = component; + } + + /// + /// Defines the associated component. + /// + public InputSystemComponent component; + } + + public InputComponentEditorAnalytic(InputSystemComponent component) + { + info = new InputAnalytics.InputAnalyticInfo(kEventName, kMaxEventsPerHour, kMaxNumberOfElements); + m_Component = component; + } + +#if UNITY_EDITOR && UNITY_2023_2_OR_NEWER + public bool TryGatherData(out UnityEngine.Analytics.IAnalytic.IData data, out Exception error) +#else + public bool TryGatherData(out InputAnalytics.IInputAnalyticData data, out Exception error) +#endif + { + data = new Data(m_Component); + error = null; + return true; + } + + public InputAnalytics.InputAnalyticInfo info { get; } + } +} +#endif diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputComponentEditorAnalytic.cs.meta b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputComponentEditorAnalytic.cs.meta new file mode 100644 index 0000000000..1b8dfba323 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputComponentEditorAnalytic.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4b36e69515ff4a45be02062b5584e1a8 +timeCreated: 1719312182 \ No newline at end of file diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputEditorAnalytics.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputEditorAnalytics.cs new file mode 100644 index 0000000000..60bc676df3 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputEditorAnalytics.cs @@ -0,0 +1,46 @@ +#if UNITY_EDITOR + +using System; + +namespace UnityEngine.InputSystem.Editor +{ + internal static partial class InputEditorAnalytics + { + /// + /// Represents notification behavior setting associated with and + /// . + /// + internal enum PlayerNotificationBehavior + { + SendMessages = 0, + BroadcastMessages = 1, + UnityEvents = 2, + CSharpEvents = 3 + } + + /// + /// Converts from current type to analytics counterpart. + /// + /// The value to be converted. + /// + /// If there is no available remapping. + internal static PlayerNotificationBehavior ToNotificationBehavior(PlayerNotifications value) + { + switch (value) + { + case PlayerNotifications.SendMessages: + return PlayerNotificationBehavior.SendMessages; + case PlayerNotifications.BroadcastMessages: + return PlayerNotificationBehavior.BroadcastMessages; + case PlayerNotifications.InvokeUnityEvents: + return PlayerNotificationBehavior.UnityEvents; + case PlayerNotifications.InvokeCSharpEvents: + return PlayerNotificationBehavior.CSharpEvents; + default: + throw new ArgumentOutOfRangeException(nameof(value)); + } + } + } +} + +#endif diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputEditorAnalytics.cs.meta b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputEditorAnalytics.cs.meta new file mode 100644 index 0000000000..f7340823b3 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputEditorAnalytics.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: af8ecd25eda841f98ce3f6555888e43b +timeCreated: 1704878014 \ No newline at end of file diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/OnScreenStickEditorAnalytic.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/OnScreenStickEditorAnalytic.cs new file mode 100644 index 0000000000..964ef9173a --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/OnScreenStickEditorAnalytic.cs @@ -0,0 +1,92 @@ +#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_ENABLE_UI +using System; +using UnityEngine.InputSystem.OnScreen; + +namespace UnityEngine.InputSystem.Editor +{ + /// + /// Analytics record for tracking engagement with Input Action Asset editor(s). + /// +#if UNITY_2023_2_OR_NEWER + [UnityEngine.Analytics.AnalyticInfo(eventName: kEventName, maxEventsPerHour: kMaxEventsPerHour, + maxNumberOfElements: kMaxNumberOfElements, vendorKey: UnityEngine.InputSystem.InputAnalytics.kVendorKey, version: 2)] +#endif // UNITY_2023_2_OR_NEWER + internal class OnScreenStickEditorAnalytic : UnityEngine.InputSystem.InputAnalytics.IInputAnalytic + { + public const string kEventName = "input_onscreenstick_editor_destroyed"; + public const int kMaxEventsPerHour = 100; // default: 1000 + public const int kMaxNumberOfElements = 100; // default: 1000 + + /// + /// Represents select configuration data of interest related to an component. + /// + [Serializable] + internal struct Data : UnityEngine.InputSystem.InputAnalytics.IInputAnalyticData + { + public enum OnScreenStickBehaviour + { + RelativePositionWithStaticOrigin = 0, + ExactPositionWithStaticOrigin = 1, + ExactPositionWithDynamicOrigin = 2, + } + + private static OnScreenStickBehaviour ToBehaviour(OnScreenStick.Behaviour value) + { + switch (value) + { + case OnScreenStick.Behaviour.RelativePositionWithStaticOrigin: + return OnScreenStickBehaviour.RelativePositionWithStaticOrigin; + case OnScreenStick.Behaviour.ExactPositionWithDynamicOrigin: + return OnScreenStickBehaviour.ExactPositionWithDynamicOrigin; + case OnScreenStick.Behaviour.ExactPositionWithStaticOrigin: + return OnScreenStickBehaviour.ExactPositionWithStaticOrigin; + default: + throw new ArgumentOutOfRangeException(nameof(value)); + } + } + + public Data(OnScreenStick value) + { + behavior = ToBehaviour(value.behaviour); + movement_range = value.movementRange; + dynamic_origin_range = value.dynamicOriginRange; + use_isolated_input_actions = value.useIsolatedInputActions; + } + + public OnScreenStickBehaviour behavior; + public float movement_range; + public float dynamic_origin_range; + public bool use_isolated_input_actions; + } + + private readonly UnityEditor.Editor m_Editor; + + public OnScreenStickEditorAnalytic(UnityEditor.Editor editor) + { + m_Editor = editor; + } + +#if UNITY_EDITOR && UNITY_2023_2_OR_NEWER + public bool TryGatherData(out UnityEngine.Analytics.IAnalytic.IData data, out Exception error) +#else + public bool TryGatherData(out InputAnalytics.IInputAnalyticData data, out Exception error) +#endif + { + try + { + data = new Data(m_Editor.target as OnScreenStick); + error = null; + } + catch (Exception e) + { + data = null; + error = e; + } + return true; + } + + public InputAnalytics.InputAnalyticInfo info => + new InputAnalytics.InputAnalyticInfo(kEventName, kMaxEventsPerHour, kMaxNumberOfElements); + } +} +#endif diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/OnScreenStickEditorAnalytic.cs.meta b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/OnScreenStickEditorAnalytic.cs.meta new file mode 100644 index 0000000000..c340bb3c32 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/OnScreenStickEditorAnalytic.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2167295510884d5eb4722df2ba677996 +timeCreated: 1719232380 \ No newline at end of file diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/PlayerInputEditorAnalytic.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/PlayerInputEditorAnalytic.cs new file mode 100644 index 0000000000..4520d2eb61 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/PlayerInputEditorAnalytic.cs @@ -0,0 +1,71 @@ +#if UNITY_EDITOR +using System; + +namespace UnityEngine.InputSystem.Editor +{ + /// + /// Analytics for tracking Player Input component user engagement in the editor. + /// +#if UNITY_2023_2_OR_NEWER + [UnityEngine.Analytics.AnalyticInfo(eventName: kEventName, maxEventsPerHour: kMaxEventsPerHour, + maxNumberOfElements: kMaxNumberOfElements, vendorKey: UnityEngine.InputSystem.InputAnalytics.kVendorKey)] +#endif // UNITY_2023_2_OR_NEWER + internal class PlayerInputEditorAnalytic : UnityEngine.InputSystem.InputAnalytics.IInputAnalytic + { + public const string kEventName = "input_playerinput_editor_destroyed"; + public const int kMaxEventsPerHour = 100; // default: 1000 + public const int kMaxNumberOfElements = 100; // default: 1000 + + private readonly UnityEditor.Editor m_Editor; + + public PlayerInputEditorAnalytic(UnityEditor.Editor editor) + { + m_Editor = editor; + } + + public InputAnalytics.InputAnalyticInfo info => + new InputAnalytics.InputAnalyticInfo(kEventName, kMaxEventsPerHour, kMaxNumberOfElements); + +#if UNITY_EDITOR && UNITY_2023_2_OR_NEWER + public bool TryGatherData(out UnityEngine.Analytics.IAnalytic.IData data, out Exception error) +#else + public bool TryGatherData(out InputAnalytics.IInputAnalyticData data, out Exception error) +#endif + { + try + { + data = new Data(m_Editor.target as PlayerInput); + error = null; + } + catch (Exception e) + { + data = null; + error = e; + } + return true; + } + + internal struct Data : UnityEngine.InputSystem.InputAnalytics.IInputAnalyticData + { + public InputEditorAnalytics.PlayerNotificationBehavior behavior; + public bool has_actions; + public bool has_default_map; + public bool has_ui_input_module; + public bool has_camera; + + public Data(PlayerInput playerInput) + { + behavior = InputEditorAnalytics.ToNotificationBehavior(playerInput.notificationBehavior); + has_actions = playerInput.actions != null; + has_default_map = playerInput.defaultActionMap != null; +#if UNITY_INPUT_SYSTEM_ENABLE_UI + has_ui_input_module = playerInput.uiInputModule != null; +#else + has_ui_input_module = false; +#endif + has_camera = playerInput.camera != null; + } + } + } +} +#endif diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/PlayerInputEditorAnalytic.cs.meta b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/PlayerInputEditorAnalytic.cs.meta new file mode 100644 index 0000000000..6ce3554929 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/PlayerInputEditorAnalytic.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 56bc028967c346c2bd33842d7b252123 +timeCreated: 1719232552 \ No newline at end of file diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/PlayerInputManagerEditorAnalytic.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/PlayerInputManagerEditorAnalytic.cs new file mode 100644 index 0000000000..77656d2f02 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/PlayerInputManagerEditorAnalytic.cs @@ -0,0 +1,84 @@ +#if UNITY_EDITOR +using System; + +namespace UnityEngine.InputSystem.Editor +{ +#if UNITY_2023_2_OR_NEWER + [UnityEngine.Analytics.AnalyticInfo(eventName: kEventName, maxEventsPerHour: kMaxEventsPerHour, + maxNumberOfElements: kMaxNumberOfElements, vendorKey: UnityEngine.InputSystem.InputAnalytics.kVendorKey)] +#endif // UNITY_2023_2_OR_NEWER + internal class PlayerInputManagerEditorAnalytic : UnityEngine.InputSystem.InputAnalytics.IInputAnalytic + { + public const string kEventName = "input_playerinputmanager_editor_destroyed"; + public const int kMaxEventsPerHour = 100; // default: 1000 + public const int kMaxNumberOfElements = 100; // default: 1000 + + public InputAnalytics.InputAnalyticInfo info => + new InputAnalytics.InputAnalyticInfo(kEventName, kMaxEventsPerHour, kMaxNumberOfElements); + + private readonly UnityEditor.Editor m_Editor; + + public PlayerInputManagerEditorAnalytic(UnityEditor.Editor editor) + { + m_Editor = editor; + } + +#if UNITY_EDITOR && UNITY_2023_2_OR_NEWER + public bool TryGatherData(out UnityEngine.Analytics.IAnalytic.IData data, out Exception error) +#else + public bool TryGatherData(out InputAnalytics.IInputAnalyticData data, out Exception error) +#endif + { + try + { + data = new Data(m_Editor.target as PlayerInputManager); + error = null; + } + catch (Exception e) + { + data = null; + error = e; + } + return true; + } + + internal struct Data : UnityEngine.InputSystem.InputAnalytics.IInputAnalyticData + { + public enum PlayerJoinBehavior + { + JoinPlayersWhenButtonIsPressed = 0, // default + JoinPlayersWhenJoinActionIsTriggered = 1, + JoinPlayersManually = 2 + } + + public InputEditorAnalytics.PlayerNotificationBehavior behavior; + public PlayerJoinBehavior join_behavior; + public bool joining_enabled_by_default; + public int max_player_count; + + public Data(PlayerInputManager value) + { + behavior = InputEditorAnalytics.ToNotificationBehavior(value.notificationBehavior); + join_behavior = ToPlayerJoinBehavior(value.joinBehavior); + joining_enabled_by_default = value.joiningEnabled; + max_player_count = value.maxPlayerCount; + } + + private static PlayerJoinBehavior ToPlayerJoinBehavior(UnityEngine.InputSystem.PlayerJoinBehavior value) + { + switch (value) + { + case UnityEngine.InputSystem.PlayerJoinBehavior.JoinPlayersWhenButtonIsPressed: + return PlayerJoinBehavior.JoinPlayersWhenButtonIsPressed; + case UnityEngine.InputSystem.PlayerJoinBehavior.JoinPlayersWhenJoinActionIsTriggered: + return PlayerJoinBehavior.JoinPlayersWhenJoinActionIsTriggered; + case UnityEngine.InputSystem.PlayerJoinBehavior.JoinPlayersManually: + return PlayerJoinBehavior.JoinPlayersManually; + default: + throw new ArgumentOutOfRangeException(nameof(value)); + } + } + } + } +} +#endif diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/PlayerInputManagerEditorAnalytic.cs.meta b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/PlayerInputManagerEditorAnalytic.cs.meta new file mode 100644 index 0000000000..fe51286bf1 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/PlayerInputManagerEditorAnalytic.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 199b31cbe22b4c269aa78f8139347afd +timeCreated: 1719232662 \ No newline at end of file diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/VirtualMouseInputEditorAnalytic.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/VirtualMouseInputEditorAnalytic.cs new file mode 100644 index 0000000000..a3c6c8e13e --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/VirtualMouseInputEditorAnalytic.cs @@ -0,0 +1,95 @@ +#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_ENABLE_UI +using System; +using UnityEngine.InputSystem.UI; + +namespace UnityEngine.InputSystem.Editor +{ + /// + /// Analytics record for tracking engagement with Input Action Asset editor(s). + /// +#if UNITY_2023_2_OR_NEWER + [UnityEngine.Analytics.AnalyticInfo(eventName: kEventName, maxEventsPerHour: kMaxEventsPerHour, + maxNumberOfElements: kMaxNumberOfElements, vendorKey: UnityEngine.InputSystem.InputAnalytics.kVendorKey)] +#endif // UNITY_2023_2_OR_NEWER + internal class VirtualMouseInputEditorAnalytic : UnityEngine.InputSystem.InputAnalytics.IInputAnalytic + { + public const string kEventName = "input_virtualmouseinput_editor_destroyed"; + public const int kMaxEventsPerHour = 100; // default: 1000 + public const int kMaxNumberOfElements = 100; // default: 1000 + + [Serializable] + internal struct Data : UnityEngine.InputSystem.InputAnalytics.IInputAnalyticData + { + /// + /// Maps to . Determines which cursor representation to use. + /// + public CursorMode cursor_mode; + + /// + /// Maps to . Speed in pixels per second with which to move the cursor. + /// + public float cursor_speed; + + /// + /// Maps to . Multiplier for values received from . + /// + public float scroll_speed; + + public enum CursorMode + { + SoftwareCursor = 0, + HardwareCursorIfAvailable = 1 + } + + private static CursorMode ToCursorMode(VirtualMouseInput.CursorMode value) + { + switch (value) + { + case VirtualMouseInput.CursorMode.SoftwareCursor: + return CursorMode.SoftwareCursor; + case VirtualMouseInput.CursorMode.HardwareCursorIfAvailable: + return CursorMode.HardwareCursorIfAvailable; + default: + throw new ArgumentOutOfRangeException(nameof(value)); + } + } + + public Data(VirtualMouseInput value) + { + cursor_mode = ToCursorMode(value.cursorMode); + cursor_speed = value.cursorSpeed; + scroll_speed = value.scrollSpeed; + } + } + + public InputAnalytics.InputAnalyticInfo info => + new InputAnalytics.InputAnalyticInfo(kEventName, kMaxEventsPerHour, kMaxNumberOfElements); + + private readonly UnityEditor.Editor m_Editor; + + public VirtualMouseInputEditorAnalytic(UnityEditor.Editor editor) + { + m_Editor = editor; + } + +#if UNITY_EDITOR && UNITY_2023_2_OR_NEWER + public bool TryGatherData(out UnityEngine.Analytics.IAnalytic.IData data, out Exception error) +#else + public bool TryGatherData(out InputAnalytics.IInputAnalyticData data, out Exception error) +#endif + { + try + { + data = new Data(m_Editor.target as VirtualMouseInput); + error = null; + } + catch (Exception e) + { + data = null; + error = e; + } + return true; + } + } +} +#endif diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/VirtualMouseInputEditorAnalytic.cs.meta b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/VirtualMouseInputEditorAnalytic.cs.meta new file mode 100644 index 0000000000..d7b144728f --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/VirtualMouseInputEditorAnalytic.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 183f7887e5104e1593ce980b9d0159e3 +timeCreated: 1719232500 \ No newline at end of file diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Commands/Commands.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Commands/Commands.cs index e2a9cfcd06..8f461cb332 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Commands/Commands.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Commands/Commands.cs @@ -36,6 +36,7 @@ public static Command AddActionMap() var actionProperty = InputActionSerializationHelpers.AddAction(newMap); InputActionSerializationHelpers.AddBinding(actionProperty, newMap); state.serializedObject.ApplyModifiedProperties(); + state.m_Analytics?.RegisterActionMapEdit(); return state.SelectActionMap(newMap); }; } @@ -53,6 +54,7 @@ public static Command AddAction() var newAction = InputActionSerializationHelpers.AddAction(actionMap); InputActionSerializationHelpers.AddBinding(newAction, actionMap); state.serializedObject.ApplyModifiedProperties(); + state.m_Analytics?.RegisterActionEdit(); return state.SelectAction(newAction); }; } @@ -71,6 +73,7 @@ public static Command AddBinding() var binding = InputActionSerializationHelpers.AddBinding(action, map); var bindingIndex = new SerializedInputBinding(binding).indexOfBinding; state.serializedObject.ApplyModifiedProperties(); + state.m_Analytics?.RegisterBindingEdit(); return state.With(selectedBindingIndex: bindingIndex, selectionType: SelectionType.Binding); }; } @@ -85,6 +88,7 @@ public static Command AddComposite(string compositeName) var composite = InputActionSerializationHelpers.AddCompositeBinding(action, map, compositeName, compositeType); var index = new SerializedInputBinding(composite).indexOfBinding; state.serializedObject.ApplyModifiedProperties(); + state.m_Analytics?.RegisterBindingEdit(); return state.With(selectedBindingIndex: index, selectionType: SelectionType.Binding); }; } @@ -98,6 +102,7 @@ public static Command DeleteActionMap(int actionMapIndex) var isCut = state.IsActionMapCut(actionMapIndex); InputActionSerializationHelpers.DeleteActionMap(state.serializedObject, actionMapID); state.serializedObject.ApplyModifiedProperties(); + state.m_Analytics?.RegisterActionMapEdit(); if (state.selectedActionMapIndex == actionMapIndex) return isCut ? SelectPrevActionMap(state).ClearCutElements() : SelectPrevActionMap(state); if (isCut) @@ -286,6 +291,7 @@ public static Command DuplicateActionMap(int actionMapIndex) var name = actionMap?.FindPropertyRelative(nameof(InputAction.m_Name)).stringValue; var newMap = CopyPasteHelper.DuplicateElement(actionMapArray, actionMap, name, actionMap.GetIndexOfArrayElement() + 1); state.serializedObject.ApplyModifiedProperties(); + state.m_Analytics?.RegisterActionMapEdit(); return state.SelectActionMap(newMap.FindPropertyRelative(nameof(InputAction.m_Name)).stringValue); }; } @@ -299,6 +305,7 @@ public static Command DuplicateAction() var actionArray = actionMap?.FindPropertyRelative(nameof(InputActionMap.m_Actions)); CopyPasteHelper.DuplicateAction(actionArray, action, actionMap, state); state.serializedObject.ApplyModifiedProperties(); + state.m_Analytics?.RegisterActionEdit(); return state.SelectAction(state.selectedActionIndex + 1); }; } @@ -313,6 +320,7 @@ public static Command DuplicateBinding() var bindingsArray = actionMap?.FindPropertyRelative(nameof(InputActionMap.m_Bindings)); var newIndex = CopyPasteHelper.DuplicateBinding(bindingsArray, binding, actionName, binding.GetIndexOfArrayElement() + 1); state.serializedObject.ApplyModifiedProperties(); + state.m_Analytics?.RegisterBindingEdit(); return state.SelectBinding(newIndex); }; } @@ -378,6 +386,7 @@ public static Command MoveComposite(int oldIndex, int actionIndex, int childInde InputActionSerializationHelpers.MoveBinding(actionMap, from, to); Selectors.GetCompositeOrBindingInMap(actionMap, to).wrappedProperty.FindPropertyRelative("m_Action").stringValue = actionTo; } + state.m_Analytics?.RegisterBindingEdit(); state.serializedObject.ApplyModifiedProperties(); return state.SelectBinding(newBindingIndex); }; @@ -400,6 +409,7 @@ private static int MoveBindingOrComposite(InputActionsEditorState state, int old newBindingIndex -= newBindingIndex > oldIndex && !actionTo.Equals(actionFrom.stringValue) ? 1 : 0; // reduce index by one in case the moved binding will be shifted underneath to another action } + state.m_Analytics?.RegisterBindingEdit(); actionFrom.stringValue = actionTo; InputActionSerializationHelpers.MoveBinding(actionMap, oldIndex, newBindingIndex); return newBindingIndex; @@ -432,6 +442,7 @@ public static Command MovePartOfComposite(int oldIndex, int newIndex, int compos var actionTo = actionMap?.FindPropertyRelative(nameof(InputActionMap.m_Bindings)).GetArrayElementAtIndex(compositeIndex).FindPropertyRelative("m_Action").stringValue; InputActionSerializationHelpers.MoveBinding(actionMap, oldIndex, newIndex); Selectors.GetCompositeOrBindingInMap(actionMap, newIndex).wrappedProperty.FindPropertyRelative("m_Action").stringValue = actionTo; + state.m_Analytics?.RegisterBindingEdit(); state.serializedObject.ApplyModifiedProperties(); return state.SelectBinding(newIndex); }; @@ -448,6 +459,7 @@ public static Command DeleteAction(int actionMapIndex, string actionName) var isCut = state.IsActionCut(actionMapIndex, actionIndex); InputActionSerializationHelpers.DeleteActionAndBindings(actionMap, actionID); state.serializedObject.ApplyModifiedProperties(); + state.m_Analytics?.RegisterActionEdit(); if (isCut) return state.With(selectedActionIndex: -1, selectionType: SelectionType.Action).ClearCutElements(); @@ -464,6 +476,7 @@ public static Command DeleteBinding(int actionMapIndex, int bindingIndex) var isCut = state.IsBindingCut(actionMapIndex, bindingIndex); InputActionSerializationHelpers.DeleteBinding(binding, actionMap); state.serializedObject.ApplyModifiedProperties(); + state.m_Analytics?.RegisterBindingEdit(); if (isCut) return state.With(selectedBindingIndex: -1, selectionType: SelectionType.Binding).ClearCutElements(); @@ -487,6 +500,7 @@ public static Command UpdatePathNameAndValues(NamedValue[] parameters, Serialize pathProperty.stringValue = nameAndParameters.ToString(); state.serializedObject.ApplyModifiedProperties(); + state.m_Analytics?.RegisterBindingEdit(); return state; }; } @@ -503,6 +517,7 @@ public static Command SetCompositeBindingType(SerializedInputBinding bindingProp }; InputActionSerializationHelpers.ChangeCompositeBindingType(bindingProperty.wrappedProperty, nameAndParameters); state.serializedObject.ApplyModifiedProperties(); + state.m_Analytics?.RegisterBindingEdit(); // Questionable if action or binding edit? return state; }; } @@ -513,6 +528,7 @@ public static Command SetCompositeBindingPartName(SerializedInputBinding binding { InputActionSerializationHelpers.SetBindingPartName(bindingProperty.wrappedProperty, partName); state.serializedObject.ApplyModifiedProperties(); + state.m_Analytics?.RegisterBindingEdit(); return state; }; } @@ -523,6 +539,7 @@ public static Command ChangeActionType(SerializedInputAction inputAction, InputA { inputAction.wrappedProperty.FindPropertyRelative(nameof(InputAction.m_Type)).intValue = (int)newValue; state.serializedObject.ApplyModifiedProperties(); + state.m_Analytics?.RegisterActionEdit(); return state; }; } @@ -537,6 +554,7 @@ public static Command ChangeInitialStateCheck(SerializedInputAction inputAction, else property.intValue &= ~(int)InputAction.ActionFlags.WantsInitialStateCheck; state.serializedObject.ApplyModifiedProperties(); + state.m_Analytics?.RegisterActionEdit(); return state; }; } @@ -551,6 +569,7 @@ public static Command ChangeActionControlType(SerializedInputAction inputAction, var controlType = (controlTypeIndex == 0) ? string.Empty : controlTypes[controlTypeIndex]; inputAction.wrappedProperty.FindPropertyRelative(nameof(InputAction.m_ExpectedControlType)).stringValue = controlType; state.serializedObject.ApplyModifiedProperties(); + state.m_Analytics?.RegisterActionEdit(); return state; }; } @@ -576,6 +595,7 @@ public static Command SaveAsset(Action postSaveAction) // TODO It makes more sense to call back to editor since editor owns target object? //InputActionAssetManager.SaveAsset(state.serializedObject.targetObject as InputActionAsset); postSaveAction?.Invoke(); + state.m_Analytics?.RegisterExplicitSave(); return state; }; } @@ -591,6 +611,7 @@ public static Command ToggleAutoSave(bool newValue, Action postSaveAction) { //InputActionAssetManager.SaveAsset(state.serializedObject.targetObject as InputActionAsset); postSaveAction?.Invoke(); + state.m_Analytics?.RegisterAutoSave(); } InputEditorUserSettings.autoSaveInputActionAssets = newValue; @@ -607,6 +628,7 @@ public static Command ChangeActionMapName(int index, string newName) var actionMap = Selectors.GetActionMapAtIndex(state, index)?.wrappedProperty; InputActionSerializationHelpers.RenameActionMap(actionMap, newName); state.serializedObject.ApplyModifiedProperties(); + state.m_Analytics?.RegisterActionMapEdit(); return state; }; } @@ -619,6 +641,7 @@ public static Command ChangeActionName(int actionMapIndex, string oldName, strin var action = Selectors.GetActionInMap(state, actionMapIndex, oldName).wrappedProperty; InputActionSerializationHelpers.RenameAction(action, actionMap, newName); state.serializedObject.ApplyModifiedProperties(); + state.m_Analytics?.RegisterActionEdit(); return state; }; } @@ -631,6 +654,7 @@ public static Command ChangeCompositeName(int actionMapIndex, int bindingIndex, var binding = Selectors.GetCompositeOrBindingInMap(actionMap, bindingIndex).wrappedProperty; InputActionSerializationHelpers.RenameComposite(binding, newName); state.serializedObject.ApplyModifiedProperties(); + state.m_Analytics?.RegisterBindingEdit(); return state; }; } @@ -640,6 +664,8 @@ public static Command ClearActionMaps() { return (in InputActionsEditorState state) => { + state.m_Analytics?.RegisterReset(); + InputActionSerializationHelpers.DeleteAllActionMaps(state.serializedObject); state.serializedObject.ApplyModifiedProperties(); return state.ClearCutElements(); @@ -664,6 +690,8 @@ public static Command ReplaceActionMaps(string inputActionAssetJsonContent) InputActionSerializationHelpers.AddActionMaps(state.serializedObject, tmp); } state.serializedObject.ApplyModifiedProperties(); + state.m_Analytics.RegisterActionMapEdit(); + return state.ClearCutElements(); }; } diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Commands/ControlSchemeCommands.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Commands/ControlSchemeCommands.cs index beb04c9269..c9989caf62 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Commands/ControlSchemeCommands.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Commands/ControlSchemeCommands.cs @@ -14,20 +14,30 @@ internal static class ControlSchemeCommands public static Command AddNewControlScheme() { - return (in InputActionsEditorState state) => state.With(selectedControlScheme: new InputControlScheme( - MakeUniqueControlSchemeName(state, kNewControlSchemeName))); + return (in InputActionsEditorState state) => + { + state.m_Analytics?.RegisterControlSchemeEdit(); + return state.With(selectedControlScheme: new InputControlScheme( + MakeUniqueControlSchemeName(state, kNewControlSchemeName))); + }; } public static Command AddDeviceRequirement(InputControlScheme.DeviceRequirement requirement) { - return (in InputActionsEditorState state) => state.With(selectedControlScheme: new InputControlScheme(state.selectedControlScheme.name, - state.selectedControlScheme.deviceRequirements.Append(requirement))); + return (in InputActionsEditorState state) => + { + state.m_Analytics?.RegisterControlSchemeEdit(); + return state.With(selectedControlScheme: new InputControlScheme(state.selectedControlScheme.name, + state.selectedControlScheme.deviceRequirements.Append(requirement))); + }; } public static Command RemoveDeviceRequirement(int selectedDeviceIndex) { return (in InputActionsEditorState state) => { + state.m_Analytics?.RegisterControlSchemeEdit(); + var newDeviceIndex = Mathf.Clamp( selectedDeviceIndex <= state.selectedDeviceRequirementIndex @@ -163,9 +173,14 @@ public static Command SelectDeviceRequirement(int deviceRequirementIndex) /// public static Command DuplicateSelectedControlScheme() { - return (in InputActionsEditorState state) => state.With(selectedControlScheme: new InputControlScheme( - MakeUniqueControlSchemeName(state, state.selectedControlScheme.name), - state.selectedControlScheme.deviceRequirements)); + return (in InputActionsEditorState state) => + { + state.m_Analytics?.RegisterControlSchemeEdit(); + + return state.With(selectedControlScheme: new InputControlScheme( + MakeUniqueControlSchemeName(state, state.selectedControlScheme.name), + state.selectedControlScheme.deviceRequirements)); + }; } public static Command DeleteSelectedControlScheme() @@ -197,6 +212,8 @@ public static Command DeleteSelectedControlScheme() selectedControlSchemeIndex: serializedArray.arraySize - 1, selectedControlScheme: new InputControlScheme(serializedArray.GetArrayElementAtIndex(serializedArray.arraySize - 1)), selectedDeviceRequirementIndex: -1); + state.m_Analytics?.RegisterControlSchemeEdit(); + return state.With( selectedControlSchemeIndex: indexOfArrayElement, selectedControlScheme: new InputControlScheme(serializedArray.GetArrayElementAtIndex(indexOfArrayElement)), selectedDeviceRequirementIndex: -1); @@ -224,6 +241,8 @@ public static Command ChangeDeviceRequirement(int deviceRequirementIndex, bool i requirement.isOptional = !isRequired; deviceRequirements[deviceRequirementIndex] = requirement; + state.m_Analytics?.RegisterControlSchemeEdit(); + return state.With(selectedControlScheme: new InputControlScheme( state.selectedControlScheme.name, deviceRequirements, @@ -240,6 +259,8 @@ public static Command ReorderDeviceRequirements(int oldPosition, int newPosition deviceRequirements.RemoveAt(oldPosition); deviceRequirements.Insert(newPosition, requirement); + state.m_Analytics?.RegisterControlSchemeEdit(); + return state.With(selectedControlScheme: new InputControlScheme( state.selectedControlScheme.name, deviceRequirements, @@ -272,6 +293,8 @@ public static Command ChangeSelectedBindingsControlSchemes(string controlScheme, .Where(s => s != controlScheme) .Join(InputBinding.kSeparatorString); + state.m_Analytics?.RegisterBindingEdit(); + state.serializedObject.ApplyModifiedProperties(); return state; }; diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorSettingsProvider.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorSettingsProvider.cs index 1d28ca1bc5..272410c2a2 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorSettingsProvider.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorSettingsProvider.cs @@ -25,10 +25,11 @@ internal class InputActionsEditorSettingsProvider : SettingsProvider private InputActionsEditorView m_View; + private InputActionsEditorSessionAnalytic m_ActionEditorAnalytics; + public InputActionsEditorSettingsProvider(string path, SettingsScope scopes, IEnumerable keywords = null) : base(path, scopes, keywords) - { - } + {} public override void OnActivate(string searchContext, VisualElement rootElement) { @@ -43,8 +44,14 @@ public override void OnActivate(string searchContext, VisualElement rootElement) // Setup root element with focus monitoring m_RootVisualElement = rootElement; m_RootVisualElement.focusable = true; - m_RootVisualElement.RegisterCallback(OnEditFocusLost); - m_RootVisualElement.RegisterCallback(OnEditFocus); + m_RootVisualElement.RegisterCallback(OnFocusOut); + m_RootVisualElement.RegisterCallback(OnFocusIn); + + // Always begin a session when activated (note that OnActivate isn't called when navigating back + // to editor from another setting category) + m_ActionEditorAnalytics = new InputActionsEditorSessionAnalytic( + InputActionsEditorSessionAnalytic.Data.Kind.EmbeddedInProjectSettings); + m_ActionEditorAnalytics.Begin(); CreateUI(); @@ -57,7 +64,7 @@ public override void OnActivate(string searchContext, VisualElement rootElement) // Note that focused element will be set if we are navigating back to an existing instance when switching // setting in the left project settings panel since this doesn't recreate the editor. if (m_RootVisualElement?.focusController?.focusedElement != null) - OnEditFocus(null); + OnFocusIn(); m_IsActivated = true; } @@ -74,8 +81,8 @@ public override void OnDeactivate() if (m_RootVisualElement != null) { - m_RootVisualElement.UnregisterCallback(OnEditFocusLost); - m_RootVisualElement.UnregisterCallback(OnEditFocus); + m_RootVisualElement.UnregisterCallback(OnFocusIn); + m_RootVisualElement.UnregisterCallback(OnFocusOut); } // Make sure any remaining changes are actually saved @@ -85,7 +92,7 @@ public override void OnDeactivate() // Hence we guard against duplicate OnDeactivate() calls. if (m_HasEditFocus) { - OnEditFocusLost(null); + OnFocusOut(); m_HasEditFocus = false; } @@ -93,14 +100,18 @@ public override void OnDeactivate() m_IsActivated = false; + // Always end a session when deactivated. + m_ActionEditorAnalytics?.End(); + m_View?.DestroyView(); } - private void OnEditFocus(FocusInEvent @event) + private void OnFocusIn(FocusInEvent @event = null) { if (!m_HasEditFocus) { m_HasEditFocus = true; + m_ActionEditorAnalytics.RegisterEditorFocusIn(); m_ActiveSettingsProvider = this; SetIMGUIDropdownVisible(false, false); } @@ -152,13 +163,16 @@ private async void DelayFocusLost(bool relatedTargetWasNull) } } - private void OnEditFocusLost(FocusOutEvent @event) + private void OnFocusOut(FocusOutEvent @event = null) { // This can be used to detect focus lost events of container elements, but will not detect window focus. // Note that `event.relatedTarget` contains the element that gains focus, which is null if we select // elements outside of project settings Editor Window. Also note that @event is null when we call this // from OnDeactivate(). var element = (VisualElement)@event?.relatedTarget; + + m_ActionEditorAnalytics.RegisterEditorFocusOut(); + DelayFocusLost(element == null); } @@ -197,7 +211,7 @@ private void BuildUI() // Construct from InputSystem.actions asset var asset = InputSystem.actions; var hasAsset = asset != null; - m_State = (asset != null) ? new InputActionsEditorState(new SerializedObject(asset)) : default; + m_State = (asset != null) ? new InputActionsEditorState(m_ActionEditorAnalytics, new SerializedObject(asset)) : default; // Dynamically show a section indicating that an asset is missing if not currently having an associated asset var missingAssetSection = m_RootVisualElement.Q("missing-asset-section"); diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorState.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorState.cs index 74be1bbbbd..74e4e52442 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorState.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorState.cs @@ -74,6 +74,8 @@ internal struct InputActionsEditorState public int selectedDeviceRequirementIndex { get {return m_selectedDeviceRequirementIndex; } } public InputControlScheme selectedControlScheme => m_ControlScheme; // TODO Bad this either po + internal InputActionsEditorSessionAnalytic m_Analytics; + [SerializeField] int m_selectedActionMapIndex; [SerializeField] int m_selectedActionIndex; [SerializeField] int m_selectedBindingIndex; @@ -84,6 +86,7 @@ internal struct InputActionsEditorState internal bool hasCutElements => m_CutElements != null && m_CutElements.Count > 0; public InputActionsEditorState( + InputActionsEditorSessionAnalytic analytics, SerializedObject inputActionAsset, int selectedActionMapIndex = 0, int selectedActionIndex = 0, @@ -97,6 +100,8 @@ public InputActionsEditorState( { Debug.Assert(inputActionAsset != null); + m_Analytics = analytics; + serializedObject = inputActionAsset; m_selectedActionMapIndex = selectedActionMapIndex; @@ -115,6 +120,8 @@ public InputActionsEditorState( public InputActionsEditorState(InputActionsEditorState other, SerializedObject asset) { + m_Analytics = other.m_Analytics; + // Assign serialized object, not that this might be equal to other.serializedObject, // a slight variation of it with any kind of changes or a completely different one. // Hence, we do our best here to keep any selections consistent by remapping objects @@ -215,6 +222,7 @@ public InputActionsEditorState With( List cutElements = null) { return new InputActionsEditorState( + m_Analytics, serializedObject, selectedActionMapIndex ?? this.selectedActionMapIndex, selectedActionIndex ?? this.selectedActionIndex, @@ -234,6 +242,7 @@ public InputActionsEditorState With( public InputActionsEditorState ClearCutElements() { return new InputActionsEditorState( + m_Analytics, serializedObject, selectedActionMapIndex, selectedActionIndex, diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorWindow.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorWindow.cs index 2ecf5c9bdd..0fee181365 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorWindow.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorWindow.cs @@ -41,6 +41,12 @@ static InputActionsEditorWindow() private StateContainer m_StateContainer; private InputActionsEditorView m_View; + private InputActionsEditorSessionAnalytic m_Analytics; + + private InputActionsEditorSessionAnalytic analytics => + m_Analytics ??= new InputActionsEditorSessionAnalytic( + InputActionsEditorSessionAnalytic.Data.Kind.EditorWindow); + [OnOpenAsset] public static bool OpenAsset(int instanceId, int line) { @@ -181,6 +187,8 @@ private void CreateGUI() // Only domain reload if (m_AssetObjectForEditing == null) { workingCopy = InputActionAssetManager.CreateWorkingCopy(asset); + if (m_State.m_Analytics == null) + m_State.m_Analytics = analytics; m_State = new InputActionsEditorState(m_State, new SerializedObject(workingCopy)); m_AssetObjectForEditing = workingCopy; } @@ -214,13 +222,16 @@ private void BuildUI() { CleanupStateContainer(); + if (m_State.m_Analytics == null) + m_State.m_Analytics = m_Analytics; + m_StateContainer = new StateContainer(m_State); m_StateContainer.StateChanged += OnStateChanged; rootVisualElement.Clear(); if (!rootVisualElement.styleSheets.Contains(InputActionsEditorWindowUtils.theme)) rootVisualElement.styleSheets.Add(InputActionsEditorWindowUtils.theme); - m_View = new InputActionsEditorView(rootVisualElement, m_StateContainer, false, Save); + m_View = new InputActionsEditorView(rootVisualElement, m_StateContainer, false, () => Save(isAutoSave: false)); m_StateContainer.Initialize(rootVisualElement.Q("action-editor")); } @@ -235,7 +246,7 @@ private void OnStateChanged(InputActionsEditorState newState) // and editor loosing focus instead. #else if (InputEditorUserSettings.autoSaveInputActionAssets) - Save(); + Save(isAutoSave: false); #endif } @@ -249,7 +260,7 @@ private InputActionAsset GetEditedAsset() return m_State.serializedObject.targetObject as InputActionAsset; } - private void Save() + private void Save(bool isAutoSave) { var path = AssetDatabase.GUIDToAssetPath(m_AssetGUID); #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS @@ -259,6 +270,11 @@ private void Save() #endif if (InputActionAssetManager.SaveAsset(path, GetEditedAsset().ToJson())) TryUpdateFromAsset(); + + if (isAutoSave) + analytics.RegisterAutoSave(); + else + analytics.RegisterExplicitSave(); } private bool HasContentChanged() @@ -285,13 +301,30 @@ private void DirtyInputActionsEditorWindow(InputActionsEditorState newState) UpdateWindowTitle(); } + private void OnEnable() + { + analytics.Begin(); + } + + private void OnDisable() + { + analytics.End(); + } + + private void OnFocus() + { + analytics.RegisterEditorFocusIn(); + } + private void OnLostFocus() { // Auto-save triggers on focus-lost instead of on every change #if UNITY_INPUT_SYSTEM_INPUT_ACTIONS_EDITOR_AUTO_SAVE_ON_FOCUS_LOST if (InputEditorUserSettings.autoSaveInputActionAssets && m_IsDirty) - Save(); + Save(isAutoSave: true); #endif + + analytics.RegisterEditorFocusOut(); } private void HandleOnDestroy() @@ -310,7 +343,7 @@ private void HandleOnDestroy() switch (result) { case Dialog.Result.Save: - Save(); + Save(isAutoSave: false); break; case Dialog.Result.Cancel: // Cancel editor quit. (open new editor window with the edited asset) @@ -443,7 +476,7 @@ public void OnAssetImported() private static void SaveShortcut(ShortcutArguments arguments) { var window = (InputActionsEditorWindow)arguments.context; - window.Save(); + window.Save(isAutoSave: false); } [Shortcut("Input Action Editor/Add Action Map", typeof(InputActionsEditorWindow), KeyCode.M, ShortcutModifiers.Alt)] diff --git a/Packages/com.unity.inputsystem/InputSystem/IInputRuntime.cs b/Packages/com.unity.inputsystem/InputSystem/IInputRuntime.cs index 50f7a61174..6913dffd28 100644 --- a/Packages/com.unity.inputsystem/InputSystem/IInputRuntime.cs +++ b/Packages/com.unity.inputsystem/InputSystem/IInputRuntime.cs @@ -1,5 +1,6 @@ using System; using Unity.Collections.LowLevel.Unsafe; +using UnityEngine.Analytics; using UnityEngine.InputSystem.Layouts; #if UNITY_EDITOR @@ -183,9 +184,8 @@ internal unsafe interface IInputRuntime // If analytics are enabled, the runtime receives analytics events from the input manager. // See InputAnalytics. #if UNITY_ANALYTICS || UNITY_EDITOR - void RegisterAnalyticsEvent(string name, int maxPerHour, int maxPropertiesPerEvent); - void SendAnalyticsEvent(string name, object data); - #endif + void SendAnalytic(InputAnalytics.IInputAnalytic analytic); + #endif // UNITY_ANALYTICS || UNITY_EDITOR bool isInBatchMode { get; } diff --git a/Packages/com.unity.inputsystem/InputSystem/InputAnalytics.cs b/Packages/com.unity.inputsystem/InputSystem/InputAnalytics.cs index d154ec0c1b..49027e7e2a 100644 --- a/Packages/com.unity.inputsystem/InputSystem/InputAnalytics.cs +++ b/Packages/com.unity.inputsystem/InputSystem/InputAnalytics.cs @@ -1,10 +1,9 @@ #if UNITY_ANALYTICS || UNITY_EDITOR using System; -using System.Collections.Generic; using UnityEngine.InputSystem.Layouts; #if UNITY_EDITOR using UnityEngine.InputSystem.Editor; -#endif +#endif // UNITY_EDITOR ////FIXME: apparently shutdown events are not coming through in the analytics backend @@ -12,73 +11,61 @@ namespace UnityEngine.InputSystem { internal static class InputAnalytics { - public const string kEventStartup = "input_startup"; - public const string kEventShutdown = "input_shutdown"; + public const string kVendorKey = "unity.input"; - public static void Initialize(InputManager manager) + // Struct similar to AnalyticInfo for simplifying usage. + public struct InputAnalyticInfo { - Debug.Assert(manager.m_Runtime != null); - } - - public static void OnStartup(InputManager manager) - { - var data = new StartupEventData - { - version = InputSystem.version.ToString(), - }; - - // Collect recognized devices. - var devices = manager.devices; - var deviceList = new List(); - for (var i = 0; i < devices.Count; ++i) + public InputAnalyticInfo(string name, int maxEventsPerHour, int maxNumberOfElements) { - var device = devices[i]; - - deviceList.Add( - StartupEventData.DeviceInfo.FromDescription(device.description, device.native, device.layout)); + Name = name; + MaxEventsPerHour = maxEventsPerHour; + MaxNumberOfElements = maxNumberOfElements; } - data.devices = deviceList.ToArray(); - // Collect unrecognized devices. - deviceList.Clear(); - var availableDevices = manager.m_AvailableDevices; - var availableDeviceCount = manager.m_AvailableDeviceCount; - for (var i = 0; i < availableDeviceCount; ++i) - { - var deviceId = availableDevices[i].deviceId; - if (manager.TryGetDeviceById(deviceId) != null) - continue; + public readonly string Name; + public readonly int MaxEventsPerHour; + public readonly int MaxNumberOfElements; + } - deviceList.Add(StartupEventData.DeviceInfo.FromDescription(availableDevices[i].description, - availableDevices[i].isNative)); - } + // Note: Needs to be externalized from interface depending on C# version. + public interface IInputAnalyticData +#if UNITY_EDITOR && UNITY_2023_2_OR_NEWER + : UnityEngine.Analytics.IAnalytic.IData +#endif + {} - data.unrecognized_devices = deviceList.ToArray(); + // Unity 2023.2+ deprecates legacy interfaces for registering and sending editor analytics and + // replaces them with attribute annotations and required interface implementations. + // The IInputAnalytic interface have been introduced here to support both variants + // of analytics reporting. Notice that a difference is that data is collected lazily as part + // of sending the analytics via the framework. + public interface IInputAnalytic +#if UNITY_EDITOR && UNITY_2023_2_OR_NEWER + : UnityEngine.Analytics.IAnalytic +#endif // UNITY_EDITOR && UNITY_2023_2_OR_NEWER + { + InputAnalyticInfo info { get; } // May be removed when only supporting 2023.2+ versions - #if UNITY_EDITOR - data.new_enabled = EditorPlayerSettingHelpers.newSystemBackendsEnabled; - data.old_enabled = EditorPlayerSettingHelpers.oldSystemBackendsEnabled; - #endif +#if !UNITY_2023_2_OR_NEWER + // Conditionally mimic UnityEngine.Analytics.IAnalytic + bool TryGatherData(out IInputAnalyticData data, out Exception error); +#endif // !UNITY_2023_2_OR_NEWER + } - manager.m_Runtime.RegisterAnalyticsEvent(kEventStartup, 10, 100); - manager.m_Runtime.SendAnalyticsEvent(kEventStartup, data); + public static void Initialize(InputManager manager) + { + Debug.Assert(manager.m_Runtime != null); + } + + public static void OnStartup(InputManager manager) + { + manager.m_Runtime.SendAnalytic(new StartupEventAnalytic(manager)); } public static void OnShutdown(InputManager manager) { - var metrics = manager.metrics; - var data = new ShutdownEventData - { - max_num_devices = metrics.maxNumDevices, - max_state_size_in_bytes = metrics.maxStateSizeInBytes, - total_event_bytes = metrics.totalEventBytes, - total_event_count = metrics.totalEventCount, - total_frame_count = metrics.totalUpdateCount, - total_event_processing_time = (float)metrics.totalEventProcessingTime, - }; - - manager.m_Runtime.RegisterAnalyticsEvent(kEventShutdown, 10, 100); - manager.m_Runtime.SendAnalyticsEvent(kEventShutdown, data); + manager.m_Runtime.SendAnalytic(new ShutdownEventDataAnalytic(manager)); } /// @@ -92,7 +79,7 @@ public static void OnShutdown(InputManager manager) /// on desktops or touchscreen on phones). /// [Serializable] - public struct StartupEventData + public struct StartupEventData : IInputAnalyticData { public string version; public DeviceInfo[] devices; @@ -136,11 +123,90 @@ public static DeviceInfo FromDescription(InputDeviceDescription description, boo } } +#if UNITY_EDITOR && UNITY_2023_2_OR_NEWER + [UnityEngine.Analytics.AnalyticInfo(eventName: kEventName, maxEventsPerHour: kMaxEventsPerHour, maxNumberOfElements: kMaxNumberOfElements, vendorKey: kVendorKey)] +#endif // UNITY_EDITOR && UNITY_2023_2_OR_NEWER + public struct StartupEventAnalytic : IInputAnalytic + { + public const string kEventName = "input_startup"; + public const int kMaxEventsPerHour = 100; + public const int kMaxNumberOfElements = 100; + + private InputManager m_InputManager; + + public StartupEventAnalytic(InputManager manager) + { + m_InputManager = manager; + } + + public InputAnalyticInfo info => new InputAnalyticInfo(kEventName, kMaxEventsPerHour, kMaxNumberOfElements); + +#if UNITY_EDITOR && UNITY_2023_2_OR_NEWER + public bool TryGatherData(out UnityEngine.Analytics.IAnalytic.IData data, out Exception error) +#else + public bool TryGatherData(out IInputAnalyticData data, out Exception error) +#endif + { + try + { + data = new StartupEventData + { + version = InputSystem.version.ToString(), + devices = CollectRecognizedDevices(m_InputManager), + unrecognized_devices = CollectUnrecognizedDevices(m_InputManager), +#if UNITY_EDITOR + new_enabled = EditorPlayerSettingHelpers.newSystemBackendsEnabled, + old_enabled = EditorPlayerSettingHelpers.oldSystemBackendsEnabled, +#endif // UNITY_EDITOR + }; + error = null; + return true; + } + catch (Exception e) + { + data = null; + error = e; + return false; + } + } + + private static StartupEventData.DeviceInfo[] CollectRecognizedDevices(InputManager manager) + { + var deviceInfo = new StartupEventData.DeviceInfo[manager.devices.Count]; + for (var i = 0; i < manager.devices.Count; ++i) + { + deviceInfo[i] = StartupEventData.DeviceInfo.FromDescription( + manager.devices[i].description, manager.devices[i].native, manager.devices[i].layout); + } + return deviceInfo; + } + + private static StartupEventData.DeviceInfo[] CollectUnrecognizedDevices(InputManager manager) + { + var n = 0; + var deviceInfo = new StartupEventData.DeviceInfo[manager.m_AvailableDeviceCount]; + for (var i = 0; i < deviceInfo.Length; ++i) + { + var deviceId = manager.m_AvailableDevices[i].deviceId; + if (manager.TryGetDeviceById(deviceId) != null) + continue; + + deviceInfo[n++] = StartupEventData.DeviceInfo.FromDescription( + manager.m_AvailableDevices[i].description, manager.m_AvailableDevices[i].isNative); + } + + if (deviceInfo.Length > n) + Array.Resize(ref deviceInfo, n); + + return deviceInfo; + } + } + /// /// Data about when after startup the user first interacted with the application. /// [Serializable] - public struct FirstUserInteractionEventData + public struct FirstUserInteractionEventData : IInputAnalyticData { } @@ -148,7 +214,7 @@ public struct FirstUserInteractionEventData /// Data about what level of data we pumped through the system throughout its lifetime. /// [Serializable] - public struct ShutdownEventData + public struct ShutdownEventData : IInputAnalyticData { public int max_num_devices; public int max_state_size_in_bytes; @@ -157,6 +223,64 @@ public struct ShutdownEventData public int total_frame_count; public float total_event_processing_time; } + +#if (UNITY_EDITOR && UNITY_2023_2_OR_NEWER) + [UnityEngine.Analytics.AnalyticInfo(eventName: kEventName, maxEventsPerHour: kMaxEventsPerHour, + maxNumberOfElements: kMaxNumberOfElements, vendorKey: kVendorKey)] +#endif // (UNITY_EDITOR && UNITY_2023_2_OR_NEWER) + public readonly struct ShutdownEventDataAnalytic : IInputAnalytic + { + public const string kEventName = "input_shutdown"; + public const int kMaxEventsPerHour = 100; + public const int kMaxNumberOfElements = 100; + + private readonly InputManager m_InputManager; + + public ShutdownEventDataAnalytic(InputManager manager) + { + m_InputManager = manager; + } + + public InputAnalyticInfo info => new InputAnalyticInfo(kEventName, kMaxEventsPerHour, kMaxNumberOfElements); + +#if UNITY_EDITOR && UNITY_2023_2_OR_NEWER + public bool TryGatherData(out UnityEngine.Analytics.IAnalytic.IData data, out Exception error) +#else + public bool TryGatherData(out IInputAnalyticData data, out Exception error) +#endif + { + try + { + var metrics = m_InputManager.metrics; + data = new ShutdownEventData + { + max_num_devices = metrics.maxNumDevices, + max_state_size_in_bytes = metrics.maxStateSizeInBytes, + total_event_bytes = metrics.totalEventBytes, + total_event_count = metrics.totalEventCount, + total_frame_count = metrics.totalUpdateCount, + total_event_processing_time = (float)metrics.totalEventProcessingTime, + }; + error = null; + return true; + } + catch (Exception e) + { + data = null; + error = e; + return false; + } + } + } + } + + internal static class AnalyticExtensions + { + internal static void Send(this TSource analytic) where TSource : InputAnalytics.IInputAnalytic + { + InputSystem.s_Manager?.m_Runtime?.SendAnalytic(analytic); + } } } + #endif // UNITY_ANALYTICS || UNITY_EDITOR diff --git a/Packages/com.unity.inputsystem/InputSystem/InputSettings.cs b/Packages/com.unity.inputsystem/InputSystem/InputSettings.cs index a17202f833..4ee37718d9 100644 --- a/Packages/com.unity.inputsystem/InputSystem/InputSettings.cs +++ b/Packages/com.unity.inputsystem/InputSystem/InputSettings.cs @@ -977,5 +977,82 @@ public enum InputActionPropertyDrawerMode /// MultilineBoth, } + + private static bool CompareFloats(float a, float b) + { + return (a - b) <= float.Epsilon; + } + + private static bool CompareSets(ReadOnlyArray a, ReadOnlyArray b) + { + if (ReferenceEquals(null, a)) + return ReferenceEquals(null, b); + if (ReferenceEquals(null, b)) + return false; + for (var i = 0; i < a.Count; ++i) + { + bool existsInB = false; + for (var j = 0; j < b.Count; ++j) + { + if (a[i].Equals(b[j])) + { + existsInB = true; + break; + } + } + + if (!existsInB) + return false; + } + + return true; + } + + private static bool CompareFeatureFlag(InputSettings a, InputSettings b, string featureName) + { + return a.IsFeatureEnabled(featureName) == b.IsFeatureEnabled(featureName); + } + + internal static bool AreEqual(InputSettings a, InputSettings b) + { + if (ReferenceEquals(null, a)) + return ReferenceEquals(null, b); + if (ReferenceEquals(null, b)) + return false; + if (ReferenceEquals(a, b)) + return true; + + return (a.updateMode == b.updateMode) && + (a.compensateForScreenOrientation == b.compensateForScreenOrientation) && + // Ignoring filterNoiseOnCurrent since deprecated + CompareFloats(a.defaultDeadzoneMin, b.defaultDeadzoneMin) && + CompareFloats(a.defaultDeadzoneMax, b.defaultDeadzoneMax) && + CompareFloats(a.defaultButtonPressPoint, b.defaultButtonPressPoint) && + CompareFloats(a.buttonReleaseThreshold, b.buttonReleaseThreshold) && + CompareFloats(a.defaultTapTime, b.defaultTapTime) && + CompareFloats(a.defaultSlowTapTime, b.defaultSlowTapTime) && + CompareFloats(a.defaultHoldTime, b.defaultHoldTime) && + CompareFloats(a.tapRadius, b.tapRadius) && + CompareFloats(a.multiTapDelayTime, b.multiTapDelayTime) && + a.backgroundBehavior == b.backgroundBehavior && + a.editorInputBehaviorInPlayMode == b.editorInputBehaviorInPlayMode && + a.inputActionPropertyDrawerMode == b.inputActionPropertyDrawerMode && + a.maxEventBytesPerUpdate == b.maxEventBytesPerUpdate && + a.maxQueuedEventsPerUpdate == b.maxQueuedEventsPerUpdate && + CompareSets(a.supportedDevices, b.supportedDevices) && + a.disableRedundantEventsMerging == b.disableRedundantEventsMerging && + a.shortcutKeysConsumeInput == b.shortcutKeysConsumeInput && + + CompareFeatureFlag(a, b, InputFeatureNames.kUseOptimizedControls) && + CompareFeatureFlag(a, b, InputFeatureNames.kUseReadValueCaching) && + CompareFeatureFlag(a, b, InputFeatureNames.kParanoidReadValueCachingChecks) && + CompareFeatureFlag(a, b, InputFeatureNames.kDisableUnityRemoteSupport) && + CompareFeatureFlag(a, b, InputFeatureNames.kRunPlayerUpdatesInEditMode) && +#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS + CompareFeatureFlag(a, b, InputFeatureNames.kUseIMGUIEditorForAssets); +#else + true; // Improves formatting +#endif + } } } diff --git a/Packages/com.unity.inputsystem/InputSystem/NativeInputRuntime.cs b/Packages/com.unity.inputsystem/InputSystem/NativeInputRuntime.cs index 0d1b49a092..34d149f461 100644 --- a/Packages/com.unity.inputsystem/InputSystem/NativeInputRuntime.cs +++ b/Packages/com.unity.inputsystem/InputSystem/NativeInputRuntime.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using Unity.Collections.LowLevel.Unsafe; +using UnityEngine.Analytics; using UnityEngine.InputSystem.Utilities; using UnityEngineInternal.Input; @@ -389,27 +390,28 @@ public Action onProjectChange #endif // UNITY_EDITOR - public void RegisterAnalyticsEvent(string name, int maxPerHour, int maxPropertiesPerEvent) - { - #if UNITY_ANALYTICS - const string vendorKey = "unity.input"; - #if UNITY_EDITOR - EditorAnalytics.RegisterEventWithLimit(name, maxPerHour, maxPropertiesPerEvent, vendorKey); - #else - Analytics.Analytics.RegisterEvent(name, maxPerHour, maxPropertiesPerEvent, vendorKey); - #endif // UNITY_EDITOR - #endif // UNITY_ANALYTICS - } + #if UNITY_ANALYTICS || UNITY_EDITOR - public void SendAnalyticsEvent(string name, object data) + public void SendAnalytic(InputAnalytics.IInputAnalytic analytic) { - #if UNITY_ANALYTICS - #if UNITY_EDITOR - EditorAnalytics.SendEventWithLimit(name, data); + #if (UNITY_EDITOR) + #if (UNITY_2023_2_OR_NEWER) + EditorAnalytics.SendAnalytic(analytic); #else - Analytics.Analytics.SendEvent(name, data); + var info = analytic.info; + EditorAnalytics.RegisterEventWithLimit(info.Name, info.MaxEventsPerHour, info.MaxNumberOfElements, InputAnalytics.kVendorKey); + EditorAnalytics.SendEventWithLimit(info.Name, analytic); + #endif // UNITY_2023_2_OR_NEWER + #elif UNITY_ANALYTICS // Implicitly: !UNITY_EDITOR + var info = analytic.info; + Analytics.Analytics.RegisterEvent(info.Name, info.MaxEventsPerHour, info.MaxNumberOfElements, InputAnalytics.kVendorKey); + if (analytic.TryGatherData(out var data, out var error)) + Analytics.Analytics.SendEvent(info.Name, data); + else + Debug.Log(error); // Non fatal #endif // UNITY_EDITOR - #endif // UNITY_ANALYTICS } + + #endif // UNITY_ANALYTICS || UNITY_EDITOR } } diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/EnhancedTouch/TouchSimulation.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/EnhancedTouch/TouchSimulation.cs index f8de763e49..bc97fb4d55 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/EnhancedTouch/TouchSimulation.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/EnhancedTouch/TouchSimulation.cs @@ -385,7 +385,16 @@ private static void OnSettingsChanged() Disable(); } - #endif + [CustomEditor(typeof(TouchSimulation))] + private class TouchSimulationEditor : UnityEditor.Editor + { + public void OnDisable() + { + new InputComponentEditorAnalytic(InputSystemComponent.TouchSimulation).Send(); + } + } + + #endif // UNITY_EDITOR ////TODO: Remove IInputStateChangeMonitor from this class when we can break the API void IInputStateChangeMonitor.NotifyControlStateChanged(InputControl control, double time, InputEventPtr eventPtr, long monitorIndex) diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/OnScreen/OnScreenButton.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/OnScreen/OnScreenButton.cs index 1afd7125eb..74308b3a2f 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/OnScreen/OnScreenButton.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/OnScreen/OnScreenButton.cs @@ -2,6 +2,10 @@ using UnityEngine.EventSystems; using UnityEngine.InputSystem.Layouts; +#if UNITY_EDITOR +using UnityEngine.InputSystem.Editor; +#endif + ////TODO: custom icon for OnScreenButton component namespace UnityEngine.InputSystem.OnScreen @@ -56,6 +60,11 @@ public void OnEnable() m_ControlPathInternal = serializedObject.FindProperty(nameof(OnScreenButton.m_ControlPath)); } + public void OnDisable() + { + new InputComponentEditorAnalytic(InputSystemComponent.OnScreenButton).Send(); + } + public override void OnInspectorGUI() { // Current implementation has UGUI dependencies (ISXB-915, ISXB-916) diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/OnScreen/OnScreenStick.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/OnScreen/OnScreenStick.cs index 0e5bae8514..153f24a580 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/OnScreen/OnScreenStick.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/OnScreen/OnScreenStick.cs @@ -10,6 +10,7 @@ #if UNITY_EDITOR using UnityEditor; using UnityEditor.AnimatedValues; +using UnityEngine.InputSystem.Editor; #endif ////TODO: custom icon for OnScreenStick component @@ -466,6 +467,13 @@ public void OnEnable() m_PointerMoveAction = serializedObject.FindProperty(nameof(OnScreenStick.m_PointerMoveAction)); } + public void OnDisable() + { + // Report analytics + new InputComponentEditorAnalytic(InputSystemComponent.OnScreenStick).Send(); + new OnScreenStickEditorAnalytic(this).Send(); + } + public override void OnInspectorGUI() { // Current implementation has UGUI dependencies (ISXB-915, ISXB-916) diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/PlayerInputEditor.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/PlayerInputEditor.cs index 265f496b71..cc078b4227 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/PlayerInputEditor.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/PlayerInputEditor.cs @@ -48,6 +48,12 @@ public void OnEnable() #endif } + public void OnDisable() + { + new InputComponentEditorAnalytic(InputSystemComponent.PlayerInput).Send(); + new PlayerInputEditorAnalytic(this).Send(); + } + public void OnDestroy() { InputActionImporter.onImport -= Refresh; diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/PlayerInputManagerEditor.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/PlayerInputManagerEditor.cs index 36cb4f72b8..48055a7373 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/PlayerInputManagerEditor.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/PlayerInputManagerEditor.cs @@ -17,6 +17,12 @@ public void OnEnable() InputUser.onChange += OnUserChange; } + public void OnDisable() + { + new InputComponentEditorAnalytic(InputSystemComponent.PlayerInputManager).Send(); + new PlayerInputManagerEditorAnalytic(this).Send(); + } + public void OnDestroy() { InputUser.onChange -= OnUserChange; diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModuleEditor.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModuleEditor.cs index 86caf47eb7..a160f2c9c0 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModuleEditor.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModuleEditor.cs @@ -100,6 +100,11 @@ public void OnEnable() .Concat(m_AvailableActionReferencesInAssetDatabase?.Select(x => MakeActionReferenceNameUsableInGenericMenu(x.name)) ?? new string[0]).ToArray(); } + public void OnDisable() + { + new InputComponentEditorAnalytic(InputSystemComponent.InputSystemUIInputModule).Send(); + } + public static void ReassignActions(InputSystemUIInputModule module, InputActionAsset action) { module.actionsAsset = action; diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/StandaloneInputModuleEditor.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/StandaloneInputModuleEditor.cs index 178a313dbb..67dd61da95 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/StandaloneInputModuleEditor.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/StandaloneInputModuleEditor.cs @@ -1,7 +1,9 @@ #if UNITY_EDITOR && UNITY_INPUT_SYSTEM_ENABLE_UI +using System; using UnityEditor; using UnityEngine.EventSystems; +using UnityEngine.InputSystem.Editor; namespace UnityEngine.InputSystem.UI.Editor { @@ -45,6 +47,11 @@ public override void OnInspectorGUI() } base.OnInspectorGUI(); } + + public void OnDisable() + { + new InputComponentEditorAnalytic(InputSystemComponent.StandaloneInputModule).Send(); + } } } #endif diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/VirtualMouseInput.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/VirtualMouseInput.cs index 41499e2052..cb8aea5535 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/VirtualMouseInput.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/VirtualMouseInput.cs @@ -3,6 +3,10 @@ using UnityEngine.InputSystem.LowLevel; using UnityEngine.UI; +#if UNITY_EDITOR +using UnityEngine.InputSystem.Editor; +#endif + ////TODO: respect cursor lock mode ////TODO: investigate how driving the HW cursor behaves when FPS drops low @@ -608,6 +612,18 @@ public enum CursorMode /// HardwareCursorIfAvailable, } + + #if UNITY_EDITOR + [UnityEditor.CustomEditor(typeof(VirtualMouseInput))] + private class VirtualMouseInputEditor : UnityEditor.Editor + { + public void OnDisable() + { + new InputComponentEditorAnalytic(InputSystemComponent.VirtualMouseInput).Send(); + new VirtualMouseInputEditorAnalytic(this).Send(); + } + } + #endif } } #endif // PACKAGE_DOCS_GENERATION || UNITY_INPUT_SYSTEM_ENABLE_UI diff --git a/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestFixture.cs b/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestFixture.cs index 14c3f6398f..2727be91be 100644 --- a/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestFixture.cs +++ b/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestFixture.cs @@ -912,5 +912,102 @@ internal void SimulateDomainReload() } #endif + + #if UNITY_EDITOR + /// + /// Represents an analytics registration event captured by test harness. + /// + protected struct AnalyticsRegistrationEventData + { + public AnalyticsRegistrationEventData(string name, int maxPerHour, int maxPropertiesPerEvent) + { + this.name = name; + this.maxPerHour = maxPerHour; + this.maxPropertiesPerEvent = maxPropertiesPerEvent; + } + + public readonly string name; + public readonly int maxPerHour; + public readonly int maxPropertiesPerEvent; + } + + /// + /// Represents an analytics data event captured by test harness. + /// + protected struct AnalyticsEventData + { + public AnalyticsEventData(string name, object data) + { + this.name = name; + this.data = data; + } + + public readonly string name; + public readonly object data; + } + + private List m_RegisteredAnalytics; + private List m_SentAnalyticsEvents; + + /// + /// Returns a read-only list of all analytics events registred by enabling capture via . + /// + protected IReadOnlyList registeredAnalytics => m_RegisteredAnalytics; + + /// + /// Returns a read-only list of all analytics events captured by enabling capture via . + /// + protected IReadOnlyList sentAnalyticsEvents => m_SentAnalyticsEvents; + + /// + /// Set up the test fixture to collect analytics registrations and events + /// + /// A filter predicate evaluating whether the given analytics name should be accepted to be stored in test fixture. + protected void CollectAnalytics(Predicate analyticsNameFilter) + { + // Make sure containers are initialized and create them if not. Otherwise just clear to avoid allocation. + if (m_RegisteredAnalytics == null) + m_RegisteredAnalytics = new List(); + else + m_RegisteredAnalytics.Clear(); + if (m_SentAnalyticsEvents == null) + m_SentAnalyticsEvents = new List(); + else + m_SentAnalyticsEvents.Clear(); + + // Store registered analytics when called if filter applies + runtime.onRegisterAnalyticsEvent = (name, maxPerHour, maxPropertiesPerEvent) => + { + if (analyticsNameFilter(name)) + m_RegisteredAnalytics.Add(new AnalyticsRegistrationEventData(name: name, maxPerHour: maxPerHour, maxPropertiesPerEvent: maxPropertiesPerEvent)); + }; + + // Store sent analytic events when called if filter applies + runtime.onSendAnalyticsEvent = (name, data) => + { + if (analyticsNameFilter(name)) + m_SentAnalyticsEvents.Add(new AnalyticsEventData(name: name, data: data)); + }; + } + + /// + /// Set up the test fixture to collect filtered analytics registrations and events. + /// + /// The analytics name to be accepted, all other registrations and data + /// will be discarded. + protected void CollectAnalytics(string acceptedName) + { + CollectAnalytics((name) => name.Equals(acceptedName)); + } + + /// + /// Set up the test fixture to collect ALL analytics registrations and events. + /// + protected void CollectAnalytics() + { + CollectAnalytics((_) => true); + } + + #endif } } diff --git a/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestRuntime.cs b/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestRuntime.cs index 17b1d9180c..83745a2af5 100644 --- a/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestRuntime.cs +++ b/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestRuntime.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using UnityEngine.InputSystem.LowLevel; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; +using UnityEngine.Analytics; using UnityEngine.InputSystem.Layouts; using UnityEngine.InputSystem.Utilities; @@ -451,14 +453,35 @@ public void SetUnityRemoteGyroUpdateInterval(float interval) public Action onRegisterAnalyticsEvent { get; set; } public Action onSendAnalyticsEvent { get; set; } - public void RegisterAnalyticsEvent(string name, int maxPerHour, int maxPropertiesPerEvent) + public void SendAnalytic(InputAnalytics.IInputAnalytic analytic) { - onRegisterAnalyticsEvent?.Invoke(name, maxPerHour, maxPropertiesPerEvent); - } + #if UNITY_2023_2_OR_NEWER + + // Mimic editor analytics for Unity 2023.2+ invoking TryGatherData to send + var analyticInfoAttribute = analytic.GetType().GetCustomAttributes( + typeof(AnalyticInfoAttribute), true).FirstOrDefault() as AnalyticInfoAttribute; + var info = analytic.info; + #if UNITY_EDITOR + // Registration handled by framework + #else + onRegisterAnalyticsEvent?.Invoke(info.Name, info.MaxEventsPerHour, info.MaxNumberOfElements); // only to avoid writing two tests per Unity version (registration handled by framework) + #endif + if (analytic.TryGatherData(out var data, out var ex) && data != null && analyticInfoAttribute != null) + onSendAnalyticsEvent?.Invoke(analyticInfoAttribute.eventName, data); + else if (ex != null) + throw ex; // rethrow for visibility in test scope + + #else + + var info = analytic.info; + onRegisterAnalyticsEvent?.Invoke(info.Name, info.MaxEventsPerHour, info.MaxNumberOfElements); + + if (analytic.TryGatherData(out var data, out var error)) + onSendAnalyticsEvent?.Invoke(info.Name, data); + else + throw error; // For visibility in tests - public void SendAnalyticsEvent(string name, object data) - { - onSendAnalyticsEvent?.Invoke(name, data); + #endif // UNITY_2023_2_OR_NEWER } #endif // UNITY_ANALYTICS || UNITY_EDITOR