From 717360bc6f1761241cb64af533cf5a797cf2274d Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Thu, 27 Aug 2026 10:26:09 +0300 Subject: [PATCH 01/22] feat: gate Editor haptics logs behind a config toggle --- CHANGELOG.md | 5 +++ Editor/Settings/MobileServicesConfig.cs | 41 +++++++++++++++++++ Editor/Settings/MobileServicesConfigEditor.cs | 3 ++ README.md | 2 +- .../Haptics/Internal/EditorHapticsBackend.cs | 13 ++++-- .../EditMode/Unit/EditorHapticsBackendTest.cs | 25 +++++++++++ docs/build-pipeline.md | 1 + docs/haptics.md | 11 +++-- 8 files changed, 94 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3fb22c..0a08e89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this package will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +**New**: +- Added an `Enable Haptics Debug Logs` Mobile Services Config toggle; Editor haptics logs are disabled by default. + ## [1.2.0] - 2026-08-14 **New**: diff --git a/Editor/Settings/MobileServicesConfig.cs b/Editor/Settings/MobileServicesConfig.cs index b38c413..dcf3218 100644 --- a/Editor/Settings/MobileServicesConfig.cs +++ b/Editor/Settings/MobileServicesConfig.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using GameLovers.MobileServices.Device; +using GameLovers.MobileServices.Haptics.Internal; using UnityEditor; using UnityEngine; @@ -257,6 +258,8 @@ public sealed class MobileServicesConfig : ScriptableObject [SerializeField] private CapabilityToggles _capabilities = new CapabilityToggles(); [SerializeField] private AndroidManifestToggles _androidManifest = new AndroidManifestToggles(); [SerializeField] private NativeDeepLinkSettings _deepLinks = new NativeDeepLinkSettings(); + [Tooltip("Logs Editor haptics play and stop calls to the Unity console. Disabled by default.")] + [SerializeField] private bool _enableHapticsDebugLogs; [SerializeField] private bool _includePlayReviewDependency = true; [SerializeField] private string _playReviewDependencyCoordinate = DefaultPlayReviewCoordinate; [SerializeField] private bool _manageNativeBuildManually; @@ -273,6 +276,12 @@ private void OnEnable() _deepLinks.IosUrlSchemes ??= new List(); _deepLinks.AndroidIntentFilters ??= new List(); _playReviewDependencyCoordinate ??= DefaultPlayReviewCoordinate; + ApplyEditorRuntimeSettings(); + } + + private void OnValidate() + { + ApplyEditorRuntimeSettings(); } /// @@ -329,6 +338,18 @@ public static MobileServicesConfig Instance /// iOS URL scheme and Android intent-filter declarations applied at build time. public NativeDeepLinkSettings DeepLinks => _deepLinks ??= new NativeDeepLinkSettings(); + /// Whether Editor haptics calls write diagnostic messages to the Unity console. + public bool EnableHapticsDebugLogs + { + get => _enableHapticsDebugLogs; + set + { + _enableHapticsDebugLogs = value; + ApplyEditorRuntimeSettings(); + Persist(); + } + } + /// /// When true (the default), the Android build postprocessor auto-injects the Play In-App /// Review dependency () into the generated Gradle @@ -736,6 +757,11 @@ private static MobileServicesConfig FindAsset() return TryGetPersistedConfig(out var config) ? config : null; } + private void ApplyEditorRuntimeSettings() + { + EditorHapticsBackend.DebugLoggingEnabled = _enableHapticsDebugLogs; + } + private static void CollectLocales(List entries, HashSet set) { foreach (var entry in entries ?? new List()) @@ -772,4 +798,19 @@ private static void SetLocaleEntry(List entries, string locale, str entries.Add(new LocaleEntry { LocaleCode = locale, UsageDescription = text }); } } + + /// Restores Mobile Services Editor runtime settings after Unity reloads the package assemblies. + [InitializeOnLoad] + internal static class MobileServicesConfigInitializer + { + static MobileServicesConfigInitializer() + { + EditorApplication.delayCall += Apply; + } + + private static void Apply() + { + EditorHapticsBackend.DebugLoggingEnabled = MobileServicesConfig.Instance.EnableHapticsDebugLogs; + } + } } diff --git a/Editor/Settings/MobileServicesConfigEditor.cs b/Editor/Settings/MobileServicesConfigEditor.cs index e0da7f1..69e12f0 100644 --- a/Editor/Settings/MobileServicesConfigEditor.cs +++ b/Editor/Settings/MobileServicesConfigEditor.cs @@ -71,6 +71,9 @@ public override VisualElement CreateInspectorGUI() root.Add(MakeSection("Build behaviour")); root.Add(new PropertyField(serializedObject.FindProperty("_manageNativeBuildManually"), "Manage Native Build Manually")); + root.Add(MakeSection("Editor diagnostics")); + root.Add(new PropertyField(serializedObject.FindProperty("_enableHapticsDebugLogs"), "Enable Haptics Debug Logs")); + root.Add(MakeSection("Tools")); root.Add(BuildScanButton(config)); root.Add(BuildSuggestCopyButton(config)); diff --git a/README.md b/README.md index ab173d9..801d673 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ Use the specific subsystem namespaces—`Notifications`, `Haptics`, `NativeUi`, | Haptics | Presets, custom output, and bounded loops | | Device | Permissions, ATT, deep links, safe-area and device helpers | | Gestures | Gesture controller for explicit gesture input ownership | -| Editor tooling | Device Simulator integration and build helpers | +| Editor tooling | Device Simulator integration, optional haptics diagnostics, and build helpers | Runtime alert calls render an interactive platform-shaped mock in the Game view even when the Device Simulator window is closed. Prefer `ShowAlertPopUpAsync` for a selected-index result on Unity's main thread; legacy callbacks also switch through Unity's `Awaitable` scheduler. The editor simulator is for exercising application paths; it is not a substitute for device permission, notification-delivery, review, or native-build validation. diff --git a/Runtime/Haptics/Internal/EditorHapticsBackend.cs b/Runtime/Haptics/Internal/EditorHapticsBackend.cs index 7930dc9..dcddb01 100644 --- a/Runtime/Haptics/Internal/EditorHapticsBackend.cs +++ b/Runtime/Haptics/Internal/EditorHapticsBackend.cs @@ -4,36 +4,43 @@ namespace GameLovers.MobileServices.Haptics.Internal { /// - /// Editor backend. Logs every call to the Unity console for visibility while developing; - /// no native haptic is produced. is always false so caller - /// code that gates on capability behaves the same as on a real unsupported device. + /// Editor backend. Optionally logs calls to the Unity console for visibility while developing; + /// no native haptic is produced. is always false so caller code + /// that gates on capability behaves the same as on a real unsupported device. /// internal sealed class EditorHapticsBackend : IHapticsBackend { + /// Controls whether Editor haptics calls write diagnostic messages to the Unity console. + internal static bool DebugLoggingEnabled { get; set; } + /// public bool IsSupported => false; /// public void PlayPresetOneShot(HapticPreset preset) { + if (!DebugLoggingEnabled) return; Debug.Log($"[Haptics] PlayPresetOneShot({preset})"); } /// public void PlayPresetLoop(HapticPreset preset) { + if (!DebugLoggingEnabled) return; Debug.Log($"[Haptics] PlayPresetLoop({preset})"); } /// public void PlayCustom(float intensity01, float durationMs) { + if (!DebugLoggingEnabled) return; Debug.Log($"[Haptics] PlayCustom(intensity={intensity01:0.00}, durationMs={durationMs:0})"); } /// public void Stop() { + if (!DebugLoggingEnabled) return; Debug.Log("[Haptics] Stop"); } } diff --git a/Tests/EditMode/Unit/EditorHapticsBackendTest.cs b/Tests/EditMode/Unit/EditorHapticsBackendTest.cs index 19ceabb..0c8e76c 100644 --- a/Tests/EditMode/Unit/EditorHapticsBackendTest.cs +++ b/Tests/EditMode/Unit/EditorHapticsBackendTest.cs @@ -12,13 +12,22 @@ namespace GameLoversEditor.MobileServices.Tests public class EditorHapticsBackendTest { private EditorHapticsBackend _backend; + private bool _debugLoggingWasEnabled; [SetUp] public void Init() { + _debugLoggingWasEnabled = EditorHapticsBackend.DebugLoggingEnabled; + EditorHapticsBackend.DebugLoggingEnabled = true; _backend = new EditorHapticsBackend(); } + [TearDown] + public void Cleanup() + { + EditorHapticsBackend.DebugLoggingEnabled = _debugLoggingWasEnabled; + } + [Test] // ADMIT: EditorHapticsBackend could claim haptic support in the Editor, so capability-gated caller code takes the device path. // RCR: EditorHapticsBackend.cs IsSupported — `=> false` → `=> true` → RED (expected False was True). Also reddens HapticsServiceTest.DefaultCtor_InEditor_SelectsEditorBackend_NotSupported. @@ -27,6 +36,22 @@ public void IsSupported_AlwaysFalse() Assert.IsFalse(_backend.IsSupported); } + [Test] + // ADMIT: EditorHapticsBackend must suppress every play and stop diagnostic when debug logging is disabled. + // RCR: EditorHapticsBackend.cs PlayPresetOneShot — remove the disabled guard → RED + // (unexpected PlayPresetOneShot log). 2026-08-14 + public void AllCalls_DebugLoggingDisabled_DoNotLog() + { + EditorHapticsBackend.DebugLoggingEnabled = false; + + _backend.PlayPresetOneShot(HapticPreset.Selection); + _backend.PlayPresetLoop(HapticPreset.Warning); + _backend.PlayCustom(0.42f, 250f); + _backend.Stop(); + + LogAssert.NoUnexpectedReceived(); + } + [Test] // ADMIT: EditorHapticsBackend.PlayPresetOneShot could change its console format, breaking the Editor's only haptic feedback signal. // RCR: EditorHapticsBackend.cs PlayPresetOneShot — log text `PlayPresetOneShot(` → `PlayOneShot(` → RED (LogAssert expected message not received). diff --git a/docs/build-pipeline.md b/docs/build-pipeline.md index 8fef43b..a5ec97b 100644 --- a/docs/build-pipeline.md +++ b/docs/build-pipeline.md @@ -16,6 +16,7 @@ The Inspector (`MobileServicesConfigEditor`) shows: - **Native deep links** — `DeepLinks.IosUrlSchemes` and `DeepLinks.AndroidIntentFilters` are deduplicated by semantic identity. Malformed rows fail before a build file is touched. - **Android dependencies** — `Include Play Review Dependency` toggle (**ON by default**) + editable `Play Review Coordinate` (default `com.google.android.play:review:2.0.2`). When on, the Play In-App Review library is auto-injected into the generated Gradle project so `NativeUiService.RequestReview()` works with zero manual setup. - **Build behaviour** — `Manage Native Build Manually` (**OFF by default**) — when ON, the package writes nothing to the iOS/Android build and skips the iOS usage-description validation. It's the single escape for teams that configure the native build (Xcode / Gradle) themselves. (This is *build* configuration — unrelated to render post-processing.) +- **Editor diagnostics** — `Enable Haptics Debug Logs` (**OFF by default**) writes Editor haptics play/stop calls to the Console when diagnostic visibility is needed. It never affects player builds or native error reporting. - **Tools** — `Scan project for used services` (reflection-based pre-fill of capability toggles), `Fill missing English descriptions with suggested copy`, `Generate iOS Privacy Nutrition Label draft`. Accessed in editor tooling via `MobileServicesConfig.Instance` (cached locator; it may return a transient default for convenience). Build callbacks use `TryGetPersistedConfig(out config)` and therefore perform no native work when no persisted config exists unless a sample or another explicit owner pushes a temporary context. diff --git a/docs/haptics.md b/docs/haptics.md index 763626e..edd8a74 100644 --- a/docs/haptics.md +++ b/docs/haptics.md @@ -62,7 +62,7 @@ Always finite; `durationMs <= 0` is a no-op. Intensity is clamped to `[0, 1]`. O |----------|---------| | iOS | `IosHapticsBackend` — UIKit `UIImpactFeedbackGenerator` / `UINotificationFeedbackGenerator` / `UISelectionFeedbackGenerator`. Looping via `NSTimer`. | | Android | `AndroidHapticsBackend` — pure JNI `Vibrator.vibrate(VibrationEffect)`. Requires API 26 (Android 8.0)+. | -| Editor | `EditorHapticsBackend` — logs only. | +| Editor | `EditorHapticsBackend` — no native output; optional console diagnostics. | | Other (Standalone, WebGL) | `NoOpHapticsBackend` — `IsSupported = false`. | The backend is selected at construction by `CreateDefaultBackend()`. Tests construct `HapticsService` with the internal injection constructor + a `FakeHapticsBackend`. @@ -77,9 +77,14 @@ Internal accessors on `HapticsService` (visible to the Editor assembly via `Inte These power the Device Simulator panel's **Haptics** status read-out. -## Editor testing on a real device +## Editor diagnostics and real-device testing -In the editor, the `EditorHapticsBackend` only logs. To test haptic feel on a paired phone: +The Editor backend is silent by default so frequent haptics do not overwhelm the Console. To inspect every +play and stop call, open **Tools > GameLovers > Mobile Services > Select Mobile Services Config** and enable +**Editor diagnostics > Enable Haptics Debug Logs**. This team-shared setting affects only the Editor and is +never included in player builds. + +To test haptic feel on a paired phone: 1. Use Unity Remote (haptics will NOT fire through Unity Remote — it only relays input/display). 2. Or deploy a debug build to the device for the iteration loop. The `HapticsPalette` sample is designed for this loop. From 18f256359fbb02e4d3b202e03b3598d86b6ded14 Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Thu, 27 Aug 2026 10:26:09 +0300 Subject: [PATCH 02/22] docs: slim CLAUDE wrappers to @AGENTS.md --- CLAUDE.md | 11 ----------- Tests/CLAUDE.md | 11 ----------- 2 files changed, 22 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 12e6eb0..43c994c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,12 +1 @@ -# Claude Code Guide — GameLovers Mobile Services - -This package's contributor/agent guide lives in `AGENTS.md`. -Claude Code will automatically import it below. - @AGENTS.md - -## Claude-Specific Notes - -- Treat `AGENTS.md` as the source of truth. -- If anything in this file appears to conflict with `AGENTS.md`, prefer `AGENTS.md`. -- For user-facing usage, see `README.md`. diff --git a/Tests/CLAUDE.md b/Tests/CLAUDE.md index 2585944..43c994c 100644 --- a/Tests/CLAUDE.md +++ b/Tests/CLAUDE.md @@ -1,12 +1 @@ -# Claude Code Guide — GameLovers Mobile Services Tests - -This folder's testing conventions live in `AGENTS.md`. -Claude Code will automatically import it below. - @AGENTS.md - -## Claude-Specific Notes - -- Treat `AGENTS.md` as the source of truth. -- If anything in this file appears to conflict with `AGENTS.md`, prefer `AGENTS.md`. -- For package-level architecture and runtime gotchas, see `../AGENTS.md`. From 7c29ffc64cf28558c91ced5d8c39623f29d95399 Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Wed, 9 Sep 2026 01:37:32 +0300 Subject: [PATCH 03/22] fix: keep Android review handles alive until the review flow callback returns The activity and ReviewManager were scoped with using var, so both AndroidJavaObjects were disposed before Play Services invoked ReviewFlowListener.onComplete, and launchReviewFlow always threw a NullReferenceException. Ownership now passes to the listener, which disposes both in a finally block, and RequestReviewAndroid disposes them itself when registration throws. Co-authored-by: Cursor --- CHANGELOG.md | 3 +++ Runtime/NativeUi/NativeUiService.cs | 13 +++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a08e89..6e1c78e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 **New**: - Added an `Enable Haptics Debug Logs` Mobile Services Config toggle; Editor haptics logs are disabled by default. +**Fixed**: +- Retained the Android activity and review manager Java references until the async `requestReviewFlow` callback completes, preventing a `NullReferenceException` in `launchReviewFlow`. + ## [1.2.0] - 2026-08-14 **New**: diff --git a/Runtime/NativeUi/NativeUiService.cs b/Runtime/NativeUi/NativeUiService.cs index 6167be0..82662b4 100644 --- a/Runtime/NativeUi/NativeUiService.cs +++ b/Runtime/NativeUi/NativeUiService.cs @@ -473,17 +473,21 @@ private static void ReleaseCurrentAndroidAlert() private static void RequestReviewAndroid() { + AndroidJavaObject activity = null; + AndroidJavaObject manager = null; try { using var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); - using var activity = unityPlayer.GetStatic("currentActivity"); + activity = unityPlayer.GetStatic("currentActivity"); using var managerFactory = new AndroidJavaClass("com.google.android.play.core.review.ReviewManagerFactory"); - using var manager = managerFactory.CallStatic("create", activity); + manager = managerFactory.CallStatic("create", activity); using var requestTask = manager.Call("requestReviewFlow"); requestTask.Call("addOnCompleteListener", new ReviewFlowListener(activity, manager)); } catch (Exception e) { + manager?.Dispose(); + activity?.Dispose(); Debug.LogError($"[GameLovers.MobileServices] RequestReview failed: {e.Message}. " + "Ensure 'com.google.android.play:review' is on the gradle classpath."); } @@ -567,6 +571,11 @@ public void onComplete(AndroidJavaObject task) { Debug.LogError($"[GameLovers.MobileServices] launchReviewFlow failed: {e.Message}"); } + finally + { + _manager?.Dispose(); + _activity?.Dispose(); + } } } #endif From 86549272ba393e8b04d59e75778bf76d94d31cb3 Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Wed, 9 Sep 2026 01:37:57 +0300 Subject: [PATCH 04/22] fix: use Device Simulator screen coordinates for safe areas Co-authored-by: Cursor --- CHANGELOG.md | 1 + README.md | 2 ++ Runtime/Device/State/SafeAreaContainer.cs | 1 + Runtime/Device/State/SafeAreaService.cs | 1 + Samples~/MobileServicesSamples/README.md | 2 ++ Tests/EditMode/Unit/SafeAreaContainerTest.cs | 1 + Tests/PlayMode/Unit/SafeAreaContainerPanelScaleTest.cs | 1 + Tests/PlayMode/Unit/SafeAreaServiceTest.cs | 1 + docs/device.md | 2 ++ 9 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e1c78e..aae1407 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 **Fixed**: - Retained the Android activity and review manager Java references until the async `requestReviewFlow` callback completes, preventing a `NullReferenceException` in `launchReviewFlow`. +- Safe-area services and UI Toolkit containers use the selected Unity Device Simulator profile when no explicit Editor override is active. ## [1.2.0] - 2026-08-14 diff --git a/README.md b/README.md index 801d673..563992e 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,8 @@ IDisposable ownedNotifications = notifications; Use the specific subsystem namespaces—`Notifications`, `Haptics`, `NativeUi`, and `Device`—rather than assuming one umbrella import exposes every type. +Safe-area coordinates follow Unity's selected Device Simulator profile through `UnityEngine.Device.Screen`; an explicit `EditorPlatformSimulator` safe-area override takes precedence. + ## Services | Area | Provides | diff --git a/Runtime/Device/State/SafeAreaContainer.cs b/Runtime/Device/State/SafeAreaContainer.cs index 1c3b1e9..12ef793 100644 --- a/Runtime/Device/State/SafeAreaContainer.cs +++ b/Runtime/Device/State/SafeAreaContainer.cs @@ -1,4 +1,5 @@ using UnityEngine; +using Screen = UnityEngine.Device.Screen; using UnityEngine.UIElements; // ReSharper disable once CheckNamespace diff --git a/Runtime/Device/State/SafeAreaService.cs b/Runtime/Device/State/SafeAreaService.cs index 88a09ec..fb2a767 100644 --- a/Runtime/Device/State/SafeAreaService.cs +++ b/Runtime/Device/State/SafeAreaService.cs @@ -1,6 +1,7 @@ using System; using GameLovers.MobileServices.Device.Internal; using UnityEngine; +using Screen = UnityEngine.Device.Screen; // ReSharper disable once CheckNamespace namespace GameLovers.MobileServices.Device diff --git a/Samples~/MobileServicesSamples/README.md b/Samples~/MobileServicesSamples/README.md index 8558dbd..588bdfb 100644 --- a/Samples~/MobileServicesSamples/README.md +++ b/Samples~/MobileServicesSamples/README.md @@ -25,6 +25,8 @@ The Overview view covers package areas that are not duplicated by the focused vi - safe-area and device status; - `GestureController` tap and swipe logs. +Safe-area coordinates follow Unity's selected Device Simulator profile through `UnityEngine.Device.Screen`; an explicit `EditorPlatformSimulator` safe-area override takes precedence. + The live device card reports battery, low-power, keep-awake, ATT, and safe-area state on separate lines. The Editor and Device Simulator use safe no-op or mock implementations where an operating-system feature is unavailable. The Overview alert controls keep the callback overload so their activity log can demonstrate legacy integration. For new consumer flows that need the selected zero-based button index, prefer `NativeUiService.ShowAlertPopUpAsync`; dismissal or replacement cancels its `Awaitable`. diff --git a/Tests/EditMode/Unit/SafeAreaContainerTest.cs b/Tests/EditMode/Unit/SafeAreaContainerTest.cs index e9f955a..5b728b5 100644 --- a/Tests/EditMode/Unit/SafeAreaContainerTest.cs +++ b/Tests/EditMode/Unit/SafeAreaContainerTest.cs @@ -2,6 +2,7 @@ using GameLovers.MobileServices.Device; using NUnit.Framework; using UnityEngine; +using Screen = UnityEngine.Device.Screen; // ReSharper disable once CheckNamespace diff --git a/Tests/PlayMode/Unit/SafeAreaContainerPanelScaleTest.cs b/Tests/PlayMode/Unit/SafeAreaContainerPanelScaleTest.cs index 07dd0f7..e97ca85 100644 --- a/Tests/PlayMode/Unit/SafeAreaContainerPanelScaleTest.cs +++ b/Tests/PlayMode/Unit/SafeAreaContainerPanelScaleTest.cs @@ -3,6 +3,7 @@ using GameLovers.MobileServices.Device; using NUnit.Framework; using UnityEngine; +using Screen = UnityEngine.Device.Screen; using UnityEngine.TestTools; using UnityEngine.UIElements; diff --git a/Tests/PlayMode/Unit/SafeAreaServiceTest.cs b/Tests/PlayMode/Unit/SafeAreaServiceTest.cs index 6407727..25b5c10 100644 --- a/Tests/PlayMode/Unit/SafeAreaServiceTest.cs +++ b/Tests/PlayMode/Unit/SafeAreaServiceTest.cs @@ -3,6 +3,7 @@ using GameLovers.MobileServices.Device.Internal; using NUnit.Framework; using UnityEngine; +using Screen = UnityEngine.Device.Screen; using UnityEngine.TestTools; // ReSharper disable once CheckNamespace diff --git a/docs/device.md b/docs/device.md index e012f93..7449fde 100644 --- a/docs/device.md +++ b/docs/device.md @@ -114,6 +114,8 @@ Literal segments match exactly (case-insensitive); `:name` segments capture into ## Safe Area +Safe-area coordinates follow Unity's selected Device Simulator profile through `UnityEngine.Device.Screen`; an explicit `EditorPlatformSimulator` safe-area override takes precedence. + `SafeAreaContainer` is a companion UI Toolkit `VisualElement` that pads itself to the safe area: ```csharp From 20fd1b98fd65aa31599393f66b18a70a7d49b249 Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Fri, 11 Sep 2026 16:03:35 +0300 Subject: [PATCH 05/22] feat(device)!: remove safe-area service and views Delete ISafeAreaService, SafeAreaService, and SafeAreaContainer with their tests; narrow DeviceService to five children; remove the simulator override and sample/docs references. BREAKING: IDeviceService.SafeArea is gone; use com.gamelovers.uiservice SafeAreaPanelView/SafeAreaContainer for UI safe-area needs. --- AGENTS.md | 4 +- CHANGELOG.md | 4 +- Editor/Simulation/EditorPlatformSimulator.cs | 32 +---- README.md | 4 +- Runtime/Device/DeviceService.cs | 12 +- Runtime/Device/IDeviceService.cs | 3 - Runtime/Device/Internal/DeviceServicesHost.cs | 2 +- Runtime/Device/State/ISafeAreaService.cs | 19 --- Runtime/Device/State/ISafeAreaService.cs.meta | 2 - Runtime/Device/State/SafeAreaContainer.cs | 90 --------------- .../Device/State/SafeAreaContainer.cs.meta | 2 - Runtime/Device/State/SafeAreaService.cs | 76 ------------ Runtime/Device/State/SafeAreaService.cs.meta | 2 - Runtime/IMobileService.cs | 2 +- .../DeepLinkRouter/DeepLinkRouter.uxml | 6 +- .../HapticsPalette/HapticsPalette.uxml | 6 +- .../MobileServicesPlayground.uxml | 10 +- .../MobileServicesPlaygroundUI.cs | 8 -- .../NotificationsScheduler.uxml | 6 +- .../NotificationsSchedulerUI.cs | 2 - Samples~/MobileServicesSamples/README.md | 8 +- Tests/AGENTS.md | 2 +- Tests/EditMode/Unit/DeviceServiceTest.cs | 13 +-- Tests/EditMode/Unit/SafeAreaContainerTest.cs | 109 ------------------ .../Unit/SafeAreaContainerTest.cs.meta | 2 - .../Unit/DeviceServicePlayModeTest.cs | 1 - Tests/PlayMode/Unit/DeviceServicesHostTest.cs | 2 +- .../Unit/SafeAreaContainerPanelScaleTest.cs | 54 --------- .../SafeAreaContainerPanelScaleTest.cs.meta | 2 - Tests/PlayMode/Unit/SafeAreaServiceTest.cs | 81 ------------- .../PlayMode/Unit/SafeAreaServiceTest.cs.meta | 2 - docs/device.md | 28 +---- docs/explorer.md | 13 +-- docs/samples.md | 4 +- 34 files changed, 43 insertions(+), 570 deletions(-) delete mode 100644 Runtime/Device/State/ISafeAreaService.cs delete mode 100644 Runtime/Device/State/ISafeAreaService.cs.meta delete mode 100644 Runtime/Device/State/SafeAreaContainer.cs delete mode 100644 Runtime/Device/State/SafeAreaContainer.cs.meta delete mode 100644 Runtime/Device/State/SafeAreaService.cs delete mode 100644 Runtime/Device/State/SafeAreaService.cs.meta delete mode 100644 Tests/EditMode/Unit/SafeAreaContainerTest.cs delete mode 100644 Tests/EditMode/Unit/SafeAreaContainerTest.cs.meta delete mode 100644 Tests/PlayMode/Unit/SafeAreaContainerPanelScaleTest.cs delete mode 100644 Tests/PlayMode/Unit/SafeAreaContainerPanelScaleTest.cs.meta delete mode 100644 Tests/PlayMode/Unit/SafeAreaServiceTest.cs delete mode 100644 Tests/PlayMode/Unit/SafeAreaServiceTest.cs.meta diff --git a/AGENTS.md b/AGENTS.md index 931fdbf..3a4c729 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,7 @@ This guide adds package-specific rules to the host repository guide. Consumer us ## Scope - Package: `com.gamelovers.mobileservices`; minimum Unity version and dependencies are authoritative in `package.json`. -- Runtime subsystems: Native UI, notifications, gestures, haptics, and device services (safe area, battery, audio session, permissions, ATT, and deep links). +- Runtime subsystems: Native UI, notifications, gestures, haptics, and device services (battery, audio session, permissions, ATT, and deep links). - Consumers must enable the Input System or Both. The gesture contract uses EnhancedTouch. - This package is render-pipeline-neutral and has no dependency on GameLovers Services. @@ -28,7 +28,7 @@ This guide adds package-specific rules to the host repository guide. Consumer us - `DeviceServicesHost` is the shared polling host. Do not create per-service update GameObjects when the host can own the callback. - The first `DeepLinkService` subscriber receives a pending cold-start link once. Construct the service early; event names are not persistent state. - ATT returning `Authorized` outside iOS means “not applicable,” not an observed user decision. -- Permission, ATT, battery, safe-area, native-alert, and review Editor overrides are process-wide statics. Simulator code must install and remove them symmetrically; tests must reset overrides they touch. +- Permission, ATT, battery, native-alert, and review Editor overrides are process-wide statics. Simulator code must install and remove them symmetrically; tests must reset overrides they touch. - The iOS location permission bridge retains each `CLLocationManager` delegate until authorization changes and removes it only after callback dispatch. Do not simplify the static delegate-retention collection into a local lifetime. - Android Photos and Notifications permissions use API-33 runtime permissions (`READ_MEDIA_IMAGES` and `POST_NOTIFICATIONS`) and short-circuit as granted on older APIs. Keep runtime checks and generated manifest entries aligned. diff --git a/CHANGELOG.md b/CHANGELOG.md index aae1407..d0901d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 **Fixed**: - Retained the Android activity and review manager Java references until the async `requestReviewFlow` callback completes, preventing a `NullReferenceException` in `launchReviewFlow`. -- Safe-area services and UI Toolkit containers use the selected Unity Device Simulator profile when no explicit Editor override is active. + +**Removed**: +- Removed `ISafeAreaService`, `SafeAreaService`, and the `SafeAreaContainer` UI Toolkit element; `IDeviceService` no longer exposes a `SafeArea` child and `EditorPlatformSimulator` no longer exposes safe-area overrides. For safe-area UI padding, use `com.gamelovers.uiservice` (`SafeAreaPanelView`/`SafeAreaContainer`). ## [1.2.0] - 2026-08-14 diff --git a/Editor/Simulation/EditorPlatformSimulator.cs b/Editor/Simulation/EditorPlatformSimulator.cs index 59b1871..692d662 100644 --- a/Editor/Simulation/EditorPlatformSimulator.cs +++ b/Editor/Simulation/EditorPlatformSimulator.cs @@ -12,7 +12,7 @@ namespace GameLovers.MobileServices.Editor.Simulation { /// - /// Editor-only façade for driving platform state (battery, connectivity, safe area, deep links, + /// Editor-only façade for driving platform state (battery, connectivity, deep links, /// permissions, ATT) from edit-mode tests and the Device Simulator panel. See /// docs/explorer.md for the comparison with Unity's Device Simulator. /// @@ -58,36 +58,6 @@ public static void SetIosLowPowerMode(bool enabled, params BatteryService[] serv } } - /// - /// Pushes a safe-area override that will report on its next - /// LateUpdate diff. Each service you pass is forced to diff immediately so the - /// Explorer surfaces the change without waiting for the host's poll. - /// - public static void SetSafeArea(Rect safeArea, params SafeAreaService[] services) - { - SafeAreaService.EditorSafeAreaOverride = safeArea; - if (services != null) - { - foreach (var s in services) - { - s?.SimulateSafeAreaChanged(); - } - } - } - - /// Clears the safe-area override and restores the live Screen.safeArea read. - public static void ClearSafeAreaOverride(params SafeAreaService[] services) - { - SafeAreaService.EditorSafeAreaOverride = null; - if (services != null) - { - foreach (var s in services) - { - s?.SimulateSafeAreaChanged(); - } - } - } - /// /// Overrides SystemInfo.batteryLevel exposure on the next poll. The package does /// not currently fan a battery-level event from the simulator (the runtime relies on diff --git a/README.md b/README.md index 563992e..4061bd3 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ IDisposable ownedNotifications = notifications; Use the specific subsystem namespaces—`Notifications`, `Haptics`, `NativeUi`, and `Device`—rather than assuming one umbrella import exposes every type. -Safe-area coordinates follow Unity's selected Device Simulator profile through `UnityEngine.Device.Screen`; an explicit `EditorPlatformSimulator` safe-area override takes precedence. +For safe-area UI padding, use `com.gamelovers.uiservice` (`SafeAreaPanelView`/`SafeAreaContainer`). ## Services @@ -77,7 +77,7 @@ Safe-area coordinates follow Unity's selected Device Simulator profile through ` | Native UI | Dismissible or blocking alerts, action sheets, toasts, review requests, and sharing | | Notifications | Local notification channels, scheduling, and management | | Haptics | Presets, custom output, and bounded loops | -| Device | Permissions, ATT, deep links, safe-area and device helpers | +| Device | Permissions, ATT, deep links, and device helpers | | Gestures | Gesture controller for explicit gesture input ownership | | Editor tooling | Device Simulator integration, optional haptics diagnostics, and build helpers | diff --git a/Runtime/Device/DeviceService.cs b/Runtime/Device/DeviceService.cs index 3010966..6134fe6 100644 --- a/Runtime/Device/DeviceService.cs +++ b/Runtime/Device/DeviceService.cs @@ -19,8 +19,6 @@ public static bool KeepAwake set => Screen.sleepTimeout = value ? SleepTimeout.NeverSleep : SleepTimeout.SystemSetting; } - /// - public ISafeAreaService SafeArea { get; } /// public IBatteryService Battery { get; } /// @@ -35,14 +33,12 @@ public static bool KeepAwake public DeviceService() : this(BuildDefaults()) { } public DeviceService( - ISafeAreaService safeArea, IBatteryService battery, IIosAudioSessionService audioSession, IPermissionsService permissions, IAttService att, IDeepLinkService deepLink) { - SafeArea = safeArea; Battery = battery; AudioSession = audioSession; Permissions = permissions; @@ -53,19 +49,18 @@ public DeviceService( // Tuple-routed delegating ctor so the host-dependent children share one explicit host // instance constructed up-front, not separate accesses to the singleton during a // constructor chain (cleaner ownership signal in the umbrella's call stack). - private DeviceService((ISafeAreaService, IBatteryService, + private DeviceService((IBatteryService, IIosAudioSessionService, IPermissionsService, IAttService, IDeepLinkService) defaults) : this(defaults.Item1, defaults.Item2, defaults.Item3, - defaults.Item4, defaults.Item5, defaults.Item6) + defaults.Item4, defaults.Item5) { } - private static (ISafeAreaService, IBatteryService, + private static (IBatteryService, IIosAudioSessionService, IPermissionsService, IAttService, IDeepLinkService) BuildDefaults() { var host = DeviceServicesHost.Instance; return ( - new SafeAreaService(host), new BatteryService(host), new IosAudioSessionService(), new PermissionsService(), @@ -76,7 +71,6 @@ private static (ISafeAreaService, IBatteryService, /// public void Dispose() { - (SafeArea as IDisposable)?.Dispose(); (Battery as IDisposable)?.Dispose(); (DeepLink as IDisposable)?.Dispose(); } diff --git a/Runtime/Device/IDeviceService.cs b/Runtime/Device/IDeviceService.cs index 436a841..01309a9 100644 --- a/Runtime/Device/IDeviceService.cs +++ b/Runtime/Device/IDeviceService.cs @@ -9,9 +9,6 @@ namespace GameLovers.MobileServices.Device /// public interface IDeviceService { - /// Display safe-area events (notch, dynamic island, orientation). - ISafeAreaService SafeArea { get; } - /// Battery level / status / low-power-mode awareness. IBatteryService Battery { get; } diff --git a/Runtime/Device/Internal/DeviceServicesHost.cs b/Runtime/Device/Internal/DeviceServicesHost.cs index d20dbff..c0eaa48 100644 --- a/Runtime/Device/Internal/DeviceServicesHost.cs +++ b/Runtime/Device/Internal/DeviceServicesHost.cs @@ -5,7 +5,7 @@ namespace GameLovers.MobileServices.Device.Internal { /// - /// Internal MonoBehaviour shared by all event-driven Device services (SafeArea, Battery). + /// Internal MonoBehaviour shared by all event-driven Device services (Battery). /// Spawned lazily on first use, marked DontDestroyOnLoad. Exists so the runtime cost of the /// Device subsystem is one auto-spawned GameObject instead of one per service. /// diff --git a/Runtime/Device/State/ISafeAreaService.cs b/Runtime/Device/State/ISafeAreaService.cs deleted file mode 100644 index ee726f2..0000000 --- a/Runtime/Device/State/ISafeAreaService.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using UnityEngine; - -// ReSharper disable once CheckNamespace -namespace GameLovers.MobileServices.Device -{ - /// - /// Wraps Screen.safeArea with change events for orientation, notch / dynamic-island - /// reveal, and any other runtime safe-area shifts. Fires on diff only. - /// - public interface ISafeAreaService - { - /// Latest known safe area in screen pixels (cached Screen.safeArea). - Rect SafeArea { get; } - - /// Fired when the safe area changes (orientation, notch reveal, etc.). - event Action OnSafeAreaChanged; - } -} diff --git a/Runtime/Device/State/ISafeAreaService.cs.meta b/Runtime/Device/State/ISafeAreaService.cs.meta deleted file mode 100644 index 133933e..0000000 --- a/Runtime/Device/State/ISafeAreaService.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 322efff88ac064790a260aaaea5a4501 \ No newline at end of file diff --git a/Runtime/Device/State/SafeAreaContainer.cs b/Runtime/Device/State/SafeAreaContainer.cs deleted file mode 100644 index 12ef793..0000000 --- a/Runtime/Device/State/SafeAreaContainer.cs +++ /dev/null @@ -1,90 +0,0 @@ -using UnityEngine; -using Screen = UnityEngine.Device.Screen; -using UnityEngine.UIElements; - -// ReSharper disable once CheckNamespace -namespace GameLovers.MobileServices.Device -{ - /// - /// UI Toolkit container that automatically pads its content to respect the device safe area. - /// Subscribe via the constructor that accepts an ; the container - /// updates its own padding whenever the safe area changes. - /// - [UxmlElement] - public sealed partial class SafeAreaContainer : VisualElement - { - private ISafeAreaService _safeAreaService; - - public SafeAreaContainer() - { - RegisterCallback(_ => Apply()); - } - - public SafeAreaContainer(ISafeAreaService safeAreaService) : this() - { - SetSafeAreaService(safeAreaService); - } - - /// - /// Wires the container to the supplied service and applies the current safe area immediately. - /// - public void SetSafeAreaService(ISafeAreaService safeAreaService) - { - if (_safeAreaService != null) - { - _safeAreaService.OnSafeAreaChanged -= OnSafeAreaChanged; - } - - _safeAreaService = safeAreaService; - - if (_safeAreaService == null) - { - return; - } - - _safeAreaService.OnSafeAreaChanged += OnSafeAreaChanged; - Apply(); - } - - /// Converts a screen-pixel inset into the active UI Toolkit panel coordinate system. - internal static float ScreenPixelsToPanelUnits(float screenPixels, float scaledPixelsPerPoint) - { - if (scaledPixelsPerPoint <= 0f) - { - return screenPixels; - } - - return screenPixels / scaledPixelsPerPoint; - } - - private void OnSafeAreaChanged(Rect _) - { - Apply(); - } - - private void Apply() - { - var safeArea = _safeAreaService?.SafeArea ?? Screen.safeArea; - var screenWidth = Screen.width; - var screenHeight = Screen.height; - if (screenWidth <= 0 || screenHeight <= 0) - { - return; - } - - // Screen.safeArea is expressed in screen pixels, while UI Toolkit styles use the - // panel's coordinate system. scaledPixelsPerPoint is Unity's authoritative conversion, - // including the PanelSettings Scale With Screen Size factor. - var scaledPixelsPerPoint = panel?.scaledPixelsPerPoint ?? 1f; - var left = ScreenPixelsToPanelUnits(safeArea.xMin, scaledPixelsPerPoint); - var right = ScreenPixelsToPanelUnits(screenWidth - safeArea.xMax, scaledPixelsPerPoint); - var top = ScreenPixelsToPanelUnits(screenHeight - safeArea.yMax, scaledPixelsPerPoint); - var bottom = ScreenPixelsToPanelUnits(safeArea.yMin, scaledPixelsPerPoint); - - style.paddingLeft = left; - style.paddingRight = right; - style.paddingTop = top; - style.paddingBottom = bottom; - } - } -} diff --git a/Runtime/Device/State/SafeAreaContainer.cs.meta b/Runtime/Device/State/SafeAreaContainer.cs.meta deleted file mode 100644 index 060ef75..0000000 --- a/Runtime/Device/State/SafeAreaContainer.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: e6962b99fd39d479fbd1d52b08f5b7ed \ No newline at end of file diff --git a/Runtime/Device/State/SafeAreaService.cs b/Runtime/Device/State/SafeAreaService.cs deleted file mode 100644 index fb2a767..0000000 --- a/Runtime/Device/State/SafeAreaService.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using GameLovers.MobileServices.Device.Internal; -using UnityEngine; -using Screen = UnityEngine.Device.Screen; - -// ReSharper disable once CheckNamespace -namespace GameLovers.MobileServices.Device -{ - /// - public sealed class SafeAreaService : ISafeAreaService, IDisposable - { - private readonly DeviceServicesHost _host; - - private Rect _lastSafeArea; - private Vector2Int _lastResolution; - -#if UNITY_EDITOR - // Editor-only simulator override. When set, the LateUpdate poll reports this rect - // instead of Screen.safeArea so the Device Simulator panel can drive notch/dynamic-island previews. - internal static Rect? EditorSafeAreaOverride; -#endif - - /// - public Rect SafeArea => _lastSafeArea; - - /// - public event Action OnSafeAreaChanged; - - public SafeAreaService() : this(DeviceServicesHost.Instance) { } - - internal SafeAreaService(DeviceServicesHost host) - { - _host = host; - _lastSafeArea = Screen.safeArea; - _lastResolution = new Vector2Int(Screen.width, Screen.height); - _host.RegisterLateUpdate(Tick); - } - - /// - public void Dispose() - { - _host.UnregisterLateUpdate(Tick); - } - -#if UNITY_EDITOR - /// - /// Editor-only simulator hook. Forces an immediate diff against - /// so the Device Simulator panel's notch-inset affordance - /// surfaces the change without waiting for the next LateUpdate tick. - /// - internal void SimulateSafeAreaChanged() - { - Tick(); - } -#endif - - private void Tick() - { -#if UNITY_EDITOR - var current = EditorSafeAreaOverride ?? Screen.safeArea; -#else - var current = Screen.safeArea; -#endif - var resolution = new Vector2Int(Screen.width, Screen.height); - - if (current == _lastSafeArea && resolution == _lastResolution) - { - return; - } - - _lastSafeArea = current; - _lastResolution = resolution; - OnSafeAreaChanged?.Invoke(current); - } - } -} diff --git a/Runtime/Device/State/SafeAreaService.cs.meta b/Runtime/Device/State/SafeAreaService.cs.meta deleted file mode 100644 index 832b65f..0000000 --- a/Runtime/Device/State/SafeAreaService.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 72d96de654ed341f78d86327d9eb3270 \ No newline at end of file diff --git a/Runtime/IMobileService.cs b/Runtime/IMobileService.cs index cd3eada..9e68185 100644 --- a/Runtime/IMobileService.cs +++ b/Runtime/IMobileService.cs @@ -28,7 +28,7 @@ public interface IMobileService IHapticsService Haptics { get; } /// - /// Device sub-services (safe area, battery, audio session, permissions, ATT, deep link). + /// Device sub-services (battery, audio session, permissions, ATT, deep link). /// IDeviceService Device { get; } } diff --git a/Samples~/MobileServicesSamples/DeepLinkRouter/DeepLinkRouter.uxml b/Samples~/MobileServicesSamples/DeepLinkRouter/DeepLinkRouter.uxml index a669bab..1246c31 100644 --- a/Samples~/MobileServicesSamples/DeepLinkRouter/DeepLinkRouter.uxml +++ b/Samples~/MobileServicesSamples/DeepLinkRouter/DeepLinkRouter.uxml @@ -1,7 +1,7 @@ - +