Bug 1357774 - Part 2: Add the ability to update orientation state of a simulated device in RDM r=gl
authorMicah Tigley <mtigley@mozilla.com>
Wed, 22 May 2019 20:07:13 +0000
changeset 475026 b638269d62f17d7a33f6d5194fcfe3a2d6fb13d9
parent 475025 a0fa1beba2b3a0d30a5b1c8077462a72d5995cfd
child 475027 6efc35cfb29a053c8a527d5a88dde4908812f40e
push id86123
push usermtigley@mozilla.com
push dateWed, 22 May 2019 21:50:42 +0000
treeherderautoland@b638269d62f1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgl
bugs1357774
milestone69.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1357774 - Part 2: Add the ability to update orientation state of a simulated device in RDM r=gl This patch gives the RDM UI the ability to update the screen orientation based on the orientation of the simulated device screen. It fixes the following issues: - Initializing the orientation state of the selected device when RDM is opened. - Updating orientation state when the rotate button in the RDM toolbar is pressed. - Updating the orientation state when a new device is selected. There are three actions creators that are responsible for notifying the ResponsiveUI manager, `changeDevice`, `restoreDeviceState`, and `rotateViewport`. In particular: - `restoreDeviceState` is dispatched when the Responsive UI has finished initializing. If a previous RDM session had a device selected, then this action creator will also dispatch the `changeDevice` action to update the RDM UI to reflect the currently selected device. - `changeDevice` is dispatched when a device is selected. - `rotateViewport` is dispatched when the rotate button is clicked in the RDM toolbar. When either of these actions is dispatched, we post a "viewport-orientation-change" message to the window that notifies the manager to update the screen orientation accordingly. Finally, when RDM is closed, we need to ensure the original physical screen orientation is restored. We do this by calling the `setRDMPaneOrientation` on the docShell's document in the content frame script. Differential Revision: https://phabricator.services.mozilla.com/D30440
devtools/client/preferences/devtools-client.js
devtools/client/responsive.html/actions/devices.js
devtools/client/responsive.html/actions/index.js
devtools/client/responsive.html/actions/viewports.js
devtools/client/responsive.html/browser/content.js
devtools/client/responsive.html/components/App.js
devtools/client/responsive.html/components/Browser.js
devtools/client/responsive.html/components/ResizableViewport.js
devtools/client/responsive.html/components/Viewports.js
devtools/client/responsive.html/constants.js
devtools/client/responsive.html/manager.js
devtools/client/responsive.html/reducers/viewports.js
devtools/client/responsive.html/test/browser/browser_orientationchange_event.js
devtools/client/responsive.html/utils/moz.build
devtools/client/responsive.html/utils/orientation.js
devtools/server/actors/emulation.js
devtools/shared/specs/emulation.js
--- a/devtools/client/preferences/devtools-client.js
+++ b/devtools/client/preferences/devtools-client.js
@@ -307,16 +307,18 @@ pref("devtools.hud.loglimit", 10000);
 pref("devtools.editor.tabsize", 2);
 pref("devtools.editor.expandtab", true);
 pref("devtools.editor.keymap", "default");
 pref("devtools.editor.autoclosebrackets", true);
 pref("devtools.editor.detectindentation", true);
 pref("devtools.editor.enableCodeFolding", true);
 pref("devtools.editor.autocomplete", true);
 
+// The angle of the viewport.
+pref("devtools.responsive.viewport.angle", 0);
 // The width of the viewport.
 pref("devtools.responsive.viewport.width", 320);
 // The height of the viewport.
 pref("devtools.responsive.viewport.height", 480);
 // The pixel ratio of the viewport.
 pref("devtools.responsive.viewport.pixelRatio", 0);
 // Whether or not the viewports are left aligned.
 pref("devtools.responsive.leftAlignViewport.enabled", false);
--- a/devtools/client/responsive.html/actions/devices.js
+++ b/devtools/client/responsive.html/actions/devices.js
@@ -17,17 +17,17 @@ const {
   REMOVE_DEVICE,
   UPDATE_DEVICE_DISPLAYED,
   UPDATE_DEVICE_MODAL,
 } = require("./index");
 const { post } = require("../utils/message");
 
 const { addDevice, editDevice, getDevices, removeDevice } = require("devtools/client/shared/devices");
 const { changeUserAgent, toggleTouchSimulation } = require("./ui");
-const { changeDevice, changePixelRatio } = require("./viewports");
+const { changeDevice, changePixelRatio, changeViewportAngle } = require("./viewports");
 
 const DISPLAYED_DEVICES_PREF = "devtools.responsive.html.displayedDeviceList";
 
 /**
  * Returns an object containing the user preference of displayed devices.
  *
  * @return {Object} containing two Sets:
  * - added: Names of the devices that were explicitly enabled by the user
@@ -108,16 +108,17 @@ module.exports = {
   editCustomDevice(viewport, oldDevice, newDevice) {
     return async function(dispatch) {
       // Edit custom device in storage
       await editDevice(oldDevice, newDevice, "custom");
       // Notify the window that the device should be updated in the device selector.
       post(window, {
         type: "change-device",
         device: newDevice,
+        viewport,
       });
 
       // Update UI if the device is selected.
       if (viewport) {
         dispatch(changeUserAgent(newDevice.userAgent));
         dispatch(toggleTouchSimulation(newDevice.touch));
       }
 
@@ -207,23 +208,26 @@ module.exports = {
       }
 
       const device = devices[deviceType].find(d => d.name === deviceName);
       if (!device) {
         // Can't find device with the same device name.
         return;
       }
 
+      const viewport = getState().viewports[0];
       post(window, {
         type: "change-device",
         device,
+        viewport,
       });
 
       dispatch(changeDevice(id, device.name, deviceType));
       dispatch(changePixelRatio(id, device.pixelRatio));
+      dispatch(changeViewportAngle(id, viewport.angle));
       dispatch(changeUserAgent(device.userAgent));
       dispatch(toggleTouchSimulation(device.touch));
     };
   },
 
   updateDeviceModal(isOpen, modalOpenedFromViewport = null) {
     return {
       type: UPDATE_DEVICE_MODAL,
--- a/devtools/client/responsive.html/actions/index.js
+++ b/devtools/client/responsive.html/actions/index.js
@@ -43,16 +43,19 @@ createEnum([
   // Change the user agent of the viewport.
   "CHANGE_USER_AGENT",
 
   // The pixel ratio of the viewport has changed. This may be triggered by the user
   // when changing the device displayed in the viewport, or when a pixel ratio is
   // selected from the device pixel ratio dropdown.
   "CHANGE_PIXEL_RATIO",
 
+  // Change the viewport angle.
+  "CHANGE_VIEWPORT_ANGLE",
+
   // Edit a device.
   "EDIT_DEVICE",
 
   // Indicates that the device list is being loaded.
   "LOAD_DEVICE_LIST_START",
 
   // Indicates that the device list loading action threw an error.
   "LOAD_DEVICE_LIST_ERROR",
--- a/devtools/client/responsive.html/actions/viewports.js
+++ b/devtools/client/responsive.html/actions/viewports.js
@@ -7,16 +7,17 @@
 "use strict";
 
 const asyncStorage = require("devtools/shared/async-storage");
 
 const {
   ADD_VIEWPORT,
   CHANGE_DEVICE,
   CHANGE_PIXEL_RATIO,
+  CHANGE_VIEWPORT_ANGLE,
   REMOVE_DEVICE_ASSOCIATION,
   RESIZE_VIEWPORT,
   ROTATE_VIEWPORT,
 } = require("./index");
 
 const { post } = require("../utils/message");
 
 module.exports = {
@@ -58,16 +59,24 @@ module.exports = {
   changePixelRatio(id, pixelRatio = 0) {
     return {
       type: CHANGE_PIXEL_RATIO,
       id,
       pixelRatio,
     };
   },
 
+  changeViewportAngle(id, angle) {
+    return {
+      type: CHANGE_VIEWPORT_ANGLE,
+      id,
+      angle,
+    };
+  },
+
   /**
    * Remove the viewport's device assocation.
    */
   removeDeviceAssociation(id) {
     return async function(dispatch) {
       post(window, "remove-device-association");
 
       dispatch({
@@ -90,27 +99,15 @@ module.exports = {
       height,
     };
   },
 
   /**
    * Rotate the viewport.
    */
   rotateViewport(id) {
-    // TODO: Add `orientation` and `angle` properties to message data. See Bug 1357774.
-
-    // There is no window object to post to when ran on XPCShell as part of the unit
-    // tests and will immediately throw an error as a result.
-    try {
-      post(window, {
-        type: "viewport-orientation-change",
-      });
-    } catch (e) {
-      console.log("Unable to post message to window");
-    }
-
     return {
       type: ROTATE_VIEWPORT,
       id,
     };
   },
 
 };
--- a/devtools/client/responsive.html/browser/content.js
+++ b/devtools/client/responsive.html/browser/content.js
@@ -116,16 +116,21 @@ var global = this;
       return;
     }
     active = false;
     removeMessageListener("ResponsiveMode:RequestScreenshot", screenshot);
     const webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
                               .getInterface(Ci.nsIWebProgress);
     webProgress.removeProgressListener(WebProgressListener);
     docShell.deviceSizeIsPageSize = gDeviceSizeWasPageSize;
+    // Restore the original physical screen orientation values before RDM is stopped.
+    // This is necessary since the window document's `setCurrentRDMPaneOrientation`
+    // WebIDL operation can only modify the window's screen orientation values while the
+    // window content is in RDM.
+    restoreScreenOrientation();
     restoreScrollbars();
     setDocumentInRDMPane(false);
     stopOnResize();
     sendAsyncMessage("ResponsiveMode:Stop:Done");
   }
 
   function makeScrollbarsFloating() {
     if (!requiresFloatingScrollbars) {
@@ -160,16 +165,20 @@ var global = this;
       const winUtils = win.windowUtils;
       try {
         winUtils.removeSheet(gFloatingScrollbarsStylesheet, win.AGENT_SHEET);
       } catch (e) { }
     }
     flushStyle();
   }
 
+  function restoreScreenOrientation() {
+    docShell.contentViewer.DOMDocument.setRDMPaneOrientation("landscape-primary", 0);
+  }
+
   function setDocumentInRDMPane(inRDMPane) {
     // We don't propegate this property to descendent documents.
     docShell.contentViewer.DOMDocument.inRDMPane = inRDMPane;
   }
 
   function flushStyle() {
     // Force presContext destruction
     const isSticky = docShell.contentViewer.sticky;
@@ -194,16 +203,23 @@ var global = this;
   }
 
   const WebProgressListener = {
     onLocationChange(webProgress, request, URI, flags) {
       if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
         return;
       }
       setDocumentInRDMPane(true);
+      // Notify the Responsive UI manager to set orientation state on a location change.
+      // This is necessary since we want to ensure that the RDM Document's orientation
+      // state persists throughout while RDM is opened.
+      sendAsyncMessage("ResponsiveMode:OnLocationChange", {
+        width: content.innerWidth,
+        height: content.innerHeight,
+      });
       makeScrollbarsFloating();
     },
     QueryInterface: ChromeUtils.generateQI(["nsIWebProgressListener",
                                             "nsISupportsWeakReference"]),
   };
 })();
 
 global.responsiveFrameScriptLoaded = true;
--- a/devtools/client/responsive.html/components/App.js
+++ b/devtools/client/responsive.html/components/App.js
@@ -1,16 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* eslint-env browser */
 
 "use strict";
 
+const Services = require("Services");
 const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 
 const Toolbar = createFactory(require("./Toolbar"));
 const Viewports = createFactory(require("./Viewports"));
 
@@ -33,20 +34,22 @@ const {
   toggleReloadOnTouchSimulation,
   toggleReloadOnUserAgent,
   toggleTouchSimulation,
   toggleUserAgentInput,
 } = require("../actions/ui");
 const {
   changeDevice,
   changePixelRatio,
+  changeViewportAngle,
   removeDeviceAssociation,
   resizeViewport,
   rotateViewport,
 } = require("../actions/viewports");
+const { getOrientation } = require("../utils/orientation");
 
 const Types = require("../types");
 
 class App extends PureComponent {
   static get propTypes() {
     return {
       devices: PropTypes.shape(Types.devices).isRequired,
       dispatch: PropTypes.func.isRequired,
@@ -62,16 +65,17 @@ class App extends PureComponent {
     this.onAddCustomDevice = this.onAddCustomDevice.bind(this);
     this.onBrowserContextMenu = this.onBrowserContextMenu.bind(this);
     this.onBrowserMounted = this.onBrowserMounted.bind(this);
     this.onChangeDevice = this.onChangeDevice.bind(this);
     this.onChangeNetworkThrottling = this.onChangeNetworkThrottling.bind(this);
     this.onChangePixelRatio = this.onChangePixelRatio.bind(this);
     this.onChangeTouchSimulation = this.onChangeTouchSimulation.bind(this);
     this.onChangeUserAgent = this.onChangeUserAgent.bind(this);
+    this.onChangeViewportOrientation = this.onChangeViewportOrientation.bind(this);
     this.onContentResize = this.onContentResize.bind(this);
     this.onDeviceListUpdate = this.onDeviceListUpdate.bind(this);
     this.onEditCustomDevice = this.onEditCustomDevice.bind(this);
     this.onExit = this.onExit.bind(this);
     this.onRemoveCustomDevice = this.onRemoveCustomDevice.bind(this);
     this.onRemoveDeviceAssociation = this.onRemoveDeviceAssociation.bind(this);
     this.doResizeViewport = this.doResizeViewport.bind(this);
     this.onResizeViewport = this.onResizeViewport.bind(this);
@@ -109,17 +113,22 @@ class App extends PureComponent {
 
   onChangeDevice(id, device, deviceType) {
     // TODO: Bug 1332754: Move messaging and logic into the action creator so that the
     // message is sent from the action creator and device property changes are sent from
     // there instead of this function.
     window.postMessage({
       type: "change-device",
       device,
+      viewport: this.props.viewports[id],
     }, "*");
+
+    const orientation = getOrientation(device, this.props.viewports[0]);
+
+    this.props.dispatch(changeViewportAngle(0, orientation.angle));
     this.props.dispatch(changeDevice(id, device.name, deviceType));
     this.props.dispatch(changePixelRatio(id, device.pixelRatio));
     this.props.dispatch(changeUserAgent(device.userAgent));
     this.props.dispatch(toggleTouchSimulation(device.touch));
   }
 
   onChangeNetworkThrottling(enabled, profile) {
     window.postMessage({
@@ -149,16 +158,25 @@ class App extends PureComponent {
   onChangeUserAgent(userAgent) {
     window.postMessage({
       type: "change-user-agent",
       userAgent,
     }, "*");
     this.props.dispatch(changeUserAgent(userAgent));
   }
 
+  onChangeViewportOrientation(id, { type, angle }) {
+    window.postMessage({
+      type: "viewport-orientation-change",
+      orientationType: type,
+      angle,
+    }, "*");
+    this.props.dispatch(changeViewportAngle(id, angle));
+  }
+
   onContentResize({ width, height }) {
     window.postMessage({
       type: "content-resize",
       width,
       height,
     }, "*");
   }
 
@@ -223,17 +241,54 @@ class App extends PureComponent {
     // and sends out a "viewport-resize" message with the new size.
     window.postMessage({
       type: "viewport-resize",
       width,
       height,
     }, "*");
   }
 
+  /**
+   * Dispatches the rotateViewport action creator. This utilized by the RDM toolbar as
+   * a prop.
+   *
+   * @param {Number} id
+   *        The viewport ID.
+   */
   onRotateViewport(id) {
+    let currentDevice;
+    const viewport = this.props.viewports[id];
+
+    for (const type of this.props.devices.types) {
+      for (const device of this.props.devices[type]) {
+        if (viewport.device === device.name) {
+          currentDevice = device;
+        }
+      }
+    }
+
+    // If no device is selected, then assume the selected device's primary orientation is
+    // opposite of the viewport orientation.
+    if (!currentDevice) {
+      currentDevice = {
+        height: viewport.width,
+        width: viewport.height,
+      };
+    }
+
+    const currentAngle = Services.prefs.getIntPref("devtools.responsive.viewport.angle");
+    // TODO: For Firefox Android, when the device is rotated clock-wise, the angle is
+    // updated to 270 degrees. This is valid since the user-agent determines whether it
+    // will be set to 90 or 270. However, we should update the angle based on the how the
+    // user-agent assigns the angle value.
+    // See https://w3c.github.io/screen-orientation/#dfn-screen-orientation-values-table
+    const angleToRotateTo = currentAngle === 270 ? 0 : 270;
+    const orientation = getOrientation(currentDevice, viewport, angleToRotateTo);
+
+    this.onChangeViewportOrientation(id, orientation);
     this.props.dispatch(rotateViewport(id));
   }
 
   onScreenshot() {
     this.props.dispatch(takeScreenshot());
   }
 
   onToggleLeftAlignment() {
@@ -271,16 +326,17 @@ class App extends PureComponent {
     const {
       onAddCustomDevice,
       onBrowserMounted,
       onChangeDevice,
       onChangeNetworkThrottling,
       onChangePixelRatio,
       onChangeTouchSimulation,
       onChangeUserAgent,
+      onChangeViewportOrientation,
       onContentResize,
       onDeviceListUpdate,
       onEditCustomDevice,
       onExit,
       onRemoveCustomDevice,
       onRemoveDeviceAssociation,
       doResizeViewport,
       onResizeViewport,
@@ -330,16 +386,17 @@ class App extends PureComponent {
           onToggleReloadOnUserAgent,
           onToggleUserAgentInput,
           onUpdateDeviceModal,
         }),
         Viewports({
           screenshot,
           viewports,
           onBrowserMounted,
+          onChangeViewportOrientation,
           onContentResize,
           onRemoveDeviceAssociation,
           doResizeViewport,
           onResizeViewport,
         }),
         devices.isModalOpen ?
           DeviceModal({
             deviceAdderViewportTemplate,
--- a/devtools/client/responsive.html/components/Browser.js
+++ b/devtools/client/responsive.html/components/Browser.js
@@ -7,42 +7,46 @@
 "use strict";
 
 const Services = require("Services");
 const flags = require("devtools/shared/flags");
 const { PureComponent } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
+const { PORTRAIT_PRIMARY, LANDSCAPE_PRIMARY } = require("../constants");
 const e10s = require("../utils/e10s");
 const message = require("../utils/message");
 const { getTopLevelWindow } = require("../utils/window");
 
 const FRAME_SCRIPT = "resource://devtools/client/responsive.html/browser/content.js";
 
 class Browser extends PureComponent {
   /**
    * This component is not allowed to depend directly on frequently changing data (width,
    * height). Any changes in props would cause the <iframe> to be removed and added again,
    * throwing away the current state of the page.
    */
   static get propTypes() {
     return {
       onBrowserMounted: PropTypes.func.isRequired,
+      onChangeViewportOrientation: PropTypes.func.isRequired,
       onContentResize: PropTypes.func.isRequired,
       onResizeViewport: PropTypes.func.isRequired,
       swapAfterMount: PropTypes.bool.isRequired,
       userContextId: PropTypes.number.isRequired,
+      viewportId: PropTypes.number.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
     this.onContentResize = this.onContentResize.bind(this);
     this.onResizeViewport = this.onResizeViewport.bind(this);
+    this.onSetScreenOrientation = this.onSetScreenOrientation.bind(this);
   }
 
   /**
    * Before the browser is mounted, listen for `remote-browser-shown` so that we know when
    * the browser is fully ready.  Without waiting for an event such as this, we don't know
    * whether all frame state for the browser is fully initialized (since some happens
    * async after the element is added), and swapping browsers can fail if this state is
    * not ready.
@@ -103,30 +107,40 @@ class Browser extends PureComponent {
     const { onResizeViewport } = this.props;
     const { width, height } = msg.data;
     onResizeViewport({
       width,
       height,
     });
   }
 
+  onSetScreenOrientation(msg) {
+    const { width, height } = msg.data;
+    const angle = Services.prefs.getIntPref("devtools.responsive.viewport.angle");
+    const type = height >= width ? PORTRAIT_PRIMARY : LANDSCAPE_PRIMARY;
+
+    this.props.onChangeViewportOrientation(this.props.viewportId, { type, angle });
+  }
+
   async startFrameScript() {
     const {
       browser,
       onContentResize,
       onResizeViewport,
+      onSetScreenOrientation,
     } = this;
     const mm = browser.frameLoader.messageManager;
 
     // Notify tests when the content has received a resize event.  This is not
     // quite the same timing as when we _set_ a new size around the browser,
     // since it still needs to do async work before the content is actually
     // resized to match.
     e10s.on(mm, "OnContentResize", onContentResize);
     e10s.on(mm, "OnResizeViewport", onResizeViewport);
+    e10s.on(mm, "OnLocationChange", onSetScreenOrientation);
 
     const ready = e10s.once(mm, "ChildScriptReady");
     mm.loadFrameScript(FRAME_SCRIPT, true);
     await ready;
 
     const browserWindow = getTopLevelWindow(window);
     const requiresFloatingScrollbars =
       !browserWindow.matchMedia("(-moz-overlay-scrollbars)").matches;
@@ -138,21 +152,23 @@ class Browser extends PureComponent {
     });
   }
 
   async stopFrameScript() {
     const {
       browser,
       onContentResize,
       onResizeViewport,
+      onSetScreenOrientation,
     } = this;
     const mm = browser.frameLoader.messageManager;
 
     e10s.off(mm, "OnContentResize", onContentResize);
     e10s.off(mm, "OnResizeViewport", onResizeViewport);
+    e10s.off(mm, "OnLocationChange", onSetScreenOrientation);
     await e10s.request(mm, "Stop");
     message.post(window, "stop-frame-script:done");
   }
 
   render() {
     const {
       userContextId,
     } = this.props;
--- a/devtools/client/responsive.html/components/ResizableViewport.js
+++ b/devtools/client/responsive.html/components/ResizableViewport.js
@@ -18,16 +18,17 @@ const Types = require("../types");
 const VIEWPORT_MIN_WIDTH = Constants.MIN_VIEWPORT_DIMENSION;
 const VIEWPORT_MIN_HEIGHT = Constants.MIN_VIEWPORT_DIMENSION;
 
 class ResizableViewport extends PureComponent {
   static get propTypes() {
     return {
       leftAlignmentEnabled: PropTypes.bool.isRequired,
       onBrowserMounted: PropTypes.func.isRequired,
+      onChangeViewportOrientation: PropTypes.func.isRequired,
       onContentResize: PropTypes.func.isRequired,
       onRemoveDeviceAssociation: PropTypes.func.isRequired,
       doResizeViewport: PropTypes.func.isRequired,
       onResizeViewport: PropTypes.func.isRequired,
       screenshot: PropTypes.shape(Types.screenshot).isRequired,
       swapAfterMount: PropTypes.bool.isRequired,
       viewport: PropTypes.shape(Types.viewport).isRequired,
     };
@@ -145,16 +146,17 @@ class ResizableViewport extends PureComp
   }
 
   render() {
     const {
       screenshot,
       swapAfterMount,
       viewport,
       onBrowserMounted,
+      onChangeViewportOrientation,
       onContentResize,
       onResizeViewport,
     } = this.props;
 
     let resizeHandleClass = "viewport-resize-handle";
     if (screenshot.isCapturing) {
       resizeHandleClass += " hidden";
     }
@@ -173,17 +175,19 @@ class ResizableViewport extends PureComp
               style: {
                 width: viewport.width + "px",
                 height: viewport.height + "px",
               },
             },
             Browser({
               swapAfterMount,
               userContextId: viewport.userContextId,
+              viewportId: viewport.id,
               onBrowserMounted,
+              onChangeViewportOrientation,
               onContentResize,
               onResizeViewport,
             })
           ),
           dom.div({
             className: resizeHandleClass,
             onMouseDown: this.onResizeStart,
           }),
--- a/devtools/client/responsive.html/components/Viewports.js
+++ b/devtools/client/responsive.html/components/Viewports.js
@@ -13,29 +13,31 @@ const ResizableViewport = createFactory(
 
 const Types = require("../types");
 
 class Viewports extends PureComponent {
   static get propTypes() {
     return {
       leftAlignmentEnabled: PropTypes.bool.isRequired,
       onBrowserMounted: PropTypes.func.isRequired,
+      onChangeViewportOrientation: PropTypes.func.isRequired,
       onContentResize: PropTypes.func.isRequired,
       onRemoveDeviceAssociation: PropTypes.func.isRequired,
       doResizeViewport: PropTypes.func.isRequired,
       onResizeViewport: PropTypes.func.isRequired,
       screenshot: PropTypes.shape(Types.screenshot).isRequired,
       viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
     };
   }
 
   render() {
     const {
       leftAlignmentEnabled,
       onBrowserMounted,
+      onChangeViewportOrientation,
       onContentResize,
       onRemoveDeviceAssociation,
       doResizeViewport,
       onResizeViewport,
       screenshot,
       viewports,
     } = this.props;
 
@@ -66,16 +68,17 @@ class Viewports extends PureComponent {
             id: "viewports",
             className: leftAlignmentEnabled ? "left-aligned" : "",
           },
           viewports.map((viewport, i) => {
             return ResizableViewport({
               key: viewport.id,
               leftAlignmentEnabled,
               onBrowserMounted,
+              onChangeViewportOrientation,
               onContentResize,
               onRemoveDeviceAssociation,
               doResizeViewport,
               onResizeViewport,
               screenshot,
               swapAfterMount: i == 0,
               viewport,
             });
--- a/devtools/client/responsive.html/constants.js
+++ b/devtools/client/responsive.html/constants.js
@@ -1,8 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 // The minimum viewport width and height
 exports.MIN_VIEWPORT_DIMENSION = 50;
+
+// Orientation types
+exports.PORTRAIT_PRIMARY = "portrait-primary";
+exports.PORTRAIT_SECONDARY = "portrait-secondary";
+exports.LANDSCAPE_PRIMARY = "landscape-primary";
+exports.LANDSCAPE_SECONDARY = "landscape-secondary";
--- a/devtools/client/responsive.html/manager.js
+++ b/devtools/client/responsive.html/manager.js
@@ -3,16 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { Ci } = require("chrome");
 const promise = require("promise");
 const Services = require("Services");
 const EventEmitter = require("devtools/shared/event-emitter");
+const { getOrientation } = require("./utils/orientation");
 
 loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/debugger-client", true);
 loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
 loader.lazyRequireGetter(this, "throttlingProfiles", "devtools/client/shared/components/throttling/profiles");
 loader.lazyRequireGetter(this, "SettingOnboardingTooltip", "devtools/client/responsive.html/setting-onboarding-tooltip");
 loader.lazyRequireGetter(this, "swapToInnerBrowser", "devtools/client/responsive.html/browser/swap", true);
 loader.lazyRequireGetter(this, "startup", "devtools/client/responsive.html/utils/window", true);
 loader.lazyRequireGetter(this, "message", "devtools/client/responsive.html/utils/message");
@@ -577,19 +578,22 @@ ResponsiveUI.prototype = {
         break;
       case "viewport-resize":
         this.onResizeViewport(event);
         break;
     }
   },
 
   async onChangeDevice(event) {
-    const { userAgent, pixelRatio, touch } = event.data.device;
+    const { device, viewport } = event.data;
+    const { pixelRatio, touch, userAgent } = event.data.device;
+
     let reloadNeeded = false;
     await this.updateDPPX(pixelRatio);
+    await this.updateScreenOrientation(getOrientation(device, viewport), true);
     reloadNeeded |= await this.updateUserAgent(userAgent) &&
                     this.reloadOnChange("userAgent");
     reloadNeeded |= await this.updateTouchSimulation(touch) &&
                     this.reloadOnChange("touchSimulation");
     if (reloadNeeded) {
       this.getViewportBrowser().reload();
     }
     // Used by tests
@@ -660,47 +664,46 @@ ResponsiveUI.prototype = {
     const { width, height } = event.data;
     this.emit("viewport-resize", {
       width,
       height,
     });
   },
 
   async onRotateViewport(event) {
-    const targetFront = await this.client.mainRoot.getTab();
-
-    // Ensure that simulateScreenOrientationChange is supported.
-    if (await targetFront.actorHasMethod("emulation",
-        "simulateScreenOrientationChange")) {
-      // TODO: From event.data, pass orientation and angle as arguments.
-      // See Bug 1357774.
-      await this.emulationFront.simulateScreenOrientationChange();
-    }
+    const { orientationType: type, angle } = event.data;
+    await this.updateScreenOrientation({ type, angle }, false);
   },
 
   /**
    * Restores the previous state of RDM.
    */
   async restoreState() {
     const deviceState = await asyncStorage.getItem("devtools.responsive.deviceState");
     if (deviceState) {
       // Return if there is a device state to restore, this will be done when the
       // device list is loaded after the post-init.
       return;
     }
 
+    const height =
+      Services.prefs.getIntPref("devtools.responsive.viewport.height", 0);
     const pixelRatio =
       Services.prefs.getIntPref("devtools.responsive.viewport.pixelRatio", 0);
     const touchSimulationEnabled =
       Services.prefs.getBoolPref("devtools.responsive.touchSimulation.enabled", false);
     const userAgent = Services.prefs.getCharPref("devtools.responsive.userAgent", "");
+    const width =
+      Services.prefs.getIntPref("devtools.responsive.viewport.width", 0);
 
     let reloadNeeded = false;
+    const viewportOrientation = this.getInitialViewportOrientation({ width, height });
 
     await this.updateDPPX(pixelRatio);
+    await this.updateScreenOrientation(viewportOrientation, true);
 
     if (touchSimulationEnabled) {
       reloadNeeded |= await this.updateTouchSimulation(touchSimulationEnabled) &&
                       this.reloadOnChange("touchSimulation");
     }
     if (userAgent) {
       reloadNeeded |= await this.updateUserAgent(userAgent) &&
                       this.reloadOnChange("userAgent");
@@ -787,16 +790,39 @@ ResponsiveUI.prototype = {
     } else {
       reloadNeeded = await this.emulationFront.clearTouchEventsOverride();
       reloadNeeded |= await this.emulationFront.clearMetaViewportOverride();
     }
     return reloadNeeded;
   },
 
   /**
+   * Sets the screen orientation values of the simulated device.
+   *
+   * @param {Object} orientation
+   *        The orientation to update the current device screen to.
+   * @param {Boolean} changeDevice
+   *        Whether or not the reason for updating the screen orientation is a result
+   *        of actually rotating the device via the RDM toolbar or if the user switched to
+   *        another device.
+   */
+  async updateScreenOrientation(orientation, changeDevice = false) {
+    const targetFront = await this.client.mainRoot.getTab();
+    const simulateOrientationChangeSupported =
+    await targetFront.actorHasMethod("emulation", "simulateScreenOrientationChange");
+
+    // Ensure that simulateScreenOrientationChange is supported.
+    if (simulateOrientationChangeSupported) {
+      const { type, angle } = orientation;
+      await this.emulationFront.simulateScreenOrientationChange(type, angle,
+                                                                changeDevice);
+    }
+  },
+
+  /**
    * Helper for tests. Assumes a single viewport for now.
    */
   getViewportSize() {
     return this.toolWindow.getViewportSize();
   },
 
   /**
    * Helper for tests, etc. Assumes a single viewport for now.
@@ -815,11 +841,17 @@ ResponsiveUI.prototype = {
 
   /**
    * Helper for contacting the viewport content. Assumes a single viewport for now.
    */
   getViewportMessageManager() {
     return this.getViewportBrowser().messageManager;
   },
 
+  /**
+   * Helper for getting the initial viewport orientation.
+   */
+  getInitialViewportOrientation(viewport) {
+    return getOrientation(viewport, viewport);
+  },
 };
 
 EventEmitter.decorate(ResponsiveUI.prototype);
--- a/devtools/client/responsive.html/reducers/viewports.js
+++ b/devtools/client/responsive.html/reducers/viewports.js
@@ -5,31 +5,34 @@
 "use strict";
 
 const Services = require("Services");
 
 const {
   ADD_VIEWPORT,
   CHANGE_DEVICE,
   CHANGE_PIXEL_RATIO,
+  CHANGE_VIEWPORT_ANGLE,
   EDIT_DEVICE,
   REMOVE_DEVICE_ASSOCIATION,
   RESIZE_VIEWPORT,
   ROTATE_VIEWPORT,
 } = require("../actions/index");
 
 const VIEWPORT_WIDTH_PREF = "devtools.responsive.viewport.width";
 const VIEWPORT_HEIGHT_PREF = "devtools.responsive.viewport.height";
 const VIEWPORT_PIXEL_RATIO_PREF = "devtools.responsive.viewport.pixelRatio";
+const VIEWPORT_ANGLE_PREF = "devtools.responsive.viewport.angle";
 
 let nextViewportId = 0;
 
 const INITIAL_VIEWPORTS = [];
 const INITIAL_VIEWPORT = {
   id: nextViewportId++,
+  angle: Services.prefs.getIntPref(VIEWPORT_ANGLE_PREF, 0),
   device: "",
   deviceType: "",
   height: Services.prefs.getIntPref(VIEWPORT_HEIGHT_PREF, 480),
   width: Services.prefs.getIntPref(VIEWPORT_WIDTH_PREF, 320),
   pixelRatio: Services.prefs.getIntPref(VIEWPORT_PIXEL_RATIO_PREF, 0),
   userContextId: 0,
 };
 
@@ -74,16 +77,31 @@ const reducers = {
 
       return {
         ...viewport,
         pixelRatio,
       };
     });
   },
 
+  [CHANGE_VIEWPORT_ANGLE](viewports, { id, angle }) {
+    return viewports.map(viewport => {
+      if (viewport.id !== id) {
+        return viewport;
+      }
+
+      Services.prefs.setIntPref(VIEWPORT_ANGLE_PREF, angle);
+
+      return {
+        ...viewport,
+        angle,
+      };
+    });
+  },
+
   [EDIT_DEVICE](viewports, { viewport, newDevice, deviceType }) {
     if (!viewport) {
       return viewports;
     }
 
     return viewports.map(v => {
       if (v.id !== viewport.id) {
         return viewport;
--- a/devtools/client/responsive.html/test/browser/browser_orientationchange_event.js
+++ b/devtools/client/responsive.html/test/browser/browser_orientationchange_event.js
@@ -4,23 +4,34 @@
 "use strict";
 
 // Test that the "orientationchange" event is fired when the "rotate button" is clicked.
 
 const TEST_URL = "data:text/html;charset=utf-8,";
 
 addRDMTask(TEST_URL, async function({ ui }) {
   info("Rotate viewport to trigger 'orientationchange' event.");
+  await pushPref("devtools.responsive.viewport.angle", 0);
   rotateViewport(ui);
 
   await ContentTask.spawn(ui.getViewportBrowser(), {},
     async function() {
+      info("Check the original orientation values before the orientationchange");
+      is(content.screen.orientation.type, "portrait-primary",
+        "Primary orientation type is portrait-primary.");
+      is(content.screen.orientation.angle, 0,
+        "Original angle is set at 0 degrees");
+
       const orientationChange = new Promise(resolve => {
         content.window.addEventListener("orientationchange", () => {
           ok(true, "'orientationchange' event fired");
+          is(content.screen.orientation.type, "landscape-primary",
+            "Orientation state was updated to landscape-primary");
+          is(content.screen.orientation.angle, 270,
+            "Orientation angle was updated to 270 degrees.");
           resolve();
         });
       });
 
       await orientationChange;
     }
   );
 });
--- a/devtools/client/responsive.html/utils/moz.build
+++ b/devtools/client/responsive.html/utils/moz.build
@@ -5,10 +5,11 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'e10s.js',
     'key.js',
     'l10n.js',
     'message.js',
     'notification.js',
+    'orientation.js',
     'window.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/utils/orientation.js
@@ -0,0 +1,75 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { PORTRAIT_PRIMARY, LANDSCAPE_PRIMARY } = require("../constants");
+
+/**
+ * Helper that gets the screen orientation of the device displayed in the RDM viewport.
+ * This function take in both a device and viewport object and an optional rotated angle.
+ * If a rotated angle is passed, then we calculate what the orientation type of the device
+ * would be in relation to its current orientation. Otherwise, return the current
+ * orientation and angle.
+ *
+ * @param {Object} device
+ *        The device whose content is displayed in the viewport. Used to determine the
+ *        primary orientation.
+ * @param {Object} viewport
+ *        The viewport displaying device content. Used to determine the current
+ *        orientation type of the device while in RDM.
+ * @param {Number|null} angleToRotateTo
+ *        Optional. The rotated angle specifies the degree to which the device WILL be
+ *        turned to. If undefined, then only return the current orientation and angle
+ *        of the device.
+ * @return {Object} the orientation of the device.
+ */
+function getOrientation(device, viewport, angleToRotateTo = null) {
+  const { width: deviceWidth, height: deviceHeight } = device;
+  const { width: viewportWidth, height: viewportHeight } = viewport;
+
+  // Determine the primary orientation of the device screen.
+  const primaryOrientation = deviceHeight >= deviceWidth ?
+    PORTRAIT_PRIMARY :
+    LANDSCAPE_PRIMARY;
+
+  // Determine the current orientation of the device screen.
+  const currentOrientation = viewportHeight >= viewportWidth ?
+    PORTRAIT_PRIMARY :
+    LANDSCAPE_PRIMARY;
+
+  // Calculate the orientation angle of the device.
+  let angle;
+
+  if (typeof angleToRotateTo === "number") {
+    angle = angleToRotateTo;
+  } else if (currentOrientation !== primaryOrientation) {
+    angle = 270;
+  } else {
+    angle = 0;
+  }
+
+  // Calculate the orientation type of the device.
+  let orientationType = currentOrientation;
+
+  // If the viewport orientation is different from the primary orientation and the angle
+  // to rotate to is 0, then we are moving the device orientation back to its primary
+  // orientation.
+  if (currentOrientation !== primaryOrientation && angleToRotateTo === 0) {
+    orientationType = primaryOrientation;
+  } else if (angleToRotateTo === 90 || angleToRotateTo === 270) {
+    if (currentOrientation.includes("portrait")) {
+      orientationType = LANDSCAPE_PRIMARY;
+    } else if (currentOrientation.includes("landscape")) {
+      orientationType = PORTRAIT_PRIMARY;
+    }
+  }
+
+  return {
+    type: orientationType,
+    angle,
+  };
+}
+
+exports.getOrientation = getOrientation;
--- a/devtools/server/actors/emulation.js
+++ b/devtools/server/actors/emulation.js
@@ -71,16 +71,20 @@ const EmulationActor = protocol.ActorCla
   get touchSimulator() {
     if (!this._touchSimulator) {
       this._touchSimulator = new TouchSimulator(this.targetActor.chromeEventHandler);
     }
 
     return this._touchSimulator;
   },
 
+  get win() {
+    return this.docShell.chromeEventHandler.ownerGlobal;
+  },
+
   onWillNavigate({ isTopLevel }) {
     // Make sure that print simulation is stopped before navigating to another page. We
     // need to do this since the browser will cache the last state of the page in its
     // session history.
     if (this._printSimulationEnabled && isTopLevel) {
       this.stopPrintMediaSimulation(true);
     }
   },
@@ -347,23 +351,44 @@ const EmulationActor = protocol.ActorCla
    *        navigating to the next page. Defaults to false, meaning we want to completely
    *        stop print simulation.
    */
   async stopPrintMediaSimulation(state = false) {
     this._printSimulationEnabled = state;
     this.targetActor.docShell.contentViewer.stopEmulatingMedium();
   },
 
+  setScreenOrientation(type, angle) {
+    if (this.win.screen.orientation.angle !== angle ||
+        this.win.screen.orientation.type !== type) {
+      this.win.document.setRDMPaneOrientation(type, angle);
+    }
+  },
+
   /**
    * Simulates the "orientationchange" event when device screen is rotated.
    *
-   * TODO: Update `window.screen.orientation` and `window.screen.angle` here.
-   * See Bug 1357774.
+   * @param {String} type
+   *        The orientation type of the rotated device.
+   * @param {Number} angle
+   *        The rotated angle of the device.
+   * @param {Boolean} deviceChange
+   *        Whether or not screen orientation change is a result of changing the device
+   *        or rotating the current device. If the latter, then dispatch the
+   *        "orientationchange" event on the content window.
    */
-  simulateScreenOrientationChange() {
-    const win = this.docShell.chromeEventHandler.ownerGlobal;
-    const { CustomEvent } = win;
+  async simulateScreenOrientationChange(type, angle, deviceChange) {
+    // Don't dispatch the "orientationchange" event if orientation change is a result
+    // of switching to a new device.
+    if (deviceChange) {
+      this.setScreenOrientation(type, angle);
+      return;
+    }
+
+    const { CustomEvent } = this.win;
     const orientationChangeEvent = new CustomEvent("orientationchange");
-    win.dispatchEvent(orientationChangeEvent);
+
+    this.setScreenOrientation(type, angle);
+    this.win.dispatchEvent(orientationChangeEvent);
   },
 });
 
 exports.EmulationActor = EmulationActor;
--- a/devtools/shared/specs/emulation.js
+++ b/devtools/shared/specs/emulation.js
@@ -146,15 +146,19 @@ const emulationSpec = generateActorSpec(
     stopPrintMediaSimulation: {
       request: {
         state: Arg(0, "boolean"),
       },
       response: {},
     },
 
     simulateScreenOrientationChange: {
-      request: {},
+      request: {
+        orientation: Arg(0, "string"),
+        angle: Arg(1, "number"),
+        deviceChange: Arg(2, "boolean"),
+      },
       response: {},
     },
   },
 });
 
 exports.emulationSpec = emulationSpec;