/// Battery and connectivity snapshot shown on the home screen. class SystemStatus { const SystemStatus({ this.batteryLevel = 0, this.charging = false, this.wifi = false, }); final int batteryLevel; final bool charging; final bool wifi; factory SystemStatus.fromMap(Map map) { return SystemStatus( batteryLevel: (map['batteryLevel'] as num?)?.toInt() ?? 0, charging: map['charging'] == true, wifi: map['wifi'] == true, ); } } /// Current media-stream volume and its maximum. class VolumeInfo { const VolumeInfo({ this.index = 0, this.max = 15, this.min = 0, this.fixed = false, this.streams = const {}, this.source = 'system', this.vendor = '', }); final int index; final int max; /// Lowest index Android accepts for the stream. When it equals [max] the /// unit exposes no usable range at all. final int min; /// True when Android delegates volume to a fixed hardware amplifier (common /// on car head units). Apps cannot change that volume. final bool fixed; /// Raw indices for every stream, used by the on-screen diagnostics panel. final Map streams; /// Which backend produced [index]: `system` for `AudioManager`, `syu` when /// the car's MCU volume module is in control. final String source; /// Native vendor state (binding/ready/unavailable), diagnostics only. final String vendor; factory VolumeInfo.fromMap(Map map) { final rawStreams = map['streams']; final streams = {}; if (rawStreams is Map) { rawStreams.forEach((key, value) { if (key is String && value is num) { streams[key] = value.toInt(); } }); } return VolumeInfo( index: (map['index'] as num?)?.toInt() ?? 0, max: (map['max'] as num?)?.toInt() ?? 15, min: (map['min'] as num?)?.toInt() ?? 0, fixed: map['fixed'] == true, streams: streams, source: map['source'] is String ? map['source'] as String : 'system', vendor: map['vendor'] is String ? map['vendor'] as String : '', ); } /// True when there is no range to move through, either because the firmware /// says so or because the stream reports a single step. bool get isControllable => !fixed && max > 0 && min < max; VolumeInfo copyWith({ int? index, int? max, int? min, bool? fixed, Map? streams, String? source, String? vendor, }) { return VolumeInfo( index: index ?? this.index, max: max ?? this.max, min: min ?? this.min, fixed: fixed ?? this.fixed, streams: streams ?? this.streams, source: source ?? this.source, vendor: vendor ?? this.vendor, ); } double get fraction => max <= 0 ? 0 : (index / max).clamp(0.0, 1.0); int get percent => (fraction * 100).round(); }