139 lines
3.5 KiB
Dart
139 lines
3.5 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import '../../../core/infra/pip/android_pip_controller.dart';
|
|
import '../../../shared/utils/app_logger.dart';
|
|
|
|
const _tag = 'AndroidPipManager';
|
|
|
|
|
|
class AndroidPipManager {
|
|
AndroidPipManager({required this._onStateChanged});
|
|
|
|
final VoidCallback _onStateChanged;
|
|
|
|
bool _isInPip = false;
|
|
bool _isDisposed = false;
|
|
bool _supported = false;
|
|
StreamSubscription<bool>? _modeSub;
|
|
StreamSubscription<PipAction>? _actionSub;
|
|
|
|
|
|
void Function(PipAction action)? onAction;
|
|
|
|
bool get isInPip => _isInPip;
|
|
bool get isSupported => _supported;
|
|
|
|
|
|
Future<void> initialize() async {
|
|
if (!Platform.isAndroid) return;
|
|
final supported = await AndroidPipController.instance.isSupported();
|
|
if (_isDisposed) return;
|
|
if (_supported != supported) {
|
|
_supported = supported;
|
|
_onStateChanged();
|
|
}
|
|
if (!_supported) return;
|
|
if (_modeSub != null || _actionSub != null) return;
|
|
|
|
final isInPip = await AndroidPipController.instance.isInPipMode();
|
|
if (_isDisposed) return;
|
|
if (_isInPip != isInPip) {
|
|
_isInPip = isInPip;
|
|
_onStateChanged();
|
|
}
|
|
|
|
_modeSub = AndroidPipController.instance.pipModeChanges.listen(
|
|
(isInPip) {
|
|
if (_isDisposed) return;
|
|
if (_isInPip == isInPip) return;
|
|
_isInPip = isInPip;
|
|
AppLogger.info(_tag, 'PiP mode changed: $isInPip');
|
|
_onStateChanged();
|
|
},
|
|
onError: (Object e) {
|
|
AppLogger.warn(_tag, 'PiP mode stream error', e);
|
|
},
|
|
);
|
|
|
|
_actionSub = AndroidPipController.instance.actionStream.listen(
|
|
(action) {
|
|
if (_isDisposed) return;
|
|
AppLogger.debug(_tag, 'PiP action: $action');
|
|
onAction?.call(action);
|
|
},
|
|
onError: (Object e) {
|
|
AppLogger.warn(_tag, 'PiP action stream error', e);
|
|
},
|
|
);
|
|
}
|
|
|
|
|
|
Future<void> configure({
|
|
int aspectX = 16,
|
|
int aspectY = 9,
|
|
bool autoEnterOnLeave = false,
|
|
bool isPlaying = false,
|
|
bool hasNext = false,
|
|
bool hasPrev = false,
|
|
bool useEpisodeActions = true,
|
|
}) async {
|
|
if (!_supported || _isDisposed) return;
|
|
await AndroidPipController.instance.configure(
|
|
aspectX: aspectX,
|
|
aspectY: aspectY,
|
|
autoEnterOnLeave: autoEnterOnLeave,
|
|
isPlaying: isPlaying,
|
|
hasNext: hasNext,
|
|
hasPrev: hasPrev,
|
|
useEpisodeActions: useEpisodeActions,
|
|
);
|
|
}
|
|
|
|
|
|
Future<bool> enter({int? aspectX, int? aspectY}) async {
|
|
if (!_supported || _isDisposed || _isInPip) return false;
|
|
return AndroidPipController.instance.enterPip(
|
|
aspectX: aspectX,
|
|
aspectY: aspectY,
|
|
);
|
|
}
|
|
|
|
|
|
Future<void> updatePlaybackState({
|
|
required bool isPlaying,
|
|
required bool hasNext,
|
|
required bool hasPrev,
|
|
required bool useEpisodeActions,
|
|
}) async {
|
|
if (!_supported || _isDisposed || !_isInPip) return;
|
|
await AndroidPipController.instance.updatePlaybackState(
|
|
isPlaying: isPlaying,
|
|
hasNext: hasNext,
|
|
hasPrev: hasPrev,
|
|
useEpisodeActions: useEpisodeActions,
|
|
);
|
|
}
|
|
|
|
void dispose() {
|
|
if (_isDisposed) return;
|
|
_isDisposed = true;
|
|
final modeSub = _modeSub;
|
|
_modeSub = null;
|
|
final actionSub = _actionSub;
|
|
_actionSub = null;
|
|
onAction = null;
|
|
unawaited(() async {
|
|
await AndroidPipController.instance.configure(
|
|
autoEnterOnLeave: false,
|
|
isPlaying: false,
|
|
);
|
|
await modeSub?.cancel();
|
|
await actionSub?.cancel();
|
|
await AndroidPipController.instance.dispose();
|
|
}());
|
|
}
|
|
}
|