Fix CCA query resolution API not available on HALv1 device bug

Bug: 965933
Test: On HALv1 device the CCA function normally without resolution
settings menu. On HALv3 device the CCA is able to capture with specified
resolution from resolution settings menu.

Change-Id: If22961c55a052a7a6061c62569d88aca69e332d1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1626063
Commit-Queue: Kuo Jen Wei <inker@chromium.org>
Reviewed-by: Sheng-hao Tsao <shenghao@chromium.org>
Cr-Commit-Position: refs/heads/master@{#663002}
diff --git a/chrome/browser/resources/chromeos/camera/src/css/main.css b/chrome/browser/resources/chromeos/camera/src/css/main.css
index 06f339a..f4f76d6 100644
--- a/chrome/browser/resources/chromeos/camera/src/css/main.css
+++ b/chrome/browser/resources/chromeos/camera/src/css/main.css
@@ -1025,6 +1025,10 @@
   background-image: url(../images/settings_timer_duration.svg);
 }
 
+body.no-resolution-settings #settings-resolution {
+  display: none;
+}
+
 #settings-resolution .icon {
   background-image: url(../images/settings_resolution.svg);
 }
diff --git a/chrome/browser/resources/chromeos/camera/src/js/views/camera.js b/chrome/browser/resources/chromeos/camera/src/js/views/camera.js
index 3c29ad2..baf23e0 100644
--- a/chrome/browser/resources/chromeos/camera/src/js/views/camera.js
+++ b/chrome/browser/resources/chromeos/camera/src/js/views/camera.js
@@ -240,9 +240,16 @@
 cca.views.Camera.prototype.startWithDevice_ = async function(deviceId) {
   let supportedModes = null;
   for (const mode of this.modes_.getModeCandidates()) {
-    const previewRs = (await this.options_.getDeviceResolutions(deviceId))[1];
-    for (const [[width, height], previewCandidates] of this.modes_
-             .getResolutionCandidates(mode, deviceId, previewRs)) {
+    try {
+      const previewRs = (await this.options_.getDeviceResolutions(deviceId))[1];
+      var resolCandidates =
+          this.modes_.getResolutionCandidates(mode, deviceId, previewRs);
+    } catch (e) {
+      // Assume the exception here is thrown from error of HALv1 not support
+      // resolution query, fallback to use v1 constraints-candidates.
+      resolCandidates = this.modes_.getResolutionCandidatesV1(mode, deviceId);
+    }
+    for (const [captureResolution, previewCandidates] of resolCandidates) {
       for (const constraints of previewCandidates) {
         try {
           const stream = await navigator.mediaDevices.getUserMedia(constraints);
@@ -256,7 +263,8 @@
           await this.preview_.start(stream);
           this.facingMode_ = this.options_.updateValues(constraints, stream);
           await this.modes_.updateModeSelectionUI(supportedModes);
-          await this.modes_.updateMode(mode, stream, deviceId, width, height);
+          await this.modes_.updateMode(
+              mode, stream, deviceId, captureResolution);
           cca.nav.close('warning', 'no-camera');
           return true;
         } catch (e) {
diff --git a/chrome/browser/resources/chromeos/camera/src/js/views/camera/modes.js b/chrome/browser/resources/chromeos/camera/src/js/views/camera/modes.js
index ba1a4ce..ea3878a 100644
--- a/chrome/browser/resources/chromeos/camera/src/js/views/camera/modes.js
+++ b/chrome/browser/resources/chromeos/camera/src/js/views/camera/modes.js
@@ -63,18 +63,10 @@
   this.modesGroup_ = document.querySelector('#modes-group');
 
   /**
-   * Captured resolution width.
-   * @type {number}
+   * @type {?[number, number]}
    * @private
    */
-  this.captureWidth_ = 0;
-
-  /**
-   * Captured resolution height.
-   * @type {number}
-   * @private
-   */
-  this.captureHeight_ = 0;
+  this.captureResolution_ = null;
 
   /**
    * Mode classname and related functions and attributes.
@@ -87,28 +79,28 @@
           new cca.views.camera.Video(this.stream_, this.doSavePicture_),
       isSupported: async () => true,
       resolutionConfig: videoResolPreferrer,
+      v1Config: cca.views.camera.Modes.videoConstraits,
       nextMode: 'photo-mode',
     },
     'photo-mode': {
       captureFactory: () => new cca.views.camera.Photo(
-          this.stream_, this.doSavePicture_, this.captureWidth_,
-          this.captureHeight_),
+          this.stream_, this.doSavePicture_, this.captureResolution_),
       isSupported: async () => true,
       resolutionConfig: photoResolPreferrer,
+      v1Config: cca.views.camera.Modes.photoConstraits,
       nextMode: 'square-mode',
     },
     'square-mode': {
       captureFactory: () => new cca.views.camera.Square(
-          this.stream_, this.doSavePicture_, this.captureWidth_,
-          this.captureHeight_),
+          this.stream_, this.doSavePicture_, this.captureResolution_),
       isSupported: async () => true,
       resolutionConfig: photoResolPreferrer,
+      v1Config: cca.views.camera.Modes.photoConstraits,
       nextMode: 'portrait-mode',
     },
     'portrait-mode': {
       captureFactory: () => new cca.views.camera.Portrait(
-          this.stream_, this.doSavePicture_, this.captureWidth_,
-          this.captureHeight_),
+          this.stream_, this.doSavePicture_, this.captureResolution_),
       isSupported: async (stream) => {
         try {
           const imageCapture =
@@ -125,6 +117,7 @@
         }
       },
       resolutionConfig: photoResolPreferrer,
+      v1Config: cca.views.camera.Modes.photoConstraits,
       nextMode: 'video-mode',
     },
   };
@@ -169,6 +162,50 @@
 };
 
 /**
+ * Returns a set of available video constraints for HALv1 device.
+ * @param {?string} deviceId Id of video device.
+ * @return {Array<Object>} Result of constraints-candidates.
+ */
+cca.views.camera.Modes.videoConstraits = function(deviceId) {
+  return [
+    {
+      aspectRatio: {ideal: 1.7777777778},
+      width: {min: 1280},
+      frameRate: {min: 24},
+    },
+    {
+      width: {min: 640},
+      frameRate: {min: 24},
+    },
+  ].map((constraint) => {
+    constraint.deviceId = {exact: deviceId};
+    return {audio: true, video: constraint};
+  });
+};
+
+/**
+ * Returns a set of available photo constraints for HALv1 device.
+ * @param {?string} deviceId Id of video device.
+ * @return {Array<Object>} Result of constraints-candidates.
+ */
+cca.views.camera.Modes.photoConstraits = function(deviceId) {
+  return [
+    {
+      aspectRatio: {ideal: 1.3333333333},
+      width: {min: 1280},
+      frameRate: {min: 24},
+    },
+    {
+      width: {min: 640},
+      frameRate: {min: 24},
+    },
+  ].map((constraint) => {
+    constraint.deviceId = {exact: deviceId};
+    return {audio: false, video: constraint};
+  });
+};
+
+/**
  * Switches mode to either video-recording or photo-taking.
  * @param {string} mode Class name of the switching mode.
  * @private
@@ -205,7 +242,7 @@
  * @param {string} mode
  * @param {string} deviceId
  * @param {ResolList} previewResolutions
- * @return {Array<[number, number, Array<Object>]>} Result capture resolution
+ * @return {Array<[?[number, number], Array<Object>]>} Result capture resolution
  *     width, height and constraints-candidates for its preview.
  */
 cca.views.camera.Modes.prototype.getResolutionCandidates = function(
@@ -215,6 +252,20 @@
 };
 
 /**
+ * Gets capture resolution and its corresponding preview constraints for the
+ * given mode on camera HALv1 device.
+ * @param {string} mode
+ * @param {string} deviceId
+ * @return {Array<[?[number, number], Array<Object>]>} Result capture resolution
+ *     width, height and constraints-candidates for its preview.
+ */
+cca.views.camera.Modes.prototype.getResolutionCandidatesV1 = function(
+    mode, deviceId) {
+  return this.allModes_[mode].v1Config(deviceId).map(
+      (constraints) => [null, [constraints]]);
+};
+
+/**
  * Gets supported modes for video device of the given stream.
  * @param {MediaStream} stream Stream of the video device.
  * @return {Array<string>} Names of all supported mode for the video device.
@@ -249,33 +300,33 @@
  * @param {string} mode Classname of mode to be updated.
  * @param {MediaStream} stream Stream of the new switching mode.
  * @param {string} deviceId Device id of currently working video device.
- * @param {number} captureWidth Capturing resolution width.
- * @param {number} captureHeight Capturing resolution height.
+ * @param {?[number, number]} captureResolution Capturing resolution width and
+ *     height.
  */
 cca.views.camera.Modes.prototype.updateMode =
-    async function(mode, stream, deviceId, captureWidth, captureHeight) {
+    async function(mode, stream, deviceId, captureResolution) {
   if (this.current != null) {
     await this.current.stopCapture();
   }
   this.updateModeUI_(mode);
   this.stream_ = stream;
-  this.captureWidth_ = captureWidth;
-  this.captureHeight_ = captureHeight;
+  this.captureResolution_ = captureResolution;
   this.current = this.allModes_[mode].captureFactory();
-  this.allModes_[mode].resolutionConfig.updateCurrentResolution(
-      deviceId, captureWidth, captureHeight);
+  if (this.captureResolution_) {
+    this.allModes_[mode].resolutionConfig.updateCurrentResolution(
+        deviceId, ...this.captureResolution_);
+  }
 };
 
 /**
  * Base class for controlling capture sequence in different camera modes.
  * @param {MediaStream} stream
  * @param {function(?Blob, boolean, string): Promise} doSavePicture
- * @param {number} captureWidth Capturing resolution width.
- * @param {number} captureHeight Capturing resolution height.
+ * @param {?[number, number]} captureResolution Capturing resolution width and
+ *     height.
  * @constructor
  */
-cca.views.camera.Mode = function(
-    stream, doSavePicture, captureWidth, captureHeight) {
+cca.views.camera.Mode = function(stream, doSavePicture, captureResolution) {
   /**
    * Stream of current mode.
    * @type {?Promise}
@@ -291,16 +342,12 @@
   this.doSavePicture_ = doSavePicture;
 
   /**
-   * @type {number}
+   * Width, height of capture resolution. May be null on device not supporting
+   * setting resolution.
+   * @type {?[number, number]}
    * @private
    */
-  this.captureWidth_ = captureWidth;
-
-  /**
-   * @type {number}
-   * @private
-   */
-  this.captureHeight_ = captureHeight;
+  this.captureResolution_ = captureResolution;
 
   /**
    * Promise for ongoing capture operation.
@@ -351,7 +398,7 @@
  * @constructor
  */
 cca.views.camera.Video = function(stream, doSavePicture) {
-  cca.views.camera.Mode.call(this, stream, doSavePicture, -1, -1);
+  cca.views.camera.Mode.call(this, stream, doSavePicture, null);
 
   /**
    * Promise for play start sound delay.
@@ -491,14 +538,11 @@
  * Photo mode capture controller.
  * @param {MediaStream} stream
  * @param {function(?Blob, boolean, string): Promise} doSavePicture
- * @param {number} captureWidth
- * @param {number} captureHeight
+ * @param {?[number, number]} captureResolution
  * @constructor
  */
-cca.views.camera.Photo = function(
-    stream, doSavePicture, captureWidth, captureHeight) {
-  cca.views.camera.Mode.call(
-      this, stream, doSavePicture, captureWidth, captureHeight);
+cca.views.camera.Photo = function(stream, doSavePicture, captureResolution) {
+  cca.views.camera.Mode.call(this, stream, doSavePicture, captureResolution);
 
   /**
    * ImageCapture object to capture still photos.
@@ -544,10 +588,18 @@
  * @private
  */
 cca.views.camera.Photo.prototype.createPhotoBlob_ = async function() {
-  const photoSettings = {
-    imageWidth: this.captureWidth_,
-    imageHeight: this.captureHeight_,
-  };
+  if (this.captureResolution_) {
+    var photoSettings = {
+      imageWidth: this.captureResolution_[0],
+      imageHeight: this.captureResolution_[1],
+    };
+  } else {
+    const caps = await this.imageCapture_.getPhotoCapabilities();
+    photoSettings = {
+      imageWidth: caps.imageWidth.max,
+      imageHeight: caps.imageHeight.max,
+    };
+  }
   return await this.imageCapture_.takePhoto(photoSettings);
 };
 
@@ -555,14 +607,11 @@
  * Square mode capture controller.
  * @param {MediaStream} stream
  * @param {function(?Blob, boolean, string): Promise} doSavePicture
- * @param {number} captureWidth
- * @param {number} captureHeight
+ * @param {?[number, number]} captureResolution
  * @constructor
  */
-cca.views.camera.Square = function(
-    stream, doSavePicture, captureWidth, captureHeight) {
-  cca.views.camera.Photo.call(
-      this, stream, doSavePicture, captureWidth, captureHeight);
+cca.views.camera.Square = function(stream, doSavePicture, captureResolution) {
+  cca.views.camera.Photo.call(this, stream, doSavePicture, captureResolution);
 
   /**
    * Picture saving callback from parent.
@@ -618,14 +667,11 @@
  * Portrait mode capture controller.
  * @param {MediaStream} stream
  * @param {function(?Blob, boolean): Promise} doSavePicture
- * @param {number} captureWidth
- * @param {number} captureHeight
+ * @param {?[number, number]} captureResolution
  * @constructor
  */
-cca.views.camera.Portrait = function(
-    stream, doSavePicture, captureWidth, captureHeight) {
-  cca.views.camera.Mode.call(
-      this, stream, doSavePicture, captureWidth, captureHeight);
+cca.views.camera.Portrait = function(stream, doSavePicture, captureResolution) {
+  cca.views.camera.Mode.call(this, stream, doSavePicture, captureResolution);
 
   /**
    * ImageCapture object to capture still photos.
@@ -655,10 +701,18 @@
       throw e;
     }
   }
-  const photoSettings = {
-    imageWidth: this.captureWidth_,
-    imageHeight: this.captureHeight_,
-  };
+  if (this.captureResolution_) {
+    var photoSettings = {
+      imageWidth: this.captureResolution_[0],
+      imageHeight: this.captureResolution_[1],
+    };
+  } else {
+    const caps = await this.imageCapture_.getPhotoCapabilities();
+    photoSettings = {
+      imageWidth: caps.imageWidth.max,
+      imageHeight: caps.imageHeight.max,
+    };
+  }
   try {
     var [reference, portrait] = this.crosImageCapture_.takePhoto(
         photoSettings, [cros.mojom.Effect.PORTRAIT_MODE]);
diff --git a/chrome/browser/resources/chromeos/camera/src/js/views/camera/options.js b/chrome/browser/resources/chromeos/camera/src/js/views/camera/options.js
index ec8ccff..341d4670 100644
--- a/chrome/browser/resources/chromeos/camera/src/js/views/camera/options.js
+++ b/chrome/browser/resources/chromeos/camera/src/js/views/camera/options.js
@@ -278,17 +278,29 @@
     this.refreshingVideoDeviceIds_ = false;
   });
 
-  this.deviceResolutions_ = this.videoDevices_.then((devices) => {
-    return Promise.all(devices.map((d) => Promise.all([
-      d,
-      cca.mojo.getPhotoResolutions(d.deviceId),
-      cca.mojo.getVideoConfigs(d.deviceId)
-          .then((v) => v.filter(([, , fps]) => fps >= 24).map(([w,
-                                                                h]) => [w, h])),
-    ])));
-  });
+  this.deviceResolutions_ =
+      this.videoDevices_
+          .then((devices) => {
+            return Promise.all(devices.map((d) => Promise.all([
+              d,
+              cca.mojo.getPhotoResolutions(d.deviceId),
+              cca.mojo.getVideoConfigs(d.deviceId)
+                  .then(
+                      (v) => v.filter(([, , fps]) => fps >= 24)
+                                 .map(([w, h]) => [w, h])),
+            ])));
+          })
+          .catch((e) => {
+            cca.state.set('no-resolution-settings', true);
+            throw e;
+          });
 
-  this.deviceResolutions_.then((deviceResolutions) => {
+  (async () => {
+    try {
+      var deviceResolutions = await this.deviceResolutions_;
+    } catch (e) {
+      return;
+    }
     let frontSetting = null;
     let backSetting = null;
     let externalSettings = [];
@@ -314,7 +326,7 @@
         frontSetting && [frontSetting[0], frontSetting[2]],
         backSetting && [backSetting[0], backSetting[2]],
         externalSettings.map(([deviceId, , videoRs]) => [deviceId, videoRs]));
-  });
+  })();
 };
 
 /**
@@ -348,6 +360,8 @@
  * @async
  * @param {string} deviceId Device id of the video device.
  * @return {[ResolList, ResolList]} Supported photo and video resolutions.
+ * @throws {Error} May fail on HALv1 device without capability of querying
+ *     supported resolutions.
  */
 cca.views.camera.Options.prototype.getDeviceResolutions =
     async function(deviceId) {