Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
717360b
feat: gate Editor haptics logs behind a config toggle
CoderGamester Aug 27, 2026
18f2563
docs: slim CLAUDE wrappers to @AGENTS.md
CoderGamester Aug 27, 2026
7c29ffc
fix: keep Android review handles alive until the review flow callback…
CoderGamester Sep 8, 2026
8654927
fix: use Device Simulator screen coordinates for safe areas
CoderGamester Sep 8, 2026
20fd1b9
feat(device)!: remove safe-area service and views
CoderGamester Sep 11, 2026
5b7f1b4
test(deps): vendor Unity-loadable NSubstitute 4.2.2 for EditMode
CoderGamester Sep 11, 2026
e1d36dd
docs(tests): tie RCR re-runs and anchor corrections to observed evidence
CoderGamester Sep 14, 2026
cd0f69a
style: prefix private fields with underscore and name constants Pasca…
CoderGamester Sep 14, 2026
f7ad2e1
fix(ios): declare framework dependencies of the native bridges
CoderGamester Oct 17, 2026
ed18aa3
feat(permissions)!: report limited access, fix Android requests, comp…
CoderGamester Oct 21, 2026
0e9eca9
feat!: add notification data and opened callbacks, background tasks, …
CoderGamester Oct 21, 2026
6965cb0
feat(media): save to gallery and pick an image without permissions
CoderGamester Oct 21, 2026
13b3067
feat(game-platform): add opt-in Game Center and Google Play Games ser…
CoderGamester Oct 21, 2026
c69cdd2
feat(cloud-save): add slot-based saves on Game Center and Play Games …
CoderGamester Oct 21, 2026
b1183ef
feat: add home-screen shortcuts and an in-app browser
CoderGamester Oct 21, 2026
94a4eff
docs: pin the 2.0.0 install tag and describe the new services
CoderGamester Oct 21, 2026
f3a57f3
docs(game-platform): define every id used by the example
CoderGamester Oct 21, 2026
e0d33d2
chore: prepare 2.0.0 release
CoderGamester Oct 21, 2026
836b07e
refactor: align the 2.0.0 code with the repository coding standards
CoderGamester Oct 21, 2026
96f463e
docs: make the README the documentation index
CoderGamester Oct 21, 2026
ce5e96f
chore: date the 2.0.0 release 2026-10-22
CoderGamester Oct 21, 2026
e01dfd5
docs: add a Device Simulator panel screenshot
CoderGamester Oct 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, device services (battery, audio session, permissions, ATT, deep links, and background tasks), and the opt-in game platform (Game Center and Play Games sign-in, leaderboards, achievements, and cloud saves).
- 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.

Expand All @@ -24,13 +24,29 @@ This guide adds package-specific rules to the host repository guide. Consumer us
- `MobileNotificationService` owns its `NotificationService` GameObject. Disposal is idempotent, releases only owned resources, and public operations depending on the host throw `ObjectDisposedException` afterward. Shared validation and lifecycle behavior stay outside platform conditionals.
- Android notification scheduling requires a registered default channel. Queue modes hand pending work to the OS on background transitions.
- Every `GestureController` balances only its own `EnhancedTouchSupport.Enable()` acquisition. Multiple live controllers and external release must remain safe.
- `IGameNotification.Data` maps to `AndroidNotification.IntentData` and `iOSNotification.Data` and must survive the queue-and-reschedule persistence. Both platforms keep returning the last opened notification, so `GameNotificationsMonoBehaviour` captures the cold-start one before the first foregrounding and de-duplicates later opens by id (title and body when unset). Opened-notification behavior is exercised through the internal `AttachPlatform` seam with a fake platform.
- Android image sharing goes through the package FileProvider: the Java class `GameLoversFileProvider` (`Plugins/Android/`), the authority suffix `NativeUiService.AndroidShareProviderAuthoritySuffix`, and the postprocessor's provider and `res/xml` paths file change together. Without the toggle an image share degrades to text with a warning.
- `NativeMediaService` needs no permission. iOS saves through `PHPhotoLibrary` only when `NSPhotoLibraryAddUsageDescription` is declared (`GL_PERMISSION_PHOTOS_ADD`) and picks through `PHPickerViewController`. Android saves through `MediaStore` (legacy storage below API 29) and picks through the transparent `GameLoversMediaPickerActivity`, which the postprocessor declares behind `IncludeMediaPicker`.
- Picked images are always a private copy the caller may delete. The `_GameLoversMedia*` exports, their callback signature, and the Java `ResultCallback` and `PathCallback` interfaces change together with their C# callers.
- `GamePlatformServices` is opt-in and holds one `IGamePlatformBackend` per platform. Its guards (signed-in player, non-empty id, positive steps, one shared sign-in) live in the service, not the backends. The Editor backend and `EditorGamePlatformState` are process-wide, so the simulator panel drives the consumer's own instance.
- The Android game backend talks to Play Games v2 through JNI and `OnCompleteListener` proxies so the package compiles without the SDK; the postprocessor adds the SDK only when `GamePlatform.PlayGamesProjectId` is set.
- Cloud saves share the game platform opt-in. `CloudSaveService` owns validation, availability and conflict resolution (policy, resolver, newest-first ordering) so both platforms behave alike.
- Cloud save backends only list, load every stored version, resolve to a chosen version, save, and delete, and report failures as `CloudSaveException`. The Android backend uses JNI like the sign-in backend.
- The iOS bridge (`CloudSave.m`) replies with tagged strings decoded by `CloudSaveProtocol` and holds its own `UIBackgroundTask` during saves, because Unity's main loop is paused while backgrounded.
- `Plugins/iOS/GamePlatform.m` links GameKit only when `GL_GAME_CENTER` is defined (Game Center capability on). The `_GameLoversGamePlatform*` exports and their callback signature change together with the C# callers.
- Shortcut taps travel through `DeepLinkService`. On Android the shortcut intent is an explicit `ACTION_VIEW` on the app activity that Unity reports as a deep link. On iOS `AppShortcuts.m` installs its `performActionForShortcutItem` hook in `+load`, chains to any existing implementation, and holds the URI until `DeepLinkService` collects it; an iOS shortcut with no subscriber becomes the cold-start link, unlike a runtime link.
- `OpenInAppBrowser` accepts only http and https (`SFSafariViewController` throws otherwise) and falls back to `Application.OpenURL` when Custom Tabs are unavailable. `GameLoversShortcuts.java` and the `_GameLoversShortcut*` exports change together with `AppShortcutsBridge`.
- `IBackgroundTaskService` is iOS-only in effect. `Plugins/iOS/BackgroundTask.m` must end the system task before its expiration handler returns, on the main thread; handle members are readable from any thread; the `_GameLoversBackgroundTask*` exports, C# `DllImport` signatures, and the expiration callback signature change together. Android and Editor return a pass-through handle.
- One haptic play replaces the previous play and cancels its pending auto-stop. Negative preset duration loops until explicit stop; `StopCurrentHaptic()` is idempotent.
- `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.
- `AndroidPermissionPolicy` owns which manifest permissions each `AppPermission` needs per API level and how grants fold into `PermissionStatus`; it stays platform-neutral so it is unit-tested. Keep it, the runtime request code, and `AndroidManifestToggles.GetPermissions` aligned.
- Android never reports a refusal directly: `Denied` is the persisted "requested" flag or the OS rationale flag, and requests are serialized through one gate because Android shows one dialog at a time. Background location is always a separate second request. Notifications below API 33 have no runtime permission and follow the enabled switch.
- `PermissionStatus` integer values are a native contract; `Limited` is 4. Limited covers partial photos, provisional notifications, approximate location, and foreground-only location when Always was asked.
- `Plugins/iOS/Permissions.m` is built in groups (camera, microphone, location, photos) gated by `GL_PERMISSION_<GROUP>`. Without `GL_PERMISSIONS_DECLARED_ONLY` every group compiles; the build postprocessor sets it and defines only the groups whose purpose string is in the final `Info.plist`. `IosPermissionGroups`, the plugin, and the postprocessor change together. Notification status is a native cache, so `Check` never blocks after the first read.

## Editor and native-build invariants

Expand All @@ -53,5 +69,6 @@ This guide adds package-specific rules to the host repository guide. Consumer us
## Verification and documentation

- Platform bridge changes require relevant Editor simulation plus real iOS/Android build or device evidence; unavailable platform evidence is `NOT VALIDATED`.
- Android App Link `AutoVerify` needs an http or https scheme and a host; validation rejects anything else before a build file is touched.
- Update `README.md`, the relevant `docs/` page, and the canonical sample README when public behavior or sample setup changes.
- Update this guide only for durable subsystem, native-boundary, build-ownership, or test conventions.
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,47 @@ 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).

## [2.0.0] - 2026-10-22

**New**:
- **Game Platform**: Added opt-in `GamePlatformServices` (`IGamePlatformServices`) for Game Center and Google Play Games Services v2: sign-in, leaderboards, achievements and their native UIs, with `PlatformId` to keep per-platform ids in one value. Enable it with the `Game Center` toggle and the `Play Games Project Id` in the Mobile Services Config; the build then adds the entitlement, or the SDK and manifest entries.
- **Cloud Save**: Added `IGamePlatformServices.CloudSave` (`ICloudSaveService`) for slot-based saves on Game Center saved games and Google Play Games saved games, with `MostRecent` or `Manual` conflict resolution and typed `CloudSaveException` failures. The `Cloud Save` config toggle adds the iCloud Documents capability on iOS.
- **Permissions**: Added `PermissionStatus.Limited` for partial access (selected photos, provisional notifications, approximate location, foreground-only location) and `DeviceService.OpenAppSettings()` to send the player to the OS Settings page after a denial.
- **Media**: Added `NativeMediaService.SaveToGalleryAsync` to save an image or video to the device gallery and `PickImageAsync` to pick an image with the system picker, both without asking for the Photos permission. Android uses the new `Include Media Picker` and `Write External Storage Legacy` config toggles; iOS gallery saving needs a `PhotoLibraryAddOnly` usage description.
- **Native UI**: Added `NativeUiService.OpenInAppBrowser` for web pages in `SFSafariViewController` or Chrome Custom Tabs (the build adds `androidx.browser` unless `Include Custom Tabs Dependency` is turned off), and `ShareScreenshotAsync` to share the current frame.
- **Notifications**: Added `IGameNotification.Data` and `NotificationBuilder.Data` for a payload such as a deep-link URI, `INotificationService.OnNotificationOpened` for taps while the app runs, and `TryGetLaunchNotification` for the notification that started the app.
- **App Shortcuts**: Added `DeviceService.SetShortcuts` and `ClearShortcuts` for home-screen quick actions whose taps arrive through `IDeepLinkService`, including as the cold-start link.
- **Background Tasks**: Added `IDeviceService.BackgroundTasks` (`IBackgroundTaskService`) to finish short work when the app moves to the background on iOS; it is a pass-through on Android.
- **Store Links**: Added `DeviceService.OpenStorePage` to open the App Store or Google Play page, optionally the iOS write-review sheet.
- **Deep Links**: Android intent-filter registrations can set `AutoVerify` so https links open as verified App Links.
- **Mobile Services Config**: The Inspector is grouped by feature and each section lists the native build entries it adds. New Android manifest toggles cover coarse and background location, legacy storage access, the share FileProvider and the media picker.
- **Haptics**: Added an `Enable Haptics Debug Logs` config toggle; Editor haptics logs are disabled by default.

**Changed**:
- **Breaking**: `IGameNotification`, `INotificationService`, `INativeUiService` and `IDeviceService` gained members, and the `DeviceService` injection constructor takes an `IBackgroundTaskService`. Custom implementations and mocks must add them.
- iOS builds compile only the permission code whose usage description is declared, so a game that never requests location, camera, microphone or photos no longer references those APIs. `Manage Native Build Manually` compiles every permission group.
- The package depends on the built-in `com.unity.modules.screencapture` and `com.unity.modules.imageconversion` modules.

**Fixed**:
- Android permissions now request precise and approximate location together, request background location separately, report `Denied` after a refusal, use the correct permissions below API 33, and no longer prompt for storage just to save to the gallery.
- Android image sharing works on Android 7+ through a package FileProvider (enable `Include Share FileProvider`); previously it used a `file://` URI the OS rejects.
- iOS notification status checks no longer block the main thread, and location permission delegates are released after delivery.
- Declared the iOS framework dependencies of the native bridges so a generated Xcode project links.
- Retained the Android activity and review manager Java references until the async `requestReviewFlow` callback completes, preventing a `NullReferenceException` in `launchReviewFlow`.

**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`).

**Docs**:
- The root README is now the documentation index and lists common issues. `docs/build-pipeline.md` is now `docs/build-configuration.md`, `docs/explorer.md` is now `docs/device-simulator.md`, and `docs/README.md` is removed.

**Migration**:
- Replace `IDeviceService.SafeArea` and `SafeAreaContainer` with `com.gamelovers.uiservice` `SafeAreaPanelView`/`SafeAreaContainer`.
- Add `Data`, `OnNotificationOpened`, `TryGetLaunchNotification`, `BackgroundTasks` and the new `INativeUiService` methods to custom implementations and mocks, and pass an `IBackgroundTaskService` to the `DeviceService` constructor.
- Treat `PermissionStatus.Limited` as usable access in code that switches on `PermissionStatus`.
- On iOS, declare a usage description in the Mobile Services Config for every permission the game requests; undeclared groups are no longer compiled in.
- `AccessFineLocation` now also declares `ACCESS_COARSE_LOCATION`, which Android 12+ requires; add `AccessBackgroundLocation` for `AppPermission.LocationAlways`.

## [1.2.0] - 2026-08-14

**New**:
Expand Down
11 changes: 0 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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`.
89 changes: 89 additions & 0 deletions Editor/Explorer/DeviceSimulatorPanel/CloudSaveSimulatorSection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System.Text;
using GameLovers.MobileServices.GamePlatform.Internal;
using UnityEngine.UIElements;

// ReSharper disable once CheckNamespace
namespace GameLovers.MobileServices.Editor.Explorer.DeviceSimulatorPanel
{
/// <summary>Device Simulator foldout that drives the process-wide Editor cloud save simulation.</summary>
internal sealed class CloudSaveSimulatorSection : Foldout
{
private readonly Toggle _available;
private readonly Label _list;
private readonly Label _status;

public CloudSaveSimulatorSection()
{
text = "Cloud Save";
value = false;
AddToClassList("msp-foldout");

var banner = new VisualElement();
banner.AddToClassList("msp-banner");
var bannerLabel = new Label("Saves from ICloudSaveService are stored under Library/GameLovers/SimulatedCloudSave. " +
"Sign the simulated player in under Game Platform first; saves also need cloud storage available.");
bannerLabel.AddToClassList("msp-banner-label");
banner.Add(bannerLabel);
Add(banner);

_available = new Toggle("Cloud storage available");
_available.RegisterValueChangedCallback(evt => EditorCloudSaveState.Available = evt.newValue);
Add(_available);

var slot = new TextField("Slot") { value = "slot1" };
Add(slot);
var conflictData = new TextField("Conflicting data") { value = "saved on another device" };
Add(conflictData);

var inject = new Button(() => InjectConflict(slot.value, conflictData.value)) { text = "Inject conflicting version" };
inject.style.marginTop = 2;
Add(inject);

var clear = new Button(() =>
{
EditorCloudSaveState.DeleteAll();
Refresh();
}) { text = "Delete all simulated saves" };
clear.style.marginTop = 2;
Add(clear);

_list = new Label();
_list.style.whiteSpace = WhiteSpace.Normal;
_list.style.fontSize = 10;
_list.style.marginTop = 4;
Add(_list);

_status = new Label();
_status.style.whiteSpace = WhiteSpace.Normal;
_status.style.fontSize = 10;
_status.style.marginTop = 4;
Add(_status);

schedule.Execute(Refresh).Every(1000);
Refresh();
}

private void InjectConflict(string slot, string data)
{
var injected = EditorCloudSaveState.InjectConflict(slot, Encoding.UTF8.GetBytes(data));
_status.text = injected
? $"Injected a newer conflicting version of '{slot}'. The next load resolves it by the conflict policy."
: $"Slot '{slot}' has no save to conflict with; save it from the game first.";
}

private void Refresh()
{
_available.SetValueWithoutNotify(EditorCloudSaveState.Available);
var slots = EditorCloudSaveState.ListSlots();
if (slots.Count == 0)
{
_list.text = "No simulated saves.";
return;
}

var builder = new StringBuilder("Saved slots:");
foreach (var item in slots) builder.Append($"\n- {item.Name} ({item.ModifiedUtc.ToLocalTime():HH:mm:ss})");
_list.text = builder.ToString();
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading