1/* 2 * Copyright (C) 2012 Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google Inc. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31/** 32 * @constructor 33 * @extends {WebInspector.Object} 34 */ 35WebInspector.OverridesSupport = function() 36{ 37 WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.MainFrameNavigated, this._deviceMetricsChanged.bind(this), this); 38 this._deviceMetricsOverrideEnabled = false; 39 this._emulateViewportEnabled = false; 40 41 WebInspector.settings.overrideUserAgent.addChangeListener(this._userAgentChanged, this); 42 WebInspector.settings.userAgent.addChangeListener(this._userAgentChanged, this); 43 44 WebInspector.settings.overrideDeviceMetrics.addChangeListener(this._deviceMetricsChanged, this); 45 WebInspector.settings.deviceMetrics.addChangeListener(this._deviceMetricsChanged, this); 46 WebInspector.settings.emulateViewport.addChangeListener(this._deviceMetricsChanged, this); 47 WebInspector.settings.deviceFitWindow.addChangeListener(this._deviceMetricsChanged, this); 48 49 WebInspector.settings.overrideGeolocation.addChangeListener(this._geolocationPositionChanged, this); 50 WebInspector.settings.geolocationOverride.addChangeListener(this._geolocationPositionChanged, this); 51 52 WebInspector.settings.overrideDeviceOrientation.addChangeListener(this._deviceOrientationChanged, this); 53 WebInspector.settings.deviceOrientationOverride.addChangeListener(this._deviceOrientationChanged, this); 54 55 WebInspector.settings.emulateTouchEvents.addChangeListener(this._emulateTouchEventsChanged, this); 56 57 WebInspector.settings.overrideCSSMedia.addChangeListener(this._cssMediaChanged, this); 58 WebInspector.settings.emulatedCSSMedia.addChangeListener(this._cssMediaChanged, this); 59} 60 61WebInspector.OverridesSupport.Events = { 62 OverridesWarningUpdated: "OverridesWarningUpdated", 63} 64 65/** 66 * @constructor 67 * @param {number} width 68 * @param {number} height 69 * @param {number} deviceScaleFactor 70 * @param {boolean} textAutosizing 71 */ 72WebInspector.OverridesSupport.DeviceMetrics = function(width, height, deviceScaleFactor, textAutosizing) 73{ 74 this.width = width; 75 this.height = height; 76 this.deviceScaleFactor = deviceScaleFactor; 77 this.textAutosizing = textAutosizing; 78} 79 80/** 81 * @return {!WebInspector.OverridesSupport.DeviceMetrics} 82 */ 83WebInspector.OverridesSupport.DeviceMetrics.parseSetting = function(value) 84{ 85 var width = 0; 86 var height = 0; 87 var deviceScaleFactor = 1; 88 var textAutosizing = true; 89 if (value) { 90 var splitMetrics = value.split("x"); 91 if (splitMetrics.length >= 3) { 92 width = parseInt(splitMetrics[0], 10); 93 height = parseInt(splitMetrics[1], 10); 94 deviceScaleFactor = parseFloat(splitMetrics[2]); 95 if (splitMetrics.length == 4) 96 textAutosizing = splitMetrics[3] == 1; 97 } 98 } 99 return new WebInspector.OverridesSupport.DeviceMetrics(width, height, deviceScaleFactor, textAutosizing); 100} 101 102/** 103 * @return {?WebInspector.OverridesSupport.DeviceMetrics} 104 */ 105WebInspector.OverridesSupport.DeviceMetrics.parseUserInput = function(widthString, heightString, deviceScaleFactorString, textAutosizing) 106{ 107 function isUserInputValid(value, isInteger) 108 { 109 if (!value) 110 return true; 111 return isInteger ? /^[0]*[1-9][\d]*$/.test(value) : /^[0]*([1-9][\d]*(\.\d+)?|\.\d+)$/.test(value); 112 } 113 114 if (!widthString ^ !heightString) 115 return null; 116 117 var isWidthValid = isUserInputValid(widthString, true); 118 var isHeightValid = isUserInputValid(heightString, true); 119 var isDeviceScaleFactorValid = isUserInputValid(deviceScaleFactorString, false); 120 121 if (!isWidthValid && !isHeightValid && !isDeviceScaleFactorValid) 122 return null; 123 124 var width = isWidthValid ? parseInt(widthString || "0", 10) : -1; 125 var height = isHeightValid ? parseInt(heightString || "0", 10) : -1; 126 var deviceScaleFactor = isDeviceScaleFactorValid ? parseFloat(deviceScaleFactorString) : -1; 127 128 return new WebInspector.OverridesSupport.DeviceMetrics(width, height, deviceScaleFactor, textAutosizing); 129} 130 131WebInspector.OverridesSupport.DeviceMetrics.prototype = { 132 /** 133 * @return {boolean} 134 */ 135 isValid: function() 136 { 137 return this.isWidthValid() && this.isHeightValid() && this.isDeviceScaleFactorValid(); 138 }, 139 140 /** 141 * @return {boolean} 142 */ 143 isWidthValid: function() 144 { 145 return this.width >= 0; 146 }, 147 148 /** 149 * @return {boolean} 150 */ 151 isHeightValid: function() 152 { 153 return this.height >= 0; 154 }, 155 156 /** 157 * @return {boolean} 158 */ 159 isDeviceScaleFactorValid: function() 160 { 161 return this.deviceScaleFactor > 0; 162 }, 163 164 /** 165 * @return {string} 166 */ 167 toSetting: function() 168 { 169 if (!this.isValid()) 170 return ""; 171 172 return this.width && this.height ? this.width + "x" + this.height + "x" + this.deviceScaleFactor + "x" + (this.textAutosizing ? "1" : "0") : ""; 173 }, 174 175 /** 176 * @return {string} 177 */ 178 widthToInput: function() 179 { 180 return this.isWidthValid() && this.width ? String(this.width) : ""; 181 }, 182 183 /** 184 * @return {string} 185 */ 186 heightToInput: function() 187 { 188 return this.isHeightValid() && this.height ? String(this.height) : ""; 189 }, 190 191 /** 192 * @return {string} 193 */ 194 deviceScaleFactorToInput: function() 195 { 196 return this.isDeviceScaleFactorValid() && this.deviceScaleFactor ? String(this.deviceScaleFactor) : ""; 197 }, 198 199 /** 200 * Compute the font scale factor. 201 * 202 * Chromium on Android uses a device scale adjustment for fonts used in text autosizing for 203 * improved legibility. This function computes this adjusted value for text autosizing. 204 * 205 * For a description of the Android device scale adjustment algorithm, see: 206 * chrome/browser/chrome_content_browser_client.cc, GetFontScaleMultiplier(...) 207 * 208 * @return {number} font scale factor. 209 */ 210 fontScaleFactor: function() 211 { 212 if (this.isValid()) { 213 var minWidth = Math.min(this.width, this.height) / this.deviceScaleFactor; 214 215 var kMinFSM = 1.05; 216 var kWidthForMinFSM = 320; 217 var kMaxFSM = 1.3; 218 var kWidthForMaxFSM = 800; 219 220 if (minWidth <= kWidthForMinFSM) 221 return kMinFSM; 222 if (minWidth >= kWidthForMaxFSM) 223 return kMaxFSM; 224 225 // The font scale multiplier varies linearly between kMinFSM and kMaxFSM. 226 var ratio = (minWidth - kWidthForMinFSM) / (kWidthForMaxFSM - kWidthForMinFSM); 227 228 return ratio * (kMaxFSM - kMinFSM) + kMinFSM; 229 } 230 231 return 1; 232 } 233} 234 235/** 236 * @constructor 237 * @param {number} latitude 238 * @param {number} longitude 239 */ 240WebInspector.OverridesSupport.GeolocationPosition = function(latitude, longitude, error) 241{ 242 this.latitude = latitude; 243 this.longitude = longitude; 244 this.error = error; 245} 246 247WebInspector.OverridesSupport.GeolocationPosition.prototype = { 248 /** 249 * @return {string} 250 */ 251 toSetting: function() 252 { 253 return (typeof this.latitude === "number" && typeof this.longitude === "number" && typeof this.error === "string") ? this.latitude + "@" + this.longitude + ":" + this.error : ""; 254 } 255} 256 257/** 258 * @return {!WebInspector.OverridesSupport.GeolocationPosition} 259 */ 260WebInspector.OverridesSupport.GeolocationPosition.parseSetting = function(value) 261{ 262 if (value) { 263 var splitError = value.split(":"); 264 if (splitError.length === 2) { 265 var splitPosition = splitError[0].split("@") 266 if (splitPosition.length === 2) 267 return new WebInspector.OverridesSupport.GeolocationPosition(parseFloat(splitPosition[0]), parseFloat(splitPosition[1]), splitError[1]); 268 } 269 } 270 return new WebInspector.OverridesSupport.GeolocationPosition(0, 0, ""); 271} 272 273/** 274 * @return {?WebInspector.OverridesSupport.GeolocationPosition} 275 */ 276WebInspector.OverridesSupport.GeolocationPosition.parseUserInput = function(latitudeString, longitudeString, errorStatus) 277{ 278 function isUserInputValid(value) 279 { 280 if (!value) 281 return true; 282 return /^[-]?[0-9]*[.]?[0-9]*$/.test(value); 283 } 284 285 if (!latitudeString ^ !latitudeString) 286 return null; 287 288 var isLatitudeValid = isUserInputValid(latitudeString); 289 var isLongitudeValid = isUserInputValid(longitudeString); 290 291 if (!isLatitudeValid && !isLongitudeValid) 292 return null; 293 294 var latitude = isLatitudeValid ? parseFloat(latitudeString) : -1; 295 var longitude = isLongitudeValid ? parseFloat(longitudeString) : -1; 296 297 return new WebInspector.OverridesSupport.GeolocationPosition(latitude, longitude, errorStatus ? "PositionUnavailable" : ""); 298} 299 300WebInspector.OverridesSupport.GeolocationPosition.clearGeolocationOverride = function() 301{ 302 PageAgent.clearGeolocationOverride(); 303} 304 305/** 306 * @constructor 307 * @param {number} alpha 308 * @param {number} beta 309 * @param {number} gamma 310 */ 311WebInspector.OverridesSupport.DeviceOrientation = function(alpha, beta, gamma) 312{ 313 this.alpha = alpha; 314 this.beta = beta; 315 this.gamma = gamma; 316} 317 318WebInspector.OverridesSupport.DeviceOrientation.prototype = { 319 /** 320 * @return {string} 321 */ 322 toSetting: function() 323 { 324 return JSON.stringify(this); 325 } 326} 327 328/** 329 * @return {!WebInspector.OverridesSupport.DeviceOrientation} 330 */ 331WebInspector.OverridesSupport.DeviceOrientation.parseSetting = function(value) 332{ 333 if (value) { 334 var jsonObject = JSON.parse(value); 335 return new WebInspector.OverridesSupport.DeviceOrientation(jsonObject.alpha, jsonObject.beta, jsonObject.gamma); 336 } 337 return new WebInspector.OverridesSupport.DeviceOrientation(0, 0, 0); 338} 339 340/** 341 * @return {?WebInspector.OverridesSupport.DeviceOrientation} 342 */ 343WebInspector.OverridesSupport.DeviceOrientation.parseUserInput = function(alphaString, betaString, gammaString) 344{ 345 function isUserInputValid(value) 346 { 347 if (!value) 348 return true; 349 return /^[-]?[0-9]*[.]?[0-9]*$/.test(value); 350 } 351 352 if (!alphaString ^ !betaString ^ !gammaString) 353 return null; 354 355 var isAlphaValid = isUserInputValid(alphaString); 356 var isBetaValid = isUserInputValid(betaString); 357 var isGammaValid = isUserInputValid(gammaString); 358 359 if (!isAlphaValid && !isBetaValid && !isGammaValid) 360 return null; 361 362 var alpha = isAlphaValid ? parseFloat(alphaString) : -1; 363 var beta = isBetaValid ? parseFloat(betaString) : -1; 364 var gamma = isGammaValid ? parseFloat(gammaString) : -1; 365 366 return new WebInspector.OverridesSupport.DeviceOrientation(alpha, beta, gamma); 367} 368 369WebInspector.OverridesSupport.DeviceOrientation.clearDeviceOrientationOverride = function() 370{ 371 PageAgent.clearDeviceOrientationOverride(); 372} 373 374WebInspector.OverridesSupport.prototype = { 375 /** 376 * @param {string} deviceMetrics 377 * @param {string} userAgent 378 */ 379 emulateDevice: function(deviceMetrics, userAgent) 380 { 381 this._deviceMetricsChangedListenerMuted = true; 382 WebInspector.settings.deviceMetrics.set(deviceMetrics); 383 WebInspector.settings.userAgent.set(userAgent); 384 WebInspector.settings.overrideDeviceMetrics.set(true); 385 WebInspector.settings.overrideUserAgent.set(true); 386 WebInspector.settings.emulateTouchEvents.set(true); 387 WebInspector.settings.emulateViewport.set(true); 388 delete this._deviceMetricsChangedListenerMuted; 389 this._deviceMetricsChanged(); 390 }, 391 392 reset: function() 393 { 394 this._deviceMetricsChangedListenerMuted = true; 395 WebInspector.settings.overrideDeviceMetrics.set(false); 396 WebInspector.settings.overrideUserAgent.set(false); 397 WebInspector.settings.emulateTouchEvents.set(false); 398 WebInspector.settings.overrideDeviceOrientation.set(false); 399 WebInspector.settings.overrideGeolocation.set(false); 400 WebInspector.settings.overrideCSSMedia.set(false); 401 WebInspector.settings.emulateViewport.set(false); 402 WebInspector.settings.deviceMetrics.set(""); 403 delete this._deviceMetricsChangedListenerMuted; 404 this._deviceMetricsChanged(); 405 }, 406 407 applyInitialOverrides: function() 408 { 409 this._deviceMetricsChangedListenerMuted = true; 410 this._userAgentChanged(); 411 this._deviceMetricsChanged(); 412 this._deviceOrientationChanged(); 413 this._geolocationPositionChanged(); 414 this._emulateTouchEventsChanged(); 415 this._cssMediaChanged(); 416 delete this._deviceMetricsChangedListenerMuted; 417 this._deviceMetricsChanged(); 418 this._revealOverridesTabIfNeeded(); 419 }, 420 421 _userAgentChanged: function() 422 { 423 if (WebInspector.isInspectingDevice()) 424 return; 425 NetworkAgent.setUserAgentOverride(WebInspector.settings.overrideUserAgent.get() ? WebInspector.settings.userAgent.get() : ""); 426 }, 427 428 _deviceMetricsChanged: function() 429 { 430 if (this._deviceMetricsChangedListenerMuted) 431 return; 432 var metrics = WebInspector.OverridesSupport.DeviceMetrics.parseSetting(WebInspector.settings.overrideDeviceMetrics.get() ? WebInspector.settings.deviceMetrics.get() : ""); 433 if (!metrics.isValid()) 434 return; 435 436 var dipWidth = Math.round(metrics.width / metrics.deviceScaleFactor); 437 var dipHeight = Math.round(metrics.height / metrics.deviceScaleFactor); 438 439 // Disable override without checks. 440 if (dipWidth && dipHeight && WebInspector.isInspectingDevice()) { 441 this._updateWarningMessage(WebInspector.UIString("Screen emulation on the device is not available.")); 442 return; 443 } 444 445 PageAgent.setDeviceMetricsOverride(dipWidth, dipHeight, metrics.deviceScaleFactor, WebInspector.settings.emulateViewport.get(), WebInspector.settings.deviceFitWindow.get(), metrics.textAutosizing, metrics.fontScaleFactor(), apiCallback.bind(this)); 446 447 /** 448 * @param {?Protocol.Error} error 449 * @this {WebInspector.OverridesSupport} 450 */ 451 function apiCallback(error) 452 { 453 if (error) { 454 this._updateWarningMessage(WebInspector.UIString("Screen emulation is not available on this page.")); 455 return; 456 } 457 458 var metricsOverrideEnabled = !!(dipWidth && dipHeight); 459 var viewportEnabled = WebInspector.settings.emulateViewport.get(); 460 this._updateWarningMessage(this._deviceMetricsOverrideEnabled !== metricsOverrideEnabled || (metricsOverrideEnabled && this._emulateViewportEnabled != viewportEnabled) ? 461 WebInspector.UIString("You might need to reload the page for proper user agent spoofing and viewport rendering.") : ""); 462 this._deviceMetricsOverrideEnabled = metricsOverrideEnabled; 463 this._emulateViewportEnabled = viewportEnabled; 464 } 465 }, 466 467 _geolocationPositionChanged: function() 468 { 469 if (!WebInspector.settings.overrideGeolocation.get()) { 470 PageAgent.clearGeolocationOverride(); 471 return; 472 } 473 var geolocation = WebInspector.OverridesSupport.GeolocationPosition.parseSetting(WebInspector.settings.geolocationOverride.get()); 474 if (geolocation.error) 475 PageAgent.setGeolocationOverride(); 476 else 477 PageAgent.setGeolocationOverride(geolocation.latitude, geolocation.longitude, 150); 478 }, 479 480 _deviceOrientationChanged: function() 481 { 482 if (!WebInspector.settings.overrideDeviceOrientation.get()) { 483 PageAgent.clearDeviceOrientationOverride(); 484 return; 485 } 486 if (WebInspector.isInspectingDevice()) 487 return; 488 489 var deviceOrientation = WebInspector.OverridesSupport.DeviceOrientation.parseSetting(WebInspector.settings.deviceOrientationOverride.get()); 490 PageAgent.setDeviceOrientationOverride(deviceOrientation.alpha, deviceOrientation.beta, deviceOrientation.gamma); 491 }, 492 493 _emulateTouchEventsChanged: function() 494 { 495 if (WebInspector.isInspectingDevice() && WebInspector.settings.emulateTouchEvents.get()) 496 return; 497 498 WebInspector.domAgent.emulateTouchEventObjects(WebInspector.settings.emulateTouchEvents.get()); 499 }, 500 501 _cssMediaChanged: function() 502 { 503 PageAgent.setEmulatedMedia(WebInspector.settings.overrideCSSMedia.get() ? WebInspector.settings.emulatedCSSMedia.get() : ""); 504 WebInspector.cssModel.mediaQueryResultChanged(); 505 }, 506 507 _anyOverrideIsEnabled: function() 508 { 509 return WebInspector.settings.overrideUserAgent.get() || WebInspector.settings.overrideDeviceMetrics.get() || 510 WebInspector.settings.overrideGeolocation.get() || WebInspector.settings.overrideDeviceOrientation.get() || 511 WebInspector.settings.emulateTouchEvents.get() || WebInspector.settings.overrideCSSMedia.get(); 512 }, 513 514 _revealOverridesTabIfNeeded: function() 515 { 516 if (this._anyOverrideIsEnabled()) { 517 if (!WebInspector.settings.showEmulationViewInDrawer.get()) 518 WebInspector.settings.showEmulationViewInDrawer.set(true); 519 WebInspector.inspectorView.showViewInDrawer("emulation"); 520 } 521 }, 522 523 /** 524 * @param {string} warningMessage 525 */ 526 _updateWarningMessage: function(warningMessage) 527 { 528 this._warningMessage = warningMessage; 529 this.dispatchEventToListeners(WebInspector.OverridesSupport.Events.OverridesWarningUpdated); 530 }, 531 532 /** 533 * @return {string} 534 */ 535 warningMessage: function() 536 { 537 return this._warningMessage || ""; 538 }, 539 540 __proto__: WebInspector.Object.prototype 541} 542 543 544/** 545 * @type {!WebInspector.OverridesSupport} 546 */ 547WebInspector.overridesSupport; 548