😇

「Bounds must be at least 50% within visible screen space」の解決

2022/05/05に公開

どういうエラー?

これはChrome102で追加されたウィンドウ配置に関するバリデーション由来のエラーです。
Chrome拡張でウィンドウを作成するときに、ウィンドウサイズが大きいときやディスプレイの表示領域から離れすぎると発生します。

[Extensions] Display window only when its bounds intersect the displays
Windows should only be displayed when they intersect the displays in a
meaningful manner (in this case, window should be visible at least 50%).
Otherwise, the window will be displayed in the current default.
This prevents for windows to be created or moved outside the displays,
and potentially been exploited.

https://source.chromium.org/chromium/chromium/src/+/d51682b36adc22496f45a8111358a8bb30914534

以下のコードで再現できます。
ここではManifest V3で記述していますが、Manifest V2でも同じです。

manifest.json
{
  "name": "Open Google",
  "version": "1.0",
  "manifest_version": 3,
  "permissions": [
    "system.display"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "action": {}
}

下は画面いっぱいにChromeを表示するためのコードです。
どのようなディスプレイでも対応できるように、widthとheightに大きな値を設定しています。

background.js
chrome.action.onClicked.addListener(() => {
  chrome.windows.create(
    {
      url: 'https://google.com',
      type: 'popup',
      width: 10000,
      height: 10000,
    },
  )
})

このChrome拡張を起動すると、このようなエラーが発生します。

error-message

Uncaught (in promise) Error: Invalid value for bounds. Bounds must be at least 50% within visible screen space.

結論 & 解決策

これはディスプレイが見えない位置に置かれることを防ぐために追加された処置の副産物です。
以下のように、ディスプレイサイズに合わせてウィンドウを作成すれば解決します。

background.js
chrome.action.onClicked.addListener(() => {
  chrome.system.display.getInfo((display) => {
    // NOTE: 現在表示しているディスプレイサイズを取得
    const { width, height } = display[0].bounds

    chrome.windows.create(
      {
        url: 'https://google.com',
        type: 'popup',
        width,
        height,
      },
    )
  })
})

なぜ上記の方法で良いのか

エラー文からではどうして上の方法で良いのか分からないので、エラーの発生条件を読んでみます。

エラー文

まず、エラー文はここで定義されています。
https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/extensions/api/tabs/tabs_constants.cc;l=111-113?q="Invalid value for bounds"&ss=chromium%2Fchromium%2Fsrc

const char kInvalidWindowBoundsError[] =
    "Invalid value for bounds. Bounds must be at least 50% within visible "
    "screen space.";

エラー発生条件

上記のkInvalidWindowBoundsErrorが呼ばれる条件はここです。
https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/extensions/api/tabs/tabs_api.cc;l=919-920;drc=e813b3c6b110bce715d55f4ce12746e2c43aedb9;bpv=0;bpt=1

set_window_boundsがtrueかつWindowBoundsIntersectDisplays(window_bounds)がfalseだと呼ばれるようです。

if (set_window_bounds && !WindowBoundsIntersectDisplays(window_bounds))
  return RespondNow(Error(tabs_constants::kInvalidWindowBoundsError));

set_window_boundsとは

コードを読んでみると、ウィンドウを作成するときにleftやtop, width, heightを設定しているとtrueになるようです。
https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/extensions/api/tabs/tabs_api.cc;l=901-917;drc=e813b3c6b110bce715d55f4ce12746e2c43aedb9;bpv=0;bpt=1

bool set_window_bounds = false;
if (params->update_info.left) {
  window_bounds.set_x(*params->update_info.left);
  set_window_bounds = true;
}
if (params->update_info.top) {
  window_bounds.set_y(*params->update_info.top);
  set_window_bounds = true;
}
if (params->update_info.width) {
  window_bounds.set_width(*params->update_info.width);
  set_window_bounds = true;
}
if (params->update_info.height) {
  window_bounds.set_height(*params->update_info.height);
  set_window_bounds = true;
}

window_boundsとは

WindowBoundsIntersectDisplaysの引数で渡されているwindow_boundsをみていきます。
こちらは、作成または再配置するウィンドウ (以後、単にウィンドウと呼びます) のBoundsという理解で良さそうです。
https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/extensions/api/tabs/tabs_api.cc;l=898-900;drc=e813b3c6b110bce715d55f4ce12746e2c43aedb9;bpv=0;bpt=1

Boundsが何かという正確な定義を見つけられなかったので、どういうものか不明です。

コードをみた感じだと, height, width, left, topといった値を持ち、Boundsが決まるとウィンドウの面積が求まるものという感じでした (ウィンドウの境界線のことと理解しておけば良いでしょうか?)。

gfx::Rect window_bounds = browser->window()->IsMinimized()
                            ? browser->window()->GetRestoredBounds()
                            : browser->window()->GetBounds();

WindowBoundsIntersectDisplaysとは

このように定義されています。
https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/extensions/api/tabs/tabs_api.cc;l=432-440;drc=e813b3c6b110bce715d55f4ce12746e2c43aedb9?q=kInvalidWindowBoundsError&ss=chromium%2Fchromium%2Fsrc

intersect_areaを合算した後、ウィンドウの面積と比較し、true, falseを返すようです。

bool WindowBoundsIntersectDisplays(const gfx::Rect& bounds) {
  int intersect_area = 0;
  for (const auto& display : display::Screen::GetScreen()->GetAllDisplays()) {
    gfx::Rect display_bounds = display.bounds();
    display_bounds.Intersect(bounds);
    intersect_area += display_bounds.size().GetArea();
  }
  return intersect_area >= (bounds.size().GetArea() / 2);
}

もう少し詳しく読みます。
まずここで、全ディスプレイについてboundsを取得しています。

for (const auto& display : display::Screen::GetScreen()->GetAllDisplays()) {
  gfx::Rect display_bounds = display.bounds();
  ...

その後、ウィンドウとのIntersectを取り、そのサイズを合算しています。

  ...
  display_bounds.Intersect(bounds);
  intersect_area += display_bounds.size().GetArea();
}

Intersectメソッドについては、ここで定義されています。
https://source.chromium.org/chromium/chromium/src/+/main:ui/gfx/geometry/rect.cc;l=162-179;drc=e813b3c6b110bce715d55f4ce12746e2c43aedb9;bpv=0;bpt=0

コードをみた感じだと、ディスプレイとウィンドウの重なり合っている箇所を取得しているみたいです。

雰囲気ですが、こんなイメージです。
ディスプレイサイズ > ウィンドウサイズ のときは、ウィンドウサイズとintersect_areaが一致
intersect_example

ディスプレイサイズ < ウィンドウサイズのときは、ディスプレイサイズとintersect_areaが一致
intersect_example2

そして、ディスプレイとウィンドウが重なってないときは、intersect_areaはゼロになります。

void Rect::Intersect(const Rect& rect) {
  if (IsEmpty() || rect.IsEmpty()) {
    SetRect(0, 0, 0, 0);  // Throws away empty position.
    return;
  }

  int left = std::max(x(), rect.x());
  int top = std::max(y(), rect.y());
  int new_right = std::min(right(), rect.right());
  int new_bottom = std::min(bottom(), rect.bottom());

  if (left >= new_right || top >= new_bottom) {
    SetRect(0, 0, 0, 0);  // Throws away empty position.
    return;
  }

  SetByBounds(left, top, new_right, new_bottom);
}

intersect_area >= (bounds.size().GetArea() / 2)

ディスプレイとウィンドウが重なってるエリアの合算とウィンドウサイズを比較する箇所です。
ここで、2で割っている箇所がBounds must be at least 50% within visible screen spaceの50%に対応しています。

少しややこしく、コードからは読み取れなかったのですが、ウィンドウサイズは正しくは利用可能なウィンドウサイズのことを指しているようです。

Macの場合は「このMacについて > ディスプレイ」でディスプレイサイズを取得できます。
以下のスクショの場合は width => 1920px, height => 1080pxです。
window-size

この値とbound.size().GetArea()で取得できるサイズが正しければ、以下でも動くはずです。
しかし、Bounds must be at least 50%...が発生します。

background.js
...
    const { width, height } = display[0].bounds

    chrome.windows.create(
      {
        url: 'https://google.com',
        type: 'popup',
        width,
        height: height * 2,
      },
    )
...

正しくは、window.screenで取得できるavailWidth, availHeightの値になってるようです。
(ちなみに、Chrome Extension Manifest V3ではService Worker上で動作するため、window関数にアクセスできず、起動時にこの値を設定できません)
screen-width

私の環境ではheight: 1055 * 2までは動作し、height: 1055 * 2 + 1ではエラーが出ることを確認できました。

background.js
    chrome.windows.create(
      {
        url: 'https://google.com',
        type: 'popup',
        width,
        height: 1055 * 2,
      },
    )

まとめ

Chrome102からウィンドウ配置に関するバリデーションが追加され、 ディスプレイとウィンドウが大きく離れる場合や、ウィンドウが大きすぎる場合にエラーが表示されるようになりました。

Chromiumのコードを見れば、どういう条件で発生するのか、どうして追加されたのかわかるので、 困ったらChromiumのコードを読んでみましょう!

参考

https://github.com/pilefort/new-chrome-window-validate-from-chrome102

以上

GitHubで編集を提案

Discussion