Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .mauidevflow
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "port": 9231 }
28 changes: 28 additions & 0 deletions src/MauiSherpa.Core/Handlers/Apple/GetSimulatorAppsHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Shiny.Mediator;
using Shiny.Mediator.Caching;
using MauiSherpa.Core.Interfaces;
using MauiSherpa.Core.Requests.Apple;

namespace MauiSherpa.Core.Handlers.Apple;

/// <summary>
/// Handler for GetSimulatorAppsRequest with 60 second caching
/// </summary>
public partial class GetSimulatorAppsHandler : IRequestHandler<GetSimulatorAppsRequest, IReadOnlyList<SimulatorApp>>
{
private readonly ISimulatorService _simulatorService;

public GetSimulatorAppsHandler(ISimulatorService simulatorService)
{
_simulatorService = simulatorService;
}

[Cache(AbsoluteExpirationSeconds = 60)]
public async Task<IReadOnlyList<SimulatorApp>> Handle(
GetSimulatorAppsRequest request,
IMediatorContext context,
CancellationToken ct)
{
return await _simulatorService.GetInstalledAppsAsync(request.Udid);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Shiny.Mediator;
using Shiny.Mediator.Caching;
using MauiSherpa.Core.Interfaces;
using MauiSherpa.Core.Requests.Apple;

namespace MauiSherpa.Core.Handlers.Apple;

/// <summary>
/// Handler for GetSimulatorDeviceTypesRequest with 10 minute caching (device types rarely change)
/// </summary>
public partial class GetSimulatorDeviceTypesHandler : IRequestHandler<GetSimulatorDeviceTypesRequest, IReadOnlyList<SimulatorDeviceType>>
{
private readonly ISimulatorService _simulatorService;

public GetSimulatorDeviceTypesHandler(ISimulatorService simulatorService)
{
_simulatorService = simulatorService;
}

[Cache(AbsoluteExpirationSeconds = 600)]
[OfflineAvailable]
public async Task<IReadOnlyList<SimulatorDeviceType>> Handle(
GetSimulatorDeviceTypesRequest request,
IMediatorContext context,
CancellationToken ct)
{
return await _simulatorService.GetDeviceTypesAsync();
}
}
29 changes: 29 additions & 0 deletions src/MauiSherpa.Core/Handlers/Apple/GetSimulatorRuntimesHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Shiny.Mediator;
using Shiny.Mediator.Caching;
using MauiSherpa.Core.Interfaces;
using MauiSherpa.Core.Requests.Apple;

namespace MauiSherpa.Core.Handlers.Apple;

/// <summary>
/// Handler for GetSimulatorRuntimesRequest with 10 minute caching (runtimes rarely change)
/// </summary>
public partial class GetSimulatorRuntimesHandler : IRequestHandler<GetSimulatorRuntimesRequest, IReadOnlyList<SimulatorRuntime>>
{
private readonly ISimulatorService _simulatorService;

public GetSimulatorRuntimesHandler(ISimulatorService simulatorService)
{
_simulatorService = simulatorService;
}

[Cache(AbsoluteExpirationSeconds = 600)]
[OfflineAvailable]
public async Task<IReadOnlyList<SimulatorRuntime>> Handle(
GetSimulatorRuntimesRequest request,
IMediatorContext context,
CancellationToken ct)
{
return await _simulatorService.GetRuntimesAsync();
}
}
29 changes: 29 additions & 0 deletions src/MauiSherpa.Core/Handlers/Apple/GetSimulatorsHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Shiny.Mediator;
using Shiny.Mediator.Caching;
using MauiSherpa.Core.Interfaces;
using MauiSherpa.Core.Requests.Apple;

namespace MauiSherpa.Core.Handlers.Apple;

/// <summary>
/// Handler for GetSimulatorsRequest with 2 minute caching
/// </summary>
public partial class GetSimulatorsHandler : IRequestHandler<GetSimulatorsRequest, IReadOnlyList<SimulatorDevice>>
{
private readonly ISimulatorService _simulatorService;

public GetSimulatorsHandler(ISimulatorService simulatorService)
{
_simulatorService = simulatorService;
}

[Cache(AbsoluteExpirationSeconds = 120)]
[OfflineAvailable]
public async Task<IReadOnlyList<SimulatorDevice>> Handle(
GetSimulatorsRequest request,
IMediatorContext context,
CancellationToken ct)
{
return await _simulatorService.GetSimulatorsAsync();
}
}
230 changes: 230 additions & 0 deletions src/MauiSherpa.Core/Interfaces.cs
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,236 @@ public interface IAppleRootCertService
IReadOnlyDictionary<string, MauiSherpa.Core.Services.CachedCertInfo>? GetCachedCerts();
}

// ============================================================================
// iOS/Apple Simulators
// ============================================================================

/// <summary>
/// Represents a simulator device from xcrun simctl
/// </summary>
public record SimulatorDevice(
string Udid,
string Name,
string State,
bool IsAvailable,
string DeviceTypeIdentifier,
string RuntimeIdentifier,
string? Runtime,
string? ProductFamily,
string? LastBootedAt
);

/// <summary>
/// Represents a simulator device type from xcrun simctl
/// </summary>
public record SimulatorDeviceType(
string Identifier,
string Name,
string ProductFamily
);

/// <summary>
/// Represents a simulator runtime from xcrun simctl
/// </summary>
public record SimulatorRuntime(
string Identifier,
string Name,
string Version,
string? Platform,
bool IsAvailable,
IReadOnlyList<SimulatorDeviceType>? SupportedDeviceTypes = null
);

/// <summary>
/// Service for managing iOS/Apple simulators via xcrun simctl
/// </summary>
public interface ISimulatorService
{
Task<IReadOnlyList<SimulatorDevice>> GetSimulatorsAsync();
Task<IReadOnlyList<SimulatorDeviceType>> GetDeviceTypesAsync();
Task<IReadOnlyList<SimulatorRuntime>> GetRuntimesAsync();
Task<bool> CreateSimulatorAsync(string name, string deviceTypeId, string runtimeId, IProgress<string>? progress = null);
Task<bool> DeleteSimulatorAsync(string udid, IProgress<string>? progress = null);
Task<bool> BootSimulatorAsync(string udid, IProgress<string>? progress = null);
Task<bool> ShutdownSimulatorAsync(string udid, IProgress<string>? progress = null);
Task<bool> EraseSimulatorAsync(string udid, IProgress<string>? progress = null);

// App management
Task<IReadOnlyList<SimulatorApp>> GetInstalledAppsAsync(string udid);
Task<bool> InstallAppAsync(string udid, string appPath, IProgress<string>? progress = null);
Task<bool> UninstallAppAsync(string udid, string bundleId, IProgress<string>? progress = null);
Task<bool> LaunchAppAsync(string udid, string bundleId, IProgress<string>? progress = null);
Task<bool> TerminateAppAsync(string udid, string bundleId, IProgress<string>? progress = null);

// Device exploration
Task<bool> TakeScreenshotAsync(string udid, string outputPath, IProgress<string>? progress = null);
Task<string?> GetAppContainerPathAsync(string udid, string bundleId, string containerType = "data");
Task<bool> OpenUrlAsync(string udid, string url, IProgress<string>? progress = null);
string GetSimulatorDataPath(string udid);
Task<bool> CloneSimulatorAsync(string udid, string newName, IProgress<string>? progress = null);

// Push notifications
Task<bool> SendPushNotificationAsync(string udid, string bundleId, string payloadJson, IProgress<string>? progress = null);

// Location simulation
Task<bool> SetLocationAsync(string udid, double latitude, double longitude, IProgress<string>? progress = null);
Task<bool> ClearLocationAsync(string udid, IProgress<string>? progress = null);

// Status bar overrides
Task<bool> OverrideStatusBarAsync(string udid, StatusBarOverride overrides, IProgress<string>? progress = null);
Task<bool> ClearStatusBarAsync(string udid, IProgress<string>? progress = null);

// Route playback
Task<bool> StartRoutePlaybackAsync(string udid, IReadOnlyList<Services.RouteWaypoint> waypoints,
double speedMps = 20, CancellationToken ct = default);
void StopRoutePlayback();
bool IsPlayingRoute { get; }
event Action? RoutePlaybackStateChanged;
}

/// <summary>
/// Status bar override options for simulator
/// </summary>
public record StatusBarOverride(
string? Time = null,
string? DataNetwork = null, // hide, wifi, 3g, 4g, lte, lte-a, lte+, 5g, 5g+, 5g-uwb, 5g-uc
string? WifiMode = null,
int? WifiBars = null,
int? CellularBars = null,
int? BatteryLevel = null,
string? BatteryState = null // charging, charged, discharging
);

// ============================================================================
// Android Device Tools (location, battery, demo mode, deep links)
// ============================================================================

/// <summary>
/// Android demo mode status bar overrides
/// </summary>
public record AndroidDemoStatus(
string? Time = null, // hhmm format e.g. "1200"
int? WifiLevel = null, // 0-4
int? MobileLevel = null, // 0-4
string? MobileDataType = null, // 1x, 3g, 4g, 4g+, 5g, 5ge, lte, lte+
int? BatteryLevel = null, // 0-100
bool? BatteryPlugged = null,
bool? HideNotifications = null
);

public interface IAndroidDeviceToolsService
{
// Location simulation
Task<bool> SetLocationAsync(string serial, double latitude, double longitude);
Task<bool> ClearLocationAsync(string serial);
Task<bool> StartRoutePlaybackAsync(string serial, IReadOnlyList<Services.RouteWaypoint> waypoints,
double speedMps = 20, CancellationToken ct = default);
void StopRoutePlayback();

// Battery simulation
Task<bool> SetBatteryLevelAsync(string serial, int level);
Task<bool> SetBatteryStatusAsync(string serial, string status); // charging, discharging, not-charging, full
Task<bool> ResetBatteryAsync(string serial);

// Demo mode (status bar overrides)
Task<bool> EnableDemoModeAsync(string serial);
Task<bool> SetDemoStatusAsync(string serial, AndroidDemoStatus status);
Task<bool> DisableDemoModeAsync(string serial);

// Deep links
Task<bool> OpenDeepLinkAsync(string serial, string url);

bool IsPlayingRoute { get; }
event Action? RoutePlaybackStateChanged;
}

// ============================================================================
// Physical iOS Device Management (via xcrun devicectl)
// ============================================================================

/// <summary>
/// Represents a physical iOS device connected via USB or network
/// </summary>
public record PhysicalDevice(
string Identifier,
string Udid,
string Name,
string Model,
string Platform,
string DeviceType,
string OsVersion,
string TransportType,
string ConnectionState,
string TunnelState
);

/// <summary>
/// Service for managing physical iOS devices via xcrun devicectl
/// </summary>
public interface IPhysicalDeviceService
{
Task<IReadOnlyList<PhysicalDevice>> GetDevicesAsync();
Task<bool> InstallAppAsync(string identifier, string appPath, IProgress<string>? progress = null);
Task<bool> LaunchAppAsync(string identifier, string bundleId, IProgress<string>? progress = null);
}

/// <summary>
/// Represents an installed app on a simulator
/// </summary>
public record SimulatorApp(
string BundleId,
string Name,
string? Version,
string ApplicationType,
string? DataContainerPath,
string? BundlePath
);

// ============================================================================
// iOS Simulator Log Streaming
// ============================================================================

/// <summary>
/// Log message type from iOS unified logging
/// </summary>
public enum SimulatorLogLevel
{
Default = 0,
Info = 1,
Debug = 2,
Error = 3,
Fault = 4
}

/// <summary>
/// A single log entry from the iOS unified logging system
/// </summary>
public record SimulatorLogEntry(
string Timestamp,
int ProcessId,
int ThreadId,
SimulatorLogLevel Level,
string ProcessName,
string? Subsystem,
string? Category,
string Message,
string RawLine
);

/// <summary>
/// Service for streaming logs from iOS simulators via xcrun simctl spawn log stream
/// </summary>
public interface ISimulatorLogService : IDisposable
{
bool IsRunning { get; }
IReadOnlyList<SimulatorLogEntry> Entries { get; }
Task StartAsync(string udid, CancellationToken ct = default);
void Stop();
void Clear();
IAsyncEnumerable<SimulatorLogEntry> StreamAsync(CancellationToken ct = default);
event Action? OnCleared;
}

// ============================================================================
// Local Signing Identities - Keychain Certificate Management
// ============================================================================
Expand Down
1 change: 1 addition & 0 deletions src/MauiSherpa.Core/MauiSherpa.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
<PackageReference Include="Microsoft.VisualStudio.Services.Client" Version="19.225.2" />
<PackageReference Include="NGitLab" Version="11.3.1" />
<PackageReference Include="Octokit" Version="14.0.0" />
<PackageReference Include="SharpKml.Core" Version="6.1.0" />
<PackageReference Include="Shiny.Mediator" Version="6.1.1" />
<PackageReference Include="Shiny.Mediator.AppSupport" Version="6.1.1" />
<PackageReference Include="Sodium.Core" Version="1.3.7" />
Expand Down
12 changes: 12 additions & 0 deletions src/MauiSherpa.Core/Requests/Apple/GetSimulatorAppsRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Shiny.Mediator;
using MauiSherpa.Core.Interfaces;

namespace MauiSherpa.Core.Requests.Apple;

/// <summary>
/// Request to get installed apps on a booted simulator
/// </summary>
public record GetSimulatorAppsRequest(string Udid) : IRequest<IReadOnlyList<SimulatorApp>>, IContractKey
{
public string GetKey() => $"apple:simulator:apps:{Udid}";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Shiny.Mediator;
using MauiSherpa.Core.Interfaces;

namespace MauiSherpa.Core.Requests.Apple;

/// <summary>
/// Request to get available simulator device types
/// </summary>
public record GetSimulatorDeviceTypesRequest() : IRequest<IReadOnlyList<SimulatorDeviceType>>, IContractKey
{
public string GetKey() => "apple:simulator:devicetypes";
}
Loading
Loading