| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'package:meta/meta.dart'; |
| |
| 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, |
| |
| /// The engine that powers Safari. |
| webkit, |
| |
| /// The engine that powers Firefox. |
| firefox, |
| |
| /// The engine that powers Edge. |
| edge, |
| |
| /// The engine that powers Internet Explorer 11. |
| ie11, |
| |
| /// The engine that powers Samsung stock browser. It is based on blink. |
| samsung, |
| |
| /// We were unable to detect the current browser engine. |
| unknown, |
| } |
| |
| /// 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. |
| late 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; |
| |
| /// 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 samsung blink variants. |
| /// |
| /// Example patterns: |
| /// Note 2 : GT-N7100 |
| /// Note 3 : SM-N900T |
| /// Tab 4 : SM-T330NU |
| /// Galaxy S4: SHV-E330S |
| /// Galaxy Note2: SHV-E250L |
| /// Note: SAMSUNG-SGH-I717 |
| /// SPH/SCH are very old Palm models. |
| bool _isSamsungBrowser(String agent) { |
| final RegExp exp = RegExp( |
| r'SAMSUNG|SGH-[I|N|T]|GT-[I|N]|SM-[A|N|P|T|Z]|SHV-E|SCH-[I|J|R|S]|SPH-L'); |
| return exp.hasMatch(agent.toUpperCase()); |
| } |
| |
| /// 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.') { |
| // Samsung browser is based on blink, check for variant. |
| if (_isSamsungBrowser(agent)) { |
| return BrowserEngine.samsung; |
| } |
| return BrowserEngine.blink; |
| } else if (vendor == 'Apple Computer, Inc.') { |
| return BrowserEngine.webkit; |
| } else if (agent.contains('edge/')) { |
| return BrowserEngine.edge; |
| } 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 (agent.contains('trident/7.0')) { |
| return BrowserEngine.ie11; |
| } 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 unknown otherwise, but issue a warning. |
| print('WARNING: failed to detect current browser engine.'); |
| return BrowserEngine.unknown; |
| } |
| |
| /// Operating system where the current browser runs. |
| /// |
| /// 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, |
| |
| /// Linux: <https://www.linux.org/> |
| linux, |
| |
| /// Windows: <https://www.microsoft.com/windows/> |
| windows, |
| |
| /// MacOs: <https://www.apple.com/macos/> |
| macOs, |
| |
| /// We were unable to detect the current operating system. |
| unknown, |
| } |
| |
| /// Lazily initialized current operating system. |
| late 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, |
| String? overrideUserAgent, |
| int? overrideMaxTouchPoints, |
| }) { |
| final String platform = overridePlatform ?? domWindow.navigator.platform!; |
| final String userAgent = overrideUserAgent ?? domWindow.navigator.userAgent; |
| |
| 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 ?? 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; |
| } |
| } |
| |
| /// List of Operating Systems we know to be working on laptops/desktops. |
| /// |
| /// 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, |
| }; |
| |
| /// 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 browser is running on macOS or iOS. |
| /// |
| /// - See [operatingSystem]. |
| /// - See [OperatingSystem]. |
| bool get isMacOrIOS => |
| operatingSystem == OperatingSystem.iOs || |
| operatingSystem == OperatingSystem.macOs; |
| |
| /// Detect iOS 15. |
| bool get isIOS15 { |
| if (debugIsIOS15 != null) { |
| return debugIsIOS15!; |
| } |
| return operatingSystem == OperatingSystem.iOs && |
| domWindow.navigator.userAgent.contains('OS 15_'); |
| } |
| |
| /// Returns true if the browser is iOS Safari, false otherwise. |
| bool get isIosSafari => |
| browserEngine == BrowserEngine.webkit && |
| operatingSystem == 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; |
| |
| /// Use in tests to simulate the detection of iOS 15. |
| bool? debugIsIOS15; |
| |
| int? _cachedWebGLVersion; |
| |
| /// The highest WebGL version supported by the current browser, or -1 if WebGL |
| /// is not supported. |
| int get webGLVersion => |
| _cachedWebGLVersion ?? (_cachedWebGLVersion = _detectWebGLVersion()); |
| |
| /// Detects the highest WebGL version supported by the current browser, or |
| /// -1 if WebGL is not supported. |
| /// |
| /// Chrome reports that `WebGL2RenderingContext` is available even when WebGL 2 is |
| /// disabled due hardware-specific issues. This happens, for example, on Chrome on |
| /// Moto E5. Therefore checking for the presence of `WebGL2RenderingContext` or |
| /// using the current [browserEngine] is insufficient. |
| /// |
| /// Our CanvasKit backend is affected due to: https://github.com/emscripten-core/emscripten/issues/11819 |
| int _detectWebGLVersion() { |
| final DomCanvasElement canvas = createDomCanvasElement( |
| width: 1, |
| height: 1, |
| ); |
| if (canvas.getContext('webgl2') != null) { |
| if (_workAroundBug91333) { |
| return WebGLVersion.webgl1; |
| } |
| return WebGLVersion.webgl2; |
| } |
| if (canvas.getContext('webgl') != null) { |
| return WebGLVersion.webgl1; |
| } |
| return -1; |
| } |