import 'dart:io'; import 'package:flutter/services.dart'; /// Minimum RAM (bytes) required for on-device Gemma 4 E2B inference. /// /// 4 GiB matches Planner AC-6 of #218. The Gemma 4 E2B weights alone are /// ~2.4GB; adding KV-cache + Flutter runtime + OS headroom puts us at 4GB /// total as the practical floor below which AC-7 cold-start budgets fail. const int kAiMinRamBytes = 4 * 1024 * 1024 * 1024; /// Abstraction over the platform-channel RAM query, so tests can inject a /// fake without touching MethodChannel. abstract class DeviceCapabilities { /// Returns total physical RAM in bytes, or `null` if unknown / unsupported /// (non-Android host, channel error). Callers must treat `null` as "do /// not enable the AI gate" (fail-closed). Future totalRamBytes(); /// Convenience: `true` iff [totalRamBytes] returns ≥ [kAiMinRamBytes]. /// `null` from [totalRamBytes] → `false` (fail-closed). Future meetsAiMinRam() async { final bytes = await totalRamBytes(); if (bytes == null) return false; return bytes >= kAiMinRamBytes; } } /// Real implementation. Calls `MainActivity.kt` over a MethodChannel. class PlatformDeviceCapabilities implements DeviceCapabilities { PlatformDeviceCapabilities({MethodChannel? channel}) : _channel = channel ?? const MethodChannel('life_helper/device_caps'); final MethodChannel _channel; @override Future totalRamBytes() async { // Channel is Android-only — return null on iOS/host tests rather than // throwing MissingPluginException. if (!Platform.isAndroid) return null; try { final v = await _channel.invokeMethod('totalMemoryBytes'); return v; } on PlatformException { return null; } on MissingPluginException { return null; } } @override Future meetsAiMinRam() async { final bytes = await totalRamBytes(); if (bytes == null) return false; return bytes >= kAiMinRamBytes; } }