chromium/ash/webui/camera_app_ui/resources/js/views/camera_intent.ts

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {assert, assertNotReached} from '../assert.js';
import {CameraManager, PhotoResult, VideoResult} from '../device/index.js';
import {I18nString} from '../i18n_string.js';
import {Intent} from '../intent.js';
import * as metrics from '../metrics.js';
import {FileAccessEntry} from '../models/file_system_access_entry.js';
import {VideoSaver} from '../models/video_saver.js';
import {ChromeHelper} from '../mojo/chrome_helper.js';
import {scaleImage} from '../thumbnailer.js';
import {Resolution} from '../type.js';
import * as util from '../util.js';

import {Camera} from './camera.js';
import * as review from './review.js';

/**
 * The maximum number of pixels in the downscaled intent photo result. Reference
 * from GCA: https://goto.google.com/gca-inline-bitmap-max-pixel-num.
 */
const DOWNSCALE_INTENT_MAX_PIXEL_NUM = 50 * 1024;

interface MetricArgs {
  resolution: Resolution;
  recordType: metrics.RecordType;
  duration?: number;
}

/**
 * Camera-intent-view controller.
 */
export class CameraIntent extends Camera {
  private videoResultFile: FileAccessEntry|null = null;

  constructor(
      private readonly intent: Intent,
      cameraManager: CameraManager,
  ) {
    super(
        {
          savePhoto: async (blob) => {
            if (intent.shouldDownScale) {
              const image = await util.blobToImage(blob);
              const ratio = Math.sqrt(
                  DOWNSCALE_INTENT_MAX_PIXEL_NUM /
                  (image.width * image.height));
              blob = await scaleImage(
                  blob, Math.floor(image.width * ratio),
                  Math.floor(image.height * ratio));
            }
            const buf = await blob.arrayBuffer();
            await this.intent.appendData(new Uint8Array(buf));
          },
          saveVideo: (file) => {
            this.videoResultFile = file;
          },
          saveGif: () => {
            assertNotReached();
          },
        },
        cameraManager);
  }

  override createVideoSaver(): Promise<VideoSaver> {
    return VideoSaver.createForIntent(this.intent, this.outputVideoRotation);
  }

  private reviewIntentResult(metricArgs: MetricArgs): Promise<void> {
    return this.prepareReview(async () => {
      const confirmed = await this.review.startReview(new review.OptionGroup({
        template: review.ButtonGroupTemplate.INTENT,
        options: [
          new review.Option(
              {
                label: I18nString.CONFIRM_REVIEW_BUTTON,
                icon: 'camera_intent_result_confirm.svg',
                templateId: 'review-intent-button-template',
                primary: true,
              },
              {exitValue: true}),
          new review.Option(
              {
                label: I18nString.CANCEL_REVIEW_BUTTON,
                icon: 'camera_intent_result_cancel.svg',
                templateId: 'review-intent-button-template',
              },
              {exitValue: false}),
        ],
      })) ??
          false;
      metrics.sendCaptureEvent({
        facing: this.getFacing(),
        ...metricArgs,
        intentResult: confirmed ? metrics.IntentResultType.CONFIRMED :
                                  metrics.IntentResultType.CANCELED,
        shutterType: this.shutterType,
        resolutionLevel:
            this.cameraManager.getPhotoResolutionLevel(metricArgs.resolution),
        aspectRatioSet:
            this.cameraManager.getAspectRatioSet(metricArgs.resolution),
      });
      if (confirmed) {
        await this.intent.finish();
        const appWindow = window.appWindow;
        if (appWindow === null) {
          window.close();
        } else {
          // For test session, we notify tests and let test close the window for
          // us.
          await appWindow.notifyClosingItself();
        }
      } else {
        await this.intent.clearData();
      }
    });
  }

  override async onPhotoCaptureDone(pendingPhotoResult: Promise<PhotoResult>):
      Promise<void> {
    await super.onPhotoCaptureDone(pendingPhotoResult);
    const {blob, resolution} = await pendingPhotoResult;
    await this.review.setReviewPhoto(blob);
    await this.reviewIntentResult({
      resolution,
      recordType: metrics.RecordType.NOT_RECORDING,
    });
    ChromeHelper.getInstance().maybeTriggerSurvey();
  }

  override async onVideoCaptureDone(videoResult: VideoResult): Promise<void> {
    await super.onVideoCaptureDone(videoResult);
    assert(this.videoResultFile !== null);
    const cleanup = await this.review.setReviewVideo(this.videoResultFile);
    await this.reviewIntentResult({
      resolution: videoResult.resolution,
      recordType: metrics.RecordType.NORMAL_VIDEO,
      duration: videoResult.duration,
    });
    cleanup();
    ChromeHelper.getInstance().maybeTriggerSurvey();
  }
}