Skip to content

Commit

Permalink
[web] Expose browser_detection through ui_web. (#52380)
Browse files Browse the repository at this point in the history
This PR moves the core of `browser_detection.dart` to a location in `dart:ui_web` so it can be used by apps and plugins.

In order for the code to be a little bit tidier in ui_web, it's encapsulated in a singleton instance that can be accessed through `BrowserDetection.instance` or a top level global `browser` in `dart:ui_web`.

## Issues

* Needed to fix: flutter/flutter#128943
* Needed to land: flutter/flutter#147346

## Tests

Updated affected tests. Mostly the update was to call the methods from `web_ui.browser.methodName` rather than a global scope. Also split the tests for this module in two files:

* `engine_browser_detect_test.dart` - with the tests specific to the engine (capability detection, etc...)
* `browser_detect_test.dart` - only the tests pertaining to the "core" of the library.

[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
  • Loading branch information
ditman committed May 2, 2024
1 parent 0d5a79e commit b25f5d5
Show file tree
Hide file tree
Showing 53 changed files with 607 additions and 533 deletions.
2 changes: 2 additions & 0 deletions ci/licenses_golden/licenses_flutter
Expand Up @@ -41428,6 +41428,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/ui.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/ui_web/src/ui_web.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/ui_web/src/ui_web/asset_manager.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/ui_web/src/ui_web/benchmarks.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/ui_web/src/ui_web/browser_detection.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/ui_web/src/ui_web/flutter_views_proxy.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/ui_web/src/ui_web/images.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/ui_web/src/ui_web/initialization.dart + ../../../flutter/LICENSE
Expand Down Expand Up @@ -44320,6 +44321,7 @@ FILE: ../../../flutter/lib/web_ui/lib/ui.dart
FILE: ../../../flutter/lib/web_ui/lib/ui_web/src/ui_web.dart
FILE: ../../../flutter/lib/web_ui/lib/ui_web/src/ui_web/asset_manager.dart
FILE: ../../../flutter/lib/web_ui/lib/ui_web/src/ui_web/benchmarks.dart
FILE: ../../../flutter/lib/web_ui/lib/ui_web/src/ui_web/browser_detection.dart
FILE: ../../../flutter/lib/web_ui/lib/ui_web/src/ui_web/flutter_views_proxy.dart
FILE: ../../../flutter/lib/web_ui/lib/ui_web/src/ui_web/images.dart
FILE: ../../../flutter/lib/web_ui/lib/ui_web/src/ui_web/initialization.dart
Expand Down
272 changes: 50 additions & 222 deletions lib/web_ui/lib/src/engine/browser_detection.dart
Expand Up @@ -3,220 +3,50 @@
// found in the LICENSE file.

import 'package:meta/meta.dart';
import 'package:ui/ui_web/src/ui_web.dart' as ui_web;

import 'dom.dart';

// iOS 15 launched WebGL 2.0, but there's something broken about it, which
// leads to apps failing to load. For now, we're forcing WebGL 1 on iOS.
//
// TODO(yjbanov): https://github.com/flutter/flutter/issues/91333
bool get _workAroundBug91333 => operatingSystem == OperatingSystem.iOs;

/// The HTML engine used by the current browser.
enum BrowserEngine {
/// The engine that powers Chrome, Samsung Internet Browser, UC Browser,
/// Microsoft Edge, Opera, and others.
///
/// Blink is assumed in case when a more precise browser engine wasn't
/// detected.
blink,

/// The engine that powers Safari.
webkit,

/// The engine that powers Firefox.
firefox,
}

/// The signature of [getUserAgent].
typedef UserAgentGetter = String Function();

/// Returns the current user agent string.
///
/// This function is read-writable, so it can be overridden in tests.
UserAgentGetter getUserAgent = defaultGetUserAgent;

/// The default implementation of [getUserAgent].
String defaultGetUserAgent() {
return domWindow.navigator.userAgent;
}

/// html webgl version qualifier constants.
abstract class WebGLVersion {
/// WebGL 1.0 is based on OpenGL ES 2.0 / GLSL 1.00
static const int webgl1 = 1;

/// WebGL 2.0 is based on OpenGL ES 3.0 / GLSL 3.00
static const int webgl2 = 2;
}

/// Lazily initialized current browser engine.
final BrowserEngine _browserEngine = _detectBrowserEngine();

/// Override the value of [browserEngine].
///
/// Setting this to `null` lets [browserEngine] detect the browser that the
/// app is running on.
///
/// This is intended to be used for testing and debugging only.
BrowserEngine? debugBrowserEngineOverride;
/// A flag to check if the current browser is running on a laptop/desktop device.
bool get isDesktop => ui_web.browser.isDesktop;

/// Returns the [BrowserEngine] used by the current browser.
///
/// This is used to implement browser-specific behavior.
BrowserEngine get browserEngine {
return debugBrowserEngineOverride ?? _browserEngine;
}

BrowserEngine _detectBrowserEngine() {
final String vendor = domWindow.navigator.vendor;
final String agent = domWindow.navigator.userAgent.toLowerCase();
return detectBrowserEngineByVendorAgent(vendor, agent);
}

/// Detects browser engine for a given vendor and agent string.
///
/// Used for testing this library.
@visibleForTesting
BrowserEngine detectBrowserEngineByVendorAgent(String vendor, String agent) {
if (vendor == 'Google Inc.') {
return BrowserEngine.blink;
} else if (vendor == 'Apple Computer, Inc.') {
return BrowserEngine.webkit;
} else if (agent.contains('Edg/')) {
// Chromium based Microsoft Edge has `Edg` in the user-agent.
// https://docs.microsoft.com/en-us/microsoft-edge/web-platform/user-agent-string
return BrowserEngine.blink;
} else if (vendor == '' && agent.contains('firefox')) {
// An empty string means firefox:
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/vendor
return BrowserEngine.firefox;
}

// Assume Blink otherwise, but issue a warning.
print(
'WARNING: failed to detect current browser engine. Assuming this is a Chromium-compatible browser.');
return BrowserEngine.blink;
}

/// Operating system where the current browser runs.
/// A flag to check if the current browser is running on a mobile device.
///
/// Taken from the navigator platform.
/// <https://developer.mozilla.org/en-US/docs/Web/API/NavigatorID/platform>
enum OperatingSystem {
/// iOS: <http://www.apple.com/ios/>
iOs,

/// Android: <https://www.android.com/>
android,
/// Flutter web considers "mobile" everything that's not [isDesktop].
bool get isMobile => ui_web.browser.isMobile;

/// Linux: <https://www.linux.org/>
linux,
/// Whether the current browser is [ui_web.BrowserEngine.blink] (Chrom(e|ium)).
bool get isChromium => ui_web.browser.isChromium;

/// Windows: <https://www.microsoft.com/windows/>
windows,
/// Whether the current browser is [ui_web.BrowserEngine.webkit] (Safari).
bool get isSafari => ui_web.browser.isSafari;

/// MacOs: <https://www.apple.com/macos/>
macOs,
/// Whether the current browser is [ui_web.BrowserEngine.firefox].
bool get isFirefox => ui_web.browser.isFirefox;

/// We were unable to detect the current operating system.
unknown,
}

/// Lazily initialized current operating system.
final OperatingSystem _operatingSystem = detectOperatingSystem();

/// Returns the [OperatingSystem] the current browsers works on.
///
/// This is used to implement operating system specific behavior such as
/// soft keyboards.
OperatingSystem get operatingSystem {
return debugOperatingSystemOverride ?? _operatingSystem;
}

/// Override the value of [operatingSystem].
///
/// Setting this to `null` lets [operatingSystem] detect the real OS that the
/// app is running on.
///
/// This is intended to be used for testing and debugging only.
OperatingSystem? debugOperatingSystemOverride;

/// Detects operating system using platform and UA used for unit testing.
@visibleForTesting
OperatingSystem detectOperatingSystem({
String? overridePlatform,
int? overrideMaxTouchPoints,
}) {
final String platform = overridePlatform ?? domWindow.navigator.platform!;
final String userAgent = getUserAgent();

if (platform.startsWith('Mac')) {
// iDevices requesting a "desktop site" spoof their UA so it looks like a Mac.
// This checks if we're in a touch device, or on a real mac.
final int maxTouchPoints = overrideMaxTouchPoints ??
domWindow.navigator.maxTouchPoints?.toInt() ??
0;
if (maxTouchPoints > 2) {
return OperatingSystem.iOs;
}
return OperatingSystem.macOs;
} else if (platform.toLowerCase().contains('iphone') ||
platform.toLowerCase().contains('ipad') ||
platform.toLowerCase().contains('ipod')) {
return OperatingSystem.iOs;
} else if (userAgent.contains('Android')) {
// The Android OS reports itself as "Linux armv8l" in
// [domWindow.navigator.platform]. So we have to check the user-agent to
// determine if the OS is Android or not.
return OperatingSystem.android;
} else if (platform.startsWith('Linux')) {
return OperatingSystem.linux;
} else if (platform.startsWith('Win')) {
return OperatingSystem.windows;
} else {
return OperatingSystem.unknown;
}
}
/// Whether the current browser is Edge.
bool get isEdge => ui_web.browser.isEdge;

/// List of Operating Systems we know to be working on laptops/desktops.
/// Whether we are running from a wasm module compiled with dart2wasm.
///
/// These devices tend to behave differently on many core issues such as events,
/// screen readers, input devices.
const Set<OperatingSystem> _desktopOperatingSystems = <OperatingSystem>{
OperatingSystem.macOs,
OperatingSystem.linux,
OperatingSystem.windows,
};
/// Note: Currently the ffi library is available from dart2wasm but not dart2js
/// or dartdevc.
bool get isWasm => ui_web.browser.isWasm;

/// A flag to check if the current operating system is a laptop/desktop
/// operating system.
///
/// See [_desktopOperatingSystems].
bool get isDesktop => _desktopOperatingSystems.contains(operatingSystem);

/// A flag to check if the current browser is running on a mobile device.
///
/// See [_desktopOperatingSystems].
/// See [isDesktop].
bool get isMobile => !isDesktop;
// Whether the detected `operatingSystem` is `OperatingSystem.iOs`.
bool get _isIOS => ui_web.browser.operatingSystem == ui_web.OperatingSystem.iOs;

/// Whether the browser is running on macOS or iOS.
///
/// - See [operatingSystem].
/// - See [OperatingSystem].
bool get isMacOrIOS =>
operatingSystem == OperatingSystem.iOs ||
operatingSystem == OperatingSystem.macOs;
bool get isMacOrIOS => _isIOS || ui_web.browser.operatingSystem == ui_web.OperatingSystem.macOs;

/// Detect iOS 15.
bool get isIOS15 {
if (debugIsIOS15 != null) {
return debugIsIOS15!;
}
return operatingSystem == OperatingSystem.iOs &&
domWindow.navigator.userAgent.contains('OS 15_');
}
bool get isIOS15 => debugIsIOS15 ?? _isIOS && ui_web.browser.userAgent.contains('OS 15_');

/// Use in tests to simulate the detection of iOS 15.
bool? debugIsIOS15;

/// Detect if running on Chrome version 110 or older.
///
Expand All @@ -234,7 +64,7 @@ bool get isChrome110OrOlder {
}
final RegExp chromeRegexp = RegExp(r'Chrom(e|ium)\/([0-9]+)\.');
final RegExpMatch? match =
chromeRegexp.firstMatch(domWindow.navigator.userAgent);
chromeRegexp.firstMatch(ui_web.browser.userAgent);
if (match != null) {
final int chromeVersion = int.parse(match.group(2)!);
return _cachedIsChrome110OrOlder = chromeVersion <= 110;
Expand All @@ -246,46 +76,38 @@ bool get isChrome110OrOlder {
// since we check this on every frame.
bool? _cachedIsChrome110OrOlder;

/// If set to true pretends that the current browser is iOS Safari.
///
/// Useful for tests. Do not use in production code.
@visibleForTesting
bool debugEmulateIosSafari = false;
/// Used in tests to simulate the detection of Chrome 110 or older on Windows.
bool? debugIsChrome110OrOlder;

/// Returns true if the browser is iOS Safari, false otherwise.
bool get isIosSafari => debugEmulateIosSafari || _isActualIosSafari;

bool get _isActualIosSafari =>
browserEngine == BrowserEngine.webkit &&
operatingSystem == OperatingSystem.iOs;
ui_web.browser.browserEngine == ui_web.BrowserEngine.webkit &&
ui_web.browser.operatingSystem == ui_web.OperatingSystem.iOs;

/// Whether the current browser is Safari.
bool get isSafari => browserEngine == BrowserEngine.webkit;

/// Whether the current browser is Firefox.
bool get isFirefox => browserEngine == BrowserEngine.firefox;

/// Whether the current browser is Edge.
bool get isEdge => domWindow.navigator.userAgent.contains('Edg/');

/// Whether we are running from a wasm module compiled with dart2wasm.
/// Note: Currently the ffi library is available from dart2wasm but not dart2js
/// or dartdevc.
bool get isWasm => const bool.fromEnvironment('dart.library.ffi');

/// Use in tests to simulate the detection of iOS 15.
bool? debugIsIOS15;
/// If set to true pretends that the current browser is iOS Safari.
///
/// Useful for tests. Do not use in production code.
@visibleForTesting
bool debugEmulateIosSafari = false;

/// Use in tests to simulated the detection of Chrome 110 or older on Windows.
bool? debugIsChrome110OrOlder;
/// html webgl version qualifier constants.
abstract class WebGLVersion {
/// WebGL 1.0 is based on OpenGL ES 2.0 / GLSL 1.00
static const int webgl1 = 1;

int? _cachedWebGLVersion;
/// WebGL 2.0 is based on OpenGL ES 3.0 / GLSL 3.00
static const int webgl2 = 2;
}

/// The highest WebGL version supported by the current browser, or -1 if WebGL
/// is not supported.
int get webGLVersion =>
_cachedWebGLVersion ?? (_cachedWebGLVersion = _detectWebGLVersion());

int? _cachedWebGLVersion;

/// Detects the highest WebGL version supported by the current browser, or
/// -1 if WebGL is not supported.
///
Expand All @@ -312,6 +134,12 @@ int _detectWebGLVersion() {
return -1;
}

// iOS 15 launched WebGL 2.0, but there's something broken about it, which
// leads to apps failing to load. For now, we're forcing WebGL 1 on iOS.
//
// TODO(yjbanov): https://github.com/flutter/flutter/issues/91333
bool get _workAroundBug91333 => _isIOS;

/// Whether the current browser supports the Chromium variant of CanvasKit.
bool get browserSupportsCanvaskitChromium =>
domIntl.v8BreakIterator != null && domIntl.Segmenter != null;

0 comments on commit b25f5d5

Please sign in to comment.