`dart format --set-exit-if-changed` over lib and test rewrites these. The analysis job never reached its formatting step, so the drift went unnoticed. No behaviour changes.
111 lines
4.1 KiB
Dart
111 lines
4.1 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:device_info_plus/device_info_plus.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import 'app_logger.dart';
|
|
import 'platform_detector.dart';
|
|
|
|
/// What this install should call itself when talking to media servers and
|
|
/// companion peers: a real platform name, the hardware model, and the
|
|
/// user-facing device name (Plex dashboards show it as the "Player";
|
|
/// Jellyfin as the "Device").
|
|
class DeviceIdentity {
|
|
/// 'Android' | 'iOS' | 'tvOS' | 'macOS' | 'Windows' | 'Linux', falling back
|
|
/// to [Platform.operatingSystem] when detection fails.
|
|
final String platform;
|
|
|
|
/// Hardware model for `X-Plex-Device`, e.g. 'AFTKM' (Fire TV), 'iPhone',
|
|
/// 'Apple TV'. Null when unresolvable.
|
|
final String? deviceModel;
|
|
|
|
/// Friendly, usually user-assigned name (Settings > About > Device name on
|
|
/// Android, computer name on desktop). Null when unresolvable — callers
|
|
/// pick their own fallback. May contain characters that are not valid in
|
|
/// HTTP headers; pass through [sanitizeHeaderValue] before sending.
|
|
final String? deviceName;
|
|
|
|
final bool isTv;
|
|
|
|
const DeviceIdentity({required this.platform, this.deviceModel, this.deviceName, this.isTv = false});
|
|
}
|
|
|
|
/// Resolves the device identity once per process and memoizes it. Never
|
|
/// throws — platform-channel failures (tests, exotic platforms) degrade to
|
|
/// [Platform.operatingSystem] with null name/model.
|
|
class DeviceIdentityService {
|
|
DeviceIdentityService._();
|
|
|
|
static Future<DeviceIdentity>? _cached;
|
|
|
|
static Future<DeviceIdentity> resolve() => _cached ??= _resolve();
|
|
|
|
@visibleForTesting
|
|
static void debugOverride(DeviceIdentity? identity) {
|
|
_cached = identity == null ? null : Future.value(identity);
|
|
}
|
|
|
|
static Future<DeviceIdentity> _resolve() async {
|
|
final deviceInfo = DeviceInfoPlugin();
|
|
final isTv = TvDetectionService.isTVSync();
|
|
|
|
try {
|
|
if (Platform.isAndroid) {
|
|
final androidInfo = await deviceInfo.androidInfo;
|
|
final assignedName = await TvDetectionService.getAndroidDeviceName();
|
|
return DeviceIdentity(
|
|
platform: 'Android',
|
|
deviceModel: androidInfo.model,
|
|
deviceName: assignedName ?? '${androidInfo.brand} ${androidInfo.model}',
|
|
isTv: isTv,
|
|
);
|
|
}
|
|
if (Platform.isIOS) {
|
|
final iosInfo = await deviceInfo.iosInfo;
|
|
if (TvDetectionService.isAppleTVSync()) {
|
|
return DeviceIdentity(platform: 'tvOS', deviceModel: 'Apple TV', deviceName: iosInfo.name, isTv: true);
|
|
}
|
|
return DeviceIdentity(platform: 'iOS', deviceModel: iosInfo.model, deviceName: iosInfo.name, isTv: isTv);
|
|
}
|
|
if (Platform.isMacOS) {
|
|
final macInfo = await deviceInfo.macOsInfo;
|
|
return DeviceIdentity(
|
|
platform: 'macOS',
|
|
deviceModel: macInfo.model,
|
|
deviceName: macInfo.computerName,
|
|
isTv: isTv,
|
|
);
|
|
}
|
|
if (Platform.isWindows) {
|
|
final windowsInfo = await deviceInfo.windowsInfo;
|
|
return DeviceIdentity(
|
|
platform: 'Windows',
|
|
deviceModel: 'Windows',
|
|
deviceName: windowsInfo.computerName,
|
|
isTv: isTv,
|
|
);
|
|
}
|
|
if (Platform.isLinux) {
|
|
final host = Platform.localHostname.trim();
|
|
final name = (host.isNotEmpty && host != 'localhost') ? host : (await deviceInfo.linuxInfo).name;
|
|
return DeviceIdentity(platform: 'Linux', deviceModel: 'Linux', deviceName: name, isTv: isTv);
|
|
}
|
|
} catch (e) {
|
|
appLogger.w('DeviceIdentity: failed to resolve device info', error: e);
|
|
}
|
|
|
|
return DeviceIdentity(platform: Platform.operatingSystem, isTv: isTv);
|
|
}
|
|
}
|
|
|
|
/// Makes a free-form device name safe to send as an HTTP header value:
|
|
/// drops HTTP control characters and code units above 0xFF (dart:io's
|
|
/// HttpHeaders rejects them), trims, and returns null when nothing usable
|
|
/// remains.
|
|
String? sanitizeHeaderValue(String? value) {
|
|
if (value == null) return null;
|
|
final filtered = String.fromCharCodes(value.codeUnits.where((unit) => unit >= 0x20 && unit != 0x7F && unit <= 0xFF));
|
|
final trimmed = filtered.trim();
|
|
return trimmed.isEmpty ? null : trimmed;
|
|
}
|