Newer
Older
SmTIAS-Capture / lib / providers / camera_provider.dart
import 'package:camera/camera.dart';
import 'package:flutter/foundation.dart';

import 'package:smtias_capture/models/display_orientation.dart';
import 'package:smtias_capture/models/preview_frame.dart';
import 'package:smtias_capture/models/preview_region.dart';
import 'package:smtias_capture/services/file_service.dart';
import 'package:smtias_capture/services/permission_service.dart';
import 'package:smtias_capture/services/preview_region_service.dart';
import 'package:smtias_capture/services/raw_capture_service.dart';
import 'package:smtias_capture/services/sound_service.dart';

/// カメラの初期化・プレビュー制御・撮影実行・ライフサイクルを管理する.
class CameraProvider extends ChangeNotifier {
  /// 依存サービスはコンストラクタで差し替え可能(省略時は既定実装を生成).
  CameraProvider({
    PermissionService? permissionService,
    FileService? fileService,
    RawCaptureService? rawCaptureService,
    SoundService? soundService,
    PreviewRegionService? previewRegionService,
  }) : _permissionService = permissionService ?? PermissionService(),
       _fileService = fileService ?? FileService(),
       _rawCaptureService = rawCaptureService ?? RawCaptureService(),
       _soundService = soundService ?? SoundService(),
       _previewRegionService = previewRegionService ?? PreviewRegionService();

  final PermissionService _permissionService;
  final FileService _fileService;
  final RawCaptureService _rawCaptureService;
  final SoundService _soundService;
  final PreviewRegionService _previewRegionService;

  CameraController? _controller;
  bool _isInitialized = false;
  bool _initializing = false;
  String? _errorMessage;
  bool _permissionDenied = false;
  bool _permissionPermanentlyDenied = false;
  String? _lastSavedFileName;
  bool _isSaving = false;
  bool _imageStreamActive = false;
  PreviewFrame? _latestFrame;
  PreviewRegion _previewRegion = PreviewRegion.full;

  CameraController? get controller => _controller;
  bool get isInitialized => _isInitialized;
  String? get errorMessage => _errorMessage;
  bool get permissionDenied => _permissionDenied;
  bool get permissionPermanentlyDenied => _permissionPermanentlyDenied;
  String? get lastSavedFileName => _lastSavedFileName;
  bool get isSaving => _isSaving;
  bool get isSoundEnabled => _soundService.isSoundEnabled;

  /// プレビュー表示のクロップ領域(撮影画像には影響しない).
  PreviewRegion get previewRegion => _previewRegion;

  /// サウンド設定を切り替える.
  Future<void> toggleSound() async {
    await _soundService.toggleSound();
    notifyListeners();
  }

  /// シャッター音を再生する.
  Future<void> playShutterSound() async {
    await _soundService.playShutterSound();
  }

  /// テスト専用: 実カメラ初期化を経ずに「初期化済み」状態を作る.
  ///
  /// `_controller` は null のままにする(`takePicture()` は `_controller?.` と
  /// null 安全に書かれているため、保存フローはそのまま駆動できる).
  /// 本番コードからは呼ばないこと.
  @visibleForTesting
  void debugSetInitializedForTest({bool initialized = true}) {
    _isInitialized = initialized;
  }

  /// カメラを初期化する.
  ///
  /// 初期化処理が進行中の場合は即座に return して二重実行を防ぐ.
  /// すでに初期化済み([isInitialized] が true かつ [controller] が非 null)の
  /// 場合も何もしない.
  Future<void> initialize() async {
    // 二重実行防止: 初期化処理が進行中なら早期 return
    if (_initializing) return;
    // 既に初期化済みなら再初期化不要
    if (_isInitialized && _controller != null) return;

    _initializing = true;
    try {
      await _initializeInternal();
    } finally {
      _initializing = false;
    }
  }

  Future<void> _initializeInternal() async {
    await _soundService.init();
    _previewRegion = await _previewRegionService.load();

    final granted = await _permissionService.requestCamera();
    if (!granted) {
      _permissionDenied = true;
      _permissionPermanentlyDenied = await _permissionService
          .isCameraPermanentlyDenied();
      _errorMessage = 'カメラの権限が必要です';
      notifyListeners();
      return;
    }

    _permissionDenied = false;
    _permissionPermanentlyDenied = false;

    try {
      final cameras = await availableCameras();
      final frontCamera = cameras.firstWhere(
        (c) => c.lensDirection == CameraLensDirection.front,
      );

      _controller = CameraController(
        frontCamera,
        ResolutionPreset.max,
        enableAudio: false,
      );

      await _controller!.initialize();
      await _controller!.setFlashMode(FlashMode.off);
      _isInitialized = true;
      _errorMessage = null;

      _startImageStream();
    } catch (e) {
      _errorMessage = 'カメラの初期化に失敗しました: $e';
      _isInitialized = false;
    }

    notifyListeners();
  }

  /// 画像ストリームを開始して最新フレームを保持する.
  void _startImageStream() {
    if (_controller == null || _imageStreamActive) return;

    try {
      _controller!.startImageStream((CameraImage image) {
        _latestFrame = PreviewFrame(
          width: image.width,
          height: image.height,
          yPlane: Uint8List.fromList(image.planes[0].bytes),
          uPlane: Uint8List.fromList(image.planes[1].bytes),
          vPlane: Uint8List.fromList(image.planes[2].bytes),
          yRowStride: image.planes[0].bytesPerRow,
          uvRowStride: image.planes[1].bytesPerRow,
          uvPixelStride: image.planes[1].bytesPerPixel ?? 1,
        );
      });
      _imageStreamActive = true;
    } catch (e) {
      debugPrint('画像ストリーム開始失敗: $e');
    }
  }

  /// 画像ストリームを停止する.
  Future<void> _stopImageStream() async {
    if (!_imageStreamActive || _controller == null) return;

    try {
      await _controller!.stopImageStream();
    } catch (_) {
      // 既に停止済みの場合等は無視
    }
    _imageStreamActive = false;
  }

  /// 最新のプレビューフレームを JPEG として取得する(オーバーレイなし).
  Future<Uint8List?> capturePreviewSnapshot() async {
    if (_latestFrame == null) return null;

    final frame = _latestFrame!;
    // 向き処理はライブ系と共通の単一定義(DisplayOrientation)から派生させる.
    // スナップショット系は生 YUV(センサ向き未適用)を native で回転・ミラーする.
    const orientation = DisplayOrientation.attachmentDownward;
    return _rawCaptureService.convertYuvToJpeg(
      width: frame.width,
      height: frame.height,
      yPlane: frame.yPlane,
      uPlane: frame.uPlane,
      vPlane: frame.vPlane,
      yRowStride: frame.yRowStride,
      uvRowStride: frame.uvRowStride,
      uvPixelStride: frame.uvPixelStride,
      rotation: orientation.snapshotRotationDegrees,
      mirror: orientation.snapshotMirror,
    );
  }

  /// Camera2 API で定量モード撮影を行い,DNG ファイルとメタデータ JSON を保存する.
  ///
  /// [previewBytes] が指定された場合,`<baseName>.preview.jpg` として保存する.
  /// 指定しない場合はプレビュー JPEG の保存をスキップする.
  Future<void> takePicture({Uint8List? previewBytes}) async {
    if (!_isInitialized || _isSaving) return;

    _isSaving = true;
    _isInitialized = false;
    notifyListeners();

    try {
      if (!await _ensureStoragePermission()) return;

      await _releaseCameraBeforeCapture();
      await _captureAndSaveQuantitative(previewBytes);
    } catch (e) {
      _errorMessage = '撮影に失敗しました: $e';
      _lastSavedFileName = null;
    }

    _isSaving = false;
    notifyListeners();

    // プレビューを再開
    await initialize();
  }

  /// ストレージ権限を確認する.拒否時は errorMessage を設定して通知する.
  ///
  /// 拒否された場合は [_isSaving] を false に戻して false を返す.
  /// 呼び出し側は false を受けたら撮影フローを中断する.
  Future<bool> _ensureStoragePermission() async {
    final storageGranted = await _permissionService.requestStorage();
    if (!storageGranted) {
      _errorMessage = 'ストレージの権限が必要です';
      _isSaving = false;
      notifyListeners();
      return false;
    }
    return true;
  }

  /// 撮影前に画像ストリームを停止しカメラを解放する.
  ///
  /// 解放が完全に反映されるまで 500ms 待機してから戻る.
  Future<void> _releaseCameraBeforeCapture() async {
    await _stopImageStream();
    await _controller?.dispose();
    _controller = null;

    // カメラが完全に解放されるまで待つ
    await Future.delayed(const Duration(milliseconds: 500));
  }

  /// 定量モード撮影を行い,DNG・メタデータ・プレビューを保存して登録する.
  ///
  /// [previewBytes] が非 null のときのみプレビュー JPEG を保存・登録する.
  /// 保存した各ファイルを MediaStore へ登録し,DNG ファイル名を
  /// [_lastSavedFileName] に反映してから保存完了音を再生する.
  Future<void> _captureAndSaveQuantitative(Uint8List? previewBytes) async {
    // 定量モード撮影(DNG 経路)
    // baseName を先に確定し,Kotlin 側で DNG を直接書き込む
    final baseName = await _fileService.generateQuantitativeBaseName();
    final capture = await _rawCaptureService.captureQuantitativeDng(
      baseName: baseName,
      directoryPath: _fileService.directoryPath,
    );
    debugPrint('DNG saved: ${capture.dngFileSize} bytes at ${capture.dngPath}');

    // メタデータ JSON は引き続き Dart 側で保存
    final metaPath = await _fileService.saveQuantitativeMetadata(
      baseName,
      capture.metadata,
      lscMap: capture.lscMap,
      lscMapRowCount: capture.lscMapRowCount,
      lscMapColumnCount: capture.lscMapColumnCount,
    );

    // プレビュー JPEG 保存(snapshot がある場合のみ)
    String? previewPath;
    if (previewBytes != null) {
      previewPath = await _fileService.savePreviewJpeg(baseName, previewBytes);
      debugPrint('Preview saved: ${previewBytes.length} bytes at $previewPath');
    }

    // MediaStore に登録(PC から MTP で見えるようにする)
    await _rawCaptureService.scanFile(capture.dngPath);
    await _rawCaptureService.scanFile(metaPath);
    if (previewPath != null) {
      await _rawCaptureService.scanFile(previewPath);
    }

    _lastSavedFileName = capture.dngPath.split('/').last;
    _errorMessage = null;

    // 保存完了音を再生(エラーで撮影フロー全体は止めない)
    await _soundService.playSaveCompleteSound();
  }

  /// カメラリソースを解放する.
  Future<void> disposeCamera() async {
    await _stopImageStream();
    _latestFrame = null;
    _controller?.dispose();
    _controller = null;
    _isInitialized = false;
    notifyListeners();
  }

  /// アプリの設定画面を開く(権限が永久拒否された場合).
  Future<void> openSettings() async {
    await _permissionService.openSettings();
  }

  @override
  void dispose() {
    _controller?.dispose();
    super.dispose();
  }
}