summaryrefslogtreecommitdiffstats
path: root/remoting
diff options
context:
space:
mode:
authordcaiafa@chromium.org <dcaiafa@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-05-13 17:50:52 +0000
committerdcaiafa@chromium.org <dcaiafa@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-05-13 17:50:52 +0000
commit55e5286f8204297aa968ef4621eeb36dec24de80 (patch)
tree24ed0bff0888adffec464c54d549fad6ec10ae21 /remoting
parent74c6fd3e872933c7e9f99150438e243d77cfcfc1 (diff)
downloadchromium_src-55e5286f8204297aa968ef4621eeb36dec24de80.zip
chromium_src-55e5286f8204297aa968ef4621eeb36dec24de80.tar.gz
chromium_src-55e5286f8204297aa968ef4621eeb36dec24de80.tar.bz2
Chromoting iOS client
This is a sub-set of: https://codereview.chromium.org/186733007/ It doesn't include the .xcodeproj files which were causing problems with "git cl" due to being in the root .gitignore. They won't be necessary after gyp integration. This is just a code drop. Nothing is being built at the moment. NOTRY=true TBR=sergeyu@chromium.org BUG=331356 Review URL: https://codereview.chromium.org/278863003 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@270143 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'remoting')
-rw-r--r--remoting/ios/Chromoting/Base.lproj/Main.storyboard426
-rw-r--r--remoting/ios/Chromoting/Chromoting-Info.plist48
-rw-r--r--remoting/ios/Chromoting/ChromotingModel.xcdatamodeld/.xccurrentversion8
-rw-r--r--remoting/ios/Chromoting/ChromotingModel.xcdatamodeld/ChromotingModel.xcdatamodel/contents13
-rw-r--r--remoting/ios/Chromoting/GTMOAuth2ViewTouch.xib494
-rw-r--r--remoting/ios/Chromoting/Images.xcassets/AppIcon.appiconset/Contents.json91
-rw-r--r--remoting/ios/Chromoting/Images.xcassets/LaunchImage.launchimage/Contents.json36
-rw-r--r--remoting/ios/Chromoting/en.lproj/InfoPlist.strings3
-rw-r--r--remoting/ios/Chromoting/main.mm44
-rw-r--r--remoting/ios/Chromoting_unittests/Chromoting_unittests-Info.plist48
-rw-r--r--remoting/ios/Chromoting_unittests/Chromoting_unittests.xcdatamodeld/.xccurrentversion8
-rw-r--r--remoting/ios/Chromoting_unittests/Chromoting_unittests.xcdatamodeld/Chromoting_unittests.xcdatamodel/contents4
-rw-r--r--remoting/ios/Chromoting_unittests/Images.xcassets/AppIcon.appiconset/Contents.json53
-rw-r--r--remoting/ios/Chromoting_unittests/Images.xcassets/LaunchImage.launchimage/Contents.json51
-rw-r--r--remoting/ios/Chromoting_unittests/en.lproj/InfoPlist.strings2
-rw-r--r--remoting/ios/Chromoting_unittests/main.mm24
-rw-r--r--remoting/ios/Chromoting_unittests/main_no_arc.cc20
-rw-r--r--remoting/ios/Chromoting_unittests/main_no_arc.h11
-rw-r--r--remoting/ios/DEPS3
-rw-r--r--remoting/ios/app_delegate.h17
-rw-r--r--remoting/ios/app_delegate.mm18
-rw-r--r--remoting/ios/authorize.h30
-rw-r--r--remoting/ios/authorize.mm123
-rw-r--r--remoting/ios/bridge/DEPS8
-rw-r--r--remoting/ios/bridge/client_instance.cc397
-rw-r--r--remoting/ios/bridge/client_instance.h164
-rw-r--r--remoting/ios/bridge/client_instance_unittest.mm319
-rw-r--r--remoting/ios/bridge/client_proxy.h62
-rw-r--r--remoting/ios/bridge/client_proxy.mm150
-rw-r--r--remoting/ios/bridge/client_proxy_delegate.h43
-rw-r--r--remoting/ios/bridge/client_proxy_delegate_wrapper.h33
-rw-r--r--remoting/ios/bridge/client_proxy_delegate_wrapper.mm31
-rw-r--r--remoting/ios/bridge/client_proxy_unittest.mm366
-rw-r--r--remoting/ios/bridge/frame_consumer_bridge.cc88
-rw-r--r--remoting/ios/bridge/frame_consumer_bridge.h65
-rw-r--r--remoting/ios/bridge/frame_consumer_bridge_unittest.cc138
-rw-r--r--remoting/ios/bridge/host_proxy.h67
-rw-r--r--remoting/ios/bridge/host_proxy.mm119
-rw-r--r--remoting/ios/bridge/host_proxy_unittest.mm51
-rw-r--r--remoting/ios/data_store.h31
-rw-r--r--remoting/ios/data_store.mm176
-rw-r--r--remoting/ios/data_store_unittest.mm119
-rw-r--r--remoting/ios/host.h28
-rw-r--r--remoting/ios/host.mm59
-rw-r--r--remoting/ios/host_cell.h20
-rw-r--r--remoting/ios/host_cell.mm20
-rw-r--r--remoting/ios/host_preferences.h32
-rw-r--r--remoting/ios/host_refresh.h37
-rw-r--r--remoting/ios/host_refresh.mm132
-rw-r--r--remoting/ios/host_refresh_test_helper.h102
-rw-r--r--remoting/ios/host_refresh_unittest.mm170
-rw-r--r--remoting/ios/key_input.h35
-rw-r--r--remoting/ios/key_input.mm111
-rw-r--r--remoting/ios/key_input_unittest.mm124
-rw-r--r--remoting/ios/key_map_us.h288
-rw-r--r--remoting/ios/ui/cursor_texture.h58
-rw-r--r--remoting/ios/ui/cursor_texture.mm181
-rw-r--r--remoting/ios/ui/desktop_texture.h38
-rw-r--r--remoting/ios/ui/desktop_texture.mm83
-rw-r--r--remoting/ios/ui/help_view_controller.h17
-rw-r--r--remoting/ios/ui/help_view_controller.mm21
-rw-r--r--remoting/ios/ui/host_list_view_controller.h39
-rw-r--r--remoting/ios/ui/host_list_view_controller.mm229
-rw-r--r--remoting/ios/ui/host_list_view_controller_unittest.mm90
-rw-r--r--remoting/ios/ui/host_view_controller.h115
-rw-r--r--remoting/ios/ui/host_view_controller.mm676
-rw-r--r--remoting/ios/ui/pin_entry_view_controller.h49
-rw-r--r--remoting/ios/ui/pin_entry_view_controller.mm71
-rw-r--r--remoting/ios/ui/pin_entry_view_controller_ipad.xib103
-rw-r--r--remoting/ios/ui/pin_entry_view_controller_iphone.xib113
-rw-r--r--remoting/ios/ui/scene_view.h171
-rw-r--r--remoting/ios/ui/scene_view.mm642
-rw-r--r--remoting/ios/ui/scene_view_unittest.mm1219
-rw-r--r--remoting/ios/utility.h64
-rw-r--r--remoting/ios/utility.mm150
75 files changed, 9289 insertions, 0 deletions
diff --git a/remoting/ios/Chromoting/Base.lproj/Main.storyboard b/remoting/ios/Chromoting/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..075e459
--- /dev/null
+++ b/remoting/ios/Chromoting/Base.lproj/Main.storyboard
@@ -0,0 +1,426 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="5056" systemVersion="13C1021" targetRuntime="iOS.CocoaTouch.iPad" propertyAccessControl="none" useAutolayout="YES" initialViewController="SPg-Bt-nEe">
+ <dependencies>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="3733"/>
+ </dependencies>
+ <scenes>
+ <!--Host List View Controller - Host List-->
+ <scene sceneID="tne-QT-ifu">
+ <objects>
+ <viewController title="Host List" id="BYZ-38-t0r" customClass="HostListViewController" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="8q6-Qi-N0G"/>
+ <viewControllerLayoutGuide type="bottom" id="Edr-Zi-ZAO"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
+ <rect key="frame" x="0.0" y="0.0" width="768" height="1024"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <subviews>
+ <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="yvd-W1-HFY">
+ <rect key="frame" x="0.0" y="64" width="768" height="60"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <subviews>
+ <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="My Computers:" lineBreakMode="wordWrap" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="99o-m0-Qt5">
+ <rect key="frame" x="20" y="20" width="137" height="24"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <constraints>
+ <constraint firstAttribute="width" constant="137" id="CYZ-XN-3nQ"/>
+ </constraints>
+ <fontDescription key="fontDescription" type="system" pointSize="20"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="stn-Kg-u2p">
+ <rect key="frame" x="695" y="20" width="53" height="30"/>
+ <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
+ <state key="normal" title="Refresh">
+ <color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
+ </state>
+ <connections>
+ <action selector="btnRefreshHostListPressed:" destination="BYZ-38-t0r" eventType="touchUpInside" id="D8z-44-B3d"/>
+ </connections>
+ </button>
+ <activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" style="gray" translatesAutoresizingMaskIntoConstraints="NO" id="3O3-op-Q0j">
+ <rect key="frame" x="374" y="20" width="20" height="20"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ </activityIndicatorView>
+ </subviews>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ <constraints>
+ <constraint firstItem="stn-Kg-u2p" firstAttribute="top" secondItem="yvd-W1-HFY" secondAttribute="top" constant="20" id="0uu-5U-bd3"/>
+ <constraint firstAttribute="height" constant="60" id="2Ki-cp-j33"/>
+ <constraint firstItem="99o-m0-Qt5" firstAttribute="leading" secondItem="yvd-W1-HFY" secondAttribute="leading" constant="20" id="D3N-bD-lZN"/>
+ <constraint firstAttribute="centerY" secondItem="3O3-op-Q0j" secondAttribute="centerY" id="DKj-rz-sK7"/>
+ <constraint firstAttribute="centerX" secondItem="3O3-op-Q0j" secondAttribute="centerX" priority="950" id="feB-Gb-RYy"/>
+ <constraint firstItem="3O3-op-Q0j" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="99o-m0-Qt5" secondAttribute="trailing" constant="40" id="mSQ-Zu-AyV"/>
+ <constraint firstAttribute="trailing" secondItem="stn-Kg-u2p" secondAttribute="trailing" constant="20" id="sbm-eR-63m"/>
+ <constraint firstItem="99o-m0-Qt5" firstAttribute="top" secondItem="yvd-W1-HFY" secondAttribute="top" constant="20" id="zOC-CB-52f"/>
+ </constraints>
+ </view>
+ <tableView opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" translatesAutoresizingMaskIntoConstraints="NO" id="I5O-uS-nev">
+ <rect key="frame" x="0.0" y="124" width="768" height="900"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
+ <color key="separatorColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
+ <view key="tableFooterView" contentMode="scaleToFill" id="nBs-wd-JTu">
+ <rect key="frame" x="0.0" y="66" width="768" height="1"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ </view>
+ <prototypes>
+ <tableViewCell opaque="NO" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="HostStatusCell" id="YtQ-XN-44a" customClass="HostCell">
+ <rect key="frame" x="0.0" y="22" width="768" height="44"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxY="YES"/>
+ <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="YtQ-XN-44a" id="kol-nM-8Ua">
+ <rect key="frame" x="0.0" y="0.0" width="735" height="43"/>
+ <autoresizingMask key="autoresizingMask"/>
+ <subviews>
+ <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="auj-6D-D06">
+ <rect key="frame" x="20" y="11" width="42" height="21"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <accessibility key="accessibilityConfiguration">
+ <accessibilityTraits key="traits" none="YES"/>
+ </accessibility>
+ <fontDescription key="fontDescription" type="system" pointSize="17"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="tc8-OT-lCr">
+ <rect key="frame" x="673" y="11" width="42" height="21"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <accessibility key="accessibilityConfiguration">
+ <accessibilityTraits key="traits" none="YES"/>
+ </accessibility>
+ <fontDescription key="fontDescription" type="system" pointSize="17"/>
+ <color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
+ <nil key="highlightedColor"/>
+ </label>
+ </subviews>
+ <constraints>
+ <constraint firstAttribute="centerY" secondItem="auj-6D-D06" secondAttribute="centerY" id="bDt-6K-qdQ"/>
+ <constraint firstAttribute="trailing" secondItem="tc8-OT-lCr" secondAttribute="trailing" constant="20" symbolic="YES" id="cux-zd-0M5"/>
+ <constraint firstAttribute="centerY" secondItem="tc8-OT-lCr" secondAttribute="centerY" id="izT-l3-468"/>
+ <constraint firstItem="auj-6D-D06" firstAttribute="leading" secondItem="kol-nM-8Ua" secondAttribute="leading" constant="20" symbolic="YES" id="wgW-bE-xNZ"/>
+ </constraints>
+ </tableViewCellContentView>
+ <connections>
+ <outlet property="labelHostName" destination="auj-6D-D06" id="Caa-yP-G2e"/>
+ <outlet property="labelStatus" destination="tc8-OT-lCr" id="F1r-sQ-TZA"/>
+ <segue destination="lPw-Vc-fSH" kind="push" identifier="ConnectToHost" id="xky-wb-FYc"/>
+ </connections>
+ </tableViewCell>
+ </prototypes>
+ </tableView>
+ <toolbar opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="uYY-Ar-yyf">
+ <rect key="frame" x="0.0" y="980" width="768" height="44"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
+ <items>
+ <barButtonItem id="Mye-pG-kOa"/>
+ </items>
+ </toolbar>
+ </subviews>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ <constraints>
+ <constraint firstAttribute="bottom" secondItem="uYY-Ar-yyf" secondAttribute="bottom" id="8wJ-Mm-mtZ"/>
+ <constraint firstItem="yvd-W1-HFY" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leading" id="Ojr-S3-fjw"/>
+ <constraint firstAttribute="trailing" secondItem="uYY-Ar-yyf" secondAttribute="trailing" id="ewc-KI-7Pa"/>
+ <constraint firstItem="Edr-Zi-ZAO" firstAttribute="top" secondItem="I5O-uS-nev" secondAttribute="bottom" id="h7Z-ei-7Ct"/>
+ <constraint firstAttribute="trailing" secondItem="I5O-uS-nev" secondAttribute="trailing" id="kxU-fk-8jC"/>
+ <constraint firstItem="yvd-W1-HFY" firstAttribute="top" secondItem="8q6-Qi-N0G" secondAttribute="bottom" id="lu1-Ap-BQY"/>
+ <constraint firstAttribute="trailing" secondItem="yvd-W1-HFY" secondAttribute="trailing" id="mKI-XH-7PK"/>
+ <constraint firstItem="I5O-uS-nev" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leading" id="r8V-M5-UZY"/>
+ <constraint firstItem="I5O-uS-nev" firstAttribute="top" secondItem="yvd-W1-HFY" secondAttribute="bottom" id="u5c-q8-ryn"/>
+ <constraint firstItem="uYY-Ar-yyf" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leading" id="wfZ-mJ-Z0K"/>
+ </constraints>
+ </view>
+ <navigationItem key="navigationItem" id="cve-Qh-DjC">
+ <barButtonItem key="rightBarButtonItem" id="86a-bA-RuA">
+ <button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="right" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" id="XbR-N0-tUY">
+ <rect key="frame" x="462" y="7" width="290" height="30"/>
+ <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
+ <state key="normal" title="Button">
+ <color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
+ </state>
+ <connections>
+ <action selector="btnAccountPressed:" destination="BYZ-38-t0r" eventType="touchUpInside" id="nLJ-sO-y0g"/>
+ </connections>
+ </button>
+ </barButtonItem>
+ </navigationItem>
+ <connections>
+ <outlet property="_btnAccount" destination="XbR-N0-tUY" id="ifY-zm-YC8"/>
+ <outlet property="_refreshActivityIndicator" destination="3O3-op-Q0j" id="Rni-y1-vra"/>
+ <outlet property="_tableHostList" destination="I5O-uS-nev" id="ZxW-4E-eca"/>
+ <outlet property="_versionInfo" destination="Mye-pG-kOa" id="6pV-0Q-bRO"/>
+ </connections>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="389" y="-237"/>
+ </scene>
+ <!--Navigation Controller-->
+ <scene sceneID="Quq-3g-lvI">
+ <objects>
+ <navigationController definesPresentationContext="YES" id="SPg-Bt-nEe" sceneMemberID="viewController">
+ <navigationBar key="navigationBar" contentMode="scaleToFill" id="ajj-SW-dz6">
+ <autoresizingMask key="autoresizingMask"/>
+ </navigationBar>
+ <connections>
+ <segue destination="BYZ-38-t0r" kind="relationship" relationship="rootViewController" id="iup-TL-tUq"/>
+ </connections>
+ </navigationController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="h9P-fz-hAV" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="-855" y="-100"/>
+ </scene>
+ <!--GLKit View Controller - Host-->
+ <scene sceneID="qZZ-nB-Drp">
+ <objects>
+ <glkViewController autoresizesArchivedViewToFullSize="NO" title="Host" preferredFramesPerSecond="30" id="lPw-Vc-fSH" customClass="HostViewController" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="lC6-3a-Lya"/>
+ <viewControllerLayoutGuide type="bottom" id="is7-yM-cwW"/>
+ </layoutGuides>
+ <glkView key="view" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="MQp-Cd-coc">
+ <rect key="frame" x="0.0" y="0.0" width="768" height="1024"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <subviews>
+ <toolbar hidden="YES" opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" translucent="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Y3E-aU-nYu">
+ <rect key="frame" x="0.0" y="64" width="768" height="44"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
+ <constraints>
+ <constraint firstAttribute="height" constant="44" id="kb2-2C-u4O"/>
+ </constraints>
+ <inset key="insetFor6xAndEarlier" minX="0.0" minY="20" maxX="0.0" maxY="-20"/>
+ <items>
+ <barButtonItem style="plain" systemItem="flexibleSpace" id="Yoj-8n-wSI"/>
+ <barButtonItem style="plain" id="DSd-fB-lMy">
+ <button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" id="bs6-DO-4n1">
+ <rect key="frame" x="712" y="2" width="40" height="40"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <state key="normal" image="disabled_select.png">
+ <color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
+ </state>
+ <connections>
+ <action selector="barBtnToolBarHidePressed:" destination="lPw-Vc-fSH" eventType="touchUpInside" id="4vw-LW-Oor"/>
+ </connections>
+ </button>
+ </barButtonItem>
+ </items>
+ </toolbar>
+ <toolbar clearsContextBeforeDrawing="NO" contentMode="scaleToFill" translucent="NO" translatesAutoresizingMaskIntoConstraints="NO" id="47J-lh-mJo">
+ <rect key="frame" x="0.0" y="20" width="768" height="44"/>
+ <autoresizingMask key="autoresizingMask" flexibleMinX="YES" widthSizable="YES" flexibleMaxY="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
+ <rect key="contentStretch" x="0.0" y="1" width="1" height="1"/>
+ <constraints>
+ <constraint firstAttribute="height" constant="44" id="609-m7-CVM"/>
+ </constraints>
+ <inset key="insetFor6xAndEarlier" minX="0.0" minY="20" maxX="0.0" maxY="-20"/>
+ <items>
+ <barButtonItem id="o9I-36-aO3">
+ <button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" showsTouchWhenHighlighted="YES" lineBreakMode="middleTruncation" id="S8p-4Q-pU1">
+ <rect key="frame" x="16" y="-10" width="142" height="64"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <fontDescription key="fontDescription" type="system" pointSize="14"/>
+ <state key="normal" title="Disconnect" image="topbar_button_close.png">
+ <color key="titleColor" red="0.0" green="0.56862747669219971" blue="1" alpha="1" colorSpace="deviceRGB"/>
+ <color key="titleShadowColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
+ </state>
+ <connections>
+ <action selector="barBtnNavigationBackPressed:" destination="lPw-Vc-fSH" eventType="touchUpInside" id="Nvc-5V-GHl"/>
+ </connections>
+ </button>
+ </barButtonItem>
+ <barButtonItem style="plain" systemItem="flexibleSpace" id="faa-fV-VqT"/>
+ <barButtonItem style="plain" id="UXJ-lQ-dYh">
+ <button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="top" buttonType="roundedRect" lineBreakMode="middleTruncation" id="gPB-Ng-iZV">
+ <rect key="frame" x="547" y="2" width="40" height="40"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <color key="tintColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/>
+ <inset key="contentEdgeInsets" minX="0.0" minY="2" maxX="0.0" maxY="-2"/>
+ <state key="normal" image="ic_action_keyboard.png"/>
+ <connections>
+ <action selector="barBtnKeyboardPressed:" destination="lPw-Vc-fSH" eventType="touchUpInside" id="3JJ-Mv-sH5"/>
+ </connections>
+ </button>
+ </barButtonItem>
+ <barButtonItem style="plain" id="cOm-P4-mMd">
+ <button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" id="jEY-dy-0Fd">
+ <rect key="frame" x="597" y="6" width="73" height="33"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <fontDescription key="fontDescription" type="system" pointSize="12"/>
+ <state key="normal" title="Ctrl+Alt+Del">
+ <color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
+ </state>
+ <connections>
+ <action selector="barBtnCtrlAltDelPressed:" destination="lPw-Vc-fSH" eventType="touchUpInside" id="Uua-F9-F2x"/>
+ </connections>
+ </button>
+ </barButtonItem>
+ <barButtonItem style="done" id="ZBR-7V-ycs">
+ <button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" id="hFI-xo-JJz">
+ <rect key="frame" x="680" y="11" width="22" height="22"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <state key="normal" image="question_mark.png"/>
+ <connections>
+ <segue destination="sPP-eR-xu8" kind="push" id="sec-Qa-coG"/>
+ </connections>
+ </button>
+ </barButtonItem>
+ <barButtonItem style="plain" id="aIG-mY-fM0">
+ <button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" id="rjy-Dr-SrF">
+ <rect key="frame" x="712" y="2" width="40" height="40"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <state key="normal" image="disabled_select.png">
+ <color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
+ </state>
+ <connections>
+ <action selector="barBtnToolBarHidePressed:" destination="lPw-Vc-fSH" eventType="touchUpInside" id="QRl-Hx-ZE5"/>
+ </connections>
+ </button>
+ </barButtonItem>
+ </items>
+ </toolbar>
+ <view userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="iuH-L7-odS">
+ <rect key="frame" x="0.0" y="0.0" width="768" height="20"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ <constraints>
+ <constraint firstAttribute="height" constant="20" id="wTH-qy-NyA"/>
+ </constraints>
+ </view>
+ <view hidden="YES" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="O2x-II-tL4" customClass="KeyInput">
+ <rect key="frame" x="409" y="168" width="384" height="512"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ </view>
+ <activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" style="whiteLarge" translatesAutoresizingMaskIntoConstraints="NO" id="12g-Wu-GoD">
+ <rect key="frame" x="366" y="494" width="37" height="37"/>
+ <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
+ </activityIndicatorView>
+ </subviews>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ <gestureRecognizers/>
+ <constraints>
+ <constraint firstItem="iuH-L7-odS" firstAttribute="leading" secondItem="MQp-Cd-coc" secondAttribute="leading" id="4GE-lE-kPa"/>
+ <constraint firstAttribute="trailing" secondItem="Y3E-aU-nYu" secondAttribute="trailing" id="HLQ-QJ-0XZ"/>
+ <constraint firstItem="iuH-L7-odS" firstAttribute="top" secondItem="MQp-Cd-coc" secondAttribute="top" id="Jbb-aA-0gB"/>
+ <constraint firstItem="47J-lh-mJo" firstAttribute="top" secondItem="MQp-Cd-coc" secondAttribute="top" constant="20" id="KBg-4j-gIC"/>
+ <constraint firstAttribute="centerX" secondItem="12g-Wu-GoD" secondAttribute="centerX" id="Oaz-q9-RWE"/>
+ <constraint firstAttribute="trailing" secondItem="iuH-L7-odS" secondAttribute="trailing" id="Y4i-20-9RR"/>
+ <constraint firstItem="Y3E-aU-nYu" firstAttribute="top" secondItem="MQp-Cd-coc" secondAttribute="top" constant="64" id="e68-0C-Yby"/>
+ <constraint firstAttribute="trailing" secondItem="47J-lh-mJo" secondAttribute="trailing" id="jD7-JK-wMe"/>
+ <constraint firstAttribute="centerY" secondItem="12g-Wu-GoD" secondAttribute="centerY" id="oq1-Aj-Sod"/>
+ <constraint firstItem="47J-lh-mJo" firstAttribute="leading" secondItem="MQp-Cd-coc" secondAttribute="leading" id="tpX-fz-QIm"/>
+ <constraint firstItem="Y3E-aU-nYu" firstAttribute="leading" secondItem="MQp-Cd-coc" secondAttribute="leading" id="x9S-Ly-syx"/>
+ </constraints>
+ <connections>
+ <outlet property="delegate" destination="lPw-Vc-fSH" id="ghP-cQ-BvF"/>
+ <outletCollection property="gestureRecognizers" destination="WZg-EH-YvF" appends="YES" id="pRl-FP-k2b"/>
+ <outletCollection property="gestureRecognizers" destination="FTW-q5-yDY" appends="YES" id="dMX-MK-3kk"/>
+ <outletCollection property="gestureRecognizers" destination="Y8x-bE-4DN" appends="YES" id="EGt-K1-MHK"/>
+ <outletCollection property="gestureRecognizers" destination="eRF-VD-855" appends="YES" id="O19-1x-Vs6"/>
+ <outletCollection property="gestureRecognizers" destination="To2-UZ-mYz" appends="YES" id="axt-lv-TdE"/>
+ <outletCollection property="gestureRecognizers" destination="zq6-Q8-ZSf" appends="YES" id="6Rt-Pc-3hM"/>
+ <outletCollection property="gestureRecognizers" destination="hmw-Pt-YWc" appends="YES" id="X8V-Ud-DeF"/>
+ </connections>
+ </glkView>
+ <navigationItem key="navigationItem" id="aai-Iq-8wO"/>
+ <connections>
+ <outlet property="_barBtnCtrlAltDel" destination="jEY-dy-0Fd" id="cDD-Jo-I88"/>
+ <outlet property="_barBtnDisconnect" destination="S8p-4Q-pU1" id="bDF-jB-odA"/>
+ <outlet property="_barBtnKeyboard" destination="gPB-Ng-iZV" id="HGO-7O-mlu"/>
+ <outlet property="_barBtnNavigation" destination="rjy-Dr-SrF" id="LHI-Jh-LUh"/>
+ <outlet property="_busyIndicator" destination="12g-Wu-GoD" id="6Jd-v5-9gJ"/>
+ <outlet property="_hiddenToolbar" destination="Y3E-aU-nYu" id="kgE-pB-bd7"/>
+ <outlet property="_hiddenToolbarYPosition" destination="e68-0C-Yby" id="xnJ-vd-PHD"/>
+ <outlet property="_keyEntryView" destination="O2x-II-tL4" id="Tml-cK-8Q8"/>
+ <outlet property="_longPressRecognizer" destination="eRF-VD-855" id="aAD-72-kcg"/>
+ <outlet property="_panRecognizer" destination="Y8x-bE-4DN" id="H0t-Id-tGg"/>
+ <outlet property="_pinchRecognizer" destination="WZg-EH-YvF" id="vw5-rO-RT9"/>
+ <outlet property="_singleTapRecognizer" destination="FTW-q5-yDY" id="zMf-7c-XZA"/>
+ <outlet property="_threeFingerPanRecognizer" destination="hmw-Pt-YWc" id="Gow-7d-bez"/>
+ <outlet property="_threeFingerTapRecognizer" destination="zq6-Q8-ZSf" id="3q9-TT-5ob"/>
+ <outlet property="_toolBarYPosition" destination="KBg-4j-gIC" id="yrM-sN-Rb6"/>
+ <outlet property="_toolbar" destination="47J-lh-mJo" id="rAv-Nw-mQa"/>
+ <outlet property="_twoFingerTapRecognizer" destination="To2-UZ-mYz" id="UhW-3t-nDp"/>
+ </connections>
+ </glkViewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="c72-YN-wvu" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ <pinchGestureRecognizer id="WZg-EH-YvF">
+ <connections>
+ <action selector="pinchGestureTriggered:" destination="lPw-Vc-fSH" id="hD7-CG-6iI"/>
+ <outlet property="delegate" destination="lPw-Vc-fSH" id="gzM-Uv-QwD"/>
+ </connections>
+ </pinchGestureRecognizer>
+ <tapGestureRecognizer id="FTW-q5-yDY">
+ <connections>
+ <action selector="tapGestureTriggered:" destination="lPw-Vc-fSH" id="4kQ-3c-Ocr"/>
+ <outlet property="delegate" destination="lPw-Vc-fSH" id="Ml4-vD-NQc"/>
+ </connections>
+ </tapGestureRecognizer>
+ <panGestureRecognizer minimumNumberOfTouches="1" maximumNumberOfTouches="2" id="Y8x-bE-4DN">
+ <connections>
+ <action selector="panGestureTriggered:" destination="lPw-Vc-fSH" id="hS1-SH-mCS"/>
+ <outlet property="delegate" destination="lPw-Vc-fSH" id="wft-mz-bVm"/>
+ </connections>
+ </panGestureRecognizer>
+ <pongPressGestureRecognizer allowableMovement="10" minimumPressDuration="0.5" id="eRF-VD-855">
+ <connections>
+ <action selector="longPressGestureTriggered:" destination="lPw-Vc-fSH" id="EiW-TC-nqA"/>
+ <outlet property="delegate" destination="lPw-Vc-fSH" id="QHj-hZ-uvi"/>
+ </connections>
+ </pongPressGestureRecognizer>
+ <tapGestureRecognizer numberOfTouchesRequired="2" id="To2-UZ-mYz">
+ <connections>
+ <action selector="twoFingerTapGestureTriggered:" destination="lPw-Vc-fSH" id="I4d-VY-TJU"/>
+ <outlet property="delegate" destination="lPw-Vc-fSH" id="Waj-rg-fv8"/>
+ </connections>
+ </tapGestureRecognizer>
+ <tapGestureRecognizer numberOfTouchesRequired="3" id="zq6-Q8-ZSf">
+ <connections>
+ <action selector="threeFingerTapGestureTriggered:" destination="lPw-Vc-fSH" id="3FN-S9-bka"/>
+ <outlet property="delegate" destination="lPw-Vc-fSH" id="MMX-7P-7nP"/>
+ </connections>
+ </tapGestureRecognizer>
+ <panGestureRecognizer minimumNumberOfTouches="3" maximumNumberOfTouches="3" id="hmw-Pt-YWc">
+ <connections>
+ <action selector="threeFingerPanGestureTriggered:" destination="lPw-Vc-fSH" id="XNo-BN-z61"/>
+ <outlet property="delegate" destination="lPw-Vc-fSH" id="aDd-Ku-dBG"/>
+ </connections>
+ </panGestureRecognizer>
+ </objects>
+ <point key="canvasLocation" x="1424" y="-194"/>
+ </scene>
+ <!--Help View Controller - Help-->
+ <scene sceneID="0IG-kv-azW">
+ <objects>
+ <viewController title="Help" id="sPP-eR-xu8" customClass="HelpViewController" sceneMemberID="viewController">
+ <webView key="view" contentMode="scaleToFill" id="olH-Du-ovq">
+ <rect key="frame" x="0.0" y="0.0" width="768" height="1024"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="calibratedRGB"/>
+ </webView>
+ <navigationItem key="navigationItem" title="Help" id="g5b-rq-fEZ">
+ <barButtonItem key="backBarButtonItem" title="Host" id="9oE-DQ-JrR"/>
+ </navigationItem>
+ <connections>
+ <outlet property="_webView" destination="olH-Du-ovq" id="Pit-4U-fB0"/>
+ </connections>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="lLO-Ob-BMh" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="2582" y="-176"/>
+ </scene>
+ </scenes>
+ <resources>
+ <image name="disabled_select.png" width="38" height="16"/>
+ <image name="ic_action_keyboard.png" width="96" height="96"/>
+ <image name="question_mark.png" width="12" height="12"/>
+ <image name="topbar_button_close.png" width="64" height="64"/>
+ </resources>
+ <simulatedMetricsContainer key="defaultSimulatedMetrics">
+ <simulatedStatusBarMetrics key="statusBar" statusBarStyle="blackOpaque"/>
+ <simulatedOrientationMetrics key="orientation"/>
+ <simulatedScreenMetrics key="destination"/>
+ </simulatedMetricsContainer>
+</document>
diff --git a/remoting/ios/Chromoting/Chromoting-Info.plist b/remoting/ios/Chromoting/Chromoting-Info.plist
new file mode 100644
index 0000000..c2d0be6
--- /dev/null
+++ b/remoting/ios/Chromoting/Chromoting-Info.plist
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleDisplayName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>net.fusionlabs.${PRODUCT_NAME:rfc1034identifier}</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>2.4</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>UIMainStoryboardFile</key>
+ <string>Main</string>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>armv7</string>
+ </array>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationPortrait</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+</dict>
+</plist>
diff --git a/remoting/ios/Chromoting/ChromotingModel.xcdatamodeld/.xccurrentversion b/remoting/ios/Chromoting/ChromotingModel.xcdatamodeld/.xccurrentversion
new file mode 100644
index 0000000..86e8cb1
--- /dev/null
+++ b/remoting/ios/Chromoting/ChromotingModel.xcdatamodeld/.xccurrentversion
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>_XCCurrentVersionName</key>
+ <string>ChromotingModel.xcdatamodel</string>
+</dict>
+</plist>
diff --git a/remoting/ios/Chromoting/ChromotingModel.xcdatamodeld/ChromotingModel.xcdatamodel/contents b/remoting/ios/Chromoting/ChromotingModel.xcdatamodeld/ChromotingModel.xcdatamodel/contents
new file mode 100644
index 0000000..b8783eb
--- /dev/null
+++ b/remoting/ios/Chromoting/ChromotingModel.xcdatamodeld/ChromotingModel.xcdatamodel/contents
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<model userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="5063" systemVersion="13C64" minimumToolsVersion="Xcode 4.3" macOSVersion="Automatic" iOSVersion="Automatic">
+ <entity name="HostPreferences" representedClassName="HostPreferences" syncable="YES">
+ <attribute name="askForPin" optional="YES" attributeType="Boolean" syncable="YES"/>
+ <attribute name="hostId" optional="YES" attributeType="String" syncable="YES"/>
+ <attribute name="hostPin" optional="YES" attributeType="String" syncable="YES"/>
+ <attribute name="pairId" optional="YES" attributeType="String" syncable="YES"/>
+ <attribute name="pairSecret" optional="YES" attributeType="String" syncable="YES"/>
+ </entity>
+ <elements>
+ <element name="HostPreferences" positionX="0" positionY="0" width="128" height="118"/>
+ </elements>
+</model> \ No newline at end of file
diff --git a/remoting/ios/Chromoting/GTMOAuth2ViewTouch.xib b/remoting/ios/Chromoting/GTMOAuth2ViewTouch.xib
new file mode 100644
index 0000000..4f91fa4
--- /dev/null
+++ b/remoting/ios/Chromoting/GTMOAuth2ViewTouch.xib
@@ -0,0 +1,494 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<archive type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="7.10">
+ <data>
+ <int key="IBDocument.SystemTarget">1024</int>
+ <string key="IBDocument.SystemVersion">12C60</string>
+ <string key="IBDocument.InterfaceBuilderVersion">2840</string>
+ <string key="IBDocument.AppKitVersion">1187.34</string>
+ <string key="IBDocument.HIToolboxVersion">625.00</string>
+ <object class="NSMutableDictionary" key="IBDocument.PluginVersions">
+ <string key="NS.key.0">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+ <string key="NS.object.0">1926</string>
+ </object>
+ <object class="NSArray" key="IBDocument.IntegratedClassDependencies">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <string>IBProxyObject</string>
+ <string>IBUIActivityIndicatorView</string>
+ <string>IBUIBarButtonItem</string>
+ <string>IBUIButton</string>
+ <string>IBUINavigationItem</string>
+ <string>IBUIView</string>
+ <string>IBUIWebView</string>
+ </object>
+ <object class="NSArray" key="IBDocument.PluginDependencies">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+ </object>
+ <object class="NSMutableDictionary" key="IBDocument.Metadata">
+ <string key="NS.key.0">PluginDependencyRecalculationVersion</string>
+ <integer value="1" key="NS.object.0"/>
+ </object>
+ <object class="NSMutableArray" key="IBDocument.RootObjects" id="1000">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <object class="IBProxyObject" id="372490531">
+ <string key="IBProxiedObjectIdentifier">IBFilesOwner</string>
+ <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
+ </object>
+ <object class="IBProxyObject" id="975951072">
+ <string key="IBProxiedObjectIdentifier">IBFirstResponder</string>
+ <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
+ </object>
+ <object class="IBUINavigationItem" id="1047805472">
+ <string key="IBUITitle">OAuth</string>
+ <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
+ </object>
+ <object class="IBUIBarButtonItem" id="961671599">
+ <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
+ <int key="IBUIStyle">1</int>
+ </object>
+ <object class="IBUIView" id="808907889">
+ <reference key="NSNextResponder"/>
+ <int key="NSvFlags">292</int>
+ <object class="NSMutableArray" key="NSSubviews">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <object class="IBUIButton" id="453250804">
+ <reference key="NSNextResponder" ref="808907889"/>
+ <int key="NSvFlags">292</int>
+ <string key="NSFrameSize">{30, 30}</string>
+ <reference key="NSSuperview" ref="808907889"/>
+ <reference key="NSWindow"/>
+ <reference key="NSNextKeyView" ref="981703116"/>
+ <bool key="IBUIOpaque">NO</bool>
+ <bool key="IBUIClearsContextBeforeDrawing">NO</bool>
+ <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
+ <int key="IBUIContentHorizontalAlignment">0</int>
+ <int key="IBUIContentVerticalAlignment">0</int>
+ <string key="IBUITitleShadowOffset">{0, -2}</string>
+ <string key="IBUINormalTitle">â—€</string>
+ <object class="NSColor" key="IBUIHighlightedTitleColor" id="193465259">
+ <int key="NSColorSpace">3</int>
+ <bytes key="NSWhite">MQA</bytes>
+ </object>
+ <object class="NSColor" key="IBUIDisabledTitleColor">
+ <int key="NSColorSpace">2</int>
+ <bytes key="NSRGB">MC41OTYwNzg0NiAwLjY4NjI3NDUzIDAuOTUyOTQxMjQgMC42MDAwMDAwMgA</bytes>
+ </object>
+ <reference key="IBUINormalTitleColor" ref="193465259"/>
+ <object class="NSColor" key="IBUINormalTitleShadowColor" id="999379443">
+ <int key="NSColorSpace">3</int>
+ <bytes key="NSWhite">MC41AA</bytes>
+ </object>
+ <object class="IBUIFontDescription" key="IBUIFontDescription" id="621440819">
+ <string key="name">Helvetica-Bold</string>
+ <string key="family">Helvetica</string>
+ <int key="traits">2</int>
+ <double key="pointSize">24</double>
+ </object>
+ <object class="NSFont" key="IBUIFont" id="530402572">
+ <string key="NSName">Helvetica-Bold</string>
+ <double key="NSSize">24</double>
+ <int key="NSfFlags">16</int>
+ </object>
+ </object>
+ <object class="IBUIButton" id="981703116">
+ <reference key="NSNextResponder" ref="808907889"/>
+ <int key="NSvFlags">292</int>
+ <string key="NSFrame">{{30, 0}, {30, 30}}</string>
+ <reference key="NSSuperview" ref="808907889"/>
+ <reference key="NSWindow"/>
+ <reference key="NSNextKeyView"/>
+ <bool key="IBUIOpaque">NO</bool>
+ <bool key="IBUIClearsContextBeforeDrawing">NO</bool>
+ <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
+ <int key="IBUIContentHorizontalAlignment">0</int>
+ <int key="IBUIContentVerticalAlignment">0</int>
+ <string key="IBUITitleShadowOffset">{0, -2}</string>
+ <string key="IBUINormalTitle">â–¶</string>
+ <reference key="IBUIHighlightedTitleColor" ref="193465259"/>
+ <object class="NSColor" key="IBUIDisabledTitleColor">
+ <int key="NSColorSpace">2</int>
+ <bytes key="NSRGB">MC41ODQzMTM3NSAwLjY3NDUwOTgyIDAuOTUyOTQxMjQgMC42MDAwMDAwMgA</bytes>
+ </object>
+ <reference key="IBUINormalTitleColor" ref="193465259"/>
+ <reference key="IBUINormalTitleShadowColor" ref="999379443"/>
+ <reference key="IBUIFontDescription" ref="621440819"/>
+ <reference key="IBUIFont" ref="530402572"/>
+ </object>
+ </object>
+ <string key="NSFrameSize">{60, 30}</string>
+ <reference key="NSSuperview"/>
+ <reference key="NSWindow"/>
+ <reference key="NSNextKeyView" ref="453250804"/>
+ <object class="NSColor" key="IBUIBackgroundColor">
+ <int key="NSColorSpace">3</int>
+ <bytes key="NSWhite">MSAwAA</bytes>
+ </object>
+ <bool key="IBUIOpaque">NO</bool>
+ <bool key="IBUIClearsContextBeforeDrawing">NO</bool>
+ <object class="IBUISimulatedOrientationMetrics" key="IBUISimulatedOrientationMetrics">
+ <int key="IBUIInterfaceOrientation">3</int>
+ <int key="interfaceOrientation">3</int>
+ </object>
+ <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
+ </object>
+ <object class="IBUIView" id="426018584">
+ <reference key="NSNextResponder"/>
+ <int key="NSvFlags">274</int>
+ <object class="NSMutableArray" key="NSSubviews">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <object class="IBUIWebView" id="663477729">
+ <reference key="NSNextResponder" ref="426018584"/>
+ <int key="NSvFlags">274</int>
+ <string key="NSFrameSize">{320, 460}</string>
+ <reference key="NSSuperview" ref="426018584"/>
+ <reference key="NSWindow"/>
+ <reference key="NSNextKeyView" ref="268967673"/>
+ <object class="NSColor" key="IBUIBackgroundColor">
+ <int key="NSColorSpace">1</int>
+ <bytes key="NSRGB">MSAxIDEAA</bytes>
+ </object>
+ <bool key="IBUIClipsSubviews">YES</bool>
+ <bool key="IBUIMultipleTouchEnabled">YES</bool>
+ <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
+ <int key="IBUIDataDetectorTypes">1</int>
+ <bool key="IBUIDetectsPhoneNumbers">YES</bool>
+ </object>
+ <object class="IBUIActivityIndicatorView" id="268967673">
+ <reference key="NSNextResponder" ref="426018584"/>
+ <int key="NSvFlags">301</int>
+ <string key="NSFrame">{{150, 115}, {20, 20}}</string>
+ <reference key="NSSuperview" ref="426018584"/>
+ <reference key="NSWindow"/>
+ <reference key="NSNextKeyView"/>
+ <string key="NSReuseIdentifierKey">_NS:9</string>
+ <bool key="IBUIOpaque">NO</bool>
+ <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
+ <bool key="IBUIHidesWhenStopped">NO</bool>
+ <bool key="IBUIAnimating">YES</bool>
+ <int key="IBUIStyle">2</int>
+ </object>
+ </object>
+ <string key="NSFrameSize">{320, 460}</string>
+ <reference key="NSSuperview"/>
+ <reference key="NSWindow"/>
+ <reference key="NSNextKeyView" ref="663477729"/>
+ <object class="NSColor" key="IBUIBackgroundColor">
+ <int key="NSColorSpace">3</int>
+ <bytes key="NSWhite">MQA</bytes>
+ <object class="NSColorSpace" key="NSCustomColorSpace">
+ <int key="NSID">2</int>
+ </object>
+ </object>
+ <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
+ </object>
+ </object>
+ <object class="IBObjectContainer" key="IBDocument.Objects">
+ <object class="NSMutableArray" key="connectionRecords">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <object class="IBConnectionRecord">
+ <object class="IBCocoaTouchOutletConnection" key="connection">
+ <string key="label">rightBarButtonItem</string>
+ <reference key="source" ref="372490531"/>
+ <reference key="destination" ref="961671599"/>
+ </object>
+ <int key="connectionID">20</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBCocoaTouchOutletConnection" key="connection">
+ <string key="label">navButtonsView</string>
+ <reference key="source" ref="372490531"/>
+ <reference key="destination" ref="808907889"/>
+ </object>
+ <int key="connectionID">22</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBCocoaTouchOutletConnection" key="connection">
+ <string key="label">backButton</string>
+ <reference key="source" ref="372490531"/>
+ <reference key="destination" ref="453250804"/>
+ </object>
+ <int key="connectionID">25</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBCocoaTouchOutletConnection" key="connection">
+ <string key="label">forwardButton</string>
+ <reference key="source" ref="372490531"/>
+ <reference key="destination" ref="981703116"/>
+ </object>
+ <int key="connectionID">26</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBCocoaTouchOutletConnection" key="connection">
+ <string key="label">view</string>
+ <reference key="source" ref="372490531"/>
+ <reference key="destination" ref="426018584"/>
+ </object>
+ <int key="connectionID">28</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBCocoaTouchOutletConnection" key="connection">
+ <string key="label">webView</string>
+ <reference key="source" ref="372490531"/>
+ <reference key="destination" ref="663477729"/>
+ </object>
+ <int key="connectionID">29</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBCocoaTouchOutletConnection" key="connection">
+ <string key="label">initialActivityIndicator</string>
+ <reference key="source" ref="372490531"/>
+ <reference key="destination" ref="268967673"/>
+ </object>
+ <int key="connectionID">33</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBCocoaTouchOutletConnection" key="connection">
+ <string key="label">delegate</string>
+ <reference key="source" ref="663477729"/>
+ <reference key="destination" ref="372490531"/>
+ </object>
+ <int key="connectionID">9</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBCocoaTouchOutletConnection" key="connection">
+ <string key="label">rightBarButtonItem</string>
+ <reference key="source" ref="1047805472"/>
+ <reference key="destination" ref="961671599"/>
+ </object>
+ <int key="connectionID">14</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBCocoaTouchEventConnection" key="connection">
+ <string key="label">goBack</string>
+ <reference key="source" ref="453250804"/>
+ <reference key="destination" ref="663477729"/>
+ <int key="IBEventType">7</int>
+ </object>
+ <int key="connectionID">18</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBCocoaTouchEventConnection" key="connection">
+ <string key="label">goForward</string>
+ <reference key="source" ref="981703116"/>
+ <reference key="destination" ref="663477729"/>
+ <int key="IBEventType">7</int>
+ </object>
+ <int key="connectionID">19</int>
+ </object>
+ </object>
+ <object class="IBMutableOrderedSet" key="objectRecords">
+ <object class="NSArray" key="orderedObjects">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <object class="IBObjectRecord">
+ <int key="objectID">0</int>
+ <object class="NSArray" key="object" id="0">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ </object>
+ <reference key="children" ref="1000"/>
+ <nil key="parent"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">-1</int>
+ <reference key="object" ref="372490531"/>
+ <reference key="parent" ref="0"/>
+ <string key="objectName">File's Owner</string>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">-2</int>
+ <reference key="object" ref="975951072"/>
+ <reference key="parent" ref="0"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">6</int>
+ <reference key="object" ref="1047805472"/>
+ <object class="NSMutableArray" key="children">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ </object>
+ <reference key="parent" ref="0"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">10</int>
+ <reference key="object" ref="961671599"/>
+ <reference key="parent" ref="0"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">15</int>
+ <reference key="object" ref="808907889"/>
+ <object class="NSMutableArray" key="children">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <reference ref="453250804"/>
+ <reference ref="981703116"/>
+ </object>
+ <reference key="parent" ref="0"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">16</int>
+ <reference key="object" ref="453250804"/>
+ <reference key="parent" ref="808907889"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">17</int>
+ <reference key="object" ref="981703116"/>
+ <reference key="parent" ref="808907889"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">27</int>
+ <reference key="object" ref="426018584"/>
+ <object class="NSMutableArray" key="children">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <reference ref="663477729"/>
+ <reference ref="268967673"/>
+ </object>
+ <reference key="parent" ref="0"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">4</int>
+ <reference key="object" ref="663477729"/>
+ <reference key="parent" ref="426018584"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">31</int>
+ <reference key="object" ref="268967673"/>
+ <reference key="parent" ref="426018584"/>
+ </object>
+ </object>
+ </object>
+ <object class="NSMutableDictionary" key="flattenedProperties">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <object class="NSArray" key="dict.sortedKeys">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <string>-1.CustomClassName</string>
+ <string>-1.IBPluginDependency</string>
+ <string>-2.CustomClassName</string>
+ <string>-2.IBPluginDependency</string>
+ <string>10.IBPluginDependency</string>
+ <string>15.IBPluginDependency</string>
+ <string>16.IBPluginDependency</string>
+ <string>17.IBPluginDependency</string>
+ <string>27.IBPluginDependency</string>
+ <string>31.IBPluginDependency</string>
+ <string>4.IBPluginDependency</string>
+ <string>6.IBPluginDependency</string>
+ </object>
+ <object class="NSArray" key="dict.values">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <string>GTMOAuth2ViewControllerTouch</string>
+ <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+ <string>UIResponder</string>
+ <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+ <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+ <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+ <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+ <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+ <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+ <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+ <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+ <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+ </object>
+ </object>
+ <object class="NSMutableDictionary" key="unlocalizedProperties">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <reference key="dict.sortedKeys" ref="0"/>
+ <reference key="dict.values" ref="0"/>
+ </object>
+ <nil key="activeLocalization"/>
+ <object class="NSMutableDictionary" key="localizations">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <reference key="dict.sortedKeys" ref="0"/>
+ <reference key="dict.values" ref="0"/>
+ </object>
+ <nil key="sourceID"/>
+ <int key="maxID">33</int>
+ </object>
+ <object class="IBClassDescriber" key="IBDocument.Classes">
+ <object class="NSMutableArray" key="referencedPartialClassDescriptions">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <object class="IBPartialClassDescription">
+ <string key="className">GTMOAuth2ViewControllerTouch</string>
+ <string key="superclassName">UIViewController</string>
+ <object class="NSMutableDictionary" key="outlets">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <object class="NSArray" key="dict.sortedKeys">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <string>backButton</string>
+ <string>forwardButton</string>
+ <string>initialActivityIndicator</string>
+ <string>navButtonsView</string>
+ <string>rightBarButtonItem</string>
+ <string>webView</string>
+ </object>
+ <object class="NSArray" key="dict.values">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <string>UIButton</string>
+ <string>UIButton</string>
+ <string>UIActivityIndicatorView</string>
+ <string>UIView</string>
+ <string>UIBarButtonItem</string>
+ <string>UIWebView</string>
+ </object>
+ </object>
+ <object class="NSMutableDictionary" key="toOneOutletInfosByName">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <object class="NSArray" key="dict.sortedKeys">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <string>backButton</string>
+ <string>forwardButton</string>
+ <string>initialActivityIndicator</string>
+ <string>navButtonsView</string>
+ <string>rightBarButtonItem</string>
+ <string>webView</string>
+ </object>
+ <object class="NSArray" key="dict.values">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <object class="IBToOneOutletInfo">
+ <string key="name">backButton</string>
+ <string key="candidateClassName">UIButton</string>
+ </object>
+ <object class="IBToOneOutletInfo">
+ <string key="name">forwardButton</string>
+ <string key="candidateClassName">UIButton</string>
+ </object>
+ <object class="IBToOneOutletInfo">
+ <string key="name">initialActivityIndicator</string>
+ <string key="candidateClassName">UIActivityIndicatorView</string>
+ </object>
+ <object class="IBToOneOutletInfo">
+ <string key="name">navButtonsView</string>
+ <string key="candidateClassName">UIView</string>
+ </object>
+ <object class="IBToOneOutletInfo">
+ <string key="name">rightBarButtonItem</string>
+ <string key="candidateClassName">UIBarButtonItem</string>
+ </object>
+ <object class="IBToOneOutletInfo">
+ <string key="name">webView</string>
+ <string key="candidateClassName">UIWebView</string>
+ </object>
+ </object>
+ </object>
+ <object class="IBClassDescriptionSource" key="sourceIdentifier">
+ <string key="majorKey">IBProjectSource</string>
+ <string key="minorKey">./Classes/GTMOAuth2ViewControllerTouch.h</string>
+ </object>
+ </object>
+ </object>
+ </object>
+ <int key="IBDocument.localizationMode">0</int>
+ <string key="IBDocument.TargetRuntimeIdentifier">IBCocoaTouchFramework</string>
+ <object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencies">
+ <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaTouchPlugin.iPhoneOS</string>
+ <real value="1024" key="NS.object.0"/>
+ </object>
+ <object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencyDefaults">
+ <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaTouchPlugin.iPhoneOS</string>
+ <real value="1536" key="NS.object.0"/>
+ </object>
+ <object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDevelopmentDependencies">
+ <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaTouchPlugin.InterfaceBuilder3</string>
+ <integer value="3000" key="NS.object.0"/>
+ </object>
+ <bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool>
+ <int key="IBDocument.defaultPropertyAccessControl">3</int>
+ <string key="IBCocoaTouchPluginVersion">1926</string>
+ </data>
+</archive>
diff --git a/remoting/ios/Chromoting/Images.xcassets/AppIcon.appiconset/Contents.json b/remoting/ios/Chromoting/Images.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..a281841
--- /dev/null
+++ b/remoting/ios/Chromoting/Images.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,91 @@
+{
+ "images" : [
+ {
+ "idiom" : "iphone",
+ "size" : "29x29",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "29x29",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "40x40",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "57x57",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "57x57",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "chromoting120.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "29x29",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "29x29",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "40x40",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "40x40",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "50x50",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "50x50",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "72x72",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "72x72",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "chromoting76.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "chromoting152.png",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+} \ No newline at end of file
diff --git a/remoting/ios/Chromoting/Images.xcassets/LaunchImage.launchimage/Contents.json b/remoting/ios/Chromoting/Images.xcassets/LaunchImage.launchimage/Contents.json
new file mode 100644
index 0000000..a0ad363
--- /dev/null
+++ b/remoting/ios/Chromoting/Images.xcassets/LaunchImage.launchimage/Contents.json
@@ -0,0 +1,36 @@
+{
+ "images" : [
+ {
+ "orientation" : "portrait",
+ "idiom" : "ipad",
+ "extent" : "full-screen",
+ "minimum-system-version" : "7.0",
+ "scale" : "1x"
+ },
+ {
+ "orientation" : "landscape",
+ "idiom" : "ipad",
+ "extent" : "full-screen",
+ "minimum-system-version" : "7.0",
+ "scale" : "1x"
+ },
+ {
+ "orientation" : "portrait",
+ "idiom" : "ipad",
+ "extent" : "full-screen",
+ "minimum-system-version" : "7.0",
+ "scale" : "2x"
+ },
+ {
+ "orientation" : "landscape",
+ "idiom" : "ipad",
+ "extent" : "full-screen",
+ "minimum-system-version" : "7.0",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+} \ No newline at end of file
diff --git a/remoting/ios/Chromoting/en.lproj/InfoPlist.strings b/remoting/ios/Chromoting/en.lproj/InfoPlist.strings
new file mode 100644
index 0000000..653031a
--- /dev/null
+++ b/remoting/ios/Chromoting/en.lproj/InfoPlist.strings
@@ -0,0 +1,3 @@
+/* This file is required */
+/* Localized versions of Info.plist keys */
+
diff --git a/remoting/ios/Chromoting/main.mm b/remoting/ios/Chromoting/main.mm
new file mode 100644
index 0000000..cd068167
--- /dev/null
+++ b/remoting/ios/Chromoting/main.mm
@@ -0,0 +1,44 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import <UIKit/UIKit.h>
+
+#include "base/at_exit.h"
+#include "base/command_line.h"
+#include "media/base/yuv_convert.h"
+#include "net/socket/ssl_server_socket.h"
+
+#import "app_delegate.h"
+
+int main(int argc, char* argv[]) {
+ // This class is designed to fulfill the dependents needs when it goes out of
+ // scope and gets destructed
+ base::AtExitManager exitManager;
+
+ // Publicize the CommandLine
+ CommandLine::Init(argc, argv);
+
+#ifdef DEBUG
+ // Set min log level for debug builds. For some reason this has to be
+ // negative.
+ logging::SetMinLogLevel(-1);
+#endif
+
+ // Allows later decoding of video frames.
+ media::InitializeCPUSpecificYUVConversions();
+
+ // Enable support for SSL server sockets, which must be done as early as
+ // possible, preferably before any NSS SSL sockets (client or server) have
+ // been created.
+ net::EnableSSLServerSockets();
+
+ @autoreleasepool {
+ return UIApplicationMain(
+ argc, argv, nil, NSStringFromClass([AppDelegate class]));
+ }
+}
diff --git a/remoting/ios/Chromoting_unittests/Chromoting_unittests-Info.plist b/remoting/ios/Chromoting_unittests/Chromoting_unittests-Info.plist
new file mode 100644
index 0000000..e48cdc6
--- /dev/null
+++ b/remoting/ios/Chromoting_unittests/Chromoting_unittests-Info.plist
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleDisplayName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>net.fusionlabs.${PRODUCT_NAME:rfc1034identifier}</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>2.4</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>armv7</string>
+ </array>
+ <key>UIStatusBarHidden</key>
+ <false/>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+</dict>
+</plist>
diff --git a/remoting/ios/Chromoting_unittests/Chromoting_unittests.xcdatamodeld/.xccurrentversion b/remoting/ios/Chromoting_unittests/Chromoting_unittests.xcdatamodeld/.xccurrentversion
new file mode 100644
index 0000000..855b6ee
--- /dev/null
+++ b/remoting/ios/Chromoting_unittests/Chromoting_unittests.xcdatamodeld/.xccurrentversion
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>_XCCurrentVersionName</key>
+ <string>Chromoting_unittests.xcdatamodel</string>
+</dict>
+</plist>
diff --git a/remoting/ios/Chromoting_unittests/Chromoting_unittests.xcdatamodeld/Chromoting_unittests.xcdatamodel/contents b/remoting/ios/Chromoting_unittests/Chromoting_unittests.xcdatamodeld/Chromoting_unittests.xcdatamodel/contents
new file mode 100644
index 0000000..193f33c
--- /dev/null
+++ b/remoting/ios/Chromoting_unittests/Chromoting_unittests.xcdatamodeld/Chromoting_unittests.xcdatamodel/contents
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<model name="Test1.xcdatamodel" userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1" systemVersion="11A491" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
+ <elements/>
+</model> \ No newline at end of file
diff --git a/remoting/ios/Chromoting_unittests/Images.xcassets/AppIcon.appiconset/Contents.json b/remoting/ios/Chromoting_unittests/Images.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..91bf9c1
--- /dev/null
+++ b/remoting/ios/Chromoting_unittests/Images.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,53 @@
+{
+ "images" : [
+ {
+ "idiom" : "iphone",
+ "size" : "29x29",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "40x40",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "60x60",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "29x29",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "29x29",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "40x40",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "40x40",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "76x76",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "76x76",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+} \ No newline at end of file
diff --git a/remoting/ios/Chromoting_unittests/Images.xcassets/LaunchImage.launchimage/Contents.json b/remoting/ios/Chromoting_unittests/Images.xcassets/LaunchImage.launchimage/Contents.json
new file mode 100644
index 0000000..6f870a4
--- /dev/null
+++ b/remoting/ios/Chromoting_unittests/Images.xcassets/LaunchImage.launchimage/Contents.json
@@ -0,0 +1,51 @@
+{
+ "images" : [
+ {
+ "orientation" : "portrait",
+ "idiom" : "iphone",
+ "extent" : "full-screen",
+ "minimum-system-version" : "7.0",
+ "scale" : "2x"
+ },
+ {
+ "orientation" : "portrait",
+ "idiom" : "iphone",
+ "subtype" : "retina4",
+ "extent" : "full-screen",
+ "minimum-system-version" : "7.0",
+ "scale" : "2x"
+ },
+ {
+ "orientation" : "portrait",
+ "idiom" : "ipad",
+ "extent" : "full-screen",
+ "minimum-system-version" : "7.0",
+ "scale" : "1x"
+ },
+ {
+ "orientation" : "landscape",
+ "idiom" : "ipad",
+ "extent" : "full-screen",
+ "minimum-system-version" : "7.0",
+ "scale" : "1x"
+ },
+ {
+ "orientation" : "portrait",
+ "idiom" : "ipad",
+ "extent" : "full-screen",
+ "minimum-system-version" : "7.0",
+ "scale" : "2x"
+ },
+ {
+ "orientation" : "landscape",
+ "idiom" : "ipad",
+ "extent" : "full-screen",
+ "minimum-system-version" : "7.0",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+} \ No newline at end of file
diff --git a/remoting/ios/Chromoting_unittests/en.lproj/InfoPlist.strings b/remoting/ios/Chromoting_unittests/en.lproj/InfoPlist.strings
new file mode 100644
index 0000000..477b28f
--- /dev/null
+++ b/remoting/ios/Chromoting_unittests/en.lproj/InfoPlist.strings
@@ -0,0 +1,2 @@
+/* Localized versions of Info.plist keys */
+
diff --git a/remoting/ios/Chromoting_unittests/main.mm b/remoting/ios/Chromoting_unittests/main.mm
new file mode 100644
index 0000000..3efc1cd
--- /dev/null
+++ b/remoting/ios/Chromoting_unittests/main.mm
@@ -0,0 +1,24 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import <UIKit/UIKit.h>
+
+#include "base/memory/scoped_ptr.h"
+#include "base/test/test_suite.h"
+#include "testing/gtest_mac.h"
+
+#import "remoting/ios/Chromoting_unittests/main_no_arc.h"
+
+int main(int argc, char* argv[]) {
+ // Initialization that must occur with no Automatic Reference Counting (ARC)
+ remoting::main_no_arc::init();
+
+ testing::InitGoogleTest(&argc, argv);
+ scoped_ptr<base::TestSuite> runner(new base::TestSuite(argc, argv));
+ runner.get()->Run();
+}
diff --git a/remoting/ios/Chromoting_unittests/main_no_arc.cc b/remoting/ios/Chromoting_unittests/main_no_arc.cc
new file mode 100644
index 0000000..2bfbd87
--- /dev/null
+++ b/remoting/ios/Chromoting_unittests/main_no_arc.cc
@@ -0,0 +1,20 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "remoting/ios/Chromoting_unittests/main_no_arc.h"
+
+#include "base/message_loop/message_loop.h"
+
+namespace remoting {
+namespace main_no_arc {
+
+void init() {
+ // Declare the pump factory before the test suite can declare it. The test
+ // suite assumed we are running in x86 simulator, but this test project runs
+ // on an actual device
+ base::MessageLoop::InitMessagePumpForUIFactory(&base::MessagePumpMac::Create);
+}
+
+} // main_no_arc
+} // remoting
diff --git a/remoting/ios/Chromoting_unittests/main_no_arc.h b/remoting/ios/Chromoting_unittests/main_no_arc.h
new file mode 100644
index 0000000..1d97761
--- /dev/null
+++ b/remoting/ios/Chromoting_unittests/main_no_arc.h
@@ -0,0 +1,11 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+namespace remoting {
+namespace main_no_arc {
+
+void init();
+
+} // main_no_arc
+} // remoting
diff --git a/remoting/ios/DEPS b/remoting/ios/DEPS
new file mode 100644
index 0000000..c3c25f4
--- /dev/null
+++ b/remoting/ios/DEPS
@@ -0,0 +1,3 @@
+include_rules = [
+ "+remoting/host",
+]
diff --git a/remoting/ios/app_delegate.h b/remoting/ios/app_delegate.h
new file mode 100644
index 0000000..bfd60ed
--- /dev/null
+++ b/remoting/ios/app_delegate.h
@@ -0,0 +1,17 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef REMOTING_IOS_APP_DELEGATE_H_
+#define REMOTING_IOS_APP_DELEGATE_H_
+
+#import <UIKit/UIKit.h>
+
+// Default created delegate class for the entire application
+@interface AppDelegate : UIResponder<UIApplicationDelegate>
+
+@property(strong, nonatomic) UIWindow* window;
+
+@end
+
+#endif // REMOTING_IOS_APP_DELEGATE_H_ \ No newline at end of file
diff --git a/remoting/ios/app_delegate.mm b/remoting/ios/app_delegate.mm
new file mode 100644
index 0000000..7b8fc36
--- /dev/null
+++ b/remoting/ios/app_delegate.mm
@@ -0,0 +1,18 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "remoting/ios/app_delegate.h"
+
+@implementation AppDelegate
+
+- (BOOL)application:(UIApplication*)application
+ didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
+ return YES;
+}
+
+@end
diff --git a/remoting/ios/authorize.h b/remoting/ios/authorize.h
new file mode 100644
index 0000000..1b3e0ea
--- /dev/null
+++ b/remoting/ios/authorize.h
@@ -0,0 +1,30 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef REMOTING_IOS_AUTHORIZE_H_
+#define REMOTING_IOS_AUTHORIZE_H_
+
+#import <UIKit/UIKit.h>
+
+// TODO (aboone) This include is for The Google Toolbox for Mac OAuth 2
+// https://code.google.com/p/gtm-oauth2/ This may need to be added as a
+// third-party or locate the proper project in Chromium.
+#import "GTMOAuth2Authentication.h"
+
+@interface Authorize : NSObject
+
++ (GTMOAuth2Authentication*)getAnyExistingAuthorization;
+
++ (void)beginRequest:(GTMOAuth2Authentication*)authorization
+ delegate:self
+ didFinishSelector:(SEL)sel;
+
++ (void)appendCredentials:(NSMutableURLRequest*)request;
+
++ (UINavigationController*)createLoginController:(id)delegate
+ finishedSelector:(SEL)finishedSelector;
+
+@end
+
+#endif // REMOTING_IOS_AUTHORIZE_H_
diff --git a/remoting/ios/authorize.mm b/remoting/ios/authorize.mm
new file mode 100644
index 0000000..92f467c
--- /dev/null
+++ b/remoting/ios/authorize.mm
@@ -0,0 +1,123 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "remoting/ios/authorize.h"
+
+// TODO (aboone) This include is for The Google Toolbox for Mac OAuth 2
+// Controllers https://code.google.com/p/gtm-oauth2/ This may need to be added
+// as a third-party or locate the proper project in Chromium.
+#import "GTMOAuth2ViewControllerTouch.h"
+
+#include "google_apis/google_api_keys.h"
+// TODO (aboone) Pulling in some service values from the host side. The cc's
+// are also compiled as part of this project because the target remoting_host
+// does not build on iOS right now.
+#include "remoting/host/service_urls.h"
+#include "remoting/host/setup/oauth_helper.h"
+
+namespace {
+static NSString* const kKeychainItemName = @"Google Chromoting iOS";
+
+NSString* ClientId() {
+ return
+ [NSString stringWithUTF8String:google_apis::GetOAuth2ClientID(
+ google_apis::CLIENT_REMOTING).c_str()];
+}
+
+NSString* ClientSecret() {
+ return
+ [NSString stringWithUTF8String:google_apis::GetOAuth2ClientSecret(
+ google_apis::CLIENT_REMOTING).c_str()];
+}
+
+NSString* Scopes() {
+ return [NSString stringWithUTF8String:remoting::GetOauthScope().c_str()];
+}
+
+NSMutableString* HostURL() {
+ return
+ [NSMutableString stringWithUTF8String:remoting::ServiceUrls::GetInstance()
+ ->directory_hosts_url()
+ .c_str()];
+}
+
+NSString* APIKey() {
+ return [NSString stringWithUTF8String:google_apis::GetAPIKey().c_str()];
+}
+
+} // namespace
+
+@implementation Authorize
+
++ (GTMOAuth2Authentication*)getAnyExistingAuthorization {
+ // Ensure the google_apis lib has keys
+ // If this check fails then google_apis was not built right
+ // TODO (aboone) For now we specify the preprocessor macros for
+ // GOOGLE_CLIENT_SECRET_REMOTING and GOOGLE_CLIENT_ID_REMOTING when building
+ // the google_apis target. The values may be developer specific, and should
+ // be well know to the project staff.
+ // See http://www.chromium.org/developers/how-tos/api-keys for more general
+ // information.
+ DCHECK(![ClientId() isEqualToString:@"dummytoken"]);
+
+ return [GTMOAuth2ViewControllerTouch
+ authForGoogleFromKeychainForName:kKeychainItemName
+ clientID:ClientId()
+ clientSecret:ClientSecret()];
+}
+
++ (void)beginRequest:(GTMOAuth2Authentication*)authReq
+ delegate:(id)delegate
+ didFinishSelector:(SEL)sel {
+ // Build request URL using API HTTP endpoint, and our api key
+ NSMutableString* hostsUrl = HostURL();
+ [hostsUrl appendString:@"?key="];
+ [hostsUrl appendString:APIKey()];
+
+ NSMutableURLRequest* theRequest =
+ [NSMutableURLRequest requestWithURL:[NSURL URLWithString:hostsUrl]];
+
+ // Add scopes if needed
+ NSString* scope = authReq.scope;
+
+ if ([scope rangeOfString:Scopes()].location == NSNotFound) {
+ scope = [GTMOAuth2Authentication scopeWithStrings:scope, Scopes(), nil];
+ authReq.scope = scope;
+ }
+
+ // Execute request async
+ [authReq authorizeRequest:theRequest delegate:delegate didFinishSelector:sel];
+}
+
++ (void)appendCredentials:(NSMutableURLRequest*)request {
+ // Add credentials for service
+ [request addValue:ClientId() forHTTPHeaderField:@"client_id"];
+ [request addValue:ClientSecret() forHTTPHeaderField:@"client_secret"];
+}
+
++ (UINavigationController*)createLoginController:(id)delegate
+ finishedSelector:(SEL)finishedSelector {
+ [GTMOAuth2ViewControllerTouch
+ removeAuthFromKeychainForName:kKeychainItemName];
+
+ // When the sign in is complete a http redirection occurs, and the
+ // user would see the output. We do not want the user to notice this
+ // transition. Wrapping the oAuth2 Controller in a
+ // UINavigationController causes the view to render as a blank/black
+ // page when a http redirection occurs.
+ return [[UINavigationController alloc]
+ initWithRootViewController:[[GTMOAuth2ViewControllerTouch alloc]
+ initWithScope:Scopes()
+ clientID:ClientId()
+ clientSecret:ClientSecret()
+ keychainItemName:kKeychainItemName
+ delegate:delegate
+ finishedSelector:finishedSelector]];
+}
+
+@end
diff --git a/remoting/ios/bridge/DEPS b/remoting/ios/bridge/DEPS
new file mode 100644
index 0000000..565acfc
--- /dev/null
+++ b/remoting/ios/bridge/DEPS
@@ -0,0 +1,8 @@
+include_rules = [
+ "+net/url_request",
+
+ "-remoting/host",
+ "+remoting/client",
+ "+remoting/jingle_glue",
+ "+remoting/protocol",
+]
diff --git a/remoting/ios/bridge/client_instance.cc b/remoting/ios/bridge/client_instance.cc
new file mode 100644
index 0000000..6066f95
--- /dev/null
+++ b/remoting/ios/bridge/client_instance.cc
@@ -0,0 +1,397 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "remoting/ios/bridge/client_instance.h"
+
+#include "base/bind.h"
+#include "base/logging.h"
+#include "base/synchronization/waitable_event.h"
+#include "net/socket/client_socket_factory.h"
+#include "remoting/base/url_request_context.h"
+#include "remoting/client/audio_player.h"
+#include "remoting/client/plugin/delegating_signal_strategy.h"
+#include "remoting/ios/bridge/client_proxy.h"
+#include "remoting/jingle_glue/chromium_port_allocator.h"
+#include "remoting/protocol/host_stub.h"
+#include "remoting/protocol/libjingle_transport_factory.h"
+
+namespace {
+const char* const kXmppServer = "talk.google.com";
+const int kXmppPort = 5222;
+const bool kXmppUseTls = true;
+
+void DoNothing() {}
+} // namespace
+
+namespace remoting {
+
+ClientInstance::ClientInstance(const base::WeakPtr<ClientProxy>& proxy,
+ const std::string& username,
+ const std::string& auth_token,
+ const std::string& host_jid,
+ const std::string& host_id,
+ const std::string& host_pubkey,
+ const std::string& pairing_id,
+ const std::string& pairing_secret)
+ : proxyToClient_(proxy), host_id_(host_id), create_pairing_(false) {
+
+ if (!base::MessageLoop::current()) {
+ VLOG(1) << "Starting main message loop";
+ ui_loop_ = new base::MessageLoopForUI();
+ ui_loop_->Attach();
+ } else {
+ VLOG(1) << "Using existing main message loop";
+ ui_loop_ = base::MessageLoopForUI::current();
+ }
+
+ VLOG(1) << "Spawning additional threads";
+
+ // |ui_loop_| runs on the main thread, so |ui_task_runner_| will run on the
+ // main thread. We can not kill the main thread when the message loop becomes
+ // idle so the callback function does nothing (as opposed to the typical
+ // base::MessageLoop::QuitClosure())
+ ui_task_runner_ = new AutoThreadTaskRunner(ui_loop_->message_loop_proxy(),
+ base::Bind(&::DoNothing));
+
+ network_task_runner_ = AutoThread::CreateWithType(
+ "native_net", ui_task_runner_, base::MessageLoop::TYPE_IO);
+
+ url_requester_ = new URLRequestContextGetter(network_task_runner_);
+
+ client_context_.reset(new ClientContext(network_task_runner_));
+
+ DCHECK(ui_task_runner_->BelongsToCurrentThread());
+
+ // Initialize XMPP config.
+ xmpp_config_.host = kXmppServer;
+ xmpp_config_.port = kXmppPort;
+ xmpp_config_.use_tls = kXmppUseTls;
+ xmpp_config_.username = username;
+ xmpp_config_.auth_token = auth_token;
+ xmpp_config_.auth_service = "oauth2";
+
+ // Initialize ClientConfig.
+ client_config_.host_jid = host_jid;
+ client_config_.host_public_key = host_pubkey;
+ client_config_.authentication_tag = host_id_;
+ client_config_.client_pairing_id = pairing_id;
+ client_config_.client_paired_secret = pairing_secret;
+ client_config_.authentication_methods.push_back(
+ protocol::AuthenticationMethod::FromString("spake2_pair"));
+ client_config_.authentication_methods.push_back(
+ protocol::AuthenticationMethod::FromString("spake2_hmac"));
+ client_config_.authentication_methods.push_back(
+ protocol::AuthenticationMethod::FromString("spake2_plain"));
+}
+
+ClientInstance::~ClientInstance() {}
+
+void ClientInstance::Start() {
+ DCHECK(ui_task_runner_->BelongsToCurrentThread());
+
+ // Creates a reference to |this|, so don't want to bind during constructor
+ client_config_.fetch_secret_callback =
+ base::Bind(&ClientInstance::FetchSecret, this);
+
+ view_.reset(new FrameConsumerBridge(
+ base::Bind(&ClientProxy::RedrawCanvas, proxyToClient_)));
+
+ // |consumer_proxy| must be created on the UI thread to proxy calls from the
+ // network or decode thread to the UI thread, but ownership will belong to a
+ // SoftwareVideoRenderer which runs on the network thread.
+ scoped_refptr<FrameConsumerProxy> consumer_proxy =
+ new FrameConsumerProxy(ui_task_runner_, view_->AsWeakPtr());
+
+ // Post a task to start connection
+ base::WaitableEvent done_event(true, false);
+ network_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&ClientInstance::ConnectToHostOnNetworkThread,
+ this,
+ consumer_proxy,
+ base::Bind(&base::WaitableEvent::Signal,
+ base::Unretained(&done_event))));
+ // Wait until initialization completes before continuing
+ done_event.Wait();
+}
+
+void ClientInstance::Cleanup() {
+ DCHECK(ui_task_runner_->BelongsToCurrentThread());
+
+ client_config_.fetch_secret_callback.Reset(); // Release ref to this
+ // |view_| must be destroyed on the UI thread before the producer is gone.
+ view_.reset();
+
+ base::WaitableEvent done_event(true, false);
+ network_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&ClientInstance::DisconnectFromHostOnNetworkThread,
+ this,
+ base::Bind(&base::WaitableEvent::Signal,
+ base::Unretained(&done_event))));
+ // Wait until we are fully disconnected before continuing
+ done_event.Wait();
+}
+
+// HOST attempts to continue automatically with previously supplied credentials,
+// if it can't it requests the user's PIN.
+void ClientInstance::FetchSecret(
+ bool pairable,
+ const protocol::SecretFetchedCallback& callback) {
+ if (!ui_task_runner_->BelongsToCurrentThread()) {
+ ui_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&ClientInstance::FetchSecret, this, pairable, callback));
+ return;
+ }
+
+ pin_callback_ = callback;
+
+ if (proxyToClient_) {
+ if (!client_config_.client_pairing_id.empty()) {
+ // We attempted to connect using an existing pairing that was rejected.
+ // Unless we forget about the stale credentials, we'll continue trying
+ // them.
+ VLOG(1) << "Deleting rejected pairing credentials";
+
+ proxyToClient_->CommitPairingCredentials(host_id_, "", "");
+ }
+ proxyToClient_->DisplayAuthenticationPrompt(pairable);
+ }
+}
+
+void ClientInstance::ProvideSecret(const std::string& pin,
+ bool create_pairing) {
+ DCHECK(ui_task_runner_->BelongsToCurrentThread());
+ create_pairing_ = create_pairing;
+
+ // Before this function can complete, FetchSecret must be called
+ DCHECK(!pin_callback_.is_null());
+ network_task_runner_->PostTask(FROM_HERE, base::Bind(pin_callback_, pin));
+}
+
+void ClientInstance::PerformMouseAction(
+ const webrtc::DesktopVector& position,
+ const webrtc::DesktopVector& wheel_delta,
+ int /* protocol::MouseEvent_MouseButton */ whichButton,
+ bool button_down) {
+ if (!network_task_runner_->BelongsToCurrentThread()) {
+ network_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&ClientInstance::PerformMouseAction,
+ this,
+ position,
+ wheel_delta,
+ whichButton,
+ button_down));
+ return;
+ }
+
+ protocol::MouseEvent_MouseButton mButton;
+
+ // Button must be within the bounds of the MouseEvent_MouseButton enum.
+ switch (whichButton) {
+ case protocol::MouseEvent_MouseButton::MouseEvent_MouseButton_BUTTON_LEFT:
+ mButton =
+ protocol::MouseEvent_MouseButton::MouseEvent_MouseButton_BUTTON_LEFT;
+ break;
+ case protocol::MouseEvent_MouseButton::MouseEvent_MouseButton_BUTTON_MAX:
+ mButton =
+ protocol::MouseEvent_MouseButton::MouseEvent_MouseButton_BUTTON_MAX;
+ break;
+ case protocol::MouseEvent_MouseButton::MouseEvent_MouseButton_BUTTON_MIDDLE:
+ mButton = protocol::MouseEvent_MouseButton::
+ MouseEvent_MouseButton_BUTTON_MIDDLE;
+ break;
+ case protocol::MouseEvent_MouseButton::MouseEvent_MouseButton_BUTTON_RIGHT:
+ mButton =
+ protocol::MouseEvent_MouseButton::MouseEvent_MouseButton_BUTTON_RIGHT;
+ break;
+ case protocol::MouseEvent_MouseButton::
+ MouseEvent_MouseButton_BUTTON_UNDEFINED:
+ mButton = protocol::MouseEvent_MouseButton::
+ MouseEvent_MouseButton_BUTTON_UNDEFINED;
+ break;
+ default:
+ LOG(FATAL) << "Invalid constant for MouseEvent_MouseButton";
+ mButton = protocol::MouseEvent_MouseButton::
+ MouseEvent_MouseButton_BUTTON_UNDEFINED;
+ break;
+ }
+
+ protocol::MouseEvent action;
+ action.set_x(position.x());
+ action.set_y(position.y());
+ action.set_wheel_delta_x(wheel_delta.x());
+ action.set_wheel_delta_y(wheel_delta.y());
+ action.set_button(mButton);
+ if (mButton != protocol::MouseEvent::BUTTON_UNDEFINED)
+ action.set_button_down(button_down);
+
+ connection_->input_stub()->InjectMouseEvent(action);
+}
+
+void ClientInstance::PerformKeyboardAction(int key_code, bool key_down) {
+ if (!network_task_runner_->BelongsToCurrentThread()) {
+ network_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(
+ &ClientInstance::PerformKeyboardAction, this, key_code, key_down));
+ return;
+ }
+
+ protocol::KeyEvent action;
+ action.set_usb_keycode(key_code);
+ action.set_pressed(key_down);
+ connection_->input_stub()->InjectKeyEvent(action);
+}
+
+void ClientInstance::OnConnectionState(protocol::ConnectionToHost::State state,
+ protocol::ErrorCode error) {
+ if (!ui_task_runner_->BelongsToCurrentThread()) {
+ ui_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&ClientInstance::OnConnectionState, this, state, error));
+ return;
+ }
+
+ // TODO (aboone) This functionality is not scheduled for QA yet.
+ // if (create_pairing_ && state == protocol::ConnectionToHost::CONNECTED) {
+ // VLOG(1) << "Attempting to pair with host";
+ // protocol::PairingRequest request;
+ // request.set_client_name("iOS");
+ // connection_->host_stub()->RequestPairing(request);
+ // }
+
+ if (proxyToClient_)
+ proxyToClient_->ReportConnectionStatus(state, error);
+}
+
+void ClientInstance::OnConnectionReady(bool ready) {
+ // We ignore this message, since OnConnectionState tells us the same thing.
+}
+
+void ClientInstance::OnRouteChanged(const std::string& channel_name,
+ const protocol::TransportRoute& route) {
+ VLOG(1) << "Using " << protocol::TransportRoute::GetTypeString(route.type)
+ << " connection for " << channel_name << " channel";
+}
+
+void ClientInstance::SetCapabilities(const std::string& capabilities) {
+ DCHECK(video_renderer_);
+ DCHECK(connection_);
+ DCHECK(connection_->state() == protocol::ConnectionToHost::CONNECTED);
+ video_renderer_->Initialize(connection_->config());
+}
+
+void ClientInstance::SetPairingResponse(
+ const protocol::PairingResponse& response) {
+ if (!ui_task_runner_->BelongsToCurrentThread()) {
+ ui_task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&ClientInstance::SetPairingResponse, this, response));
+ return;
+ }
+
+ VLOG(1) << "Successfully established pairing with host";
+
+ if (proxyToClient_)
+ proxyToClient_->CommitPairingCredentials(
+ host_id_, response.client_id(), response.shared_secret());
+}
+
+void ClientInstance::DeliverHostMessage(
+ const protocol::ExtensionMessage& message) {
+ NOTIMPLEMENTED();
+}
+
+// Returning interface of protocol::ClipboardStub
+protocol::ClipboardStub* ClientInstance::GetClipboardStub() { return this; }
+
+// Returning interface of protocol::CursorShapeStub
+protocol::CursorShapeStub* ClientInstance::GetCursorShapeStub() { return this; }
+
+scoped_ptr<protocol::ThirdPartyClientAuthenticator::TokenFetcher>
+ClientInstance::GetTokenFetcher(const std::string& host_public_key) {
+ // Returns null when third-party authentication is unsupported.
+ return scoped_ptr<protocol::ThirdPartyClientAuthenticator::TokenFetcher>();
+}
+
+void ClientInstance::InjectClipboardEvent(
+ const protocol::ClipboardEvent& event) {
+ NOTIMPLEMENTED();
+}
+
+void ClientInstance::SetCursorShape(const protocol::CursorShapeInfo& shape) {
+ if (!ui_task_runner_->BelongsToCurrentThread()) {
+ ui_task_runner_->PostTask(
+ FROM_HERE, base::Bind(&ClientInstance::SetCursorShape, this, shape));
+ return;
+ }
+ if (proxyToClient_)
+ proxyToClient_->UpdateCursorShape(shape);
+}
+
+void ClientInstance::ConnectToHostOnNetworkThread(
+ scoped_refptr<FrameConsumerProxy> consumer_proxy,
+ const base::Closure& done) {
+ DCHECK(network_task_runner_->BelongsToCurrentThread());
+
+ client_context_->Start();
+
+ video_renderer_.reset(
+ new SoftwareVideoRenderer(client_context_->main_task_runner(),
+ client_context_->decode_task_runner(),
+ consumer_proxy));
+
+ view_->Initialize(video_renderer_.get());
+
+ connection_.reset(new protocol::ConnectionToHost(true));
+
+ client_.reset(new ChromotingClient(client_config_,
+ client_context_.get(),
+ connection_.get(),
+ this,
+ video_renderer_.get(),
+ scoped_ptr<AudioPlayer>()));
+
+ signaling_.reset(
+ new XmppSignalStrategy(net::ClientSocketFactory::GetDefaultFactory(),
+ url_requester_,
+ xmpp_config_));
+
+ NetworkSettings network_settings(NetworkSettings::NAT_TRAVERSAL_ENABLED);
+
+ scoped_ptr<ChromiumPortAllocator> port_allocator(
+ ChromiumPortAllocator::Create(url_requester_, network_settings));
+
+ scoped_ptr<protocol::TransportFactory> transport_factory(
+ new protocol::LibjingleTransportFactory(
+ signaling_.get(),
+ port_allocator.PassAs<cricket::HttpPortAllocatorBase>(),
+ network_settings));
+
+ client_->Start(signaling_.get(), transport_factory.Pass());
+
+ if (!done.is_null())
+ done.Run();
+}
+
+void ClientInstance::DisconnectFromHostOnNetworkThread(
+ const base::Closure& done) {
+ DCHECK(network_task_runner_->BelongsToCurrentThread());
+
+ host_id_.clear();
+
+ // |client_| must be torn down before |signaling_|.
+ connection_.reset();
+ client_.reset();
+ signaling_.reset();
+ video_renderer_.reset();
+ client_context_->Stop();
+ if (!done.is_null())
+ done.Run();
+}
+
+} // namespace remoting
diff --git a/remoting/ios/bridge/client_instance.h b/remoting/ios/bridge/client_instance.h
new file mode 100644
index 0000000..18f1cef
--- /dev/null
+++ b/remoting/ios/bridge/client_instance.h
@@ -0,0 +1,164 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef REMOTING_IOS_BRIDGE_CLIENT_INSTANCE_H_
+#define REMOTING_IOS_BRIDGE_CLIENT_INSTANCE_H_
+
+#include <string>
+
+#include "base/memory/ref_counted.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "base/message_loop/message_loop.h"
+#include "net/url_request/url_request_context_getter.h"
+#include "remoting/base/auto_thread.h"
+#include "remoting/client/chromoting_client.h"
+#include "remoting/client/client_config.h"
+#include "remoting/client/client_context.h"
+#include "remoting/client/client_user_interface.h"
+#include "remoting/client/frame_consumer_proxy.h"
+#include "remoting/client/software_video_renderer.h"
+
+#include "remoting/ios/bridge/frame_consumer_bridge.h"
+
+#include "remoting/jingle_glue/network_settings.h"
+#include "remoting/jingle_glue/xmpp_signal_strategy.h"
+#include "remoting/protocol/clipboard_stub.h"
+#include "remoting/protocol/connection_to_host.h"
+#include "remoting/protocol/cursor_shape_stub.h"
+
+namespace remoting {
+
+class ClientProxy;
+
+// ClientUserInterface that indirectly makes and receives OBJ_C calls from the
+// UI application
+class ClientInstance : public ClientUserInterface,
+ public protocol::ClipboardStub,
+ public protocol::CursorShapeStub,
+ public base::RefCountedThreadSafe<ClientInstance> {
+ public:
+ // Initiates a connection with the specified host. Call from the UI thread. To
+ // connect with an unpaired host, pass in |pairing_id| and |pairing_secret| as
+ // empty strings.
+ ClientInstance(const base::WeakPtr<ClientProxy>& proxy,
+ const std::string& username,
+ const std::string& auth_token,
+ const std::string& host_jid,
+ const std::string& host_id,
+ const std::string& host_pubkey,
+ const std::string& pairing_id,
+ const std::string& pairing_secret);
+
+ // Begins the connection process. Should not be called again until after
+ // |CleanUp|
+ void Start();
+
+ // Terminates the current connection (if it hasn't already failed) and cleans
+ // up. Must be called before destruction can occur or a memory leak may occur.
+ void Cleanup();
+
+ // Notifies the user interface that the user needs to enter a PIN. The
+ // current authentication attempt is put on hold until |callback| is invoked.
+ // May be called on any thread.
+ void FetchSecret(bool pairable,
+ const protocol::SecretFetchedCallback& callback);
+
+ // Provides the user's PIN and resumes the host authentication attempt. Call
+ // on the UI thread once the user has finished entering this PIN into the UI,
+ // but only after the UI has been asked to provide a PIN (via FetchSecret()).
+ void ProvideSecret(const std::string& pin, bool create_pair);
+
+ // Moves the host's cursor to the specified coordinates, optionally with some
+ // mouse button depressed. If |button| is BUTTON_UNDEFINED, no click is made.
+ void PerformMouseAction(
+ const webrtc::DesktopVector& position,
+ const webrtc::DesktopVector& wheel_delta,
+ int /* protocol::MouseEvent_MouseButton */ whichButton,
+ bool button_down);
+
+ // Sends the provided keyboard scan code to the host.
+ void PerformKeyboardAction(int key_code, bool key_down);
+
+ // ClientUserInterface implementation.
+ virtual void OnConnectionState(protocol::ConnectionToHost::State state,
+ protocol::ErrorCode error) OVERRIDE;
+ virtual void OnConnectionReady(bool ready) OVERRIDE;
+ virtual void OnRouteChanged(const std::string& channel_name,
+ const protocol::TransportRoute& route) OVERRIDE;
+ virtual void SetCapabilities(const std::string& capabilities) OVERRIDE;
+ virtual void SetPairingResponse(const protocol::PairingResponse& response)
+ OVERRIDE;
+ virtual void DeliverHostMessage(const protocol::ExtensionMessage& message)
+ OVERRIDE;
+ virtual protocol::ClipboardStub* GetClipboardStub() OVERRIDE;
+ virtual protocol::CursorShapeStub* GetCursorShapeStub() OVERRIDE;
+ virtual scoped_ptr<protocol::ThirdPartyClientAuthenticator::TokenFetcher>
+ GetTokenFetcher(const std::string& host_public_key) OVERRIDE;
+
+ // CursorShapeStub implementation.
+ virtual void InjectClipboardEvent(const protocol::ClipboardEvent& event)
+ OVERRIDE;
+
+ // ClipboardStub implementation.
+ virtual void SetCursorShape(const protocol::CursorShapeInfo& shape) OVERRIDE;
+
+ private:
+ // This object is ref-counted, so it cleans itself up.
+ virtual ~ClientInstance();
+
+ void ConnectToHostOnNetworkThread(
+ scoped_refptr<FrameConsumerProxy> consumer_proxy,
+ const base::Closure& done);
+ void DisconnectFromHostOnNetworkThread(const base::Closure& done);
+
+ // Proxy to exchange messages between the
+ // common Chromoting protocol and UI Application.
+ base::WeakPtr<ClientProxy> proxyToClient_;
+
+ // ID of the host we are connecting to.
+ std::string host_id_;
+
+ // This group of variables is to be used on the display thread.
+ scoped_ptr<SoftwareVideoRenderer> video_renderer_;
+ scoped_ptr<FrameConsumerBridge> view_;
+
+ // This group of variables is to be used on the network thread.
+ ClientConfig client_config_;
+ scoped_ptr<ClientContext> client_context_;
+ scoped_ptr<protocol::ConnectionToHost> connection_;
+ scoped_ptr<ChromotingClient> client_;
+ XmppSignalStrategy::XmppServerConfig xmpp_config_;
+ scoped_ptr<XmppSignalStrategy> signaling_; // Must outlive client_
+
+ // Pass this the user's PIN once we have it. To be assigned and accessed on
+ // the UI thread, but must be posted to the network thread to call it.
+ protocol::SecretFetchedCallback pin_callback_;
+
+ // Indicates whether to establish a new pairing with this host. This is
+ // modified in ProvideSecret(), but thereafter to be used only from the
+ // network thread. (This is safe because ProvideSecret() is invoked at most
+ // once per run, and always before any reference to this flag.)
+ bool create_pairing_;
+
+ // Chromium code's connection to the OBJ_C message loop. Once created the
+ // MessageLoop will live for the life of the program. An attempt was made to
+ // create the primary message loop earlier in the programs life, but a
+ // MessageLoop requires ARC to be disabled.
+ base::MessageLoopForUI* ui_loop_;
+
+ // References to native threads.
+ scoped_refptr<AutoThreadTaskRunner> ui_task_runner_;
+ scoped_refptr<AutoThreadTaskRunner> network_task_runner_;
+
+ scoped_refptr<net::URLRequestContextGetter> url_requester_;
+
+ friend class base::RefCountedThreadSafe<ClientInstance>;
+
+ DISALLOW_COPY_AND_ASSIGN(ClientInstance);
+};
+
+} // namespace remoting
+
+#endif // REMOTING_IOS_BRIDGE_CLIENT_INSTANCE_H_
diff --git a/remoting/ios/bridge/client_instance_unittest.mm b/remoting/ios/bridge/client_instance_unittest.mm
new file mode 100644
index 0000000..8470e7a
--- /dev/null
+++ b/remoting/ios/bridge/client_instance_unittest.mm
@@ -0,0 +1,319 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "remoting/ios/bridge/client_instance.h"
+
+#include "base/compiler_specific.h"
+#include "base/run_loop.h"
+#include "base/mac/scoped_nsobject.h"
+#include "base/synchronization/waitable_event.h"
+#include "remoting/protocol/libjingle_transport_factory.h"
+#import "testing/gtest_mac.h"
+
+#include "remoting/ios/data_store.h"
+#include "remoting/ios/bridge/client_proxy.h"
+#include "remoting/ios/bridge/client_proxy_delegate_wrapper.h"
+
+@interface ClientProxyDelegateForClientInstanceTester
+ : NSObject<ClientProxyDelegate>
+
+- (void)resetDidReceiveSomething;
+
+// Validating what was received is outside of the scope for this test unit. See
+// ClientProxyUnittest for those tests.
+@property(nonatomic, assign) BOOL didReceiveSomething;
+
+@end
+
+@implementation ClientProxyDelegateForClientInstanceTester
+
+@synthesize didReceiveSomething = _didReceiveSomething;
+
+- (void)resetDidReceiveSomething {
+ _didReceiveSomething = false;
+}
+
+- (void)requestHostPin:(BOOL)pairingSupported {
+ _didReceiveSomething = true;
+}
+
+- (void)connected {
+ _didReceiveSomething = true;
+}
+
+- (void)connectionStatus:(NSString*)statusMessage {
+ _didReceiveSomething = true;
+}
+
+- (void)connectionFailed:(NSString*)errorMessage {
+ _didReceiveSomething = true;
+}
+
+- (void)applyFrame:(const webrtc::DesktopSize&)size
+ stride:(NSInteger)stride
+ data:(uint8_t*)data
+ regions:(const std::vector<webrtc::DesktopRect>&)regions {
+ _didReceiveSomething = true;
+}
+
+- (void)applyCursor:(const webrtc::DesktopSize&)size
+ hotspot:(const webrtc::DesktopVector&)hotspot
+ cursorData:(uint8_t*)data {
+ _didReceiveSomething = true;
+}
+
+@end
+
+namespace remoting {
+
+namespace {
+
+const std::string kHostId = "HostIdTest";
+const std::string kPairingId = "PairingIdTest";
+const std::string kPairingSecret = "PairingSecretTest";
+const std::string kSecretPin = "SecretPinTest";
+
+// TODO(aboone) should be able to call RunLoop().RunUntilIdle() instead but
+// MessagePumpUIApplication::DoRun is marked NOTREACHED()
+void RunCFMessageLoop() {
+ int result;
+ do { // Repeat until no messages remain
+ result = CFRunLoopRunInMode(
+ kCFRunLoopDefaultMode,
+ 0, // Execute queued messages, do not wait for additional messages
+ YES); // Do only one message at a time
+ } while (result != kCFRunLoopRunStopped && result != kCFRunLoopRunFinished &&
+ result != kCFRunLoopRunTimedOut);
+}
+
+void SecretPinCallBack(const std::string& secret) {
+ ASSERT_STREQ(kSecretPin.c_str(), secret.c_str());
+}
+
+} // namespace
+
+class ClientInstanceTest : public ::testing::Test {
+ protected:
+ virtual void SetUp() OVERRIDE {
+ testDelegate_.reset(
+ [[ClientProxyDelegateForClientInstanceTester alloc] init]);
+ proxy_.reset(new ClientProxy(
+ [ClientProxyDelegateWrapper wrapDelegate:testDelegate_]));
+ instance_ =
+ new ClientInstance(proxy_->AsWeakPtr(), "", "", "", "", "", "", "");
+ }
+ virtual void TearDown() OVERRIDE {
+ // Ensure memory is not leaking
+ // Notice Cleanup is safe to call, regardless of if Start() was ever called.
+ instance_->Cleanup();
+ RunCFMessageLoop();
+ // An object on the network thread which owns a reference to |instance_| may
+ // be cleaned up 'soon', but not immediately. Lets wait it out, up to 1
+ // second.
+ for (int i = 0; i < 100; i++) {
+ if (!instance_->HasOneRef()) {
+ [NSThread sleepForTimeInterval:.01];
+ } else {
+ break;
+ }
+ }
+
+ // Remove the last reference from |instance_|, and destructor is called.
+ ASSERT_TRUE(instance_->HasOneRef());
+ instance_ = NULL;
+ }
+
+ void AssertAcknowledged(BOOL wasAcknowledged) {
+ ASSERT_EQ(wasAcknowledged, [testDelegate_ didReceiveSomething]);
+ // Reset for the next test
+ [testDelegate_ resetDidReceiveSomething];
+ }
+
+ void TestStatusAndError(protocol::ConnectionToHost::State state,
+ protocol::ErrorCode error) {
+ instance_->OnConnectionState(state, error);
+ AssertAcknowledged(true);
+ }
+
+ void TestConnectionStatus(protocol::ConnectionToHost::State state) {
+ TestStatusAndError(state, protocol::ErrorCode::OK);
+ TestStatusAndError(state, protocol::ErrorCode::PEER_IS_OFFLINE);
+ TestStatusAndError(state, protocol::ErrorCode::SESSION_REJECTED);
+ TestStatusAndError(state, protocol::ErrorCode::INCOMPATIBLE_PROTOCOL);
+ TestStatusAndError(state, protocol::ErrorCode::AUTHENTICATION_FAILED);
+ TestStatusAndError(state, protocol::ErrorCode::CHANNEL_CONNECTION_ERROR);
+ TestStatusAndError(state, protocol::ErrorCode::SIGNALING_ERROR);
+ TestStatusAndError(state, protocol::ErrorCode::SIGNALING_TIMEOUT);
+ TestStatusAndError(state, protocol::ErrorCode::HOST_OVERLOAD);
+ TestStatusAndError(state, protocol::ErrorCode::UNKNOWN_ERROR);
+ }
+
+ base::scoped_nsobject<ClientProxyDelegateForClientInstanceTester>
+ testDelegate_;
+ scoped_ptr<ClientProxy> proxy_;
+ scoped_refptr<ClientInstance> instance_;
+};
+
+TEST_F(ClientInstanceTest, Create) {
+ // This is a test for memory leaking. Ensure a completely unused instance of
+ // ClientInstance is destructed.
+
+ ASSERT_TRUE(instance_ != NULL);
+ ASSERT_TRUE(instance_->HasOneRef());
+}
+
+TEST_F(ClientInstanceTest, CreateAndStart) {
+ // This is a test for memory leaking. Ensure a properly used instance of
+ // ClientInstance is destructed.
+
+ ASSERT_TRUE(instance_ != NULL);
+ ASSERT_TRUE(instance_->HasOneRef());
+
+ instance_->Start();
+ RunCFMessageLoop();
+ ASSERT_TRUE(!instance_->HasOneRef()); // more than one
+}
+
+TEST_F(ClientInstanceTest, SecretPin) {
+ NSString* hostId = [NSString stringWithUTF8String:kHostId.c_str()];
+ NSString* pairingId = [NSString stringWithUTF8String:kPairingId.c_str()];
+ NSString* pairingSecret =
+ [NSString stringWithUTF8String:kPairingSecret.c_str()];
+
+ DataStore* store = [DataStore sharedStore];
+
+ const HostPreferences* host = [store createHost:hostId];
+ host.pairId = pairingId;
+ host.pairSecret = pairingSecret;
+ [store saveChanges];
+
+ // Suggesting that our pairing Id is known, but since its obviously not we
+ // expect it to be discarded when requesting the PIN.
+ instance_ = new ClientInstance(
+ proxy_->AsWeakPtr(), "", "", "", kHostId, "", kPairingId, kPairingSecret);
+
+ instance_->Start();
+ RunCFMessageLoop();
+
+ instance_->FetchSecret(false, base::Bind(&SecretPinCallBack));
+ RunCFMessageLoop();
+ AssertAcknowledged(true);
+
+ // The pairing information was discarded
+ host = [store getHostForId:hostId];
+ ASSERT_NSEQ(@"", host.pairId);
+ ASSERT_NSEQ(@"", host.pairSecret);
+
+ instance_->ProvideSecret(kSecretPin, false);
+ RunCFMessageLoop();
+}
+
+TEST_F(ClientInstanceTest, NoProxy) {
+ // After the proxy is released, we still expect quite a few functions to be
+ // able to run, but not produce any output. Some of these are just being
+ // executed for code coverage, the outputs are not pertinent to this test
+ // unit.
+ proxy_.reset();
+
+ instance_->Start();
+ RunCFMessageLoop();
+
+ instance_->FetchSecret(false, base::Bind(&SecretPinCallBack));
+ AssertAcknowledged(false);
+
+ instance_->ProvideSecret(kSecretPin, false);
+ AssertAcknowledged(false);
+
+ instance_->PerformMouseAction(
+ webrtc::DesktopVector(0, 0), webrtc::DesktopVector(0, 0), 0, false);
+ AssertAcknowledged(false);
+
+ instance_->PerformKeyboardAction(0, false);
+ AssertAcknowledged(false);
+
+ instance_->OnConnectionState(protocol::ConnectionToHost::State::CONNECTED,
+ protocol::ErrorCode::OK);
+ AssertAcknowledged(false);
+
+ instance_->OnConnectionReady(false);
+ AssertAcknowledged(false);
+
+ instance_->OnRouteChanged("", protocol::TransportRoute());
+ AssertAcknowledged(false);
+
+ // SetCapabilities requires a host connection to be established
+ // instance_->SetCapabilities("");
+ // AssertAcknowledged(false);
+
+ instance_->SetPairingResponse(protocol::PairingResponse());
+ AssertAcknowledged(false);
+
+ instance_->DeliverHostMessage(protocol::ExtensionMessage());
+ AssertAcknowledged(false);
+
+ ASSERT_TRUE(instance_->GetClipboardStub() != NULL);
+ ASSERT_TRUE(instance_->GetCursorShapeStub() != NULL);
+ ASSERT_TRUE(instance_->GetTokenFetcher("") == NULL);
+
+ instance_->InjectClipboardEvent(protocol::ClipboardEvent());
+ AssertAcknowledged(false);
+
+ instance_->SetCursorShape(protocol::CursorShapeInfo());
+ AssertAcknowledged(false);
+}
+
+TEST_F(ClientInstanceTest, OnConnectionStateINITIALIZING) {
+ TestConnectionStatus(protocol::ConnectionToHost::State::INITIALIZING);
+}
+
+TEST_F(ClientInstanceTest, OnConnectionStateCONNECTING) {
+ TestConnectionStatus(protocol::ConnectionToHost::State::CONNECTING);
+}
+
+TEST_F(ClientInstanceTest, OnConnectionStateAUTHENTICATED) {
+ TestConnectionStatus(protocol::ConnectionToHost::State::AUTHENTICATED);
+}
+
+TEST_F(ClientInstanceTest, OnConnectionStateCONNECTED) {
+ TestConnectionStatus(protocol::ConnectionToHost::State::CONNECTED);
+}
+
+TEST_F(ClientInstanceTest, OnConnectionStateFAILED) {
+ TestConnectionStatus(protocol::ConnectionToHost::State::FAILED);
+}
+
+TEST_F(ClientInstanceTest, OnConnectionStateCLOSED) {
+ TestConnectionStatus(protocol::ConnectionToHost::State::CLOSED);
+}
+
+TEST_F(ClientInstanceTest, OnConnectionReady) {
+ instance_->OnConnectionReady(true);
+ AssertAcknowledged(false);
+ instance_->OnConnectionReady(false);
+ AssertAcknowledged(false);
+}
+
+TEST_F(ClientInstanceTest, OnRouteChanged) {
+ // Not expecting anything to happen
+ protocol::TransportRoute route;
+
+ route.type = protocol::TransportRoute::DIRECT;
+ instance_->OnRouteChanged("", route);
+ AssertAcknowledged(false);
+
+ route.type = protocol::TransportRoute::STUN;
+ instance_->OnRouteChanged("", route);
+ AssertAcknowledged(false);
+
+ route.type = protocol::TransportRoute::RELAY;
+ instance_->OnRouteChanged("", route);
+ AssertAcknowledged(false);
+}
+
+TEST_F(ClientInstanceTest, SetCursorShape) {
+ instance_->SetCursorShape(protocol::CursorShapeInfo());
+ AssertAcknowledged(true);
+}
+
+} // namespace remoting \ No newline at end of file
diff --git a/remoting/ios/bridge/client_proxy.h b/remoting/ios/bridge/client_proxy.h
new file mode 100644
index 0000000..8088570
--- /dev/null
+++ b/remoting/ios/bridge/client_proxy.h
@@ -0,0 +1,62 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef REMOTING_IOS_BRIDGE_HOST_PROXY_H_
+#define REMOTING_IOS_BRIDGE_HOST_PROXY_H_
+
+#include <string>
+
+#include <objc/objc.h>
+#include "base/memory/weak_ptr.h"
+#include "remoting/protocol/connection_to_host.h"
+#include "third_party/webrtc/modules/desktop_capture/desktop_frame.h"
+
+#if defined(__OBJC__)
+@class ClientProxyDelegateWrapper;
+#else // __OBJC__
+class ClientProxyDelegateWrapper;
+#endif // __OBJC__
+
+namespace remoting {
+
+// Proxies incoming common Chromoting protocol (HOST) to the UI Application
+// (CLIENT). The HOST will have a Weak reference to call member functions on
+// the UI Thread.
+class ClientProxy : public base::SupportsWeakPtr<ClientProxy> {
+ public:
+ ClientProxy(ClientProxyDelegateWrapper* wrapper);
+
+ // Notifies the user of the current connection status.
+ void ReportConnectionStatus(protocol::ConnectionToHost::State state,
+ protocol::ErrorCode error);
+
+ // Display a dialog box asking the user to enter a PIN.
+ void DisplayAuthenticationPrompt(bool pairing_supported);
+
+ // Saves new pairing credentials to permanent storage.
+ void CommitPairingCredentials(const std::string& hostId,
+ const std::string& pairId,
+ const std::string& pairSecret);
+
+ // Delivers the latest image buffer for the canvas.
+ void RedrawCanvas(const webrtc::DesktopSize& view_size,
+ webrtc::DesktopFrame* buffer,
+ const webrtc::DesktopRegion& region);
+
+ // Updates cursor.
+ void UpdateCursorShape(const protocol::CursorShapeInfo& cursor_shape);
+
+ private:
+ // Pointer to the UI application which implements the ClientProxyDelegate.
+ // (id) is similar to a (void*) |delegate_| is set from accepting a
+ // strongly typed @interface which wraps the @protocol ClientProxyDelegate.
+ // see comments for host_proxy_delegate_wrapper.h
+ id delegate_;
+
+ DISALLOW_COPY_AND_ASSIGN(ClientProxy);
+};
+
+} // namespace remoting
+
+#endif // REMOTING_IOS_BRIDGE_HOST_PROXY_H_
diff --git a/remoting/ios/bridge/client_proxy.mm b/remoting/ios/bridge/client_proxy.mm
new file mode 100644
index 0000000..8568b9d
--- /dev/null
+++ b/remoting/ios/bridge/client_proxy.mm
@@ -0,0 +1,150 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#include "remoting/ios/bridge/client_proxy.h"
+
+#import "remoting/ios/data_store.h"
+#import "remoting/ios/host_preferences.h"
+#import "remoting/ios/bridge/client_proxy_delegate_wrapper.h"
+
+namespace {
+// The value indicating a successful connection has been established via a call
+// to ReportConnectionStatus
+const static int kSuccessfulConnection = 3;
+
+// Translate a connection status code integer to a NSString description
+NSString* GetStatusMsgFromInteger(NSInteger code) {
+ switch (code) {
+ case 0: // INITIALIZING
+ return @"Initializing connection";
+ case 1: // CONNECTING
+ return @"Connecting";
+ case 2: // AUTHENTICATED
+ return @"Authenticated";
+ case 3: // CONNECTED
+ return @"Connected";
+ case 4: // FAILED
+ return @"Connection Failed";
+ case 5: // CLOSED
+ return @"Connection closed";
+ default:
+ return @"Unknown connection state";
+ }
+}
+
+// Translate a connection error code integer to a NSString description
+NSString* GetErrorMsgFromInteger(NSInteger code) {
+ switch (code) {
+ case 1: // PEER_IS_OFFLINE
+ return @"Requested host is offline.";
+ case 2: // SESSION_REJECTED
+ return @"Session was rejected by the host.";
+ case 3: // INCOMPATIBLE_PROTOCOL
+ return @"Incompatible Protocol.";
+ case 4: // AUTHENTICATION_FAILED
+ return @"Authentication Failed.";
+ case 5: // CHANNEL_CONNECTION_ERROR
+ return @"Channel Connection Error";
+ case 6: // SIGNALING_ERROR
+ return @"Signaling Error";
+ case 7: // SIGNALING_TIMEOUT
+ return @"Signaling Timeout";
+ case 8: // HOST_OVERLOAD
+ return @"Host Overload";
+ case 9: // UNKNOWN_ERROR
+ return @"An unknown error has occurred, preventing the session "
+ "from opening.";
+ default:
+ return @"An unknown error code has occurred.";
+ }
+}
+
+} // namespace
+
+namespace remoting {
+
+ClientProxy::ClientProxy(ClientProxyDelegateWrapper* wrapper) {
+ delegate_ = [wrapper delegate];
+}
+
+void ClientProxy::ReportConnectionStatus(
+ protocol::ConnectionToHost::State state,
+ protocol::ErrorCode error) {
+ DCHECK(delegate_);
+ if (state <= kSuccessfulConnection && error == protocol::ErrorCode::OK) {
+ // Report Progress
+ [delegate_ connectionStatus:GetStatusMsgFromInteger(state)];
+
+ if (state == kSuccessfulConnection) {
+ [delegate_ connected];
+ }
+ } else {
+ [delegate_ connectionStatus:GetStatusMsgFromInteger(state)];
+ if (error != protocol::ErrorCode::OK) {
+ [delegate_ connectionFailed:GetErrorMsgFromInteger(error)];
+ }
+ }
+}
+
+void ClientProxy::DisplayAuthenticationPrompt(bool pairing_supported) {
+ DCHECK(delegate_);
+ [delegate_ requestHostPin:pairing_supported];
+}
+
+void ClientProxy::CommitPairingCredentials(const std::string& hostId,
+ const std::string& pairId,
+ const std::string& pairSecret) {
+ DCHECK(delegate_);
+ NSString* nsHostId = [[NSString alloc] initWithUTF8String:hostId.c_str()];
+ NSString* nsPairId = [[NSString alloc] initWithUTF8String:pairId.c_str()];
+ NSString* nsPairSecret =
+ [[NSString alloc] initWithUTF8String:pairSecret.c_str()];
+
+ const HostPreferences* hostPrefs =
+ [[DataStore sharedStore] getHostForId:nsHostId];
+ if (hostPrefs == nil) {
+ hostPrefs = [[DataStore sharedStore] createHost:nsHostId];
+ }
+ if (hostPrefs) {
+ hostPrefs.pairId = nsPairId;
+ hostPrefs.pairSecret = nsPairSecret;
+
+ [[DataStore sharedStore] saveChanges];
+ }
+}
+
+void ClientProxy::RedrawCanvas(const webrtc::DesktopSize& view_size,
+ webrtc::DesktopFrame* buffer,
+ const webrtc::DesktopRegion& region) {
+ DCHECK(delegate_);
+ std::vector<webrtc::DesktopRect> regions;
+
+ for (webrtc::DesktopRegion::Iterator i(region); !i.IsAtEnd(); i.Advance()) {
+ const webrtc::DesktopRect& rect(i.rect());
+
+ regions.push_back(webrtc::DesktopRect::MakeXYWH(
+ rect.left(), rect.top(), rect.width(), rect.height()));
+ }
+
+ [delegate_ applyFrame:view_size
+ stride:buffer->stride()
+ data:buffer->data()
+ regions:regions];
+}
+
+void ClientProxy::UpdateCursorShape(
+ const protocol::CursorShapeInfo& cursor_shape) {
+ DCHECK(delegate_);
+ [delegate_ applyCursor:webrtc::DesktopSize(cursor_shape.width(),
+ cursor_shape.height())
+ hotspot:webrtc::DesktopVector(cursor_shape.hotspot_x(),
+ cursor_shape.hotspot_y())
+ cursorData:(uint8_t*)cursor_shape.data().c_str()];
+}
+
+} // namespace remoting
diff --git a/remoting/ios/bridge/client_proxy_delegate.h b/remoting/ios/bridge/client_proxy_delegate.h
new file mode 100644
index 0000000..7810a2a
--- /dev/null
+++ b/remoting/ios/bridge/client_proxy_delegate.h
@@ -0,0 +1,43 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef REMOTING_IOS_BRIDGE_HOST_PROXY_DELEGATE_H_
+#define REMOTING_IOS_BRIDGE_HOST_PROXY_DELEGATE_H_
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+#include <vector>
+
+#include "third_party/webrtc/modules/desktop_capture/desktop_geometry.h"
+
+// Contract to provide for callbacks from the common Chromoting protocol to the
+// UI Application.
+@protocol ClientProxyDelegate<NSObject>
+
+// HOST request for client to input their PIN.
+- (void)requestHostPin:(BOOL)pairingSupported;
+
+// HOST notification that a connection has been successfully opened.
+- (void)connected;
+
+// HOST notification for a change in connections status.
+- (void)connectionStatus:(NSString*)statusMessage;
+
+// HOST notification that a connection has failed.
+- (void)connectionFailed:(NSString*)errorMessage;
+
+// A new Canvas (desktop) update has arrived.
+- (void)applyFrame:(const webrtc::DesktopSize&)size
+ stride:(NSInteger)stride
+ data:(uint8_t*)data
+ regions:(const std::vector<webrtc::DesktopRect>&)regions;
+
+// A new Cursor (mouse) update has arrived.
+- (void)applyCursor:(const webrtc::DesktopSize&)size
+ hotspot:(const webrtc::DesktopVector&)hotspot
+ cursorData:(uint8_t*)data;
+@end
+
+#endif // REMOTING_IOS_BRIDGE_HOST_PROXY_DELEGATE_H_ \ No newline at end of file
diff --git a/remoting/ios/bridge/client_proxy_delegate_wrapper.h b/remoting/ios/bridge/client_proxy_delegate_wrapper.h
new file mode 100644
index 0000000..e57045b
--- /dev/null
+++ b/remoting/ios/bridge/client_proxy_delegate_wrapper.h
@@ -0,0 +1,33 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef REMOTING_IOS_BRIDGE_HOST_PROXY_DELEGATE_WRAPPER_H_
+#define REMOTING_IOS_BRIDGE_HOST_PROXY_DELEGATE_WRAPPER_H_
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+#include <vector>
+
+#include "third_party/webrtc/modules/desktop_capture/desktop_geometry.h"
+
+#import "remoting/ios/bridge/client_proxy_delegate.h"
+
+// Wraps ClientProxyDelegate in a class so C++ can accept a strongly typed
+// pointer. C++ does not understand the id<> convention of passing around a
+// OBJ_C @protocol pointer. So the @protocol is wrapped and the class is passed
+// around. After accepting an instance of ClientProxyDelegateWrapper, the
+// @protocol can be referenced as type (id), which is similar to a (void*),
+
+@interface ClientProxyDelegateWrapper : NSObject
+
+@property(nonatomic, retain, readonly) id<ClientProxyDelegate> delegate;
+
+- (id)init __unavailable;
+
++ (id)wrapDelegate:(id<ClientProxyDelegate>)delegate;
+
+@end
+
+#endif // REMOTING_IOS_BRIDGE_HOST_PROXY_DELEGATE_WRAPPER_H_
diff --git a/remoting/ios/bridge/client_proxy_delegate_wrapper.mm b/remoting/ios/bridge/client_proxy_delegate_wrapper.mm
new file mode 100644
index 0000000..af558dc
--- /dev/null
+++ b/remoting/ios/bridge/client_proxy_delegate_wrapper.mm
@@ -0,0 +1,31 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "remoting/ios/bridge/client_proxy_delegate_wrapper.h"
+
+@interface ClientProxyDelegateWrapper (Private)
+- (id)initWithDelegate:(id<ClientProxyDelegate>)delegate;
+@end
+
+@implementation ClientProxyDelegateWrapper
+
+@synthesize delegate = _delegate;
+
+- (id)initWithDelegate:(id<ClientProxyDelegate>)delegate {
+ self = [super init];
+ if (self) {
+ _delegate = delegate;
+ }
+ return self;
+}
+
++ (id)wrapDelegate:(id<ClientProxyDelegate>)delegate {
+ return [[ClientProxyDelegateWrapper alloc] initWithDelegate:delegate];
+}
+
+@end
diff --git a/remoting/ios/bridge/client_proxy_unittest.mm b/remoting/ios/bridge/client_proxy_unittest.mm
new file mode 100644
index 0000000..1624298
--- /dev/null
+++ b/remoting/ios/bridge/client_proxy_unittest.mm
@@ -0,0 +1,366 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "remoting/ios/bridge/client_proxy.h"
+
+#import "base/compiler_specific.h"
+#import "testing/gtest_mac.h"
+
+#import "remoting/ios/data_store.h"
+#import "remoting/ios/bridge/client_proxy_delegate_wrapper.h"
+
+@interface ClientProxyDelegateTester : NSObject<ClientProxyDelegate>
+@property(nonatomic, assign) BOOL isConnected;
+@property(nonatomic, copy) NSString* statusMessage;
+@property(nonatomic, copy) NSString* errorMessage;
+@property(nonatomic, assign) BOOL isPairingSupported;
+@property(nonatomic, assign) webrtc::DesktopSize size;
+@property(nonatomic, assign) NSInteger stride;
+@property(nonatomic, assign) uint8_t* data;
+@property(nonatomic, assign) std::vector<webrtc::DesktopRect> regions;
+@property(nonatomic, assign) webrtc::DesktopVector hotspot;
+@end
+
+@implementation ClientProxyDelegateTester
+
+@synthesize isConnected = _isConnected;
+@synthesize statusMessage = _statusMessage;
+@synthesize errorMessage = _errorMessage;
+@synthesize isPairingSupported = _isPairingSupported;
+@synthesize size = _size;
+@synthesize stride = _stride;
+@synthesize data = _data;
+@synthesize regions = _regions;
+@synthesize hotspot = _hotspot;
+
+- (void)connected {
+ _isConnected = true;
+}
+
+- (void)connectionStatus:(NSString*)statusMessage {
+ _statusMessage = statusMessage;
+}
+
+- (void)connectionFailed:(NSString*)errorMessage {
+ _errorMessage = errorMessage;
+}
+
+- (void)requestHostPin:(BOOL)pairingSupported {
+ _isPairingSupported = pairingSupported;
+}
+
+- (void)applyFrame:(const webrtc::DesktopSize&)size
+ stride:(NSInteger)stride
+ data:(uint8_t*)data
+ regions:(const std::vector<webrtc::DesktopRect>&)regions {
+ _size = size;
+ _stride = stride;
+ _data = data;
+ _regions.assign(regions.begin(), regions.end());
+}
+
+- (void)applyCursor:(const webrtc::DesktopSize&)size
+ hotspot:(const webrtc::DesktopVector&)hotspot
+ cursorData:(uint8_t*)data {
+ _size = size;
+ _hotspot = hotspot;
+ _data = data;
+}
+
+@end
+
+namespace remoting {
+
+namespace {
+
+NSString* kStatusINITIALIZING = @"Initializing connection";
+NSString* kStatusCONNECTING = @"Connecting";
+NSString* kStatusAUTHENTICATED = @"Authenticated";
+NSString* kStatusCONNECTED = @"Connected";
+NSString* kStatusFAILED = @"Connection Failed";
+NSString* kStatusCLOSED = @"Connection closed";
+NSString* kStatusDEFAULT = @"Unknown connection state";
+
+NSString* kErrorPEER_IS_OFFLINE = @"Requested host is offline.";
+NSString* kErrorSESSION_REJECTED = @"Session was rejected by the host.";
+NSString* kErrorINCOMPATIBLE_PROTOCOL = @"Incompatible Protocol.";
+NSString* kErrorAUTHENTICATION_FAILED = @"Authentication Failed.";
+NSString* kErrorCHANNEL_CONNECTION_ERROR = @"Channel Connection Error";
+NSString* kErrorSIGNALING_ERROR = @"Signaling Error";
+NSString* kErrorSIGNALING_TIMEOUT = @"Signaling Timeout";
+NSString* kErrorHOST_OVERLOAD = @"Host Overload";
+NSString* kErrorUNKNOWN_ERROR =
+ @"An unknown error has occurred, preventing the session from opening.";
+NSString* kErrorDEFAULT = @"An unknown error code has occurred.";
+
+const webrtc::DesktopSize kFrameSize(100, 100);
+
+// Note these are disjoint regions. Testing intersecting regions is beyond the
+// scope of this test class.
+const webrtc::DesktopRect kFrameSubRect1 =
+ webrtc::DesktopRect::MakeXYWH(0, 0, 10, 10);
+const webrtc::DesktopRect kFrameSubRect2 =
+ webrtc::DesktopRect::MakeXYWH(11, 11, 10, 10);
+const webrtc::DesktopRect kFrameSubRect3 =
+ webrtc::DesktopRect::MakeXYWH(22, 22, 10, 10);
+
+const int kCursorHeight = 10;
+const int kCursorWidth = 20;
+const int kCursorHotSpotX = 4;
+const int kCursorHotSpotY = 8;
+// |kCursorDataLength| is assumed to be evenly divisible by 4
+const int kCursorDataLength = kCursorHeight * kCursorWidth;
+const uint32_t kCursorDataPattern = 0xF0E1D2C3;
+
+const std::string kHostName = "ClientProxyHostNameTest";
+const std::string kPairingId = "ClientProxyPairingIdTest";
+const std::string kPairingSecret = "ClientProxyPairingSecretTest";
+
+} // namespace
+
+class ClientProxyTest : public ::testing::Test {
+ protected:
+ virtual void SetUp() OVERRIDE {
+ delegateTester_ = [[ClientProxyDelegateTester alloc] init];
+ clientProxy_.reset(new ClientProxy(
+ [ClientProxyDelegateWrapper wrapDelegate:delegateTester_]));
+ }
+
+ void ResetIsConnected() { delegateTester_.isConnected = false; }
+
+ void TestConnnectionStatus(protocol::ConnectionToHost::State state,
+ NSString* expectedStatusMsg) {
+ ResetIsConnected();
+ clientProxy_->ReportConnectionStatus(state, protocol::ErrorCode::OK);
+ EXPECT_NSEQ(expectedStatusMsg, delegateTester_.statusMessage);
+
+ if (state == protocol::ConnectionToHost::State::CONNECTED) {
+ EXPECT_TRUE(delegateTester_.isConnected);
+ } else {
+ EXPECT_FALSE(delegateTester_.isConnected);
+ }
+
+ TestErrorMessages(state, expectedStatusMsg);
+ }
+
+ void TestForError(protocol::ConnectionToHost::State state,
+ protocol::ErrorCode errorCode,
+ NSString* expectedStatusMsg,
+ NSString* expectedErrorMsg) {
+ ResetIsConnected();
+ clientProxy_->ReportConnectionStatus(state, errorCode);
+ EXPECT_FALSE(delegateTester_.isConnected);
+ EXPECT_NSEQ(expectedStatusMsg, delegateTester_.statusMessage);
+ EXPECT_NSEQ(expectedErrorMsg, delegateTester_.errorMessage);
+ }
+
+ void TestErrorMessages(protocol::ConnectionToHost::State state,
+ NSString* expectedStatusMsg) {
+ TestForError(state,
+ protocol::ErrorCode::AUTHENTICATION_FAILED,
+ expectedStatusMsg,
+ kErrorAUTHENTICATION_FAILED);
+ TestForError(state,
+ protocol::ErrorCode::CHANNEL_CONNECTION_ERROR,
+ expectedStatusMsg,
+ kErrorCHANNEL_CONNECTION_ERROR);
+ TestForError(state,
+ protocol::ErrorCode::HOST_OVERLOAD,
+ expectedStatusMsg,
+ kErrorHOST_OVERLOAD);
+ TestForError(state,
+ protocol::ErrorCode::INCOMPATIBLE_PROTOCOL,
+ expectedStatusMsg,
+ kErrorINCOMPATIBLE_PROTOCOL);
+ TestForError(state,
+ protocol::ErrorCode::PEER_IS_OFFLINE,
+ expectedStatusMsg,
+ kErrorPEER_IS_OFFLINE);
+ TestForError(state,
+ protocol::ErrorCode::SESSION_REJECTED,
+ expectedStatusMsg,
+ kErrorSESSION_REJECTED);
+ TestForError(state,
+ protocol::ErrorCode::SIGNALING_ERROR,
+ expectedStatusMsg,
+ kErrorSIGNALING_ERROR);
+ TestForError(state,
+ protocol::ErrorCode::SIGNALING_TIMEOUT,
+ expectedStatusMsg,
+ kErrorSIGNALING_TIMEOUT);
+ TestForError(state,
+ protocol::ErrorCode::UNKNOWN_ERROR,
+ expectedStatusMsg,
+ kErrorUNKNOWN_ERROR);
+ TestForError(state,
+ static_cast<protocol::ErrorCode>(999),
+ expectedStatusMsg,
+ kErrorDEFAULT);
+ }
+
+ void ValidateHost(const std::string& hostName,
+ const std::string& pairingId,
+ const std::string& pairingSecret) {
+ DataStore* store = [DataStore sharedStore];
+ NSString* hostNameAsNSString =
+ [NSString stringWithUTF8String:hostName.c_str()];
+ const HostPreferences* host = [store getHostForId:hostNameAsNSString];
+ if (host != nil) {
+ [store removeHost:host];
+ }
+
+ clientProxy_->CommitPairingCredentials(hostName, pairingId, pairingSecret);
+
+ host = [store getHostForId:hostNameAsNSString];
+
+ ASSERT_TRUE(host != nil);
+ ASSERT_STREQ(hostName.c_str(), [host.hostId UTF8String]);
+ ASSERT_STREQ(pairingId.c_str(), [host.pairId UTF8String]);
+ ASSERT_STREQ(pairingSecret.c_str(), [host.pairSecret UTF8String]);
+ }
+
+ scoped_ptr<ClientProxy> clientProxy_;
+ ClientProxyDelegateTester* delegateTester_;
+ ClientProxyDelegateWrapper* delegateWrapper_;
+};
+
+TEST_F(ClientProxyTest, ReportConnectionStatusINITIALIZING) {
+ TestConnnectionStatus(protocol::ConnectionToHost::State::INITIALIZING,
+ kStatusINITIALIZING);
+}
+
+TEST_F(ClientProxyTest, ReportConnectionStatusCONNECTING) {
+ TestConnnectionStatus(protocol::ConnectionToHost::State::CONNECTING,
+ kStatusCONNECTING);
+}
+
+TEST_F(ClientProxyTest, ReportConnectionStatusAUTHENTICATED) {
+ TestConnnectionStatus(protocol::ConnectionToHost::State::AUTHENTICATED,
+ kStatusAUTHENTICATED);
+}
+
+TEST_F(ClientProxyTest, ReportConnectionStatusCONNECTED) {
+ TestConnnectionStatus(protocol::ConnectionToHost::State::CONNECTED,
+ kStatusCONNECTED);
+}
+
+TEST_F(ClientProxyTest, ReportConnectionStatusFAILED) {
+ TestConnnectionStatus(protocol::ConnectionToHost::State::FAILED,
+ kStatusFAILED);
+}
+
+TEST_F(ClientProxyTest, ReportConnectionStatusCLOSED) {
+ TestConnnectionStatus(protocol::ConnectionToHost::State::CLOSED,
+ kStatusCLOSED);
+}
+
+TEST_F(ClientProxyTest, ReportConnectionStatusDEFAULT) {
+ TestConnnectionStatus(static_cast<protocol::ConnectionToHost::State>(999),
+ kStatusDEFAULT);
+}
+
+TEST_F(ClientProxyTest, DisplayAuthenticationPrompt) {
+ clientProxy_->DisplayAuthenticationPrompt(true);
+ ASSERT_TRUE(delegateTester_.isPairingSupported);
+ clientProxy_->DisplayAuthenticationPrompt(false);
+ ASSERT_FALSE(delegateTester_.isPairingSupported);
+}
+
+TEST_F(ClientProxyTest, CommitPairingCredentialsBasic) {
+ ValidateHost("", "", "");
+}
+
+TEST_F(ClientProxyTest, CommitPairingCredentialsExtended) {
+ ValidateHost(kHostName, kPairingId, kPairingSecret);
+}
+
+TEST_F(ClientProxyTest, RedrawCanvasBasic) {
+
+ webrtc::BasicDesktopFrame frame(webrtc::DesktopSize(1, 1));
+ webrtc::DesktopRegion regions;
+ regions.AddRect(webrtc::DesktopRect::MakeLTRB(0, 0, 1, 1));
+
+ clientProxy_->RedrawCanvas(webrtc::DesktopSize(1, 1), &frame, regions);
+
+ ASSERT_TRUE(webrtc::DesktopSize(1, 1).equals(delegateTester_.size));
+ ASSERT_EQ(4, delegateTester_.stride);
+ ASSERT_TRUE(delegateTester_.data != NULL);
+ ASSERT_EQ(1, delegateTester_.regions.size());
+ ASSERT_TRUE(delegateTester_.regions[0].equals(
+ webrtc::DesktopRect::MakeLTRB(0, 0, 1, 1)));
+}
+TEST_F(ClientProxyTest, RedrawCanvasExtended) {
+
+ webrtc::BasicDesktopFrame frame(kFrameSize);
+ webrtc::DesktopRegion regions;
+ regions.AddRect(kFrameSubRect1);
+ regions.AddRect(kFrameSubRect2);
+ regions.AddRect(kFrameSubRect3);
+
+ clientProxy_->RedrawCanvas(kFrameSize, &frame, regions);
+
+ ASSERT_TRUE(kFrameSize.equals(delegateTester_.size));
+ ASSERT_EQ(kFrameSize.width() * webrtc::DesktopFrame::kBytesPerPixel,
+ delegateTester_.stride);
+ ASSERT_TRUE(delegateTester_.data != NULL);
+ ASSERT_EQ(3, delegateTester_.regions.size());
+ ASSERT_TRUE(delegateTester_.regions[0].equals(kFrameSubRect1));
+ ASSERT_TRUE(delegateTester_.regions[1].equals(kFrameSubRect2));
+ ASSERT_TRUE(delegateTester_.regions[2].equals(kFrameSubRect3));
+}
+
+TEST_F(ClientProxyTest, UpdateCursorBasic) {
+ protocol::CursorShapeInfo cursor_proto;
+ cursor_proto.set_width(1);
+ cursor_proto.set_height(1);
+ cursor_proto.set_hotspot_x(0);
+ cursor_proto.set_hotspot_y(0);
+
+ char data[4];
+ memset(data, 0xFF, 4);
+
+ cursor_proto.set_data(data);
+
+ clientProxy_->UpdateCursorShape(cursor_proto);
+
+ ASSERT_EQ(1, delegateTester_.size.width());
+ ASSERT_EQ(1, delegateTester_.size.height());
+ ASSERT_EQ(0, delegateTester_.hotspot.x());
+ ASSERT_EQ(0, delegateTester_.hotspot.y());
+ ASSERT_TRUE(delegateTester_.data != NULL);
+ for (int i = 0; i < 4; i++) {
+ ASSERT_EQ(0xFF, delegateTester_.data[i]);
+ }
+}
+
+TEST_F(ClientProxyTest, UpdateCursorExtended) {
+ protocol::CursorShapeInfo cursor_proto;
+ cursor_proto.set_width(kCursorWidth);
+ cursor_proto.set_height(kCursorHeight);
+ cursor_proto.set_hotspot_x(kCursorHotSpotX);
+ cursor_proto.set_hotspot_y(kCursorHotSpotY);
+
+ char data[kCursorDataLength];
+ memset_pattern4(data, &kCursorDataPattern, kCursorDataLength);
+
+ cursor_proto.set_data(data);
+
+ clientProxy_->UpdateCursorShape(cursor_proto);
+
+ ASSERT_EQ(kCursorWidth, delegateTester_.size.width());
+ ASSERT_EQ(kCursorHeight, delegateTester_.size.height());
+ ASSERT_EQ(kCursorHotSpotX, delegateTester_.hotspot.x());
+ ASSERT_EQ(kCursorHotSpotY, delegateTester_.hotspot.y());
+ ASSERT_TRUE(delegateTester_.data != NULL);
+ for (int i = 0; i < kCursorDataLength / 4; i++) {
+ ASSERT_TRUE(memcmp(&delegateTester_.data[i * 4], &kCursorDataPattern, 4) ==
+ 0);
+ }
+}
+
+} // namespace remoting \ No newline at end of file
diff --git a/remoting/ios/bridge/frame_consumer_bridge.cc b/remoting/ios/bridge/frame_consumer_bridge.cc
new file mode 100644
index 0000000..fbc9606
--- /dev/null
+++ b/remoting/ios/bridge/frame_consumer_bridge.cc
@@ -0,0 +1,88 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "remoting/ios/bridge/frame_consumer_bridge.h"
+
+#include "base/bind.h"
+#include "base/logging.h"
+#include "base/synchronization/waitable_event.h"
+#include "remoting/base/util.h"
+#include "third_party/webrtc/modules/desktop_capture/desktop_frame.h"
+
+namespace remoting {
+
+FrameConsumerBridge::FrameConsumerBridge(OnFrameCallback callback)
+ : callback_(callback), frame_producer_(NULL) {}
+
+FrameConsumerBridge::~FrameConsumerBridge() {
+ // The producer should now return any pending buffers. At this point, however,
+ // the buffers are returned via tasks which may not be scheduled before the
+ // producer, so we free all the buffers once the producer's queue is empty.
+ // And the scheduled tasks will die quietly.
+ if (frame_producer_) {
+ base::WaitableEvent done_event(true, false);
+ frame_producer_->RequestReturnBuffers(base::Bind(
+ &base::WaitableEvent::Signal, base::Unretained(&done_event)));
+ done_event.Wait();
+ }
+}
+
+void FrameConsumerBridge::Initialize(FrameProducer* producer) {
+ DCHECK(!frame_producer_);
+ frame_producer_ = producer;
+ DCHECK(frame_producer_);
+}
+
+void FrameConsumerBridge::ApplyBuffer(const webrtc::DesktopSize& view_size,
+ const webrtc::DesktopRect& clip_area,
+ webrtc::DesktopFrame* buffer,
+ const webrtc::DesktopRegion& region,
+ const webrtc::DesktopRegion& shape) {
+ DCHECK(frame_producer_);
+ if (!view_size_.equals(view_size)) {
+ // Drop the frame, since the data belongs to the previous generation,
+ // before SetSourceSize() called SetOutputSizeAndClip().
+ ReturnBuffer(buffer);
+ return;
+ }
+
+ // This call completes synchronously.
+ callback_.Run(view_size, buffer, region);
+
+ // Recycle |buffer| by returning it to |frame_producer_| as the next buffer
+ frame_producer_->DrawBuffer(buffer);
+}
+
+void FrameConsumerBridge::ReturnBuffer(webrtc::DesktopFrame* buffer) {
+ DCHECK(frame_producer_);
+ ScopedVector<webrtc::DesktopFrame>::iterator it =
+ std::find(buffers_.begin(), buffers_.end(), buffer);
+
+ DCHECK(it != buffers_.end());
+ buffers_.erase(it);
+}
+
+void FrameConsumerBridge::SetSourceSize(const webrtc::DesktopSize& source_size,
+ const webrtc::DesktopVector& dpi) {
+ DCHECK(frame_producer_);
+ view_size_ = source_size;
+ webrtc::DesktopRect clip_area = webrtc::DesktopRect::MakeSize(view_size_);
+ frame_producer_->SetOutputSizeAndClip(view_size_, clip_area);
+
+ // Now that the size is well known, ask the producer to start drawing
+ DrawWithNewBuffer();
+}
+
+FrameConsumerBridge::PixelFormat FrameConsumerBridge::GetPixelFormat() {
+ return FORMAT_RGBA;
+}
+
+void FrameConsumerBridge::DrawWithNewBuffer() {
+ DCHECK(frame_producer_);
+ webrtc::DesktopFrame* buffer = new webrtc::BasicDesktopFrame(view_size_);
+ buffers_.push_back(buffer);
+ frame_producer_->DrawBuffer(buffer);
+}
+
+} // namespace remoting
diff --git a/remoting/ios/bridge/frame_consumer_bridge.h b/remoting/ios/bridge/frame_consumer_bridge.h
new file mode 100644
index 0000000..70eca2f
--- /dev/null
+++ b/remoting/ios/bridge/frame_consumer_bridge.h
@@ -0,0 +1,65 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef REMOTING_IOS_BRIDGE_FRAME_CONSUMER_BRIDGE_H_
+#define REMOTING_IOS_BRIDGE_FRAME_CONSUMER_BRIDGE_H_
+
+#include <list>
+
+#include "base/callback.h"
+#include "base/compiler_specific.h"
+#include "base/memory/scoped_vector.h"
+#include "base/memory/weak_ptr.h"
+#include "remoting/client/frame_consumer.h"
+#include "remoting/client/frame_producer.h"
+#include "third_party/webrtc/modules/desktop_capture/desktop_geometry.h"
+
+namespace remoting {
+
+class FrameConsumerBridge : public base::SupportsWeakPtr<FrameConsumerBridge>,
+ public FrameConsumer {
+ public:
+ typedef base::Callback<void(const webrtc::DesktopSize& view_size,
+ webrtc::DesktopFrame* buffer,
+ const webrtc::DesktopRegion& region)>
+ OnFrameCallback;
+
+ // A callback is provided to return frame updates asynchronously.
+ explicit FrameConsumerBridge(OnFrameCallback callback);
+
+ virtual ~FrameConsumerBridge();
+ // This must be called before any other functional use.
+ void Initialize(FrameProducer* producer);
+
+ // FrameConsumer implementation.
+ virtual void ApplyBuffer(const webrtc::DesktopSize& view_size,
+ const webrtc::DesktopRect& clip_area,
+ webrtc::DesktopFrame* buffer,
+ const webrtc::DesktopRegion& region,
+ const webrtc::DesktopRegion& shape) OVERRIDE;
+ virtual void ReturnBuffer(webrtc::DesktopFrame* buffer) OVERRIDE;
+ virtual void SetSourceSize(const webrtc::DesktopSize& source_size,
+ const webrtc::DesktopVector& dpi) OVERRIDE;
+ virtual PixelFormat GetPixelFormat() OVERRIDE;
+
+ private:
+ // Allocates a new buffer of |view_size_|, and tells the producer to draw onto
+ // it. This can be called as soon as the producer is known, but is not
+ // required until ready to receive frames.
+ void DrawWithNewBuffer();
+
+ OnFrameCallback callback_;
+
+ FrameProducer* frame_producer_;
+ webrtc::DesktopSize view_size_;
+
+ // List of allocated image buffers.
+ ScopedVector<webrtc::DesktopFrame> buffers_;
+
+ DISALLOW_COPY_AND_ASSIGN(FrameConsumerBridge);
+};
+
+} // namespace remoting
+
+#endif // REMOTING_IOS_BRIDGE_FRAME_CONSUMER_BRIDGE_H_
diff --git a/remoting/ios/bridge/frame_consumer_bridge_unittest.cc b/remoting/ios/bridge/frame_consumer_bridge_unittest.cc
new file mode 100644
index 0000000..30d63d0
--- /dev/null
+++ b/remoting/ios/bridge/frame_consumer_bridge_unittest.cc
@@ -0,0 +1,138 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "remoting/ios/bridge/frame_consumer_bridge.h"
+
+#include <queue>
+#include <gtest/gtest.h>
+
+#include "base/bind.h"
+#include "base/memory/scoped_ptr.h"
+#include "third_party/webrtc/modules/desktop_capture/desktop_frame.h"
+#include "third_party/webrtc/modules/desktop_capture/desktop_region.h"
+
+namespace {
+const webrtc::DesktopSize kFrameSize(100, 100);
+const webrtc::DesktopVector kDpi(100, 100);
+
+const webrtc::DesktopRect FrameRect() {
+ return webrtc::DesktopRect::MakeSize(kFrameSize);
+}
+
+webrtc::DesktopRegion FrameRegion() {
+ return webrtc::DesktopRegion(FrameRect());
+}
+
+void FrameDelivery(const webrtc::DesktopSize& view_size,
+ webrtc::DesktopFrame* buffer,
+ const webrtc::DesktopRegion& region) {
+ ASSERT_TRUE(view_size.equals(kFrameSize));
+ ASSERT_TRUE(region.Equals(FrameRegion()));
+};
+
+} // namespace
+
+namespace remoting {
+
+class FrameProducerTester : public FrameProducer {
+ public:
+ virtual ~FrameProducerTester() {};
+
+ virtual void DrawBuffer(webrtc::DesktopFrame* buffer) OVERRIDE {
+ frames.push(buffer);
+ };
+
+ virtual void InvalidateRegion(const webrtc::DesktopRegion& region) OVERRIDE {
+ NOTIMPLEMENTED();
+ };
+
+ virtual void RequestReturnBuffers(const base::Closure& done) OVERRIDE {
+ // Don't have to actually return the buffers. This function is really
+ // saying don't use the references anymore, they are now invalid.
+ while (!frames.empty()) {
+ frames.pop();
+ }
+ done.Run();
+ };
+
+ virtual void SetOutputSizeAndClip(const webrtc::DesktopSize& view_size,
+ const webrtc::DesktopRect& clip_area)
+ OVERRIDE {
+ viewSize = view_size;
+ clipArea = clip_area;
+ };
+
+ std::queue<webrtc::DesktopFrame*> frames;
+ webrtc::DesktopSize viewSize;
+ webrtc::DesktopRect clipArea;
+};
+
+class FrameConsumerBridgeTest : public ::testing::Test {
+ protected:
+ virtual void SetUp() OVERRIDE {
+ frameProducer_.reset(new FrameProducerTester());
+ frameConsumer_.reset(new FrameConsumerBridge(base::Bind(&FrameDelivery)));
+ frameConsumer_->Initialize(frameProducer_.get());
+ }
+ virtual void TearDown() OVERRIDE {}
+
+ scoped_ptr<FrameProducerTester> frameProducer_;
+ scoped_ptr<FrameConsumerBridge> frameConsumer_;
+};
+
+TEST(FrameConsumerBridgeTest_NotInitialized, CreateAndRelease) {
+ scoped_ptr<FrameConsumerBridge> frameConsumer_(
+ new FrameConsumerBridge(base::Bind(&FrameDelivery)));
+ ASSERT_TRUE(frameConsumer_.get() != NULL);
+ frameConsumer_.reset();
+ ASSERT_TRUE(frameConsumer_.get() == NULL);
+}
+
+TEST_F(FrameConsumerBridgeTest, ApplyBuffer) {
+ webrtc::DesktopFrame* frame = NULL;
+ ASSERT_EQ(0, frameProducer_->frames.size());
+ frameConsumer_->SetSourceSize(kFrameSize, kDpi);
+ ASSERT_EQ(1, frameProducer_->frames.size());
+
+ // Return the frame, and ensure we get it back
+ frame = frameProducer_->frames.front();
+ frameProducer_->frames.pop();
+ ASSERT_EQ(0, frameProducer_->frames.size());
+ frameConsumer_->ApplyBuffer(
+ kFrameSize, FrameRect(), frame, FrameRegion(), FrameRegion());
+ ASSERT_EQ(1, frameProducer_->frames.size());
+ ASSERT_TRUE(frame == frameProducer_->frames.front());
+ ASSERT_TRUE(frame->data() == frameProducer_->frames.front()->data());
+
+ // Change the SourceSize, we should get a new frame, but when the old frame is
+ // submitted we will not get it back.
+ frameConsumer_->SetSourceSize(webrtc::DesktopSize(1, 1), kDpi);
+ ASSERT_EQ(2, frameProducer_->frames.size());
+ frame = frameProducer_->frames.front();
+ frameProducer_->frames.pop();
+ ASSERT_EQ(1, frameProducer_->frames.size());
+ frameConsumer_->ApplyBuffer(
+ kFrameSize, FrameRect(), frame, FrameRegion(), FrameRegion());
+ ASSERT_EQ(1, frameProducer_->frames.size());
+}
+
+TEST_F(FrameConsumerBridgeTest, SetSourceSize) {
+ frameConsumer_->SetSourceSize(webrtc::DesktopSize(0, 0),
+ webrtc::DesktopVector(0, 0));
+ ASSERT_TRUE(frameProducer_->viewSize.equals(webrtc::DesktopSize(0, 0)));
+ ASSERT_TRUE(frameProducer_->clipArea.equals(
+ webrtc::DesktopRect::MakeLTRB(0, 0, 0, 0)));
+ ASSERT_EQ(1, frameProducer_->frames.size());
+ ASSERT_TRUE(
+ frameProducer_->frames.front()->size().equals(webrtc::DesktopSize(0, 0)));
+
+ frameConsumer_->SetSourceSize(kFrameSize, kDpi);
+ ASSERT_TRUE(frameProducer_->viewSize.equals(kFrameSize));
+ ASSERT_TRUE(frameProducer_->clipArea.equals(FrameRect()));
+ ASSERT_EQ(2, frameProducer_->frames.size());
+ frameProducer_->frames.pop();
+ ASSERT_TRUE(frameProducer_->frames.front()->size().equals(kFrameSize));
+}
+
+} // namespace remoting \ No newline at end of file
diff --git a/remoting/ios/bridge/host_proxy.h b/remoting/ios/bridge/host_proxy.h
new file mode 100644
index 0000000..3c0f129
--- /dev/null
+++ b/remoting/ios/bridge/host_proxy.h
@@ -0,0 +1,67 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef REMOTING_IOS_BRIDGE_CLIENT_PROXY_H_
+#define REMOTING_IOS_BRIDGE_CLIENT_PROXY_H_
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+#include "base/memory/ref_counted.h"
+#include "base/memory/scoped_ptr.h"
+#include "third_party/webrtc/modules/desktop_capture/desktop_geometry.h"
+
+#import "remoting/ios/bridge/client_proxy_delegate_wrapper.h"
+
+namespace remoting {
+class ClientInstance;
+class ClientProxy;
+} // namespace remoting
+
+// HostProxy is one channel of a bridge from the UI Application (CLIENT) and the
+// common Chromoting protocol (HOST). HostProxy proxies message from the UI
+// application to the host. The reverse channel, ClientProxy, is owned by the
+// HostProxy to control deconstruction order, but is shared with the
+// ClientInstance to perform work.
+
+@interface HostProxy : NSObject {
+ @private
+ // Host to Client channel
+ scoped_ptr<remoting::ClientProxy> _hostToClientChannel;
+ // Client to Host channel, must be released before |_hostToClientChannel|
+ scoped_refptr<remoting::ClientInstance> _clientToHostChannel;
+ // Connection state
+ BOOL _isConnected;
+}
+
+// TRUE when a connection has been established successfully.
+- (BOOL)isConnected;
+
+// Forwards credentials from CLIENT and to HOST and begins establishing a
+// connection.
+- (void)connectToHost:(NSString*)username
+ authToken:(NSString*)token
+ jabberId:(NSString*)jid
+ hostId:(NSString*)hostId
+ publicKey:(NSString*)hostPublicKey
+ delegate:(id<ClientProxyDelegate>)delegate;
+
+// Report from CLIENT with the user's PIN.
+- (void)authenticationResponse:(NSString*)pin createPair:(BOOL)createPair;
+
+// CLIENT initiated disconnection
+- (void)disconnectFromHost;
+
+// Report from CLIENT of mouse input
+- (void)mouseAction:(const webrtc::DesktopVector&)position
+ wheelDelta:(const webrtc::DesktopVector&)wheelDelta
+ whichButton:(NSInteger)buttonPressed
+ buttonDown:(BOOL)buttonIsDown;
+
+// Report from CLIENT of keyboard input
+- (void)keyboardAction:(NSInteger)keyCode keyDown:(BOOL)keyIsDown;
+
+@end
+
+#endif // REMOTING_IOS_BRIDGE_CLIENT_PROXY_H_
diff --git a/remoting/ios/bridge/host_proxy.mm b/remoting/ios/bridge/host_proxy.mm
new file mode 100644
index 0000000..1573f9c
--- /dev/null
+++ b/remoting/ios/bridge/host_proxy.mm
@@ -0,0 +1,119 @@
+// Copyright 2014 The Chromium 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 "remoting/ios/bridge/host_proxy.h"
+
+#import "remoting/ios/data_store.h"
+#import "remoting/ios/host_preferences.h"
+#import "remoting/ios/bridge/client_instance.h"
+#import "remoting/ios/bridge/client_proxy.h"
+
+@implementation HostProxy
+
+// Override default constructor and initialize internals
+- (id)init {
+ self = [super init];
+ if (self) {
+ _isConnected = false;
+ }
+ return self;
+}
+
+// Override default destructor
+- (void)dealloc {
+ if (_isConnected) {
+ [self disconnectFromHost];
+ }
+
+ [super dealloc];
+}
+
+- (BOOL)isConnected {
+ return _isConnected;
+}
+
+- (void)connectToHost:(NSString*)username
+ authToken:(NSString*)token
+ jabberId:(NSString*)jid
+ hostId:(NSString*)hostId
+ publicKey:(NSString*)hostPublicKey
+ delegate:(id<ClientProxyDelegate>)delegate {
+ // Implicitly, if currently connected, discard the connection and begin a new
+ // connection.
+ [self disconnectFromHost];
+
+ NSString* pairId = @"";
+ NSString* pairSecret = @"";
+
+ const HostPreferences* hostPrefs =
+ [[DataStore sharedStore] getHostForId:hostId];
+
+ // Use the pairing id and secret when known
+ if (hostPrefs && hostPrefs.pairId && hostPrefs.pairSecret) {
+ pairId = [hostPrefs.pairId copy];
+ pairSecret = [hostPrefs.pairSecret copy];
+ }
+
+ _hostToClientChannel.reset(new remoting::ClientProxy(
+ [ClientProxyDelegateWrapper wrapDelegate:delegate]));
+
+ DCHECK(!_clientToHostChannel);
+ _clientToHostChannel =
+ new remoting::ClientInstance(_hostToClientChannel->AsWeakPtr(),
+ [username UTF8String],
+ [token UTF8String],
+ [jid UTF8String],
+ [hostId UTF8String],
+ [hostPublicKey UTF8String],
+ [pairId UTF8String],
+ [pairSecret UTF8String]);
+
+ _clientToHostChannel->Start();
+ _isConnected = YES;
+}
+
+- (void)authenticationResponse:(NSString*)pin createPair:(BOOL)createPair {
+ if (_isConnected) {
+ _clientToHostChannel->ProvideSecret([pin UTF8String], createPair);
+ }
+}
+
+- (void)disconnectFromHost {
+ if (_isConnected) {
+ VLOG(1) << "Disconnecting from Host";
+
+ // |_clientToHostChannel| must be closed before releasing
+ // |_hostToClientChannel|
+
+ // |_clientToHostChannel| owns several objects that have references to
+ // itself. These objects need to be cleaned up before we can release
+ // |_clientToHostChannel|.
+ _clientToHostChannel->Cleanup();
+ // All other references to |_clientToHostChannel| should now be free. When
+ // the next statement is executed the destructor is called automatically.
+ _clientToHostChannel = NULL;
+
+ _hostToClientChannel.reset();
+
+ _isConnected = NO;
+ }
+}
+
+- (void)mouseAction:(const webrtc::DesktopVector&)position
+ wheelDelta:(const webrtc::DesktopVector&)wheelDelta
+ whichButton:(NSInteger)buttonPressed
+ buttonDown:(BOOL)buttonIsDown {
+ if (_isConnected) {
+ _clientToHostChannel->PerformMouseAction(
+ position, wheelDelta, buttonPressed, buttonIsDown);
+ }
+}
+
+- (void)keyboardAction:(NSInteger)keyCode keyDown:(BOOL)keyIsDown {
+ if (_isConnected) {
+ _clientToHostChannel->PerformKeyboardAction(keyCode, keyIsDown);
+ }
+}
+
+@end
diff --git a/remoting/ios/bridge/host_proxy_unittest.mm b/remoting/ios/bridge/host_proxy_unittest.mm
new file mode 100644
index 0000000..9641e63
--- /dev/null
+++ b/remoting/ios/bridge/host_proxy_unittest.mm
@@ -0,0 +1,51 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "remoting/ios/bridge/host_proxy.h"
+
+#import "base/compiler_specific.h"
+#import "testing/gtest_mac.h"
+
+namespace remoting {
+
+class HostProxyTest : public ::testing::Test {
+ protected:
+ virtual void SetUp() OVERRIDE { hostProxy_ = [[HostProxy alloc] init]; }
+
+ void CallPassThroughFunctions() {
+ [hostProxy_ mouseAction:webrtc::DesktopVector(0, 0)
+ wheelDelta:webrtc::DesktopVector(0, 0)
+ whichButton:0
+ buttonDown:NO];
+ [hostProxy_ keyboardAction:0 keyDown:NO];
+ }
+
+ HostProxy* hostProxy_;
+};
+
+TEST_F(HostProxyTest, ConnectDisconnect) {
+ CallPassThroughFunctions();
+
+ ASSERT_FALSE([hostProxy_ isConnected]);
+ [hostProxy_ connectToHost:@""
+ authToken:@""
+ jabberId:@""
+ hostId:@""
+ publicKey:@""
+ delegate:nil];
+ ASSERT_TRUE([hostProxy_ isConnected]);
+
+ CallPassThroughFunctions();
+
+ [hostProxy_ disconnectFromHost];
+ ASSERT_FALSE([hostProxy_ isConnected]);
+
+ CallPassThroughFunctions();
+}
+
+} // namespace remoting \ No newline at end of file
diff --git a/remoting/ios/data_store.h b/remoting/ios/data_store.h
new file mode 100644
index 0000000..0e4a34a
--- /dev/null
+++ b/remoting/ios/data_store.h
@@ -0,0 +1,31 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef REMOTING_IOS_DATA_STORE_H_
+#define REMOTING_IOS_DATA_STORE_H_
+
+#import <CoreData/CoreData.h>
+
+#import "remoting/ios/host_preferences.h"
+
+// A local data store backed by SQLLite to hold instances of HostPreferences.
+// HostPreference is defined by the Core Data Model templates see
+// ChromotingModel.xcdatamodel
+@interface DataStore : NSObject
+
+// Static pointer to the managed data store
++ (DataStore*)sharedStore;
+
+// General methods
+- (BOOL)saveChanges;
+
+// Access methods for Hosts
+- (NSArray*)allHosts;
+- (const HostPreferences*)createHost:(NSString*)hostId;
+- (void)removeHost:(const HostPreferences*)p;
+- (const HostPreferences*)getHostForId:(NSString*)hostId;
+
+@end
+
+#endif // REMOTING_IOS_DATA_STORE_H_
diff --git a/remoting/ios/data_store.mm b/remoting/ios/data_store.mm
new file mode 100644
index 0000000..1cfbc3f
--- /dev/null
+++ b/remoting/ios/data_store.mm
@@ -0,0 +1,176 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "remoting/ios/data_store.h"
+
+@interface DataStore (Private)
+- (NSString*)itemArchivePath;
+@end
+
+@implementation DataStore {
+ @private
+ NSMutableArray* _allHosts;
+ NSManagedObjectContext* _context;
+ NSManagedObjectModel* _model;
+}
+
+// Create or Get a static data store
++ (DataStore*)sharedStore {
+ static DataStore* sharedStore = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken,
+ ^{ sharedStore = [[super allocWithZone:nil] init]; });
+
+ return sharedStore;
+}
+
+// General methods
++ (id)allocWithZone:(NSZone*)zone {
+ return [self sharedStore];
+}
+
+// Load data store from SQLLite backing store
+- (id)init {
+ self = [super init];
+
+ if (self) {
+ // Read in ChromotingModel.xdatamodeld
+ _model = [NSManagedObjectModel mergedModelFromBundles:nil];
+
+ NSPersistentStoreCoordinator* psc = [[NSPersistentStoreCoordinator alloc]
+ initWithManagedObjectModel:_model];
+
+ NSString* path = [self itemArchivePath];
+ NSURL* storeUrl = [NSURL fileURLWithPath:path];
+
+ NSError* error = nil;
+
+ NSDictionary* tryOptions = @{
+ NSMigratePersistentStoresAutomaticallyOption : @YES,
+ NSInferMappingModelAutomaticallyOption : @YES
+ };
+ NSDictionary* makeOptions =
+ @{NSMigratePersistentStoresAutomaticallyOption : @YES};
+
+ if (![psc addPersistentStoreWithType:NSSQLiteStoreType
+ configuration:nil
+ URL:storeUrl
+ options:tryOptions
+ error:&error]) {
+ // An incompatible version of the store exists, delete it and start over
+ [[NSFileManager defaultManager] removeItemAtURL:storeUrl error:nil];
+
+ [psc addPersistentStoreWithType:NSSQLiteStoreType
+ configuration:nil
+ URL:storeUrl
+ options:makeOptions
+ error:&error];
+ [NSException raise:@"Open failed"
+ format:@"Reason: %@", [error localizedDescription]];
+ }
+
+ // Create the managed object context
+ _context = [[NSManagedObjectContext alloc] init];
+ [_context setPersistentStoreCoordinator:psc];
+
+ // The managed object context can manage undo, but we don't need it
+ [_context setUndoManager:nil];
+
+ _allHosts = nil;
+ }
+ return self;
+}
+
+// Committing to backing store
+- (BOOL)saveChanges {
+ NSError* err = nil;
+ BOOL successful = [_context save:&err];
+ return successful;
+}
+
+// Looking up the backing store path
+- (NSString*)itemArchivePath {
+ NSArray* documentDirectories = NSSearchPathForDirectoriesInDomains(
+ NSDocumentDirectory, NSUserDomainMask, YES);
+
+ // Get one and only document directory from that list
+ NSString* documentDirectory = [documentDirectories objectAtIndex:0];
+
+ return [documentDirectory stringByAppendingPathComponent:@"store.data"];
+}
+
+// Return an array of all known hosts, if the list hasn't been loaded yet, then
+// load it now
+- (NSArray*)allHosts {
+ if (!_allHosts) {
+ NSFetchRequest* request = [[NSFetchRequest alloc] init];
+
+ NSEntityDescription* e =
+ [[_model entitiesByName] objectForKey:@"HostPreferences"];
+
+ [request setEntity:e];
+
+ NSError* error;
+ NSArray* result = [_context executeFetchRequest:request error:&error];
+ if (!result) {
+ [NSException raise:@"Fetch failed"
+ format:@"Reason: %@", [error localizedDescription]];
+ }
+ _allHosts = [result mutableCopy];
+ }
+
+ return _allHosts;
+}
+
+// Return a HostPreferences if it already exists, otherwise create a new
+// HostPreferences to use
+- (const HostPreferences*)createHost:(NSString*)hostId {
+
+ const HostPreferences* p = [self getHostForId:hostId];
+
+ if (p == nil) {
+ p = [NSEntityDescription insertNewObjectForEntityForName:@"HostPreferences"
+ inManagedObjectContext:_context];
+ p.hostId = hostId;
+ [_allHosts addObject:p];
+ }
+ return p;
+}
+
+- (void)removeHost:(HostPreferences*)p {
+ [_context deleteObject:p];
+ [_allHosts removeObjectIdenticalTo:p];
+}
+
+// Search the store for any matching HostPreferences
+// return the 1st match or nil
+- (const HostPreferences*)getHostForId:(NSString*)hostId {
+ NSFetchRequest* request = [[NSFetchRequest alloc] init];
+
+ NSEntityDescription* e =
+ [[_model entitiesByName] objectForKey:@"HostPreferences"];
+ [request setEntity:e];
+
+ NSPredicate* predicate =
+ [NSPredicate predicateWithFormat:@"(hostId = %@)", hostId];
+ [request setPredicate:predicate];
+
+ NSError* error;
+ NSArray* result = [_context executeFetchRequest:request error:&error];
+ if (!result) {
+ [NSException raise:@"Fetch failed"
+ format:@"Reason: %@", [error localizedDescription]];
+ }
+
+ for (HostPreferences* curHost in result) {
+ return curHost;
+ }
+ return nil;
+}
+
+@end
diff --git a/remoting/ios/data_store_unittest.mm b/remoting/ios/data_store_unittest.mm
new file mode 100644
index 0000000..f75dc28
--- /dev/null
+++ b/remoting/ios/data_store_unittest.mm
@@ -0,0 +1,119 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "remoting/ios/data_store.h"
+
+#import "base/compiler_specific.h"
+#import "testing/gtest_mac.h"
+
+namespace remoting {
+
+namespace {
+
+NSString* kHostId = @"testHost";
+NSString* kHostPin = @"testHostPin";
+NSString* kPairId = @"testPairId";
+NSString* kPairSecret = @"testPairSecret";
+
+} // namespace
+
+class DataStoreTest : public ::testing::Test {
+ protected:
+ virtual void SetUp() OVERRIDE {
+ store_ = [[DataStore allocWithZone:nil] init];
+ RemoveAllHosts();
+ EXPECT_EQ(0, HostCount());
+ }
+ virtual void TearDown() OVERRIDE { RemoveAllHosts(); }
+
+ int HostCount() { return [[store_ allHosts] count]; }
+
+ void RemoveAllHosts() {
+ while (HostCount() > 0) {
+ [store_ removeHost:[store_ allHosts].firstObject];
+ }
+ [store_ saveChanges];
+ }
+
+ DataStore* store_;
+};
+
+TEST(DataStoreTest_Static, IsSingleInstance) {
+ DataStore* firstStore = [DataStore sharedStore];
+
+ ASSERT_NSEQ(firstStore, [DataStore sharedStore]);
+}
+
+TEST(DataStoreTest_Static, RemoveAllHost) {
+ // Test this functionality independently before expecting the fixture to do
+ // this correctly during cleanup
+ DataStore* store = [DataStore sharedStore];
+
+ while ([[store allHosts] count]) {
+ [store removeHost:[store allHosts].firstObject];
+ }
+
+ ASSERT_EQ(0, [[store allHosts] count]);
+ store = nil;
+}
+
+TEST_F(DataStoreTest, CreateHost) {
+
+ const HostPreferences* host = [store_ createHost:kHostId];
+ ASSERT_STREQ([kHostId UTF8String], [host.hostId UTF8String]);
+ ASSERT_EQ(1, HostCount());
+}
+
+TEST_F(DataStoreTest, GetHostForId) {
+ const HostPreferences* host = [store_ getHostForId:kHostId];
+ ASSERT_TRUE(host == nil);
+
+ [store_ createHost:kHostId];
+
+ host = [store_ getHostForId:kHostId];
+
+ ASSERT_TRUE(host != nil);
+ ASSERT_STREQ([kHostId UTF8String], [host.hostId UTF8String]);
+}
+
+TEST_F(DataStoreTest, SaveChanges) {
+
+ const HostPreferences* newHost = [store_ createHost:kHostId];
+
+ ASSERT_EQ(1, HostCount());
+
+ // Default values for a new host
+ ASSERT_TRUE([newHost.askForPin boolValue] == NO);
+ ASSERT_TRUE(newHost.hostPin == nil);
+ ASSERT_TRUE(newHost.pairId == nil);
+ ASSERT_TRUE(newHost.pairSecret == nil);
+
+ // Set new values and save
+ newHost.askForPin = [NSNumber numberWithBool:YES];
+ newHost.hostPin = kHostPin;
+ newHost.pairId = kPairId;
+ newHost.pairSecret = kPairSecret;
+
+ [store_ saveChanges];
+
+ // The next time the store is loaded the host will still be present, even
+ // though we are about to release and reinit a new object
+ store_ = nil;
+ store_ = [[DataStore allocWithZone:nil] init];
+ ASSERT_EQ(1, HostCount());
+
+ const HostPreferences* host = [store_ getHostForId:kHostId];
+ ASSERT_TRUE(host != nil);
+ ASSERT_STREQ([kHostId UTF8String], [host.hostId UTF8String]);
+ ASSERT_TRUE([host.askForPin boolValue] == YES);
+ ASSERT_STREQ([kHostPin UTF8String], [host.hostPin UTF8String]);
+ ASSERT_STREQ([kPairId UTF8String], [host.pairId UTF8String]);
+ ASSERT_STREQ([kPairSecret UTF8String], [host.pairSecret UTF8String]);
+}
+
+} // namespace remoting
diff --git a/remoting/ios/host.h b/remoting/ios/host.h
new file mode 100644
index 0000000..62b4fbd
--- /dev/null
+++ b/remoting/ios/host.h
@@ -0,0 +1,28 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef REMOTING_IOS_HOST_H_
+#define REMOTING_IOS_HOST_H_
+
+#import <Foundation/Foundation.h>
+
+// A detail record for a Chromoting Host
+@interface Host : NSObject
+
+// Various properties of the Chromoting Host
+@property(nonatomic, copy) NSString* createdTime;
+@property(nonatomic, copy) NSString* hostId;
+@property(nonatomic, copy) NSString* hostName;
+@property(nonatomic, copy) NSString* hostVersion;
+@property(nonatomic, copy) NSString* jabberId;
+@property(nonatomic, copy) NSString* kind;
+@property(nonatomic, copy) NSString* publicKey;
+@property(nonatomic, copy) NSString* status;
+@property(nonatomic, copy) NSString* updatedTime;
+
++ (NSMutableArray*)parseListFromJSON:(NSMutableData*)data;
+
+@end
+
+#endif // REMOTING_IOS_HOST_H_
diff --git a/remoting/ios/host.mm b/remoting/ios/host.mm
new file mode 100644
index 0000000..592be87
--- /dev/null
+++ b/remoting/ios/host.mm
@@ -0,0 +1,59 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "remoting/ios/host.h"
+
+@implementation Host
+
+@synthesize createdTime = _createdTime;
+@synthesize hostId = _hostId;
+@synthesize hostName = _hostName;
+@synthesize hostVersion = _hostVersion;
+@synthesize jabberId = _jabberId;
+@synthesize kind = _kind;
+@synthesize publicKey = _publicKey;
+@synthesize status = _status;
+@synthesize updatedTime = _updatedTime;
+
+// Parse jsonData into Host list
++ (NSMutableArray*)parseListFromJSON:(NSMutableData*)data {
+ NSError* error;
+
+ NSDictionary* json = [NSJSONSerialization JSONObjectWithData:data
+ options:kNilOptions
+ error:&error];
+
+ NSDictionary* dataDict = [json objectForKey:@"data"];
+
+ NSArray* availableServers = [dataDict objectForKey:@"items"];
+
+ NSMutableArray* serverList = [[NSMutableArray alloc] init];
+
+ NSUInteger idx = 0;
+ NSDictionary* svr;
+ NSUInteger count = [availableServers count];
+
+ while (idx < count) {
+ svr = [availableServers objectAtIndex:idx++];
+ Host* host = [[Host alloc] init];
+ host.createdTime = [svr objectForKey:@"createdTime"];
+ host.hostId = [svr objectForKey:@"hostId"];
+ host.hostName = [svr objectForKey:@"hostName"];
+ host.hostVersion = [svr objectForKey:@"hostVersion"];
+ host.jabberId = [svr objectForKey:@"jabberId"];
+ host.kind = [svr objectForKey:@"kind"];
+ host.publicKey = [svr objectForKey:@"publicKey"];
+ host.status = [svr objectForKey:@"status"];
+ host.updatedTime = [svr objectForKey:@"updatedTime"];
+ [serverList addObject:host];
+ }
+
+ return serverList;
+}
+
+@end
diff --git a/remoting/ios/host_cell.h b/remoting/ios/host_cell.h
new file mode 100644
index 0000000..d18b933
--- /dev/null
+++ b/remoting/ios/host_cell.h
@@ -0,0 +1,20 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef REMOTING_IOS_HOST_CELL_H_
+#define REMOTING_IOS_HOST_CELL_H_
+
+#import <UIKit/UIKit.h>
+
+// HostCell represents a Host as a row in a tableView, where the row
+// contains a single cell. Several button and outlet are reserved here for
+// future functionality
+@interface HostCell : UITableViewCell
+
+@property(weak, nonatomic) IBOutlet UILabel* labelHostName;
+@property(weak, nonatomic) IBOutlet UILabel* labelStatus;
+
+@end
+
+#endif // REMOTING_IOS_HOST_CELL_H_
diff --git a/remoting/ios/host_cell.mm b/remoting/ios/host_cell.mm
new file mode 100644
index 0000000..490a94e
--- /dev/null
+++ b/remoting/ios/host_cell.mm
@@ -0,0 +1,20 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "remoting/ios/host_cell.h"
+
+@implementation HostCell
+
+// Override UITableViewCell
+- (id)initWithStyle:(UITableViewCellStyle)style
+ reuseIdentifier:(NSString*)reuseIdentifier {
+ self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
+ return self;
+}
+
+@end
diff --git a/remoting/ios/host_preferences.h b/remoting/ios/host_preferences.h
new file mode 100644
index 0000000..6becae5
--- /dev/null
+++ b/remoting/ios/host_preferences.h
@@ -0,0 +1,32 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef REMOTING_IOS_HOST_PREFERENCES_H_
+#define REMOTING_IOS_HOST_PREFERENCES_H_
+
+#import <CoreData/CoreData.h>
+
+// A HostPreferences contains details to negotiate and maintain a connection
+// to a remote Chromoting host. This is a entity in a backing store. The
+// implementation file is ChromotingModel.xcdatamodeld. If this file is
+// updated, also update the model. The model MUST be properly versioned to
+// ensure backwards compatibility.
+// https://developer.apple.com/library/ios/recipes/xcode_help-core_data_modeling_tool/Articles/creating_new_version.html
+// Or the app must be uninstalled, and reinstalled which will erase the previous
+// version of the backing store.
+@interface HostPreferences : NSManagedObject
+
+// Is a prompt is needed to reconnect or continue the connection to
+// the host
+@property(nonatomic, copy) NSNumber* askForPin;
+// Several properties are populated from the jabber jump server
+@property(nonatomic, copy) NSString* hostId;
+// Supplied by client via UI interaction
+@property(nonatomic, copy) NSString* hostPin;
+@property(nonatomic, copy) NSString* pairId;
+@property(nonatomic, copy) NSString* pairSecret;
+
+@end
+
+#endif // REMOTING_IOS_HOST_PREFERENCES_H_ \ No newline at end of file
diff --git a/remoting/ios/host_refresh.h b/remoting/ios/host_refresh.h
new file mode 100644
index 0000000..8cdef3e
--- /dev/null
+++ b/remoting/ios/host_refresh.h
@@ -0,0 +1,37 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef REMOTING_IOS_HOST_REFRESH_H_
+#define REMOTING_IOS_HOST_REFRESH_H_
+
+#import <Foundation/Foundation.h>
+
+#import "GTMOAuth2Authentication.h"
+
+// HostRefresh encapsulates a fetch of the Chromoting host list from
+// the jabber service.
+
+// Contract to handle the host list result of a Chromoting host list fetch.
+@protocol HostRefreshDelegate<NSObject>
+
+- (void)hostListRefresh:(NSArray*)hostList errorMessage:(NSString*)errorMessage;
+
+@end
+
+// Fetches the host list from the jabber service async. Authenticates,
+// and parses the results to provide to a HostListViewController
+@interface HostRefresh : NSObject<NSURLConnectionDataDelegate>
+
+// Store data read while the connection is active, and can be used after the
+// connection has been discarded
+@property(nonatomic, copy) NSMutableData* jsonData;
+@property(nonatomic, copy) NSString* errorMessage;
+@property(nonatomic, assign) id<HostRefreshDelegate> delegate;
+
+- (void)refreshHostList:(GTMOAuth2Authentication*)authReq
+ delegate:(id<HostRefreshDelegate>)delegate;
+
+@end
+
+#endif // REMOTING_IOS_HOST_REFRESH_H_
diff --git a/remoting/ios/host_refresh.mm b/remoting/ios/host_refresh.mm
new file mode 100644
index 0000000..bf5e67e
--- /dev/null
+++ b/remoting/ios/host_refresh.mm
@@ -0,0 +1,132 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "remoting/ios/host_refresh.h"
+
+#import "remoting/ios/authorize.h"
+#import "remoting/ios/host.h"
+#import "remoting/ios/utility.h"
+
+namespace {
+NSString* kDefaultErrorMessage = @"The Host list refresh is not available at "
+ @"this time. Please try again later.";
+} // namespace
+
+@interface HostRefresh (Private)
+- (void)authentication:(GTMOAuth2Authentication*)auth
+ request:(NSMutableURLRequest*)request
+ error:(NSError*)error;
+- (void)formatErrorMessage:(NSString*)error;
+- (void)notifyDelegate;
+@end
+
+// Logic flow begins with refreshHostList, and continues until an error occurs,
+// or the host list is returned to the delegate
+@implementation HostRefresh
+
+@synthesize jsonData = _jsonData;
+@synthesize errorMessage = _errorMessage;
+@synthesize delegate = _delegate;
+
+// Override default constructor and initialize internals
+- (id)init {
+ self = [super init];
+ if (self) {
+ _jsonData = [[NSMutableData alloc] init];
+ }
+ return self;
+}
+
+// Begin the authentication and authorization process. Begin the process by
+// creating an oAuth2 request to google api's including the needed scopes to
+// fetch the users host list.
+- (void)refreshHostList:(GTMOAuth2Authentication*)authReq
+ delegate:(id<HostRefreshDelegate>)delegate {
+
+ CHECK(_delegate == nil); // Do not reuse an instance of this class
+
+ _delegate = delegate;
+
+ [Authorize beginRequest:authReq
+ delegate:self
+ didFinishSelector:@selector(authentication:request:error:)];
+}
+
+// Handle completion of the authorization process. Append service credentials
+// for jabber. If an error occurred, notify user.
+- (void)authentication:(NSObject*)auth
+ request:(NSMutableURLRequest*)request
+ error:(NSError*)error {
+ if (error != nil) {
+ [self formatErrorMessage:error.localizedDescription];
+ } else {
+ // Add credentials for service
+ [Authorize appendCredentials:request];
+
+ // Begin connection, the returned reference is not useful right now and
+ // marked as __unused
+ __unused NSURLConnection* connection =
+ [[NSURLConnection alloc] initWithRequest:request delegate:self];
+ }
+}
+
+// @protocol NSURLConnectionDelegate, handle any error during connection
+- (void)connection:(NSURLConnection*)connection
+ didFailWithError:(NSError*)error {
+ [self formatErrorMessage:[error localizedDescription]];
+
+ [self notifyDelegate];
+}
+
+// @protocol NSURLConnectionDataDelegate, may be called async multiple times.
+// Each call appends the new data to the known data until completed.
+- (void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)data {
+ [_jsonData appendData:data];
+}
+
+// @protocol NSURLConnectionDataDelegate
+// Ensure connection succeeded: HTTP 200 OK
+- (void)connection:(NSURLConnection*)connection
+ didReceiveResponse:(NSURLResponse*)response {
+ NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
+ if ([response respondsToSelector:@selector(allHeaderFields)]) {
+ NSNumber* responseCode =
+ [[NSNumber alloc] initWithInteger:[httpResponse statusCode]];
+ if (responseCode.intValue != 200) {
+ [self formatErrorMessage:[NSString
+ stringWithFormat:@"HTTP STATUS CODE: %d",
+ [httpResponse statusCode]]];
+ }
+ }
+}
+
+// @protocol NSURLConnectionDataDelegate handle a completed connection, parse
+// received data, and return host list to delegate
+- (void)connectionDidFinishLoading:(NSURLConnection*)connection {
+ [self notifyDelegate];
+}
+
+// Store a formatted error message to return later
+- (void)formatErrorMessage:(NSString*)error {
+ _errorMessage = kDefaultErrorMessage;
+ if (error != nil && error.length > 0) {
+ _errorMessage = [_errorMessage
+ stringByAppendingString:[@" " stringByAppendingString:error]];
+ }
+}
+
+// The connection has finished, call to delegate
+- (void)notifyDelegate {
+ if (_jsonData.length == 0 && _errorMessage == nil) {
+ [self formatErrorMessage:nil];
+ }
+
+ [_delegate hostListRefresh:[Host parseListFromJSON:_jsonData]
+ errorMessage:_errorMessage];
+}
+@end
diff --git a/remoting/ios/host_refresh_test_helper.h b/remoting/ios/host_refresh_test_helper.h
new file mode 100644
index 0000000..aac365b
--- /dev/null
+++ b/remoting/ios/host_refresh_test_helper.h
@@ -0,0 +1,102 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef REMOTING_IOS_HOST_REFRESH_TEST_HELPER_H_
+#define REMOTING_IOS_HOST_REFRESH_TEST_HELPER_H_
+
+#import <Foundation/Foundation.h>
+
+namespace remoting {
+
+class HostRefreshTestHelper {
+ public:
+ constexpr static NSString* CloseTag = @"\",";
+
+ constexpr static NSString* CreatedTimeTag = @"\"createdTime\":\"";
+ constexpr static NSString* HostIdTag = @"\"hostId\":\"";
+ constexpr static NSString* HostNameTag = @"\"hostName\":\"";
+ constexpr static NSString* HostVersionTag = @"\"hostVersion\":\"";
+ constexpr static NSString* KindTag = @"\"kind\":\"";
+ constexpr static NSString* JabberIdTag = @"\"jabberId\":\"";
+ constexpr static NSString* PublicKeyTag = @"\"publicKey\":\"";
+ constexpr static NSString* StatusTag = @"\"status\":\"";
+ constexpr static NSString* UpdatedTimeTag = @"\"updatedTime\":\"";
+
+ constexpr static NSString* CreatedTimeTest = @"2000-01-01T00:00:01.000Z";
+ constexpr static NSString* HostIdTest = @"Host1";
+ constexpr static NSString* HostNameTest = @"HostName1";
+ constexpr static NSString* HostVersionTest = @"2.22.5.4";
+ constexpr static NSString* KindTest = @"chromoting#host";
+ constexpr static NSString* JabberIdTest = @"JabberingOn";
+ constexpr static NSString* PublicKeyTest = @"AAAAABBBBBZZZZZ";
+ constexpr static NSString* StatusTest = @"TESTING";
+ constexpr static NSString* UpdatedTimeTest = @"2004-01-01T00:00:01.000Z";
+
+ static NSMutableData* GetHostList(int numHosts) {
+ return [NSMutableData
+ dataWithData:[GetMultipleHosts(numHosts)
+ dataUsingEncoding:NSUTF8StringEncoding]];
+ }
+
+ static NSMutableData* GetHostList(NSString* hostList) {
+ return [NSMutableData
+ dataWithData:[hostList dataUsingEncoding:NSUTF8StringEncoding]];
+ }
+
+ static NSString* GetMultipleHosts(int numHosts) {
+ NSString* client = [NSString
+ stringWithFormat:
+ @"%@%@%@%@%@%@%@%@%@%@%@%@%@%@%@%@%@%@%@%@%@%@%@%@%@%@%@%@%@%@%@",
+ @"{",
+ CreatedTimeTag,
+ CreatedTimeTest,
+ CloseTag,
+ HostIdTag,
+ HostIdTest,
+ CloseTag,
+ HostNameTag,
+ HostNameTest,
+ CloseTag,
+ HostNameTag,
+ HostNameTest,
+ CloseTag,
+ HostVersionTag,
+ HostVersionTest,
+ CloseTag,
+ KindTag,
+ KindTest,
+ CloseTag,
+ JabberIdTag,
+ JabberIdTest,
+ CloseTag,
+ PublicKeyTag,
+ PublicKeyTest,
+ CloseTag,
+ StatusTag,
+ StatusTest,
+ CloseTag,
+ UpdatedTimeTag,
+ UpdatedTimeTest,
+ @"\"}"];
+
+ NSMutableString* hostList = [NSMutableString
+ stringWithString:
+ @"{\"data\":{\"kind\":\"chromoting#hostList\",\"items\":["];
+
+ for (int i = 0; i < numHosts; i++) {
+ [hostList appendString:client];
+ if (i < numHosts - 1) {
+ [hostList appendString:@","]; // common separated
+ }
+ }
+
+ [hostList appendString:@"]}}"];
+
+ return [hostList copy];
+ }
+};
+
+} // namespace remoting
+
+#endif // REMOTING_IOS_HOST_REFRESH_TEST_HELPER_H_ \ No newline at end of file
diff --git a/remoting/ios/host_refresh_unittest.mm b/remoting/ios/host_refresh_unittest.mm
new file mode 100644
index 0000000..54f2ee7
--- /dev/null
+++ b/remoting/ios/host_refresh_unittest.mm
@@ -0,0 +1,170 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "remoting/ios/host_refresh.h"
+
+#import "base/compiler_specific.h"
+#import "testing/gtest_mac.h"
+
+#import "remoting/ios/host.h"
+#import "remoting/ios/host_refresh_test_helper.h"
+
+@interface HostRefreshDelegateTester : NSObject<HostRefreshDelegate>
+
+@property(nonatomic) NSArray* hostList;
+@property(nonatomic) NSString* errorMessage;
+
+@end
+
+@implementation HostRefreshDelegateTester
+
+@synthesize hostList = _hostList;
+@synthesize errorMessage = _errorMessage;
+
+- (void)hostListRefresh:(NSArray*)hostList
+ errorMessage:(NSString*)errorMessage {
+ _hostList = hostList;
+ _errorMessage = errorMessage;
+}
+
+- (bool)receivedHosts {
+ return (_hostList.count > 0);
+}
+
+- (bool)receivedErrorMessage {
+ return (_errorMessage != nil);
+}
+
+@end
+
+namespace remoting {
+
+namespace {
+
+NSString* kErrorMessageTest = @"TestErrorMessage";
+
+} // namespace
+
+class HostRefreshTest : public ::testing::Test {
+ protected:
+ virtual void SetUp() OVERRIDE {
+ hostRefreshProcessor_ = [[HostRefresh allocWithZone:nil] init];
+ delegateTester_ = [[HostRefreshDelegateTester alloc] init];
+ [hostRefreshProcessor_ setDelegate:delegateTester_];
+ }
+
+ void CreateHostList(int numHosts) {
+ [hostRefreshProcessor_
+ setJsonData:HostRefreshTestHelper::GetHostList(numHosts)];
+ }
+
+ NSError* CreateErrorFromString(NSString* message) {
+ NSDictionary* errorDictionary = nil;
+
+ if (message != nil) {
+ errorDictionary = @{NSLocalizedDescriptionKey : message};
+ }
+
+ return [[NSError alloc] initWithDomain:@"HostRefreshTest"
+ code:EPERM
+ userInfo:errorDictionary];
+ }
+
+ HostRefresh* hostRefreshProcessor_;
+ HostRefreshDelegateTester* delegateTester_;
+};
+
+TEST_F(HostRefreshTest, ErrorFormatter) {
+ [hostRefreshProcessor_ connection:nil
+ didFailWithError:CreateErrorFromString(nil)];
+ ASSERT_FALSE(hostRefreshProcessor_.errorMessage == nil);
+
+ [hostRefreshProcessor_ connection:nil
+ didFailWithError:CreateErrorFromString(@"")];
+ ASSERT_FALSE(hostRefreshProcessor_.errorMessage == nil);
+
+ [hostRefreshProcessor_ connection:nil
+ didFailWithError:CreateErrorFromString(kErrorMessageTest)];
+ ASSERT_TRUE([hostRefreshProcessor_.errorMessage
+ rangeOfString:kErrorMessageTest].location != NSNotFound);
+}
+
+TEST_F(HostRefreshTest, JSONParsing) {
+ // There were no hosts returned
+ CreateHostList(0);
+ [hostRefreshProcessor_ connectionDidFinishLoading:nil];
+ ASSERT_TRUE(delegateTester_.hostList.count == 0);
+
+ CreateHostList(1);
+ [hostRefreshProcessor_ connectionDidFinishLoading:nil];
+ ASSERT_TRUE(delegateTester_.hostList.count == 1);
+
+ Host* host = static_cast<Host*>([delegateTester_.hostList objectAtIndex:0]);
+ ASSERT_NSEQ(HostRefreshTestHelper::CreatedTimeTest, host.createdTime);
+ ASSERT_NSEQ(HostRefreshTestHelper::HostIdTest, host.hostId);
+ ASSERT_NSEQ(HostRefreshTestHelper::HostNameTest, host.hostName);
+ ASSERT_NSEQ(HostRefreshTestHelper::HostVersionTest, host.hostVersion);
+ ASSERT_NSEQ(HostRefreshTestHelper::KindTest, host.kind);
+ ASSERT_NSEQ(HostRefreshTestHelper::JabberIdTest, host.jabberId);
+ ASSERT_NSEQ(HostRefreshTestHelper::PublicKeyTest, host.publicKey);
+ ASSERT_NSEQ(HostRefreshTestHelper::StatusTest, host.status);
+ ASSERT_NSEQ(HostRefreshTestHelper::UpdatedTimeTest, host.updatedTime);
+
+ CreateHostList(11);
+ [hostRefreshProcessor_ connectionDidFinishLoading:nil];
+ ASSERT_TRUE(delegateTester_.hostList.count == 11);
+
+ // An error in parsing returns no hosts
+ [hostRefreshProcessor_
+ setJsonData:
+ [NSMutableData
+ dataWithData:
+ [@"{\"dataaaaaafa\":{\"kiffffnd\":\"chromoting#hostList\"}}"
+ dataUsingEncoding:NSUTF8StringEncoding]]];
+ [hostRefreshProcessor_ connectionDidFinishLoading:nil];
+ ASSERT_TRUE(delegateTester_.hostList.count == 0);
+}
+
+TEST_F(HostRefreshTest, HostListDelegateNoList) {
+ // Hosts were not processed at all
+ [hostRefreshProcessor_ connectionDidFinishLoading:nil];
+ ASSERT_FALSE([delegateTester_ receivedHosts]);
+ ASSERT_TRUE([delegateTester_ receivedErrorMessage]);
+
+ // There were no hosts returned
+ CreateHostList(0);
+ [hostRefreshProcessor_ connectionDidFinishLoading:nil];
+ ASSERT_FALSE([delegateTester_ receivedHosts]);
+ ASSERT_TRUE([delegateTester_ receivedErrorMessage]);
+}
+
+TEST_F(HostRefreshTest, HostListDelegateHasList) {
+ CreateHostList(1);
+ [hostRefreshProcessor_ connectionDidFinishLoading:nil];
+ ASSERT_TRUE([delegateTester_ receivedHosts]);
+ ASSERT_FALSE([delegateTester_ receivedErrorMessage]);
+}
+
+TEST_F(HostRefreshTest, HostListDelegateHasListWithError) {
+ CreateHostList(1);
+
+ [hostRefreshProcessor_ connection:nil
+ didFailWithError:CreateErrorFromString(kErrorMessageTest)];
+
+ [hostRefreshProcessor_ connectionDidFinishLoading:nil];
+ ASSERT_TRUE([delegateTester_ receivedHosts]);
+ ASSERT_TRUE([delegateTester_ receivedErrorMessage]);
+}
+
+TEST_F(HostRefreshTest, ConnectionFailed) {
+ [hostRefreshProcessor_ connection:nil didFailWithError:nil];
+ ASSERT_FALSE([delegateTester_ receivedHosts]);
+ ASSERT_TRUE([delegateTester_ receivedErrorMessage]);
+}
+
+} // namespace remoting \ No newline at end of file
diff --git a/remoting/ios/key_input.h b/remoting/ios/key_input.h
new file mode 100644
index 0000000..302a63c
--- /dev/null
+++ b/remoting/ios/key_input.h
@@ -0,0 +1,35 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef REMOTING_IOS_KEY_INPUT_H_
+#define REMOTING_IOS_KEY_INPUT_H_
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+// Key codes are translated from the on screen keyboard to the scan codes
+// needed for Chromoting input. We don't have a good automated approach to do
+// this. Instead we have created a mapping manually via trial and error. To
+// support other keyboards in this context we would have to test and create a
+// mapping for each keyboard manually.
+
+// Contract to handle translated key presses from the on-screen keyboard to
+// the format required for Chromoting keyboard input
+@protocol KeyInputDelegate<NSObject>
+
+- (void)keyboardActionKeyCode:(uint32_t)keyPressed isKeyDown:(BOOL)keyDown;
+
+- (void)keyboardDismissed;
+
+@end
+
+@interface KeyInput : UIView<UIKeyInput>
+
+@property(weak, nonatomic) id<KeyInputDelegate> delegate;
+
+- (void)ctrlAltDel;
+
+@end
+
+#endif // REMOTING_IOS_KEY_INPUT_H_ \ No newline at end of file
diff --git a/remoting/ios/key_input.mm b/remoting/ios/key_input.mm
new file mode 100644
index 0000000..0e51efa
--- /dev/null
+++ b/remoting/ios/key_input.mm
@@ -0,0 +1,111 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "remoting/ios/key_input.h"
+#import "remoting/ios/key_map_us.h"
+
+@interface KeyInput (Private)
+- (void)transmitAppropriateKeyCode:(NSString*)text;
+- (void)transmitKeyCode:(NSInteger)keyCode needShift:(bool)needShift;
+@end
+
+@implementation KeyInput
+
+@synthesize delegate = _delegate;
+
+// Override UIKeyInput::UITextInputTraits property
+- (UIKeyboardType)keyboardType {
+ return UIKeyboardTypeAlphabet;
+}
+
+// Override UIView::UIResponder, when this interface is the first responder
+// on-screen keyboard input will create events for Chromoting keyboard input
+- (BOOL)canBecomeFirstResponder {
+ return YES;
+}
+
+// Override UIView::UIResponder
+// Keyboard was dismissed
+- (BOOL)resignFirstResponder {
+ BOOL wasFirstResponder = self.isFirstResponder;
+ BOOL didResignFirstReponder =
+ [super resignFirstResponder]; // I'm not sure that this returns YES when
+ // first responder was resigned, but for
+ // now I don't actually need to know what
+ // the return from super means.
+ if (wasFirstResponder) {
+ [_delegate keyboardDismissed];
+ }
+
+ return didResignFirstReponder;
+}
+
+// @protocol UIKeyInput, Send backspace
+- (void)deleteBackward {
+ [self transmitKeyCode:kKeyCodeUS[kBackspaceIndex] needShift:false];
+}
+
+// @protocol UIKeyInput, Assume this is a text input
+- (BOOL)hasText {
+ return YES;
+}
+
+// @protocol UIKeyInput, Translate inserted text to key presses, one char at a
+// time
+- (void)insertText:(NSString*)text {
+ [self transmitAppropriateKeyCode:text];
+}
+
+- (void)ctrlAltDel {
+ if (_delegate) {
+ [_delegate keyboardActionKeyCode:kKeyCodeUS[kCtrlIndex] isKeyDown:YES];
+ [_delegate keyboardActionKeyCode:kKeyCodeUS[kAltIndex] isKeyDown:YES];
+ [_delegate keyboardActionKeyCode:kKeyCodeUS[kDelIndex] isKeyDown:YES];
+ [_delegate keyboardActionKeyCode:kKeyCodeUS[kDelIndex] isKeyDown:NO];
+ [_delegate keyboardActionKeyCode:kKeyCodeUS[kAltIndex] isKeyDown:NO];
+ [_delegate keyboardActionKeyCode:kKeyCodeUS[kCtrlIndex] isKeyDown:NO];
+ }
+}
+
+// When inserting multiple characters, process them one at a time. |text| is as
+// it was output on the device. The shift key is not naturally presented in the
+// input stream, and must be inserted by inspecting each char and considering
+// that if the key was input on a traditional keyboard that the character would
+// have required a shift. Assume caps lock does not exist.
+- (void)transmitAppropriateKeyCode:(NSString*)text {
+ for (int i = 0; i < [text length]; ++i) {
+ NSInteger charToSend = [text characterAtIndex:i];
+
+ if (charToSend <= kKeyboardKeyMaxUS) {
+ [self transmitKeyCode:kKeyCodeUS[charToSend]
+ needShift:kIsShiftRequiredUS[charToSend]];
+ }
+ }
+}
+
+// |charToSend| is as it was output on the device. Some call this a
+// 'key press'. For Chromoting this must be transferred as a key down (press
+// down with a finger), followed by a key up (finger is removed from the
+// keyboard)
+//
+// The delivery may be an upper case or special character. Chromoting is just
+// interested in the button that was pushed, so to create an upper case
+// character, first send a shift press, then the button, then release shift
+- (void)transmitKeyCode:(NSInteger)keyCode needShift:(bool)needShift {
+ if (keyCode > 0 && _delegate) {
+ if (needShift) {
+ [_delegate keyboardActionKeyCode:kKeyCodeUS[kShiftIndex] isKeyDown:YES];
+ }
+ [_delegate keyboardActionKeyCode:keyCode isKeyDown:YES];
+ [_delegate keyboardActionKeyCode:keyCode isKeyDown:NO];
+ if (needShift) {
+ [_delegate keyboardActionKeyCode:kKeyCodeUS[kShiftIndex] isKeyDown:NO];
+ }
+ }
+}
+@end
diff --git a/remoting/ios/key_input_unittest.mm b/remoting/ios/key_input_unittest.mm
new file mode 100644
index 0000000..b893b9f
--- /dev/null
+++ b/remoting/ios/key_input_unittest.mm
@@ -0,0 +1,124 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "remoting/ios/key_input.h"
+#import "remoting/ios/key_map_us.h"
+
+#include <vector>
+
+#import "base/compiler_specific.h"
+#import "testing/gtest_mac.h"
+
+@interface KeyInputDelegateTester : NSObject<KeyInputDelegate> {
+ @private
+ std::vector<uint32_t> _keyList;
+}
+
+@property(nonatomic, assign) int numKeysDown;
+@property(nonatomic, assign) BOOL wasDismissed;
+
+- (std::vector<uint32_t>&)getKeyList;
+
+@end
+
+@implementation KeyInputDelegateTester
+
+- (std::vector<uint32_t>&)getKeyList {
+ return _keyList;
+}
+
+- (void)keyboardDismissed {
+ // This can not be tested, because we can not set |keyInput_| as
+ // FirstResponder in this test harness
+ _wasDismissed = true;
+}
+
+- (void)keyboardActionKeyCode:(uint32_t)keyPressed isKeyDown:(BOOL)keyDown {
+ if (keyDown) {
+ _keyList.push_back(keyPressed);
+ _numKeysDown++;
+ } else {
+ _numKeysDown--;
+ }
+}
+
+@end
+
+namespace remoting {
+
+class KeyInputTest : public ::testing::Test {
+ protected:
+ virtual void SetUp() OVERRIDE {
+ keyInput_ = [[KeyInput allocWithZone:nil] init];
+ delegateTester_ = [[KeyInputDelegateTester alloc] init];
+ keyInput_.delegate = delegateTester_;
+ }
+
+ KeyInput* keyInput_;
+ KeyInputDelegateTester* delegateTester_;
+};
+
+TEST_F(KeyInputTest, SendKey) {
+ // Empty
+ [keyInput_ insertText:@""];
+ ASSERT_EQ(0, delegateTester_.numKeysDown);
+ ASSERT_EQ(0, [delegateTester_ getKeyList].size());
+
+ // Value is out of bounds
+ [keyInput_ insertText:@"ó"];
+ ASSERT_EQ(0, delegateTester_.numKeysDown);
+ ASSERT_EQ(0, [delegateTester_ getKeyList].size());
+
+ // Lower case
+ [keyInput_ insertText:@"a"];
+ ASSERT_EQ(0, delegateTester_.numKeysDown);
+ ASSERT_EQ(1, [delegateTester_ getKeyList].size());
+ ASSERT_EQ(kKeyCodeUS['a'], [delegateTester_ getKeyList][0]);
+ // Upper Case
+ [delegateTester_ getKeyList].clear();
+ [keyInput_ insertText:@"A"];
+ ASSERT_EQ(0, delegateTester_.numKeysDown);
+ ASSERT_EQ(2, [delegateTester_ getKeyList].size());
+ ASSERT_EQ(kKeyCodeUS[kShiftIndex], [delegateTester_ getKeyList][0]);
+ ASSERT_EQ(kKeyCodeUS['A'], [delegateTester_ getKeyList][1]);
+
+ // Multiple characters and mixed case
+ [delegateTester_ getKeyList].clear();
+ [keyInput_ insertText:@"ABCabc"];
+ ASSERT_EQ(0, delegateTester_.numKeysDown);
+ ASSERT_EQ(9, [delegateTester_ getKeyList].size());
+ ASSERT_EQ(kKeyCodeUS[kShiftIndex], [delegateTester_ getKeyList][0]);
+ ASSERT_EQ(kKeyCodeUS['A'], [delegateTester_ getKeyList][1]);
+ ASSERT_EQ(kKeyCodeUS[kShiftIndex], [delegateTester_ getKeyList][2]);
+ ASSERT_EQ(kKeyCodeUS['B'], [delegateTester_ getKeyList][3]);
+ ASSERT_EQ(kKeyCodeUS[kShiftIndex], [delegateTester_ getKeyList][4]);
+ ASSERT_EQ(kKeyCodeUS['C'], [delegateTester_ getKeyList][5]);
+ ASSERT_EQ(kKeyCodeUS['a'], [delegateTester_ getKeyList][6]);
+ ASSERT_EQ(kKeyCodeUS['b'], [delegateTester_ getKeyList][7]);
+ ASSERT_EQ(kKeyCodeUS['c'], [delegateTester_ getKeyList][8]);
+}
+
+TEST_F(KeyInputTest, CtrlAltDel) {
+ [keyInput_ ctrlAltDel];
+
+ ASSERT_EQ(0, delegateTester_.numKeysDown);
+ ASSERT_EQ(3, [delegateTester_ getKeyList].size());
+ ASSERT_EQ(kKeyCodeUS[kCtrlIndex], [delegateTester_ getKeyList][0]);
+ ASSERT_EQ(kKeyCodeUS[kAltIndex], [delegateTester_ getKeyList][1]);
+ ASSERT_EQ(kKeyCodeUS[kDelIndex], [delegateTester_ getKeyList][2]);
+}
+
+TEST_F(KeyInputTest, Backspace) {
+ [keyInput_ deleteBackward];
+
+ ASSERT_EQ(0, delegateTester_.numKeysDown);
+ ASSERT_EQ(1, [delegateTester_ getKeyList].size());
+ ASSERT_EQ(kKeyCodeUS[kBackspaceIndex], [delegateTester_ getKeyList][0]);
+}
+
+} // namespace remoting \ No newline at end of file
diff --git a/remoting/ios/key_map_us.h b/remoting/ios/key_map_us.h
new file mode 100644
index 0000000..c8283f0
--- /dev/null
+++ b/remoting/ios/key_map_us.h
@@ -0,0 +1,288 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef REMOTING_IOS_KEY_MAP_US_H_
+#define REMOTING_IOS_KEY_MAP_US_H_
+
+// A mapping for the US keyboard on a US IPAD to Chromoting Scancodes
+
+// This must be less than or equal to the size of
+// kIsShiftRequiredUS and kKeyCodeUS.
+const int kKeyboardKeyMaxUS = 126;
+
+// Index for specific keys
+const uint32_t kShiftIndex = 128;
+const uint32_t kBackspaceIndex = 129;
+const uint32_t kCtrlIndex = 130;
+const uint32_t kAltIndex = 131;
+const uint32_t kDelIndex = 132;
+
+const BOOL kIsShiftRequiredUS[] = {
+ NO, // [0] Numbering fields by index, not by count
+ NO, //
+ NO, //
+ NO, //
+ NO, //
+ NO, //
+ NO, //
+ NO, //
+ NO, //
+ NO, //
+ NO, // [10] ENTER
+ NO, //
+ NO, //
+ NO, //
+ NO, //
+ NO, //
+ NO, //
+ NO, //
+ NO, //
+ NO, //
+ NO, // [20]
+ NO, //
+ NO, //
+ NO, //
+ NO, //
+ NO, //
+ NO, //
+ NO, //
+ NO, //
+ NO, //
+ NO, // [30]
+ NO, //
+ NO, // SPACE
+ YES, // !
+ YES, // "
+ YES, // #
+ YES, // $
+ YES, // %
+ YES, // &
+ NO, // '
+ YES, // [40] (
+ YES, // )
+ YES, // *
+ YES, // +
+ NO, // ,
+ NO, // -
+ NO, // .
+ NO, // /
+ NO, // 0
+ NO, // 1
+ NO, // [50] 2
+ NO, // 3
+ NO, // 4
+ NO, // 5
+ NO, // 6
+ NO, // 7
+ NO, // 8
+ NO, // 9
+ YES, // :
+ NO, // ;
+ YES, // [60] <
+ NO, // =
+ YES, // >
+ YES, // ?
+ YES, // @
+ YES, // A
+ YES, // B
+ YES, // C
+ YES, // D
+ YES, // E
+ YES, // [70] F
+ YES, // G
+ YES, // H
+ YES, // I
+ YES, // J
+ YES, // K
+ YES, // L
+ YES, // M
+ YES, // N
+ YES, // O
+ YES, // [80] P
+ YES, // Q
+ YES, // R
+ YES, // S
+ YES, // T
+ YES, // U
+ YES, // V
+ YES, // W
+ YES, // X
+ YES, // Y
+ YES, // [90] Z
+ NO, // [
+ NO, // BACKSLASH
+ NO, // ]
+ YES, // ^
+ YES, // _
+ NO, //
+ NO, // a
+ NO, // b
+ NO, // c
+ NO, // [100] d
+ NO, // e
+ NO, // f
+ NO, // g
+ NO, // h
+ NO, // i
+ NO, // j
+ NO, // k
+ NO, // l
+ NO, // m
+ NO, // [110] n
+ NO, // o
+ NO, // p
+ NO, // q
+ NO, // r
+ NO, // s
+ NO, // t
+ NO, // u
+ NO, // v
+ NO, // w
+ NO, // [120] x
+ NO, // y
+ NO, // z
+ YES, // {
+ YES, // |
+ YES, // }
+ YES, // ~
+ NO // [127]
+};
+
+const uint32_t kKeyCodeUS[] = {
+ 0, // [0] Numbering fields by index, not by count
+ 0, //
+ 0, //
+ 0, //
+ 0, //
+ 0, //
+ 0, //
+ 0, //
+ 0, //
+ 0, //
+ 0x070028, // [10] ENTER
+ 0, //
+ 0, //
+ 0, //
+ 0, //
+ 0, //
+ 0, //
+ 0, //
+ 0, //
+ 0, //
+ 0, // [20]
+ 0, //
+ 0, //
+ 0, //
+ 0, //
+ 0, //
+ 0, //
+ 0, //
+ 0, //
+ 0, //
+ 0, // [30]
+ 0, //
+ 0x07002c, // SPACE
+ 0x07001e, // !
+ 0x070034, // "
+ 0x070020, // #
+ 0x070021, // $
+ 0x070022, // %
+ 0x070024, // &
+ 0x070034, // '
+ 0x070026, // [40] (
+ 0x070027, // )
+ 0x070025, // *
+ 0x07002e, // +
+ 0x070036, // ,
+ 0x07002d, // -
+ 0x070037, // .
+ 0x070038, // /
+ 0x070027, // 0
+ 0x07001e, // 1
+ 0x07001f, // [50] 2
+ 0x070020, // 3
+ 0x070021, // 4
+ 0x070022, // 5
+ 0x070023, // 6
+ 0x070024, // 7
+ 0x070025, // 8
+ 0x070026, // 9
+ 0x070033, // :
+ 0x070033, // ;
+ 0x070036, // [60] <
+ 0x07002e, // =
+ 0x070037, // >
+ 0x070038, // ?
+ 0x07001f, // @
+ 0x070004, // A
+ 0x070005, // B
+ 0x070006, // C
+ 0x070007, // D
+ 0x070008, // E
+ 0x070009, // [70] F
+ 0x07000a, // G
+ 0x07000b, // H
+ 0x07000c, // I
+ 0x07000d, // J
+ 0x07000e, // K
+ 0x07000f, // L
+ 0x070010, // M
+ 0x070011, // N
+ 0x070012, // O
+ 0x070013, // [80] P
+ 0x070014, // Q
+ 0x070015, // R
+ 0x070016, // S
+ 0x070017, // T
+ 0x070018, // U
+ 0x070019, // V
+ 0x07001a, // W
+ 0x07001b, // X
+ 0x07001c, // Y
+ 0x07001d, // [90] Z
+ 0x07002f, // [
+ 0x070031, // BACKSLASH
+ 0x070030, // ]
+ 0x070023, // ^
+ 0x07002d, // _
+ 0, //
+ 0x070004, // a
+ 0x070005, // b
+ 0x070006, // c
+ 0x070007, // [100] d
+ 0x070008, // e
+ 0x070009, // f
+ 0x07000a, // g
+ 0x07000b, // h
+ 0x07000c, // i
+ 0x07000d, // j
+ 0x07000e, // k
+ 0x07000f, // l
+ 0x070010, // m
+ 0x070011, // [110] n
+ 0x070012, // o
+ 0x070013, // p
+ 0x070014, // q
+ 0x070015, // r
+ 0x070016, // s
+ 0x070017, // t
+ 0x070018, // u
+ 0x070019, // v
+ 0x07001a, // w
+ 0x07001b, // [120] x
+ 0x07001c, // y
+ 0x07001d, // z
+ 0x07002f, // {
+ 0x070031, // |
+ 0x070030, // }
+ 0x070035, // ~
+ 0, // [127]
+ 0x0700e1, // SHIFT
+ 0x07002a, // BACKSPACE
+ 0x0700e0, // CTRL
+ 0x0700e2, // ALT
+ 0x07004c, // DEL
+};
+
+#endif // REMOTING_IOS_KEY_MAP_US_H_
diff --git a/remoting/ios/ui/cursor_texture.h b/remoting/ios/ui/cursor_texture.h
new file mode 100644
index 0000000..398a424
--- /dev/null
+++ b/remoting/ios/ui/cursor_texture.h
@@ -0,0 +1,58 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef REMOTING_IOS_UI_CURSOR_TEXTURE_H_
+#define REMOTING_IOS_UI_CURSOR_TEXTURE_H_
+
+#import <Foundation/Foundation.h>
+#import <GLKit/GLKit.h>
+
+#import "base/memory/scoped_ptr.h"
+
+#import "remoting/ios/utility.h"
+#include "third_party/webrtc/modules/desktop_capture/desktop_geometry.h"
+#include "third_party/webrtc/modules/desktop_capture/mouse_cursor.h"
+
+@interface CursorTexture : NSObject {
+ @private
+ // GL name
+ GLuint _textureId;
+ webrtc::DesktopSize _textureSize;
+ BOOL _needInitialize;
+
+ // The current cursor
+ scoped_ptr<webrtc::MouseCursor> _cursor;
+
+ BOOL _needCursorRedraw;
+
+ // Rectangle of the most recent cursor drawn to a GL Texture. On each
+ // successive frame when a new cursor is available this region is cleared on
+ // the GL Texture, so that the GL Texture is completely transparent again, and
+ // the cursor is then redrawn.
+ webrtc::DesktopRect _cursorDrawnToGL;
+}
+
+- (const webrtc::DesktopSize&)textureSize;
+
+- (void)setTextureSize:(const webrtc::DesktopSize&)size;
+
+- (const webrtc::MouseCursor&)cursor;
+
+- (void)setCursor:(webrtc::MouseCursor*)cursor;
+
+// bind this object to an effect's via the effects properties
+- (void)bindToEffect:(GLKEffectPropertyTexture*)effectProperty;
+
+// True if the cursor has changed in a way that requires it to be redrawn
+- (BOOL)needDrawAtPosition:(const webrtc::DesktopVector&)position;
+
+// needDrawAtPosition must be checked prior to calling drawWithMousePosition.
+// Draw mouse at the new position.
+- (void)drawWithMousePosition:(const webrtc::DesktopVector&)position;
+
+- (void)releaseTexture;
+
+@end
+
+#endif // REMOTING_IOS_UI_CURSOR_TEXTURE_H_ \ No newline at end of file
diff --git a/remoting/ios/ui/cursor_texture.mm b/remoting/ios/ui/cursor_texture.mm
new file mode 100644
index 0000000..9ffa5f7
--- /dev/null
+++ b/remoting/ios/ui/cursor_texture.mm
@@ -0,0 +1,181 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "remoting/ios/ui/cursor_texture.h"
+
+@implementation CursorTexture
+
+- (id)init {
+ self = [super init];
+ if (self) {
+ _needCursorRedraw = NO;
+ _cursorDrawnToGL = webrtc::DesktopRect::MakeXYWH(0, 0, 0, 0);
+ }
+ return self;
+}
+
+- (const webrtc::DesktopSize&)textureSize {
+ return _textureSize;
+}
+
+- (void)setTextureSize:(const webrtc::DesktopSize&)size {
+ if (!_textureSize.equals(size)) {
+ _textureSize.set(size.width(), size.height());
+ _needInitialize = true;
+ }
+}
+
+- (const webrtc::MouseCursor&)cursor {
+ return *_cursor.get();
+}
+
+- (void)setCursor:(webrtc::MouseCursor*)cursor {
+ _cursor.reset(cursor);
+
+ if (_cursor.get() != NULL && _cursor->image().data()) {
+ _needCursorRedraw = true;
+ }
+}
+
+- (void)bindToEffect:(GLKEffectPropertyTexture*)effectProperty {
+ glGenTextures(1, &_textureId);
+ [Utility bindTextureForIOS:_textureId];
+
+ // This is the Cursor layer, and is stamped on top of Desktop as a
+ // transparent image
+ effectProperty.target = GLKTextureTarget2D;
+ effectProperty.name = _textureId;
+ effectProperty.envMode = GLKTextureEnvModeDecal;
+ effectProperty.enabled = GL_TRUE;
+
+ [Utility logGLErrorCode:@"CursorTexture bindToTexture"];
+ // Release context
+ glBindTexture(GL_TEXTURE_2D, 0);
+}
+
+- (BOOL)needDrawAtPosition:(const webrtc::DesktopVector&)position {
+ return (_cursor.get() != NULL &&
+ (_needInitialize || _needCursorRedraw == YES ||
+ _cursorDrawnToGL.left() != position.x() - _cursor->hotspot().x() ||
+ _cursorDrawnToGL.top() != position.y() - _cursor->hotspot().y()));
+}
+
+- (void)drawWithMousePosition:(const webrtc::DesktopVector&)position {
+ if (_textureSize.height() == 0 && _textureSize.width() == 0) {
+ return;
+ }
+
+ [Utility bindTextureForIOS:_textureId];
+
+ if (_needInitialize) {
+ glTexImage2D(GL_TEXTURE_2D,
+ 0,
+ GL_RGBA,
+ _textureSize.width(),
+ _textureSize.height(),
+ 0,
+ GL_RGBA,
+ GL_UNSIGNED_BYTE,
+ NULL);
+
+ [Utility logGLErrorCode:@"CursorTexture initializeTextureSurfaceWithSize"];
+ _needInitialize = false;
+ }
+ // When the cursor needs to be redraw in a different spot then we must clear
+ // the previous area.
+
+ DCHECK([self needDrawAtPosition:position]);
+
+ if (_cursorDrawnToGL.width() > 0 && _cursorDrawnToGL.height() > 0) {
+ webrtc::BasicDesktopFrame transparentCursor(_cursorDrawnToGL.size());
+
+ if (transparentCursor.data() != NULL) {
+ DCHECK(transparentCursor.kBytesPerPixel ==
+ _cursor->image().kBytesPerPixel);
+ memset(transparentCursor.data(),
+ 0,
+ transparentCursor.stride() * transparentCursor.size().height());
+
+ [Utility drawSubRectToGLFromRectOfSize:_textureSize
+ subRect:_cursorDrawnToGL
+ data:transparentCursor.data()];
+
+ // there is no longer any cursor drawn to screen
+ _cursorDrawnToGL = webrtc::DesktopRect::MakeXYWH(0, 0, 0, 0);
+ }
+ }
+
+ if (_cursor.get() != NULL) {
+
+ CGRect screen =
+ CGRectMake(0.0, 0.0, _textureSize.width(), _textureSize.height());
+ CGRect cursor = CGRectMake(position.x() - _cursor->hotspot().x(),
+ position.y() - _cursor->hotspot().y(),
+ _cursor->image().size().width(),
+ _cursor->image().size().height());
+
+ if (CGRectContainsRect(screen, cursor)) {
+ _cursorDrawnToGL = webrtc::DesktopRect::MakeXYWH(cursor.origin.x,
+ cursor.origin.y,
+ cursor.size.width,
+ cursor.size.height);
+
+ [Utility drawSubRectToGLFromRectOfSize:_textureSize
+ subRect:_cursorDrawnToGL
+ data:_cursor->image().data()];
+
+ } else if (CGRectIntersectsRect(screen, cursor)) {
+ // Some of the cursor falls off screen, need to clip it
+ CGRect intersection = CGRectIntersection(screen, cursor);
+ _cursorDrawnToGL =
+ webrtc::DesktopRect::MakeXYWH(intersection.origin.x,
+ intersection.origin.y,
+ intersection.size.width,
+ intersection.size.height);
+
+ webrtc::BasicDesktopFrame partialCursor(_cursorDrawnToGL.size());
+
+ if (partialCursor.data()) {
+ DCHECK(partialCursor.kBytesPerPixel == _cursor->image().kBytesPerPixel);
+
+ uint32_t src_stride = _cursor->image().stride();
+ uint32_t dst_stride = partialCursor.stride();
+
+ uint8_t* source = _cursor->image().data();
+ source += abs((static_cast<int32_t>(cursor.origin.y) -
+ _cursorDrawnToGL.top())) *
+ src_stride;
+ source += abs((static_cast<int32_t>(cursor.origin.x) -
+ _cursorDrawnToGL.left())) *
+ _cursor->image().kBytesPerPixel;
+ uint8_t* dst = partialCursor.data();
+
+ for (uint32_t y = 0; y < _cursorDrawnToGL.height(); y++) {
+ memcpy(dst, source, dst_stride);
+ source += src_stride;
+ dst += dst_stride;
+ }
+
+ [Utility drawSubRectToGLFromRectOfSize:_textureSize
+ subRect:_cursorDrawnToGL
+ data:partialCursor.data()];
+ }
+ }
+ }
+
+ _needCursorRedraw = false;
+ [Utility logGLErrorCode:@"CursorTexture drawWithMousePosition"];
+ // Release context
+ glBindTexture(GL_TEXTURE_2D, 0);
+}
+
+- (void)releaseTexture {
+ glDeleteTextures(1, &_textureId);
+}
+
+@end
diff --git a/remoting/ios/ui/desktop_texture.h b/remoting/ios/ui/desktop_texture.h
new file mode 100644
index 0000000..28a330c
--- /dev/null
+++ b/remoting/ios/ui/desktop_texture.h
@@ -0,0 +1,38 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef REMOTING_IOS_UI_DESKTOP_TEXTURE_H_
+#define REMOTING_IOS_UI_DESKTOP_TEXTURE_H_
+
+#import <Foundation/Foundation.h>
+#import <GLKit/GLKit.h>
+
+#import "remoting/ios/utility.h"
+#include "third_party/webrtc/modules/desktop_capture/desktop_geometry.h"
+
+@interface DesktopTexture : NSObject {
+ @private
+ // GL name
+ GLuint _textureId;
+ webrtc::DesktopSize _textureSize;
+ BOOL _needInitialize;
+}
+
+- (const webrtc::DesktopSize&)textureSize;
+
+- (void)setTextureSize:(const webrtc::DesktopSize&)size;
+
+// bind this object to an effect's via the effects properties
+- (void)bindToEffect:(GLKEffectPropertyTexture*)effectProperty;
+
+- (BOOL)needDraw;
+
+// draw a region of the texture
+- (void)drawRegion:(GLRegion*)region rect:(CGRect)rect;
+
+- (void)releaseTexture;
+
+@end
+
+#endif // REMOTING_IOS_UI_DESKTOP_TEXTURE_H_ \ No newline at end of file
diff --git a/remoting/ios/ui/desktop_texture.mm b/remoting/ios/ui/desktop_texture.mm
new file mode 100644
index 0000000..d806dee
--- /dev/null
+++ b/remoting/ios/ui/desktop_texture.mm
@@ -0,0 +1,83 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "remoting/ios/ui/desktop_texture.h"
+
+@implementation DesktopTexture
+
+- (const webrtc::DesktopSize&)textureSize {
+ return _textureSize;
+}
+
+- (void)setTextureSize:(const webrtc::DesktopSize&)size {
+ if (!_textureSize.equals(size)) {
+ _textureSize.set(size.width(), size.height());
+ _needInitialize = true;
+ }
+}
+
+- (void)bindToEffect:(GLKEffectPropertyTexture*)effectProperty {
+ glGenTextures(1, &_textureId);
+ [Utility bindTextureForIOS:_textureId];
+
+ // This is the HOST Desktop layer, and each draw will always replace what is
+ // currently in the draw context
+ effectProperty.target = GLKTextureTarget2D;
+ effectProperty.name = _textureId;
+ effectProperty.envMode = GLKTextureEnvModeReplace;
+ effectProperty.enabled = GL_TRUE;
+
+ [Utility logGLErrorCode:@"DesktopTexture bindToTexture"];
+ // Release context
+ glBindTexture(GL_TEXTURE_2D, 0);
+}
+
+- (BOOL)needDraw {
+ return _needInitialize;
+}
+
+- (void)drawRegion:(GLRegion*)region rect:(CGRect)rect {
+ if (_textureSize.height() == 0 && _textureSize.width() == 0) {
+ return;
+ }
+
+ [Utility bindTextureForIOS:_textureId];
+
+ if (_needInitialize) {
+ glTexImage2D(GL_TEXTURE_2D,
+ 0,
+ GL_RGBA,
+ _textureSize.width(),
+ _textureSize.height(),
+ 0,
+ GL_RGBA,
+ GL_UNSIGNED_BYTE,
+ NULL);
+
+ [Utility logGLErrorCode:@"DesktopTexture initializeTextureSurfaceWithSize"];
+ _needInitialize = false;
+ }
+
+ [Utility drawSubRectToGLFromRectOfSize:_textureSize
+ subRect:webrtc::DesktopRect::MakeXYWH(
+ region->offset->x(),
+ region->offset->y(),
+ region->image->size().width(),
+ region->image->size().height())
+ data:region->image->data()];
+
+ [Utility logGLErrorCode:@"DesktopTexture drawRegion"];
+ // Release context
+ glBindTexture(GL_TEXTURE_2D, 0);
+}
+
+- (void)releaseTexture {
+ glDeleteTextures(1, &_textureId);
+}
+
+@end
diff --git a/remoting/ios/ui/help_view_controller.h b/remoting/ios/ui/help_view_controller.h
new file mode 100644
index 0000000..036a7b5
--- /dev/null
+++ b/remoting/ios/ui/help_view_controller.h
@@ -0,0 +1,17 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef REMOTING_IOS_UI_HELP_VIEW_CONTROLLER_H_
+#define REMOTING_IOS_UI_HELP_VIEW_CONTROLLER_H_
+
+#import <UIKit/UIKit.h>
+
+@interface HelpViewController : UIViewController {
+ @private
+ IBOutlet UIWebView* _webView;
+}
+
+@end
+
+#endif // REMOTING_IOS_UI_HELP_VIEW_CONTROLLER_H_ \ No newline at end of file
diff --git a/remoting/ios/ui/help_view_controller.mm b/remoting/ios/ui/help_view_controller.mm
new file mode 100644
index 0000000..1a3c705
--- /dev/null
+++ b/remoting/ios/ui/help_view_controller.mm
@@ -0,0 +1,21 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "remoting/ios/ui/help_view_controller.h"
+
+@implementation HelpViewController
+
+// Override UIViewController
+- (void)viewWillAppear:(BOOL)animated {
+ [self.navigationController setNavigationBarHidden:NO animated:YES];
+ NSString* string = @"https://support.google.com/chrome/answer/1649523";
+ NSURL* url = [NSURL URLWithString:string];
+ [_webView loadRequest:[NSURLRequest requestWithURL:url]];
+}
+
+@end
diff --git a/remoting/ios/ui/host_list_view_controller.h b/remoting/ios/ui/host_list_view_controller.h
new file mode 100644
index 0000000..5f3c1bf
--- /dev/null
+++ b/remoting/ios/ui/host_list_view_controller.h
@@ -0,0 +1,39 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef REMOTING_IOS_UI_HOST_LIST_VIEW_CONTROLLER_H_
+#define REMOTING_IOS_UI_HOST_LIST_VIEW_CONTROLLER_H_
+
+#import <UIKit/UIKit.h>
+#import <GLKit/GLKit.h>
+
+#import "host_refresh.h"
+
+// HostListViewController presents the user with a list of hosts which has
+// been shared from other platforms to connect to
+@interface HostListViewController : UIViewController<HostRefreshDelegate,
+ UITableViewDelegate,
+ UITableViewDataSource> {
+ @private
+ IBOutlet UITableView* _tableHostList;
+ IBOutlet UIButton* _btnAccount;
+ IBOutlet UIActivityIndicatorView* _refreshActivityIndicator;
+ IBOutlet UIBarButtonItem* _versionInfo;
+
+ NSArray* _hostList;
+}
+
+@property(nonatomic, readonly) GTMOAuth2Authentication* authorization;
+@property(nonatomic, readonly) NSString* userEmail;
+
+// Triggered by UI 'refresh' button
+- (IBAction)btnRefreshHostListPressed:(id)sender;
+// Triggered by UI 'log in' button, if user is already logged in then the user
+// is logged out and a new session begins by requesting the user to log in,
+// possibly with a different account
+- (IBAction)btnAccountPressed:(id)sender;
+
+@end
+
+#endif // REMOTING_IOS_UI_HOST_LIST_VIEW_CONTROLLER_H_ \ No newline at end of file
diff --git a/remoting/ios/ui/host_list_view_controller.mm b/remoting/ios/ui/host_list_view_controller.mm
new file mode 100644
index 0000000..7dd7fa2
--- /dev/null
+++ b/remoting/ios/ui/host_list_view_controller.mm
@@ -0,0 +1,229 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "remoting/ios/ui/host_list_view_controller.h"
+
+#import "remoting/ios/authorize.h"
+#import "remoting/ios/host.h"
+#import "remoting/ios/host_cell.h"
+#import "remoting/ios/host_refresh.h"
+#import "remoting/ios/utility.h"
+#import "remoting/ios/ui/host_view_controller.h"
+
+@interface HostListViewController (Private)
+- (void)refreshHostList;
+- (void)checkUserAndRefreshHostList;
+- (BOOL)isSignedIn;
+- (void)signInUser;
+// Callback from [Authorize createLoginController...]
+- (void)viewController:(UIViewController*)viewController
+ finishedWithAuth:(GTMOAuth2Authentication*)authResult
+ error:(NSError*)error;
+@end
+
+@implementation HostListViewController
+
+@synthesize userEmail = _userEmail;
+@synthesize authorization = _authorization;
+
+// Override default setter
+- (void)setAuthorization:(GTMOAuth2Authentication*)authorization {
+ _authorization = authorization;
+ if (_authorization.canAuthorize) {
+ _userEmail = _authorization.userEmail;
+ } else {
+ _userEmail = nil;
+ }
+
+ NSString* userName = _userEmail;
+
+ if (userName == nil) {
+ userName = @"Not logged in";
+ }
+
+ [_btnAccount setTitle:userName forState:UIControlStateNormal];
+
+ [self refreshHostList];
+}
+
+// Override UIViewController
+// Create google+ service for google authentication and oAuth2 authorization.
+- (void)viewDidLoad {
+ [super viewDidLoad];
+
+ [_tableHostList setDataSource:self];
+ [_tableHostList setDelegate:self];
+
+ _versionInfo.title = [Utility appVersionNumberDisplayString];
+}
+
+// Override UIViewController
+- (void)viewWillAppear:(BOOL)animated {
+ [super viewWillAppear:animated];
+ [self.navigationController setNavigationBarHidden:NO animated:NO];
+ [self setAuthorization:[Authorize getAnyExistingAuthorization]];
+}
+
+// Override UIViewController
+// Cancel segue when host status is not online
+- (BOOL)shouldPerformSegueWithIdentifier:(NSString*)identifier
+ sender:(id)sender {
+ if ([identifier isEqualToString:@"ConnectToHost"]) {
+ Host* host = [self hostAtIndex:[_tableHostList indexPathForCell:sender]];
+ if (![host.status isEqualToString:@"ONLINE"]) {
+ return NO;
+ }
+ }
+ return YES;
+}
+
+// Override UIViewController
+// check for segues defined in the storyboard by identifier, and set a few
+// properties before transitioning
+- (void)prepareForSegue:(UIStoryboardSegue*)segue sender:(id)sender {
+ if ([segue.identifier isEqualToString:@"ConnectToHost"]) {
+ // the designationViewController type is defined by the storyboard
+ HostViewController* hostView =
+ static_cast<HostViewController*>(segue.destinationViewController);
+
+ NSString* authToken =
+ [_authorization.parameters valueForKey:@"access_token"];
+
+ if (authToken == nil) {
+ authToken = _authorization.authorizationTokenKey;
+ }
+
+ [hostView setHostDetails:[self hostAtIndex:[_tableHostList
+ indexPathForCell:sender]]
+ userEmail:_userEmail
+ authorizationToken:authToken];
+ }
+}
+
+// @protocol HostRefreshDelegate, remember received host list for the table
+// view to refresh from
+- (void)hostListRefresh:(NSArray*)hostList
+ errorMessage:(NSString*)errorMessage {
+ if (hostList != nil) {
+ _hostList = hostList;
+ [_tableHostList reloadData];
+ }
+ [_refreshActivityIndicator stopAnimating];
+ if (errorMessage != nil) {
+ [Utility showAlert:@"Host Refresh Failed" message:errorMessage];
+ }
+}
+
+// @protocol UITableViewDataSource
+// Only have 1 section and it contains all the hosts
+- (NSInteger)tableView:(UITableView*)tableView
+ numberOfRowsInSection:(NSInteger)section {
+ return [_hostList count];
+}
+
+// @protocol UITableViewDataSource
+// Convert a host entry to a table row
+- (HostCell*)tableView:(UITableView*)tableView
+ cellForRowAtIndexPath:(NSIndexPath*)indexPath {
+ static NSString* CellIdentifier = @"HostStatusCell";
+
+ HostCell* cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier
+ forIndexPath:indexPath];
+
+ Host* host = [self hostAtIndex:indexPath];
+ cell.labelHostName.text = host.hostName;
+ cell.labelStatus.text = host.status;
+
+ UIColor* statColor = nil;
+ if ([host.status isEqualToString:@"ONLINE"]) {
+ statColor = [[UIColor alloc] initWithRed:0 green:1 blue:0 alpha:1];
+ } else {
+ statColor = [[UIColor alloc] initWithRed:1 green:0 blue:0 alpha:1];
+ }
+ [cell.labelStatus setTextColor:statColor];
+
+ return cell;
+}
+
+// @protocol UITableViewDataSource
+// Rows are not editable via standard UI mechanisms
+- (BOOL)tableView:(UITableView*)tableView
+ canEditRowAtIndexPath:(NSIndexPath*)indexPath {
+ return NO;
+}
+
+- (IBAction)btnRefreshHostListPressed:(id)sender {
+ [self refreshHostList];
+}
+
+- (IBAction)btnAccountPressed:(id)sender {
+ [self signInUser];
+}
+
+- (void)refreshHostList {
+ [_refreshActivityIndicator startAnimating];
+ _hostList = [[NSArray alloc] init];
+ [_tableHostList reloadData];
+
+ // Insert a small delay so the user is well informed that something is
+ // happening by the animating activity indicator
+ [self performSelector:@selector(checkUserAndRefreshHostList)
+ withObject:nil
+ afterDelay:.5];
+}
+
+// Most likely you want to call refreshHostList
+- (void)checkUserAndRefreshHostList {
+ if (![self isSignedIn]) {
+ [self signInUser];
+ } else {
+ HostRefresh* hostRefresh = [[HostRefresh alloc] init];
+ [hostRefresh refreshHostList:_authorization delegate:self];
+ }
+}
+
+- (BOOL)isSignedIn {
+ return (_userEmail != nil);
+}
+
+// Launch the google.com authentication and authorization process. If a user is
+// already signed in, begin by signing out so another account could be
+// signed in.
+- (void)signInUser {
+ [self presentViewController:
+ [Authorize createLoginController:self
+ finishedSelector:@selector(viewController:
+ finishedWithAuth:
+ error:)]
+ animated:YES
+ completion:nil];
+}
+
+// Callback from [Authorize createLoginController...]
+// Handle completion of the authentication process, and updates the service
+// with the new credentials.
+- (void)viewController:(UIViewController*)viewController
+ finishedWithAuth:(GTMOAuth2Authentication*)authResult
+ error:(NSError*)error {
+ [viewController.presentingViewController dismissViewControllerAnimated:NO
+ completion:nil];
+
+ if (error != nil) {
+ [Utility showAlert:@"Authentication Error"
+ message:error.localizedDescription];
+ [self setAuthorization:nil];
+ } else {
+ [self setAuthorization:authResult];
+ }
+}
+
+- (Host*)hostAtIndex:(NSIndexPath*)indexPath {
+ return [_hostList objectAtIndex:indexPath.row];
+}
+
+@end
diff --git a/remoting/ios/ui/host_list_view_controller_unittest.mm b/remoting/ios/ui/host_list_view_controller_unittest.mm
new file mode 100644
index 0000000..7e75739
--- /dev/null
+++ b/remoting/ios/ui/host_list_view_controller_unittest.mm
@@ -0,0 +1,90 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "remoting/ios/ui/host_list_view_controller.h"
+
+#import "base/compiler_specific.h"
+#import "testing/gtest_mac.h"
+
+#import "remoting/ios/host.h"
+#import "remoting/ios/host_refresh_test_helper.h"
+#import "remoting/ios/ui/host_view_controller.h"
+
+namespace remoting {
+
+class HostListViewControllerTest : public ::testing::Test {
+ protected:
+ virtual void SetUp() OVERRIDE {
+ controller_ = [[HostListViewController alloc] init];
+ SetHostByCount(1);
+ }
+
+ void SetHostByCount(int numHosts) {
+ NSArray* array =
+ [Host parseListFromJSON:HostRefreshTestHelper::GetHostList(numHosts)];
+ RefreshHostList(array);
+ }
+
+ void SetHostByString(NSString* string) {
+ NSArray* array =
+ [Host parseListFromJSON:HostRefreshTestHelper::GetHostList(string)];
+ RefreshHostList(array);
+ }
+
+ void RefreshHostList(NSArray* array) {
+ [controller_ hostListRefresh:array errorMessage:nil];
+ }
+
+ HostListViewController* controller_;
+};
+
+TEST_F(HostListViewControllerTest, DefaultAuthorization) {
+ ASSERT_TRUE(controller_.authorization == nil);
+
+ [controller_ viewWillAppear:YES];
+
+ ASSERT_TRUE(controller_.authorization != nil);
+}
+
+TEST_F(HostListViewControllerTest, hostListRefresh) {
+ SetHostByCount(2);
+ ASSERT_EQ(2, [controller_ tableView:nil numberOfRowsInSection:0]);
+
+ SetHostByCount(10);
+ ASSERT_EQ(10, [controller_ tableView:nil numberOfRowsInSection:0]);
+}
+
+TEST_F(HostListViewControllerTest,
+ ShouldPerformSegueWithIdentifierOfConnectToHost) {
+ ASSERT_FALSE([controller_ shouldPerformSegueWithIdentifier:@"ConnectToHost"
+ sender:nil]);
+
+ NSString* host = HostRefreshTestHelper::GetMultipleHosts(1);
+ host = [host stringByReplacingOccurrencesOfString:@"TESTING"
+ withString:@"ONLINE"];
+ SetHostByString(host);
+ ASSERT_TRUE([controller_ shouldPerformSegueWithIdentifier:@"ConnectToHost"
+ sender:nil]);
+}
+
+TEST_F(HostListViewControllerTest, prepareSegueWithIdentifierOfConnectToHost) {
+ HostViewController* destination = [[HostViewController alloc] init];
+
+ ASSERT_NSNE(HostRefreshTestHelper::HostNameTest, destination.host.hostName);
+
+ UIStoryboardSegue* seque =
+ [[UIStoryboardSegue alloc] initWithIdentifier:@"ConnectToHost"
+ source:controller_
+ destination:destination];
+
+ [controller_ prepareForSegue:seque sender:nil];
+
+ ASSERT_NSEQ(HostRefreshTestHelper::HostNameTest, destination.host.hostName);
+}
+
+} // namespace remoting \ No newline at end of file
diff --git a/remoting/ios/ui/host_view_controller.h b/remoting/ios/ui/host_view_controller.h
new file mode 100644
index 0000000..6e0f287
--- /dev/null
+++ b/remoting/ios/ui/host_view_controller.h
@@ -0,0 +1,115 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef REMOTING_IOS_UI_HOST_VIEW_CONTROLLER_H_
+#define REMOTING_IOS_UI_HOST_VIEW_CONTROLLER_H_
+
+#import <GLKit/GLKit.h>
+
+#include "base/memory/scoped_ptr.h"
+#include "base/memory/scoped_vector.h"
+
+#import "remoting/ios/host.h"
+#import "remoting/ios/key_input.h"
+#import "remoting/ios/utility.h"
+#import "remoting/ios/bridge/host_proxy.h"
+#import "remoting/ios/ui/desktop_texture.h"
+#import "remoting/ios/ui/cursor_texture.h"
+#import "remoting/ios/ui/pin_entry_view_controller.h"
+#import "remoting/ios/ui/scene_view.h"
+
+@interface HostViewController
+ : GLKViewController<PinEntryViewControllerDelegate,
+ KeyInputDelegate,
+ // Communication channel from HOST to CLIENT
+ ClientProxyDelegate,
+ UIGestureRecognizerDelegate,
+ UIToolbarDelegate> {
+ @private
+ IBOutlet UIActivityIndicatorView* _busyIndicator;
+ IBOutlet UIButton* _barBtnDisconnect;
+ IBOutlet UIButton* _barBtnKeyboard;
+ IBOutlet UIButton* _barBtnNavigation;
+ IBOutlet UIButton* _barBtnCtrlAltDel;
+ IBOutlet UILongPressGestureRecognizer* _longPressRecognizer;
+ IBOutlet UIPanGestureRecognizer* _panRecognizer;
+ IBOutlet UIPanGestureRecognizer* _threeFingerPanRecognizer;
+ IBOutlet UIPinchGestureRecognizer* _pinchRecognizer;
+ IBOutlet UITapGestureRecognizer* _singleTapRecognizer;
+ IBOutlet UITapGestureRecognizer* _twoFingerTapRecognizer;
+ IBOutlet UITapGestureRecognizer* _threeFingerTapRecognizer;
+ IBOutlet UIToolbar* _toolbar;
+ IBOutlet UIToolbar* _hiddenToolbar;
+ IBOutlet NSLayoutConstraint* _toolBarYPosition;
+ IBOutlet NSLayoutConstraint* _hiddenToolbarYPosition;
+
+ KeyInput* _keyEntryView;
+ NSString* _statusMessage;
+
+ // The GLES2 context being drawn too.
+ EAGLContext* _context;
+
+ // GLKBaseEffect encapsulates the GL Shaders needed to draw at most two
+ // textures |_textureIds| given vertex information. The draw surface consists
+ // of two layers (GL Textures). The bottom layer is the desktop of the HOST.
+ // The top layer is mostly transparent and is used to overlay the current
+ // cursor.
+ GLKBaseEffect* _effect;
+
+ // All the details needed to draw our GL Scene, and our two textures.
+ SceneView* _scene;
+ DesktopTexture* _desktop;
+ CursorTexture* _mouse;
+
+ // List of regions and data that have pending draws to |_desktop| .
+ ScopedVector<GLRegion> _glRegions;
+
+ // Lock for |_glRegions|, regions are delivered from HOST on another thread,
+ // and drawn to |_desktop| from a GL Context thread
+ NSLock* _glBufferLock;
+
+ // Lock for |_mouse.cursor|, cursor updates are delivered from HOST on another
+ // thread, and drawn to |_mouse| from a GL Context thread
+ NSLock* _glCursorLock;
+
+ // Communication channel from CLIENT to HOST
+ HostProxy* _clientToHostProxy;
+}
+
+// Details for the host and user
+@property(nonatomic, readonly) Host* host;
+@property(nonatomic, readonly) NSString* userEmail;
+@property(nonatomic, readonly) NSString* userAuthorizationToken;
+
+- (void)setHostDetails:(Host*)host
+ userEmail:(NSString*)userEmail
+ authorizationToken:(NSString*)authorizationToken;
+
+// Zoom in/out
+- (IBAction)pinchGestureTriggered:(UIPinchGestureRecognizer*)sender;
+// Left mouse click, moves cursor
+- (IBAction)tapGestureTriggered:(UITapGestureRecognizer*)sender;
+// Scroll the view in 2d
+- (IBAction)panGestureTriggered:(UIPanGestureRecognizer*)sender;
+// Right mouse click and drag, moves cursor
+- (IBAction)longPressGestureTriggered:(UILongPressGestureRecognizer*)sender;
+// Right mouse click
+- (IBAction)twoFingerTapGestureTriggered:(UITapGestureRecognizer*)sender;
+// Middle mouse click
+- (IBAction)threeFingerTapGestureTriggered:(UITapGestureRecognizer*)sender;
+// Show hidden menus. Swipe up for keyboard, swipe down for navigation menu
+- (IBAction)threeFingerPanGestureTriggered:(UIPanGestureRecognizer*)sender;
+
+// Do navigation 'back'
+- (IBAction)barBtnNavigationBackPressed:(id)sender;
+// Show keyboard
+- (IBAction)barBtnKeyboardPressed:(id)sender;
+// Trigger |_toolbar| animation
+- (IBAction)barBtnToolBarHidePressed:(id)sender;
+// Send Keys for ctrl, atl, delete
+- (IBAction)barBtnCtrlAltDelPressed:(id)sender;
+
+@end
+
+#endif // REMOTING_IOS_UI_HOST_VIEW_CONTROLLER_H_
diff --git a/remoting/ios/ui/host_view_controller.mm b/remoting/ios/ui/host_view_controller.mm
new file mode 100644
index 0000000..d87e767
--- /dev/null
+++ b/remoting/ios/ui/host_view_controller.mm
@@ -0,0 +1,676 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "remoting/ios/ui/host_view_controller.h"
+
+#include <OpenGLES/ES2/gl.h>
+
+#import "remoting/ios/data_store.h"
+
+namespace {
+
+// TODO (aboone) Some of the layout is not yet set in stone, so variables have
+// been used to position and turn items on and off. Eventually these may be
+// stabilized and removed.
+
+// Scroll speed multiplier for mouse wheel
+const static int kMouseWheelSensitivity = 20;
+
+// Area the navigation bar consumes when visible in pixels
+const static int kTopMargin = 20;
+// Area the footer consumes when visible (no footer currently exists)
+const static int kBottomMargin = 0;
+
+} // namespace
+
+@interface HostViewController (Private)
+- (void)setupGL;
+- (void)tearDownGL;
+- (void)goBack;
+- (void)updateLabels;
+- (BOOL)isToolbarHidden;
+- (void)updatePanVelocityShouldCancel:(bool)canceled;
+- (void)orientationChanged:(NSNotification*)note;
+- (void)applySceneChange:(CGPoint)translation scaleBy:(float)ratio;
+- (void)showToolbar:(BOOL)visible;
+@end
+
+@implementation HostViewController
+
+@synthesize host = _host;
+@synthesize userEmail = _userEmail;
+@synthesize userAuthorizationToken = _userAuthorizationToken;
+
+// Override UIViewController
+- (void)viewDidLoad {
+ [super viewDidLoad];
+
+ _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
+ DCHECK(_context);
+ static_cast<GLKView*>(self.view).context = _context;
+
+ [_keyEntryView setDelegate:self];
+
+ _clientToHostProxy = [[HostProxy alloc] init];
+
+ // There is a 1 pixel top border which is actually the background not being
+ // covered. There is no obvious way to remove that pixel 'border'. Set the
+ // background clear, and also reset the backgroundimage and shawdowimage to an
+ // empty image any time the view is moved.
+ _hiddenToolbar.backgroundColor = [UIColor clearColor];
+ if ([_hiddenToolbar respondsToSelector:@selector(setBackgroundImage:
+ forToolbarPosition:
+ barMetrics:)]) {
+ [_hiddenToolbar setBackgroundImage:[UIImage new]
+ forToolbarPosition:UIToolbarPositionAny
+ barMetrics:UIBarMetricsDefault];
+ }
+ if ([_hiddenToolbar
+ respondsToSelector:@selector(setShadowImage:forToolbarPosition:)]) {
+ [_hiddenToolbar setShadowImage:[UIImage new]
+ forToolbarPosition:UIToolbarPositionAny];
+ }
+
+ // 1/2 circle rotation for an icon ~ 180 degree ~ 1 radian
+ _barBtnNavigation.imageView.transform = CGAffineTransformMakeRotation(M_PI);
+
+ _scene = [[SceneView alloc] init];
+ [_scene setMarginsFromLeft:0 right:0 top:kTopMargin bottom:kBottomMargin];
+ _desktop = [[DesktopTexture alloc] init];
+ _mouse = [[CursorTexture alloc] init];
+
+ _glBufferLock = [[NSLock alloc] init];
+ _glCursorLock = [[NSLock alloc] init];
+
+ [_scene
+ setContentSize:[Utility getOrientatedSize:self.view.bounds.size
+ shouldWidthBeLongestSide:[Utility isInLandscapeMode]]];
+ [self showToolbar:YES];
+ [self updateLabels];
+
+ [self setupGL];
+
+ [_singleTapRecognizer requireGestureRecognizerToFail:_twoFingerTapRecognizer];
+ [_twoFingerTapRecognizer
+ requireGestureRecognizerToFail:_threeFingerTapRecognizer];
+ //[_pinchRecognizer requireGestureRecognizerToFail:_twoFingerTapRecognizer];
+ [_panRecognizer requireGestureRecognizerToFail:_singleTapRecognizer];
+ [_threeFingerPanRecognizer
+ requireGestureRecognizerToFail:_threeFingerTapRecognizer];
+ //[_pinchRecognizer requireGestureRecognizerToFail:_threeFingerPanRecognizer];
+
+ // Subscribe to changes in orientation
+ [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(orientationChanged:)
+ name:UIDeviceOrientationDidChangeNotification
+ object:[UIDevice currentDevice]];
+}
+
+- (void)setupGL {
+ [EAGLContext setCurrentContext:_context];
+
+ _effect = [[GLKBaseEffect alloc] init];
+ [Utility logGLErrorCode:@"setupGL begin"];
+
+ // Initialize each texture
+ [_desktop bindToEffect:[_effect texture2d0]];
+ [_mouse bindToEffect:[_effect texture2d1]];
+ [Utility logGLErrorCode:@"setupGL textureComplete"];
+}
+
+// Override UIViewController
+- (void)viewDidUnload {
+ [super viewDidUnload];
+ [self tearDownGL];
+
+ if ([EAGLContext currentContext] == _context) {
+ [EAGLContext setCurrentContext:nil];
+ }
+ _context = nil;
+}
+
+- (void)tearDownGL {
+ [EAGLContext setCurrentContext:_context];
+
+ // Release Textures
+ [_desktop releaseTexture];
+ [_mouse releaseTexture];
+}
+
+// Override UIViewController
+- (void)viewWillAppear:(BOOL)animated {
+ [super viewWillAppear:NO];
+ [self.navigationController setNavigationBarHidden:YES animated:YES];
+ [self updateLabels];
+ if (![_clientToHostProxy isConnected]) {
+ [_busyIndicator startAnimating];
+
+ [_clientToHostProxy connectToHost:_userEmail
+ authToken:_userAuthorizationToken
+ jabberId:_host.jabberId
+ hostId:_host.hostId
+ publicKey:_host.publicKey
+ delegate:self];
+ }
+}
+
+// Override UIViewController
+- (void)viewWillDisappear:(BOOL)animated {
+ [super viewWillDisappear:NO];
+ NSArray* viewControllers = self.navigationController.viewControllers;
+ if (viewControllers.count > 1 &&
+ [viewControllers objectAtIndex:viewControllers.count - 2] == self) {
+ // View is disappearing because a new view controller was pushed onto the
+ // stack
+ } else if ([viewControllers indexOfObject:self] == NSNotFound) {
+ // View is disappearing because it was popped from the stack
+ [_clientToHostProxy disconnectFromHost];
+ }
+}
+
+// "Back" goes to the root controller for now
+- (void)goBack {
+ [self.navigationController popToRootViewControllerAnimated:YES];
+}
+
+// @protocol PinEntryViewControllerDelegate
+// Return the PIN input by User, indicate if the User should be prompted to
+// re-enter the pin in the future
+- (void)connectToHostWithPin:(UIViewController*)controller
+ hostPin:(NSString*)hostPin
+ shouldPrompt:(BOOL)shouldPrompt {
+ const HostPreferences* hostPrefs =
+ [[DataStore sharedStore] getHostForId:_host.hostId];
+ if (!hostPrefs) {
+ hostPrefs = [[DataStore sharedStore] createHost:_host.hostId];
+ }
+ if (hostPrefs) {
+ hostPrefs.hostPin = hostPin;
+ hostPrefs.askForPin = [NSNumber numberWithBool:shouldPrompt];
+ [[DataStore sharedStore] saveChanges];
+ }
+
+ [[controller presentingViewController] dismissViewControllerAnimated:NO
+ completion:nil];
+
+ [_clientToHostProxy authenticationResponse:hostPin createPair:!shouldPrompt];
+}
+
+// @protocol PinEntryViewControllerDelegate
+// Returns if the user canceled while entering their PIN
+- (void)cancelledConnectToHostWithPin:(UIViewController*)controller {
+ [[controller presentingViewController] dismissViewControllerAnimated:NO
+ completion:nil];
+
+ [self goBack];
+}
+
+- (void)setHostDetails:(Host*)host
+ userEmail:(NSString*)userEmail
+ authorizationToken:(NSString*)authorizationToken {
+ DCHECK(host.jabberId);
+ _host = host;
+ _userEmail = userEmail;
+ _userAuthorizationToken = authorizationToken;
+}
+
+// Set various labels on the form for iPad vs iPhone, and orientation
+- (void)updateLabels {
+ if (![Utility isPad] && ![Utility isInLandscapeMode]) {
+ [_barBtnDisconnect setTitle:@"" forState:(UIControlStateNormal)];
+ [_barBtnCtrlAltDel setTitle:@"CtAtD" forState:UIControlStateNormal];
+ } else {
+ [_barBtnCtrlAltDel setTitle:@"Ctrl+Alt+Del" forState:UIControlStateNormal];
+
+ NSString* hostStatus = _host.hostName;
+ if (![_statusMessage isEqual:@"Connected"]) {
+ hostStatus = [NSString
+ stringWithFormat:@"%@ - %@", _host.hostName, _statusMessage];
+ }
+ [_barBtnDisconnect setTitle:hostStatus forState:UIControlStateNormal];
+ }
+
+ [_barBtnDisconnect sizeToFit];
+ [_barBtnCtrlAltDel sizeToFit];
+}
+
+// Resize the view of the desktop - Zoom in/out. This can occur during a Pan.
+- (IBAction)pinchGestureTriggered:(UIPinchGestureRecognizer*)sender {
+ if ([sender state] == UIGestureRecognizerStateChanged) {
+ [self applySceneChange:CGPointMake(0.0, 0.0) scaleBy:sender.scale];
+
+ sender.scale = 1.0; // reset scale so next iteration is a relative ratio
+ }
+}
+
+- (IBAction)tapGestureTriggered:(UITapGestureRecognizer*)sender {
+ if ([_scene containsTouchPoint:[sender locationInView:self.view]]) {
+ [Utility leftClickOn:_clientToHostProxy at:_scene.mousePosition];
+ }
+}
+
+// Change position of scene. This can occur during a pinch or longpress.
+// Or perform a Mouse Wheel Scroll
+- (IBAction)panGestureTriggered:(UIPanGestureRecognizer*)sender {
+ CGPoint translation = [sender translationInView:self.view];
+
+ // If we start with 2 touches, and the pinch gesture is not in progress yet,
+ // then disable it, so mouse scrolling and zoom do not occur at the same
+ // time.
+ if ([sender numberOfTouches] == 2 &&
+ [sender state] == UIGestureRecognizerStateBegan &&
+ !(_pinchRecognizer.state == UIGestureRecognizerStateBegan ||
+ _pinchRecognizer.state == UIGestureRecognizerStateChanged)) {
+ _pinchRecognizer.enabled = NO;
+ }
+
+ if (!_pinchRecognizer.enabled) {
+ // Began with 2 touches, so this is a scroll event
+ translation.x *= kMouseWheelSensitivity;
+ translation.y *= kMouseWheelSensitivity;
+ [Utility mouseScroll:_clientToHostProxy
+ at:_scene.mousePosition
+ delta:webrtc::DesktopVector(translation.x, translation.y)];
+ } else {
+ // Did not begin with 2 touches, doing a pan event
+ if ([sender state] == UIGestureRecognizerStateChanged) {
+ CGPoint translation = [sender translationInView:self.view];
+
+ [self applySceneChange:translation scaleBy:1.0];
+
+ } else if ([sender state] == UIGestureRecognizerStateEnded) {
+ // After user removes their fingers from the screen, apply an acceleration
+ // effect
+ [_scene setPanVelocity:[sender velocityInView:self.view]];
+ }
+ }
+
+ // Finished the event chain
+ if (!([sender state] == UIGestureRecognizerStateBegan ||
+ [sender state] == UIGestureRecognizerStateChanged)) {
+ _pinchRecognizer.enabled = YES;
+ }
+
+ // Reset translation so next iteration is relative.
+ [sender setTranslation:CGPointZero inView:self.view];
+}
+
+// Click-Drag mouse operation. This can occur during a Pan.
+- (IBAction)longPressGestureTriggered:(UILongPressGestureRecognizer*)sender {
+
+ if ([sender state] == UIGestureRecognizerStateBegan) {
+ [_clientToHostProxy mouseAction:_scene.mousePosition
+ wheelDelta:webrtc::DesktopVector(0, 0)
+ whichButton:1
+ buttonDown:YES];
+ } else if (!([sender state] == UIGestureRecognizerStateBegan ||
+ [sender state] == UIGestureRecognizerStateChanged)) {
+ [_clientToHostProxy mouseAction:_scene.mousePosition
+ wheelDelta:webrtc::DesktopVector(0, 0)
+ whichButton:1
+ buttonDown:NO];
+ }
+}
+
+- (IBAction)twoFingerTapGestureTriggered:(UITapGestureRecognizer*)sender {
+ if ([_scene containsTouchPoint:[sender locationInView:self.view]]) {
+ [Utility rightClickOn:_clientToHostProxy at:_scene.mousePosition];
+ }
+}
+
+- (IBAction)threeFingerTapGestureTriggered:(UITapGestureRecognizer*)sender {
+
+ if ([_scene containsTouchPoint:[sender locationInView:self.view]]) {
+ [Utility middleClickOn:_clientToHostProxy at:_scene.mousePosition];
+ }
+}
+
+- (IBAction)threeFingerPanGestureTriggered:(UIPanGestureRecognizer*)sender {
+ if ([sender state] == UIGestureRecognizerStateChanged) {
+ CGPoint translation = [sender translationInView:self.view];
+ if (translation.y > 0) {
+ // Swiped down
+ [self showToolbar:YES];
+ } else if (translation.y < 0) {
+ // Swiped up
+ [_keyEntryView becomeFirstResponder];
+ [self updateLabels];
+ }
+ [sender setTranslation:CGPointZero inView:self.view];
+ }
+}
+
+- (IBAction)barBtnNavigationBackPressed:(id)sender {
+ [self goBack];
+}
+
+- (IBAction)barBtnKeyboardPressed:(id)sender {
+ if ([_keyEntryView isFirstResponder]) {
+ [_keyEntryView endEditing:NO];
+ } else {
+ [_keyEntryView becomeFirstResponder];
+ }
+
+ [self updateLabels];
+}
+
+- (IBAction)barBtnToolBarHidePressed:(id)sender {
+ [self showToolbar:[self isToolbarHidden]]; // Toolbar is either on
+ // screen or off screen
+}
+
+- (IBAction)barBtnCtrlAltDelPressed:(id)sender {
+ [_keyEntryView ctrlAltDel];
+}
+
+// Override UIResponder
+// When any gesture begins, remove any acceleration effects currently being
+// applied. Example, Panning view and let it shoot off into the distance, but
+// then I see a spot I'm interested in so I will touch to capture that locations
+// focus.
+- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
+ [self updatePanVelocityShouldCancel:YES];
+ [super touchesBegan:touches withEvent:event];
+}
+
+// @protocol UIGestureRecognizerDelegate
+// Allow panning and zooming to occur simultaneously.
+// Allow panning and long press to occur simultaneously.
+// Pinch requires 2 touches, and long press requires a single touch, so they are
+// mutually exclusive regardless of if panning is the initiating gesture
+- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
+ shouldRecognizeSimultaneouslyWithGestureRecognizer:
+ (UIGestureRecognizer*)otherGestureRecognizer {
+ if (gestureRecognizer == _pinchRecognizer ||
+ (gestureRecognizer == _panRecognizer)) {
+ if (otherGestureRecognizer == _pinchRecognizer ||
+ otherGestureRecognizer == _panRecognizer) {
+ return YES;
+ }
+ }
+
+ if (gestureRecognizer == _longPressRecognizer ||
+ gestureRecognizer == _panRecognizer) {
+ if (otherGestureRecognizer == _longPressRecognizer ||
+ otherGestureRecognizer == _panRecognizer) {
+ return YES;
+ }
+ }
+ return NO;
+}
+
+// @protocol ClientControllerDelegate
+// Prompt the user for their PIN if pairing has not already been established
+- (void)requestHostPin:(BOOL)pairingSupported {
+ BOOL requestPin = YES;
+ const HostPreferences* hostPrefs =
+ [[DataStore sharedStore] getHostForId:_host.hostId];
+ if (hostPrefs) {
+ requestPin = [hostPrefs.askForPin boolValue];
+ if (!requestPin) {
+ if (hostPrefs.hostPin == nil || hostPrefs.hostPin.length == 0) {
+ requestPin = YES;
+ }
+ }
+ }
+ if (requestPin == YES) {
+ PinEntryViewController* pinEntry = [[PinEntryViewController alloc] init];
+ [pinEntry setDelegate:self];
+ [pinEntry setHostName:_host.hostName];
+ [pinEntry setShouldPrompt:YES];
+ [pinEntry setPairingSupported:pairingSupported];
+
+ [self presentViewController:pinEntry animated:YES completion:nil];
+ } else {
+ [_clientToHostProxy authenticationResponse:hostPrefs.hostPin
+ createPair:pairingSupported];
+ }
+}
+
+// @protocol ClientControllerDelegate
+// Occurs when a connection to a HOST is established successfully
+- (void)connected {
+ // Everything is good, nothing to do
+}
+
+// @protocol ClientControllerDelegate
+- (void)connectionStatus:(NSString*)statusMessage {
+ _statusMessage = statusMessage;
+
+ if ([_statusMessage isEqual:@"Connection closed"]) {
+ [self goBack];
+ } else {
+ [self updateLabels];
+ }
+}
+
+// @protocol ClientControllerDelegate
+// Occurs when a connection to a HOST has failed
+- (void)connectionFailed:(NSString*)errorMessage {
+ [_busyIndicator stopAnimating];
+ NSString* errorMsg;
+ if ([_clientToHostProxy isConnected]) {
+ errorMsg = @"Lost Connection";
+ } else {
+ errorMsg = @"Unable to connect";
+ }
+ [Utility showAlert:errorMsg message:errorMessage];
+ [self goBack];
+}
+
+// @protocol ClientControllerDelegate
+// Copy the updated regions to a backing store to be consumed by the GL Context
+// on a different thread. A region is stored in disjoint memory locations, and
+// must be transformed to a contiguous memory buffer for a GL Texture write.
+// /-----\
+// | 2-4| This buffer is 5x3 bytes large, a region exists at bytes 2 to 4 and
+// | 7-9| bytes 7 to 9. The region is extracted to a new contiguous buffer
+// | | of 6 bytes in length.
+// \-----/
+// More than 1 region may exist in the frame from each call, in which case a new
+// buffer is created for each region
+- (void)applyFrame:(const webrtc::DesktopSize&)size
+ stride:(NSInteger)stride
+ data:(uint8_t*)data
+ regions:(const std::vector<webrtc::DesktopRect>&)regions {
+ [_glBufferLock lock]; // going to make changes to |_glRegions|
+
+ if (!_scene.frameSize.equals(size)) {
+ // When this is the initial frame, the busyIndicator is still spinning. Now
+ // is a good time to stop it.
+ [_busyIndicator stopAnimating];
+
+ // If the |_toolbar| is still showing, hide it.
+ [self showToolbar:NO];
+ [_scene setContentSize:
+ [Utility getOrientatedSize:self.view.bounds.size
+ shouldWidthBeLongestSide:[Utility isInLandscapeMode]]];
+ [_scene setFrameSize:size];
+ [_desktop setTextureSize:size];
+ [_mouse setTextureSize:size];
+ }
+
+ uint32_t src_stride = stride;
+
+ for (uint32_t i = 0; i < regions.size(); i++) {
+ scoped_ptr<GLRegion> region(new GLRegion());
+
+ if (region.get()) {
+ webrtc::DesktopRect rect = regions.at(i);
+
+ webrtc::DesktopSize(rect.width(), rect.height());
+ region->offset.reset(new webrtc::DesktopVector(rect.left(), rect.top()));
+ region->image.reset(new webrtc::BasicDesktopFrame(
+ webrtc::DesktopSize(rect.width(), rect.height())));
+
+ if (region->image->data()) {
+ uint32_t bytes_per_row =
+ region->image->kBytesPerPixel * region->image->size().width();
+
+ uint32_t offset =
+ (src_stride * region->offset->y()) + // row
+ (region->offset->x() * region->image->kBytesPerPixel); // column
+
+ uint8_t* src_buffer = data + offset;
+ uint8_t* dst_buffer = region->image->data();
+
+ // row by row copy
+ for (uint32_t j = 0; j < region->image->size().height(); j++) {
+ memcpy(dst_buffer, src_buffer, bytes_per_row);
+ dst_buffer += bytes_per_row;
+ src_buffer += src_stride;
+ }
+ _glRegions.push_back(region.release());
+ }
+ }
+ }
+ [_glBufferLock unlock]; // done making changes to |_glRegions|
+}
+
+// @protocol ClientControllerDelegate
+// Copy the delivered cursor to a backing store to be consumed by the GL Context
+// on a different thread. Note only the most recent cursor is of importance,
+// discard the previous cursor.
+- (void)applyCursor:(const webrtc::DesktopSize&)size
+ hotspot:(const webrtc::DesktopVector&)hotspot
+ cursorData:(uint8_t*)data {
+
+ [_glCursorLock lock]; // going to make changes to |_cursor|
+
+ // MouseCursor takes ownership of DesktopFrame
+ [_mouse setCursor:new webrtc::MouseCursor(new webrtc::BasicDesktopFrame(size),
+ hotspot)];
+
+ if (_mouse.cursor.image().data()) {
+ memcpy(_mouse.cursor.image().data(),
+ data,
+ size.width() * size.height() * _mouse.cursor.image().kBytesPerPixel);
+ } else {
+ [_mouse setCursor:NULL];
+ }
+
+ [_glCursorLock unlock]; // done making changes to |_cursor|
+}
+
+// @protocol GLKViewDelegate
+// There is quite a few gotchas involved in working with this function. For
+// sanity purposes, I've just assumed calls to the function are on a different
+// thread which I've termed GL Context. Any variables consumed by this function
+// should be thread safe.
+//
+// Clear Screen, update desktop, update cursor, define position, and finally
+// present
+//
+// In general, avoid expensive work in this function to maximize frame rate.
+- (void)glkView:(GLKView*)view drawInRect:(CGRect)rect {
+ [self updatePanVelocityShouldCancel:NO];
+
+ // Clear to black, to give the background color
+ glClearColor(0.0, 0.0, 0.0, 1.0);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ [Utility logGLErrorCode:@"drawInRect bindBuffer"];
+
+ if (_glRegions.size() > 0 || [_desktop needDraw]) {
+ [_glBufferLock lock];
+
+ for (uint32_t i = 0; i < _glRegions.size(); i++) {
+ // |_glRegions[i].data| has been properly ordered by [self applyFrame]
+ [_desktop drawRegion:_glRegions[i] rect:rect];
+ }
+
+ _glRegions.clear();
+ [_glBufferLock unlock];
+ }
+
+ if ([_mouse needDrawAtPosition:_scene.mousePosition]) {
+ [_glCursorLock lock];
+ [_mouse drawWithMousePosition:_scene.mousePosition];
+ [_glCursorLock unlock];
+ }
+
+ [_effect transform].projectionMatrix = _scene.projectionMatrix;
+ [_effect transform].modelviewMatrix = _scene.modelViewMatrix;
+ [_effect prepareToDraw];
+
+ [Utility logGLErrorCode:@"drawInRect prepareToDrawComplete"];
+
+ [_scene draw];
+}
+
+// @protocol KeyInputDelegate
+- (void)keyboardDismissed {
+ [self updateLabels];
+}
+
+// @protocol KeyInputDelegate
+// Send keyboard input to HOST
+- (void)keyboardActionKeyCode:(uint32_t)keyPressed isKeyDown:(BOOL)keyDown {
+ [_clientToHostProxy keyboardAction:keyPressed keyDown:keyDown];
+}
+
+- (BOOL)isToolbarHidden {
+ return (_toolbar.frame.origin.y < 0);
+}
+
+// Update the scene acceleration vector
+- (void)updatePanVelocityShouldCancel:(bool)canceled {
+ if (canceled) {
+ [_scene setPanVelocity:CGPointMake(0, 0)];
+ }
+ BOOL inMotion = [_scene tickPanVelocity];
+
+ _singleTapRecognizer.enabled = !inMotion;
+ _longPressRecognizer.enabled = !inMotion;
+}
+
+- (void)applySceneChange:(CGPoint)translation scaleBy:(float)ratio {
+ [_scene panAndZoom:translation scaleBy:ratio];
+ // Notify HOST that the mouse moved
+ [Utility moveMouse:_clientToHostProxy at:_scene.mousePosition];
+}
+
+// Callback from NSNotificationCenter when the User changes orientation
+- (void)orientationChanged:(NSNotification*)note {
+ [_scene
+ setContentSize:[Utility getOrientatedSize:self.view.bounds.size
+ shouldWidthBeLongestSide:[Utility isInLandscapeMode]]];
+ [self showToolbar:![self isToolbarHidden]];
+ [self updateLabels];
+}
+
+// Animate |_toolbar| by moving it on or offscreen
+- (void)showToolbar:(BOOL)visible {
+ CGRect frame = [_toolbar frame];
+
+ _toolBarYPosition.constant = -frame.size.height;
+ int topOffset = kTopMargin;
+
+ if (visible) {
+ topOffset += frame.size.height;
+ _toolBarYPosition.constant = kTopMargin;
+ }
+
+ _hiddenToolbarYPosition.constant = topOffset;
+ [_scene setMarginsFromLeft:0 right:0 top:topOffset bottom:kBottomMargin];
+
+ // hidden when |_toolbar| is |visible|
+ _hiddenToolbar.hidden = (visible == YES);
+
+ [UIView animateWithDuration:0.5
+ animations:^{ [self.view layoutIfNeeded]; }
+ completion:^(BOOL finished) {// Nothing to do for now
+ }];
+
+ // Center view if needed for any reason.
+ // Specificallly, if the top anchor is active.
+ [self applySceneChange:CGPointMake(0.0, 0.0) scaleBy:1.0];
+}
+@end
diff --git a/remoting/ios/ui/pin_entry_view_controller.h b/remoting/ios/ui/pin_entry_view_controller.h
new file mode 100644
index 0000000..aaf854a
--- /dev/null
+++ b/remoting/ios/ui/pin_entry_view_controller.h
@@ -0,0 +1,49 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef REMOTING_IOS_UI_PIN_ENTRY_VIEW_CONTROLLER_H_
+#define REMOTING_IOS_UI_PIN_ENTRY_VIEW_CONTROLLER_H_
+
+#import <UIKit/UIKit.h>
+
+// Contract to handle finalization for Pin Prompt
+@protocol PinEntryViewControllerDelegate<NSObject>
+
+// Returns with user's Pin. Pin has not been validated with the server yet.
+// |shouldPrompt| indicates whether a prompt should be needed for the next login
+// attempt with this host.
+- (void)connectToHostWithPin:(UIViewController*)controller
+ hostPin:(NSString*)hostPin
+ shouldPrompt:(BOOL)shouldPrompt;
+
+// Returns when the user has cancelled the input, effectively closing the
+// connection attempt.
+- (void)cancelledConnectToHostWithPin:(UIViewController*)controller;
+
+@end
+
+// Dialog for user's Pin input. If a host has |pairingSupported| then user has
+// the option to save a token for authentication.
+@interface PinEntryViewController : UIViewController<UITextFieldDelegate> {
+ @private
+ IBOutlet UIView* _controlView;
+ IBOutlet UIButton* _cancelButton;
+ IBOutlet UIButton* _connectButton;
+ IBOutlet UILabel* _host;
+ IBOutlet UISwitch* _switchAskAgain;
+ IBOutlet UILabel* _shouldSavePin;
+ IBOutlet UITextField* _hostPin;
+}
+
+@property(weak, nonatomic) id<PinEntryViewControllerDelegate> delegate;
+@property(nonatomic, copy) NSString* hostName;
+@property(nonatomic) BOOL shouldPrompt;
+@property(nonatomic) BOOL pairingSupported;
+
+- (IBAction)buttonCancelClicked:(id)sender;
+- (IBAction)buttonConnectClicked:(id)sender;
+
+@end
+
+#endif // REMOTING_IOS_UI_PIN_ENTRY_VIEW_CONTROLLER_H_ \ No newline at end of file
diff --git a/remoting/ios/ui/pin_entry_view_controller.mm b/remoting/ios/ui/pin_entry_view_controller.mm
new file mode 100644
index 0000000..9a37677
--- /dev/null
+++ b/remoting/ios/ui/pin_entry_view_controller.mm
@@ -0,0 +1,71 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "remoting/ios/ui/pin_entry_view_controller.h"
+
+#import "remoting/ios/utility.h"
+
+@implementation PinEntryViewController
+
+@synthesize delegate = _delegate;
+@synthesize shouldPrompt = _shouldPrompt;
+@synthesize pairingSupported = _pairingSupported;
+
+// Override UIViewController
+- (id)initWithNibName:(NSString*)nibNameOrNil bundle:(NSBundle*)nibBundleOrNil {
+ // NibName is the * part of your *.xib file
+
+ if ([Utility isPad]) {
+ self = [super initWithNibName:@"pin_entry_view_controller_ipad" bundle:nil];
+ } else {
+ self =
+ [super initWithNibName:@"pin_entry_view_controller_iphone" bundle:nil];
+ }
+ if (self) {
+ // Custom initialization
+ }
+ return self;
+}
+
+// Override UIViewController
+// Controls are not created immediately, properties must be set before the form
+// is displayed
+- (void)viewWillAppear:(BOOL)animated {
+ _host.text = _hostName;
+
+ [_switchAskAgain setOn:!_shouldPrompt];
+
+ // TODO (aboone) The switch is being hidden in all cases, this functionality
+ // is not scheduled for QA yet.
+ // if (!_pairingSupported) {
+ _switchAskAgain.hidden = YES;
+ _shouldSavePin.hidden = YES;
+ _switchAskAgain.enabled = NO;
+ //}
+ [_hostPin becomeFirstResponder];
+}
+
+// @protocol UITextFieldDelegate, called when the 'enter' key is pressed
+- (BOOL)textFieldShouldReturn:(UITextField*)textField {
+ [textField resignFirstResponder];
+ if (textField == _hostPin)
+ [self buttonConnectClicked:self];
+ return YES;
+}
+
+- (IBAction)buttonCancelClicked:(id)sender {
+ [_delegate cancelledConnectToHostWithPin:self];
+}
+
+- (IBAction)buttonConnectClicked:(id)sender {
+ [_delegate connectToHostWithPin:self
+ hostPin:_hostPin.text
+ shouldPrompt:!_switchAskAgain.isOn];
+}
+
+@end
diff --git a/remoting/ios/ui/pin_entry_view_controller_ipad.xib b/remoting/ios/ui/pin_entry_view_controller_ipad.xib
new file mode 100644
index 0000000..6846c81
--- /dev/null
+++ b/remoting/ios/ui/pin_entry_view_controller_ipad.xib
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.iPad.XIB" version="3.0" toolsVersion="5053" systemVersion="13C64" targetRuntime="iOS.CocoaTouch.iPad" propertyAccessControl="none" useAutolayout="YES">
+ <dependencies>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="3733"/>
+ </dependencies>
+ <objects>
+ <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="PinEntryViewController">
+ <connections>
+ <outlet property="_cancelButton" destination="BKJ-ke-HyF" id="zYI-hk-6kg"/>
+ <outlet property="_connectButton" destination="Tf7-gd-ldS" id="xQf-zj-uJ9"/>
+ <outlet property="_controlView" destination="Cqg-ut-ayj" id="H7q-tt-WHK"/>
+ <outlet property="_host" destination="qjI-DX-ED7" id="vcr-tb-2Fe"/>
+ <outlet property="_hostPin" destination="c2o-Fx-DQH" id="A7i-R4-95W"/>
+ <outlet property="_shouldSavePin" destination="ZLq-E5-uGf" id="ade-Tz-kSo"/>
+ <outlet property="_switchAskAgain" destination="Bl9-pn-tsA" id="BxE-lI-u6t"/>
+ <outlet property="view" destination="2" id="3"/>
+ </connections>
+ </placeholder>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+ <view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="2">
+ <rect key="frame" x="0.0" y="0.0" width="768" height="1024"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <subviews>
+ <view contentMode="scaleAspectFit" translatesAutoresizingMaskIntoConstraints="NO" id="Cqg-ut-ayj">
+ <rect key="frame" x="192" y="127" width="384" height="205"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <subviews>
+ <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="&lt;hostname>" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="qjI-DX-ED7">
+ <rect key="frame" x="142" y="20" width="225" height="21"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <fontDescription key="fontDescription" type="system" pointSize="17"/>
+ <color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Enter the host's PIN" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="VOw-pP-wKz">
+ <rect key="frame" x="20" y="49" width="239" height="21"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <fontDescription key="fontDescription" type="system" pointSize="17"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="PIN" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="c2o-Fx-DQH">
+ <rect key="frame" x="20" y="78" width="351" height="30"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <fontDescription key="fontDescription" type="system" pointSize="14"/>
+ <textInputTraits key="textInputTraits" autocorrectionType="no" keyboardType="numberPad" returnKeyType="go" secureTextEntry="YES"/>
+ <connections>
+ <outlet property="delegate" destination="-1" id="jaH-uT-ejT"/>
+ </connections>
+ </textField>
+ <switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Bl9-pn-tsA">
+ <rect key="frame" x="20" y="116" width="51" height="31"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ </switch>
+ <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Don't ask in the future" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ZLq-E5-uGf">
+ <rect key="frame" x="88" y="121" width="139" height="21"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <fontDescription key="fontDescription" type="system" pointSize="12"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="BKJ-ke-HyF">
+ <rect key="frame" x="20" y="155" width="160" height="30"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <state key="normal" title="Cancel">
+ <color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
+ </state>
+ <connections>
+ <action selector="buttonCancelClicked:" destination="-1" eventType="touchUpInside" id="oxw-Oo-Npc"/>
+ </connections>
+ </button>
+ <button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Tf7-gd-ldS">
+ <rect key="frame" x="207" y="155" width="160" height="30"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <state key="normal" title="Connect">
+ <color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
+ </state>
+ <connections>
+ <action selector="buttonConnectClicked:" destination="-1" eventType="touchUpInside" id="2Fi-pu-xH9"/>
+ </connections>
+ </button>
+ <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Authenticate to" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Abs-bA-a7i">
+ <rect key="frame" x="20" y="20" width="124" height="21"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <fontDescription key="fontDescription" type="system" pointSize="17"/>
+ <nil key="highlightedColor"/>
+ </label>
+ </subviews>
+ <color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
+ <constraints>
+ <constraint firstAttribute="height" constant="205" id="Z7v-Xg-IQu"/>
+ <constraint firstAttribute="width" constant="384" id="clt-7j-cb7"/>
+ </constraints>
+ </view>
+ </subviews>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ <constraints>
+ <constraint firstAttribute="centerX" secondItem="Cqg-ut-ayj" secondAttribute="centerX" id="Gs7-u0-yxZ"/>
+ <constraint firstItem="Cqg-ut-ayj" firstAttribute="top" secondItem="2" secondAttribute="top" constant="127" id="ivh-U7-Oc1"/>
+ </constraints>
+ <simulatedStatusBarMetrics key="simulatedStatusBarMetrics" statusBarStyle="lightContent"/>
+ <simulatedScreenMetrics key="simulatedDestinationMetrics"/>
+ </view>
+ </objects>
+</document>
diff --git a/remoting/ios/ui/pin_entry_view_controller_iphone.xib b/remoting/ios/ui/pin_entry_view_controller_iphone.xib
new file mode 100644
index 0000000..f7bb2a2
--- /dev/null
+++ b/remoting/ios/ui/pin_entry_view_controller_iphone.xib
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="5053" systemVersion="13C64" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES">
+ <dependencies>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="3733"/>
+ </dependencies>
+ <objects>
+ <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="PinEntryViewController">
+ <connections>
+ <outlet property="_cancelButton" destination="2Vw-9K-cVY" id="1wb-If-df2"/>
+ <outlet property="_connectButton" destination="NLw-jM-z2p" id="q5b-w6-cxk"/>
+ <outlet property="_host" destination="iat-rb-As1" id="azU-LC-CEu"/>
+ <outlet property="_hostPin" destination="Uow-Fu-2Yx" id="8iF-9q-f4R"/>
+ <outlet property="_shouldSavePin" destination="OPh-84-JII" id="Zby-0g-zE0"/>
+ <outlet property="_switchAskAgain" destination="5pF-pi-Stf" id="Ny5-lv-bsh"/>
+ <outlet property="view" destination="2" id="3"/>
+ </connections>
+ </placeholder>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+ <view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="2">
+ <rect key="frame" x="0.0" y="0.0" width="320" height="480"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <subviews>
+ <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="&lt;hostname>" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="iat-rb-As1">
+ <rect key="frame" x="155" y="104" width="70" height="15"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <fontDescription key="fontDescription" type="system" pointSize="12"/>
+ <color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enter the host's PIN" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="a63-kY-SHe">
+ <rect key="frame" x="67" y="127" width="112" height="15"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <fontDescription key="fontDescription" type="system" pointSize="12"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="PIN" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="Uow-Fu-2Yx">
+ <rect key="frame" x="67" y="150" width="115" height="30"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <constraints>
+ <constraint firstAttribute="width" constant="115" id="1Yc-ng-yRZ"/>
+ <constraint firstAttribute="height" constant="30" id="BCY-xe-HQx"/>
+ </constraints>
+ <fontDescription key="fontDescription" type="system" pointSize="14"/>
+ <textInputTraits key="textInputTraits" autocorrectionType="no" keyboardType="numberPad" returnKeyType="go" secureTextEntry="YES"/>
+ <connections>
+ <outlet property="delegate" destination="-1" id="eEn-tS-i45"/>
+ </connections>
+ </textField>
+ <switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="5pF-pi-Stf">
+ <rect key="frame" x="190" y="149" width="51" height="31"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ </switch>
+ <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Save PIN" textAlignment="center" lineBreakMode="wordWrap" numberOfLines="2" minimumFontSize="7" adjustsLetterSpacingToFitWidth="YES" preferredMaxLayoutWidth="56" translatesAutoresizingMaskIntoConstraints="NO" id="OPh-84-JII">
+ <rect key="frame" x="247" y="157" width="56" height="16"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <fontDescription key="fontDescription" type="system" pointSize="13"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2Vw-9K-cVY">
+ <rect key="frame" x="79" y="188" width="45" height="31"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <fontDescription key="fontDescription" type="system" pointSize="14"/>
+ <state key="normal" title="Cancel">
+ <color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
+ </state>
+ <connections>
+ <action selector="buttonCancelClicked:" destination="-1" eventType="touchUpInside" id="a0p-ci-esP"/>
+ </connections>
+ </button>
+ <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NLw-jM-z2p">
+ <rect key="frame" x="155" y="189" width="55" height="29"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <fontDescription key="fontDescription" type="system" pointSize="14"/>
+ <state key="normal" title="Connect">
+ <color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
+ </state>
+ <connections>
+ <action selector="buttonConnectClicked:" destination="-1" eventType="touchUpInside" id="CG9-X4-tEa"/>
+ </connections>
+ </button>
+ <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Authenticate to" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="G1n-4q-Knh">
+ <rect key="frame" x="67" y="104" width="85" height="15"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <fontDescription key="fontDescription" type="system" pointSize="12"/>
+ <nil key="highlightedColor"/>
+ </label>
+ </subviews>
+ <color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
+ <constraints>
+ <constraint firstItem="NLw-jM-z2p" firstAttribute="top" secondItem="Uow-Fu-2Yx" secondAttribute="bottom" constant="9" id="3jN-pC-2rt"/>
+ <constraint firstAttribute="centerY" secondItem="Uow-Fu-2Yx" secondAttribute="centerY" constant="75" id="5pL-cU-39Z"/>
+ <constraint firstItem="5pF-pi-Stf" firstAttribute="leading" secondItem="Uow-Fu-2Yx" secondAttribute="trailing" constant="8" id="9Qh-pj-al1"/>
+ <constraint firstItem="a63-kY-SHe" firstAttribute="top" secondItem="G1n-4q-Knh" secondAttribute="bottom" constant="8" id="Aep-CT-GtV"/>
+ <constraint firstItem="Uow-Fu-2Yx" firstAttribute="top" secondItem="a63-kY-SHe" secondAttribute="bottom" constant="8" id="Af6-MG-6hM"/>
+ <constraint firstItem="Uow-Fu-2Yx" firstAttribute="centerY" secondItem="5pF-pi-Stf" secondAttribute="centerY" id="Cxm-6Z-rBa"/>
+ <constraint firstAttribute="centerX" secondItem="Uow-Fu-2Yx" secondAttribute="centerX" constant="36" id="L6n-kv-1cb"/>
+ <constraint firstItem="NLw-jM-z2p" firstAttribute="leading" secondItem="2Vw-9K-cVY" secondAttribute="trailing" constant="31" id="OGl-yE-cFq"/>
+ <constraint firstItem="2Vw-9K-cVY" firstAttribute="leading" secondItem="Uow-Fu-2Yx" secondAttribute="leading" constant="12" id="Onp-Z7-Xp2"/>
+ <constraint firstItem="iat-rb-As1" firstAttribute="centerY" secondItem="G1n-4q-Knh" secondAttribute="centerY" id="RI9-Jx-K5Z"/>
+ <constraint firstItem="iat-rb-As1" firstAttribute="leading" secondItem="G1n-4q-Knh" secondAttribute="trailing" constant="3" id="XQd-6a-62O"/>
+ <constraint firstItem="NLw-jM-z2p" firstAttribute="centerY" secondItem="2Vw-9K-cVY" secondAttribute="centerY" id="baU-9W-Ab2"/>
+ <constraint firstItem="2Vw-9K-cVY" firstAttribute="top" secondItem="Uow-Fu-2Yx" secondAttribute="bottom" constant="8" id="dCn-aX-MNJ"/>
+ <constraint firstItem="a63-kY-SHe" firstAttribute="leading" secondItem="Uow-Fu-2Yx" secondAttribute="leading" id="ddk-qx-Ldm"/>
+ <constraint firstItem="Uow-Fu-2Yx" firstAttribute="centerY" secondItem="OPh-84-JII" secondAttribute="centerY" id="fwM-yD-GQh"/>
+ <constraint firstItem="5pF-pi-Stf" firstAttribute="centerY" secondItem="OPh-84-JII" secondAttribute="centerY" id="jfD-pi-EWm"/>
+ <constraint firstItem="OPh-84-JII" firstAttribute="leading" secondItem="5pF-pi-Stf" secondAttribute="trailing" constant="8" id="qjc-e2-rkS"/>
+ <constraint firstItem="a63-kY-SHe" firstAttribute="leading" secondItem="G1n-4q-Knh" secondAttribute="leading" id="tIh-JU-ubp"/>
+ </constraints>
+ <simulatedStatusBarMetrics key="simulatedStatusBarMetrics" statusBarStyle="lightContent"/>
+ <simulatedScreenMetrics key="simulatedDestinationMetrics"/>
+ </view>
+ </objects>
+</document>
diff --git a/remoting/ios/ui/scene_view.h b/remoting/ios/ui/scene_view.h
new file mode 100644
index 0000000..8f082ff
--- /dev/null
+++ b/remoting/ios/ui/scene_view.h
@@ -0,0 +1,171 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef REMOTING_IOS_UI_SCENE_VIEW_H_
+#define REMOTING_IOS_UI_SCENE_VIEW_H_
+
+#import <Foundation/Foundation.h>
+#import <GLKit/GLKit.h>
+
+#include "third_party/webrtc/modules/desktop_capture/desktop_geometry.h"
+
+typedef struct {
+ bool left;
+ bool right;
+ bool top;
+ bool bottom;
+} AnchorPosition;
+
+typedef struct {
+ int left;
+ int right;
+ int top;
+ int bottom;
+} MarginQuad;
+
+typedef struct {
+ CGPoint geometryVertex;
+ CGPoint textureVertex;
+} TexturedVertex;
+
+typedef struct {
+ TexturedVertex bl;
+ TexturedVertex br;
+ TexturedVertex tl;
+ TexturedVertex tr;
+} TexturedQuad;
+
+@interface SceneView : NSObject {
+ @private
+
+ // GL name
+ GLuint _textureId;
+
+ GLKMatrix4 _projectionMatrix;
+ GLKMatrix4 _modelViewMatrix;
+
+ // The draw surface is a triangle strip (triangles defined by the intersecting
+ // vertexes) to create a rectangle surface.
+ // 1****3
+ // | / |
+ // | / |
+ // 2****4
+ // This also determines the resolution of our surface, being a unit (NxN) grid
+ // with finite divisions. For our surface N = 1, and the number of divisions
+ // respects the CLIENT's desktop resolution.
+ TexturedQuad _glQuad;
+
+ // Cache of the CLIENT's desktop resolution.
+ webrtc::DesktopSize _contentSize;
+ // Cache of the HOST's desktop resolution.
+ webrtc::DesktopSize _frameSize;
+
+ // Location of the mouse according to the CLIENT in the prospective of the
+ // HOST resolution
+ webrtc::DesktopVector _mousePosition;
+
+ // When a user pans they expect the view to experience acceleration after
+ // they release the pan gesture. We track that velocity vector as a position
+ // delta factored over the frame rate of the GL Context. Velocity is
+ // accounted as a float.
+ CGPoint _panVelocity;
+}
+
+// The position of the scene is tracked in the prospective of the CLIENT
+// resolution. The Z-axis is used to track the scale of the render, our scene
+// never changes position on the Z-axis.
+@property(nonatomic, readonly) GLKVector3 position;
+
+// Space around border consumed by non-scene elements, we can not draw here
+@property(nonatomic, readonly) MarginQuad margin;
+
+@property(nonatomic, readonly) AnchorPosition anchored;
+
+- (const GLKMatrix4&)projectionMatrix;
+
+// calculate and return the current model view matrix
+- (const GLKMatrix4&)modelViewMatrix;
+
+- (const webrtc::DesktopSize&)contentSize;
+
+// Update the CLIENT resolution and draw scene size, accounting for margins
+- (void)setContentSize:(const CGSize&)size;
+
+- (const webrtc::DesktopSize&)frameSize;
+
+// Update the HOST resolution and reinitialize the scene positioning
+- (void)setFrameSize:(const webrtc::DesktopSize&)size;
+
+- (const webrtc::DesktopVector&)mousePosition;
+
+- (void)setPanVelocity:(const CGPoint&)delta;
+
+- (void)setMarginsFromLeft:(int)left
+ right:(int)right
+ top:(int)top
+ bottom:(int)bottom;
+
+// Draws to a GL Context
+- (void)draw;
+
+- (BOOL)containsTouchPoint:(CGPoint)point;
+
+// Applies translation and zoom. Translation is bounded to screen edges.
+// Zooming is bounded on the lower side to the maximum of width and height, and
+// on the upper side by a constant, experimentally chosen.
+- (void)panAndZoom:(CGPoint)translation scaleBy:(float)scale;
+
+// Mouse is tracked in the perspective of the HOST desktop, but the projection
+// to the user is in the perspective of the CLIENT resolution. Find the HOST
+// position that is the center of the current CLIENT view. If the mouse is in
+// the half of the CLIENT screen that is closest to an anchor, then move the
+// mouse, otherwise the mouse should be centered.
+- (void)updateMousePositionAndAnchorsWithTranslation:(CGPoint)translation
+ scale:(float)scale;
+
+// When zoom is changed the scene is translated to keep an anchored point
+// (an anchored edge, or the spot the user is touching) at the same place in the
+// User's perspective. Return the delta of the position of the lower endpoint
+// of the axis
++ (float)positionDeltaFromScaling:(float)ratio
+ position:(float)position
+ length:(float)length
+ anchor:(float)anchor;
+
+// Return the delta of the position of the lower endpoint of the axis
++ (int)positionDeltaFromTranslation:(int)translation
+ position:(int)position
+ freeSpace:(int)freeSpace
+ scaleingPositionDelta:(int)scaleingPositionDelta
+ isAnchoredLow:(BOOL)isAnchoredLow
+ isAnchoredHigh:(BOOL)isAnchoredHigh;
+
+// |position + delta| is snapped to the bounds, return the delta in respect to
+// the bounding.
++ (int)boundDeltaFromPosition:(float)position
+ delta:(int)delta
+ lowerBound:(int)lowerBound
+ upperBound:(int)upperBound;
+
+// Return |nextPosition| when it is anchored and still in the respective 1/2 of
+// the screen. When |nextPosition| is outside scene's edge, snap to edge.
+// Otherwise return |centerPosition|
++ (int)boundMouseGivenNextPosition:(int)nextPosition
+ maxPosition:(int)maxPosition
+ centerPosition:(int)centerPosition
+ isAnchoredLow:(BOOL)isAnchoredLow
+ isAnchoredHigh:(BOOL)isAnchoredHigh;
+
+// If the mouse is at an edge return zero, otherwise return |velocity|
++ (float)boundVelocity:(float)velocity
+ axisLength:(int)axisLength
+ mousePosition:(int)mousePosition;
+
+// Update the scene acceleration vector.
+// Returns true if velocity before 'ticking' is non-zero.
+- (BOOL)tickPanVelocity;
+
+@end
+
+#endif // REMOTING_IOS_UI_SCENE_VIEW_H_ \ No newline at end of file
diff --git a/remoting/ios/ui/scene_view.mm b/remoting/ios/ui/scene_view.mm
new file mode 100644
index 0000000..97a577d
--- /dev/null
+++ b/remoting/ios/ui/scene_view.mm
@@ -0,0 +1,642 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "remoting/ios/ui/scene_view.h"
+
+#import "remoting/ios/utility.h"
+
+namespace {
+
+// TODO (aboone) Some of the layout is not yet set in stone, so variables have
+// been used to position and turn items on and off. Eventually these may be
+// stabilized and removed.
+
+// Scroll speed multiplier for swiping
+const static int kMouseSensitivity = 2.5;
+
+// Input Axis inversion
+// 1 for standard, -1 for inverted
+const static int kXAxisInversion = -1;
+const static int kYAxisInversion = -1;
+
+// Experimental value for bounding the maximum zoom ratio
+const static int kMaxZoomSize = 3;
+} // namespace
+
+@interface SceneView (Private)
+// Returns the number of pixels displayed per device pixel when the scaling is
+// such that the entire frame would fit perfectly in content. Note the ratios
+// are different for width and height, some people have multiple monitors, some
+// have 16:9 or 4:3 while iPad is always single screen, but different iOS
+// devices have different resolutions.
+- (CGPoint)pixelRatio;
+
+// Return the FrameSize in perspective of the CLIENT resolution
+- (webrtc::DesktopSize)frameSizeToScale:(float)scale;
+
+// When bounded on the top and right, this point is where the scene must be
+// positioned given a scene size
+- (webrtc::DesktopVector)getBoundsForSize:(const webrtc::DesktopSize&)size;
+
+// Converts a point in the the CLIENT resolution to a similar point in the HOST
+// resolution. Additionally, CLIENT resolution is expressed in float values
+// while HOST operates in integer values.
+- (BOOL)convertTouchPointToMousePoint:(CGPoint)touchPoint
+ targetPoint:(webrtc::DesktopVector&)desktopPoint;
+
+// Converts a point in the the HOST resolution to a similar point in the CLIENT
+// resolution. Additionally, CLIENT resolution is expressed in float values
+// while HOST operates in integer values.
+- (BOOL)convertMousePointToTouchPoint:(const webrtc::DesktopVector&)mousePoint
+ targetPoint:(CGPoint&)touchPoint;
+@end
+
+@implementation SceneView
+
+- (id)init {
+ self = [super init];
+ if (self) {
+
+ _frameSize = webrtc::DesktopSize(1, 1);
+ _contentSize = webrtc::DesktopSize(1, 1);
+ _mousePosition = webrtc::DesktopVector(0, 0);
+
+ _position = GLKVector3Make(0, 0, 1);
+ _margin.left = 0;
+ _margin.right = 0;
+ _margin.top = 0;
+ _margin.bottom = 0;
+ _anchored.left = false;
+ _anchored.right = false;
+ _anchored.top = false;
+ _anchored.bottom = false;
+ }
+ return self;
+}
+
+- (const GLKMatrix4&)projectionMatrix {
+ return _projectionMatrix;
+}
+
+- (const GLKMatrix4&)modelViewMatrix {
+ // Start by using the entire scene
+ _modelViewMatrix = GLKMatrix4Identity;
+
+ // Position scene according to any panning or bounds
+ _modelViewMatrix = GLKMatrix4Translate(_modelViewMatrix,
+ _position.x + _margin.left,
+ _position.y + _margin.bottom,
+ 0.0);
+
+ // Apply zoom
+ _modelViewMatrix = GLKMatrix4Scale(_modelViewMatrix,
+ _position.z / self.pixelRatio.x,
+ _position.z / self.pixelRatio.y,
+ 1.0);
+
+ // We are directly above the screen and looking down.
+ static const GLKMatrix4 viewMatrix = GLKMatrix4MakeLookAt(
+ 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); // center view
+
+ _modelViewMatrix = GLKMatrix4Multiply(viewMatrix, _modelViewMatrix);
+
+ return _modelViewMatrix;
+}
+
+- (const webrtc::DesktopSize&)contentSize {
+ return _contentSize;
+}
+
+- (void)setContentSize:(const CGSize&)size {
+
+ _contentSize.set(size.width, size.height);
+
+ _projectionMatrix = GLKMatrix4MakeOrtho(
+ 0.0, _contentSize.width(), 0.0, _contentSize.height(), 1.0, -1.0);
+
+ TexturedQuad newQuad;
+ newQuad.bl.geometryVertex = CGPointMake(0.0, 0.0);
+ newQuad.br.geometryVertex = CGPointMake(_contentSize.width(), 0.0);
+ newQuad.tl.geometryVertex = CGPointMake(0.0, _contentSize.height());
+ newQuad.tr.geometryVertex =
+ CGPointMake(_contentSize.width(), _contentSize.height());
+
+ newQuad.bl.textureVertex = CGPointMake(0.0, 1.0);
+ newQuad.br.textureVertex = CGPointMake(1.0, 1.0);
+ newQuad.tl.textureVertex = CGPointMake(0.0, 0.0);
+ newQuad.tr.textureVertex = CGPointMake(1.0, 0.0);
+
+ _glQuad = newQuad;
+}
+
+- (const webrtc::DesktopSize&)frameSize {
+ return _frameSize;
+}
+
+- (void)setFrameSize:(const webrtc::DesktopSize&)size {
+ DCHECK(size.width() > 0 && size.height() > 0);
+ // Don't do anything if the size has not changed.
+ if (_frameSize.equals(size))
+ return;
+
+ _frameSize.set(size.width(), size.height());
+
+ _position.x = 0;
+ _position.y = 0;
+
+ float verticalPixelScaleRatio =
+ (static_cast<float>(_contentSize.height() - _margin.top -
+ _margin.bottom) /
+ static_cast<float>(_frameSize.height())) /
+ _position.z;
+
+ // Anchored at the position (0,0)
+ _anchored.left = YES;
+ _anchored.right = NO;
+ _anchored.top = NO;
+ _anchored.bottom = YES;
+
+ [self panAndZoom:CGPointMake(0.0, 0.0) scaleBy:verticalPixelScaleRatio];
+
+ // Center the mouse on the CLIENT screen
+ webrtc::DesktopVector centerMouseLocation;
+ if ([self convertTouchPointToMousePoint:CGPointMake(_contentSize.width() / 2,
+ _contentSize.height() / 2)
+ targetPoint:centerMouseLocation]) {
+ _mousePosition.set(centerMouseLocation.x(), centerMouseLocation.y());
+ }
+
+#if DEBUG
+ NSLog(@"resized frame:%d:%d scale:%f",
+ _frameSize.width(),
+ _frameSize.height(),
+ _position.z);
+#endif // DEBUG
+}
+
+- (const webrtc::DesktopVector&)mousePosition {
+ return _mousePosition;
+}
+
+- (void)setPanVelocity:(const CGPoint&)delta {
+ _panVelocity.x = delta.x;
+ _panVelocity.y = delta.y;
+}
+
+- (void)setMarginsFromLeft:(int)left
+ right:(int)right
+ top:(int)top
+ bottom:(int)bottom {
+ _margin.left = left;
+ _margin.right = right;
+ _margin.top = top;
+ _margin.bottom = bottom;
+}
+
+- (void)draw {
+ glEnableVertexAttribArray(GLKVertexAttribPosition);
+ glEnableVertexAttribArray(GLKVertexAttribTexCoord0);
+ glEnableVertexAttribArray(GLKVertexAttribTexCoord1);
+
+ // Define our scene space
+ glVertexAttribPointer(GLKVertexAttribPosition,
+ 2,
+ GL_FLOAT,
+ GL_FALSE,
+ sizeof(TexturedVertex),
+ &(_glQuad.bl.geometryVertex));
+ // Define the desktop plane
+ glVertexAttribPointer(GLKVertexAttribTexCoord0,
+ 2,
+ GL_FLOAT,
+ GL_FALSE,
+ sizeof(TexturedVertex),
+ &(_glQuad.bl.textureVertex));
+ // Define the cursor plane
+ glVertexAttribPointer(GLKVertexAttribTexCoord1,
+ 2,
+ GL_FLOAT,
+ GL_FALSE,
+ sizeof(TexturedVertex),
+ &(_glQuad.bl.textureVertex));
+
+ // Draw!
+ glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
+
+ [Utility logGLErrorCode:@"SceneView draw"];
+}
+
+- (CGPoint)pixelRatio {
+
+ CGPoint r = CGPointMake(static_cast<float>(_contentSize.width()) /
+ static_cast<float>(_frameSize.width()),
+ static_cast<float>(_contentSize.height()) /
+ static_cast<float>(_frameSize.height()));
+ return r;
+}
+
+- (webrtc::DesktopSize)frameSizeToScale:(float)scale {
+ return webrtc::DesktopSize(_frameSize.width() * scale,
+ _frameSize.height() * scale);
+}
+
+- (webrtc::DesktopVector)getBoundsForSize:(const webrtc::DesktopSize&)size {
+ webrtc::DesktopVector r(
+ _contentSize.width() - _margin.left - _margin.right - size.width(),
+ _contentSize.height() - _margin.bottom - _margin.top - size.height());
+
+ if (r.x() > 0) {
+ r.set((_contentSize.width() - size.width()) / 2, r.y());
+ }
+
+ if (r.y() > 0) {
+ r.set(r.x(), (_contentSize.height() - size.height()) / 2);
+ }
+
+ return r;
+}
+
+- (BOOL)containsTouchPoint:(CGPoint)point {
+ // Here frame is from the top-left corner, most other calculations are framed
+ // from the bottom left.
+ CGRect frame =
+ CGRectMake(_margin.left,
+ _margin.top,
+ _contentSize.width() - _margin.left - _margin.right,
+ _contentSize.height() - _margin.top - _margin.bottom);
+ return CGRectContainsPoint(frame, point);
+}
+
+- (BOOL)convertTouchPointToMousePoint:(CGPoint)touchPoint
+ targetPoint:(webrtc::DesktopVector&)mousePoint {
+ if (![self containsTouchPoint:touchPoint]) {
+ return NO;
+ }
+ // A touch location occurs in respect to the user's entire view surface.
+
+ // The GL Context is upside down from the User's perspective so flip it.
+ CGPoint glOrientedTouchPoint =
+ CGPointMake(touchPoint.x, _contentSize.height() - touchPoint.y);
+
+ // The GL surface generally is not at the same origination point as the touch,
+ // so translate by the scene's position.
+ CGPoint glOrientedPointInRespectToFrame =
+ CGPointMake(glOrientedTouchPoint.x - _position.x,
+ glOrientedTouchPoint.y - _position.y);
+
+ // The perspective exists in relative to the CLIENT resolution at 1:1, zoom
+ // our perspective so we are relative to the HOST at 1:1
+ CGPoint glOrientedPointInFrame =
+ CGPointMake(glOrientedPointInRespectToFrame.x / _position.z,
+ glOrientedPointInRespectToFrame.y / _position.z);
+
+ // Finally, flip the perspective back over to the Users, but this time in
+ // respect to the HOST desktop. Floor to ensure the result is always in
+ // frame.
+ CGPoint deskTopOrientedPointInFrame =
+ CGPointMake(floorf(glOrientedPointInFrame.x),
+ floorf(_frameSize.height() - glOrientedPointInFrame.y));
+
+ // Convert from float to integer
+ mousePoint.set(deskTopOrientedPointInFrame.x, deskTopOrientedPointInFrame.y);
+
+ return CGRectContainsPoint(
+ CGRectMake(0, 0, _frameSize.width(), _frameSize.height()),
+ deskTopOrientedPointInFrame);
+}
+
+- (BOOL)convertMousePointToTouchPoint:(const webrtc::DesktopVector&)mousePoint
+ targetPoint:(CGPoint&)touchPoint {
+ // A mouse point is in respect to the desktop frame.
+
+ // Flip the perspective back over to the Users, in
+ // respect to the HOST desktop.
+ CGPoint deskTopOrientedPointInFrame =
+ CGPointMake(mousePoint.x(), _frameSize.height() - mousePoint.y());
+
+ // The perspective exists in relative to the CLIENT resolution at 1:1, zoom
+ // our perspective so we are relative to the HOST at 1:1
+ CGPoint glOrientedPointInFrame =
+ CGPointMake(deskTopOrientedPointInFrame.x * _position.z,
+ deskTopOrientedPointInFrame.y * _position.z);
+
+ // The GL surface generally is not at the same origination point as the touch,
+ // so translate by the scene's position.
+ CGPoint glOrientedPointInRespectToFrame =
+ CGPointMake(glOrientedPointInFrame.x + _position.x,
+ glOrientedPointInFrame.y + _position.y);
+
+ // Convert from float to integer
+ touchPoint.x = floorf(glOrientedPointInRespectToFrame.x);
+ touchPoint.y = floorf(glOrientedPointInRespectToFrame.y);
+
+ return [self containsTouchPoint:touchPoint];
+}
+
+- (void)panAndZoom:(CGPoint)translation scaleBy:(float)ratio {
+ CGPoint ratios = [self pixelRatio];
+
+ // New Scaling factor bounded by a min and max
+ float resultScale = _position.z * ratio;
+ float scaleUpperBound = MAX(ratios.x, MAX(ratios.y, kMaxZoomSize));
+ float scaleLowerBound = MIN(ratios.x, ratios.y);
+
+ if (resultScale < scaleLowerBound) {
+ resultScale = scaleLowerBound;
+ } else if (resultScale > scaleUpperBound) {
+ resultScale = scaleUpperBound;
+ }
+
+ DCHECK(isnormal(resultScale) && resultScale > 0);
+
+ // The GL perspective is upside down in relation to the User's view, so flip
+ // the translation
+ translation.y = -translation.y;
+
+ // The constants here could be user options later.
+ translation.x =
+ translation.x * kXAxisInversion * (1 / (ratios.x * kMouseSensitivity));
+ translation.y =
+ translation.y * kYAxisInversion * (1 / (ratios.y * kMouseSensitivity));
+
+ CGPoint delta = CGPointMake(0, 0);
+ CGPoint scaleDelta = CGPointMake(0, 0);
+
+ webrtc::DesktopSize currentSize = [self frameSizeToScale:_position.z];
+
+ {
+ // Closure for this variable, so the variable is not available to the rest
+ // of this function
+ webrtc::DesktopVector currentBounds = [self getBoundsForSize:currentSize];
+ // There are rounding errors in the scope of this function, see the
+ // butterfly effect. In successive calls, the resulting position isn't
+ // always exactly the calculated position. If we know we are Anchored, then
+ // go ahead and reposition it to the values above.
+ if (_anchored.right) {
+ _position.x = currentBounds.x();
+ }
+
+ if (_anchored.top) {
+ _position.y = currentBounds.y();
+ }
+ }
+
+ if (_position.z != resultScale) {
+ // When scaling the scene, the origination of scaling is the mouse's
+ // location. But when the frame is anchored, adjust the origination to the
+ // anchor point.
+
+ CGPoint mousePositionInClientResolution;
+ [self convertMousePointToTouchPoint:_mousePosition
+ targetPoint:mousePositionInClientResolution];
+
+ // Prefer to zoom based on the left anchor when there is a choice
+ if (_anchored.left) {
+ mousePositionInClientResolution.x = 0;
+ } else if (_anchored.right) {
+ mousePositionInClientResolution.x = _contentSize.width();
+ }
+
+ // Prefer to zoom out from the top anchor when there is a choice
+ if (_anchored.top) {
+ mousePositionInClientResolution.y = _contentSize.height();
+ } else if (_anchored.bottom) {
+ mousePositionInClientResolution.y = 0;
+ }
+
+ scaleDelta.x -=
+ [SceneView positionDeltaFromScaling:ratio
+ position:_position.x
+ length:currentSize.width()
+ anchor:mousePositionInClientResolution.x];
+
+ scaleDelta.y -=
+ [SceneView positionDeltaFromScaling:ratio
+ position:_position.y
+ length:currentSize.height()
+ anchor:mousePositionInClientResolution.y];
+ }
+
+ delta.x = [SceneView
+ positionDeltaFromTranslation:translation.x
+ position:_position.x
+ freeSpace:_contentSize.width() - currentSize.width()
+ scaleingPositionDelta:scaleDelta.x
+ isAnchoredLow:_anchored.left
+ isAnchoredHigh:_anchored.right];
+
+ delta.y = [SceneView
+ positionDeltaFromTranslation:translation.y
+ position:_position.y
+ freeSpace:_contentSize.height() - currentSize.height()
+ scaleingPositionDelta:scaleDelta.y
+ isAnchoredLow:_anchored.bottom
+ isAnchoredHigh:_anchored.top];
+ {
+ // Closure for this variable, so the variable is not available to the rest
+ // of this function
+ webrtc::DesktopVector bounds =
+ [self getBoundsForSize:[self frameSizeToScale:resultScale]];
+
+ delta.x = [SceneView boundDeltaFromPosition:_position.x
+ delta:delta.x
+ lowerBound:bounds.x()
+ upperBound:0];
+
+ delta.y = [SceneView boundDeltaFromPosition:_position.y
+ delta:delta.y
+ lowerBound:bounds.y()
+ upperBound:0];
+ }
+
+ BOOL isLeftAndRightAnchored = _anchored.left && _anchored.right;
+ BOOL isTopAndBottomAnchored = _anchored.top && _anchored.bottom;
+
+ [self updateMousePositionAndAnchorsWithTranslation:translation
+ scale:resultScale];
+
+ // If both anchors were lost, then keep the one that is easier to predict
+ if (isLeftAndRightAnchored && !_anchored.left && !_anchored.right) {
+ delta.x = -_position.x;
+ _anchored.left = YES;
+ }
+
+ // If both anchors were lost, then keep the one that is easier to predict
+ if (isTopAndBottomAnchored && !_anchored.top && !_anchored.bottom) {
+ delta.y = -_position.y;
+ _anchored.bottom = YES;
+ }
+
+ // FINALLY, update the scene's position
+ _position.x += delta.x;
+ _position.y += delta.y;
+ _position.z = resultScale;
+}
+
+- (void)updateMousePositionAndAnchorsWithTranslation:(CGPoint)translation
+ scale:(float)scale {
+ webrtc::DesktopVector centerMouseLocation;
+ [self convertTouchPointToMousePoint:CGPointMake(_contentSize.width() / 2,
+ _contentSize.height() / 2)
+ targetPoint:centerMouseLocation];
+
+ webrtc::DesktopVector currentBounds =
+ [self getBoundsForSize:[self frameSizeToScale:_position.z]];
+ webrtc::DesktopVector nextBounds =
+ [self getBoundsForSize:[self frameSizeToScale:scale]];
+
+ webrtc::DesktopVector predictedMousePosition(
+ _mousePosition.x() - translation.x, _mousePosition.y() + translation.y);
+
+ _mousePosition.set(
+ [SceneView boundMouseGivenNextPosition:predictedMousePosition.x()
+ maxPosition:_frameSize.width()
+ centerPosition:centerMouseLocation.x()
+ isAnchoredLow:_anchored.left
+ isAnchoredHigh:_anchored.right],
+ [SceneView boundMouseGivenNextPosition:predictedMousePosition.y()
+ maxPosition:_frameSize.height()
+ centerPosition:centerMouseLocation.y()
+ isAnchoredLow:_anchored.top
+ isAnchoredHigh:_anchored.bottom]);
+
+ _panVelocity.x = [SceneView boundVelocity:_panVelocity.x
+ axisLength:_frameSize.width()
+ mousePosition:_mousePosition.x()];
+ _panVelocity.y = [SceneView boundVelocity:_panVelocity.y
+ axisLength:_frameSize.height()
+ mousePosition:_mousePosition.y()];
+
+ _anchored.left = (nextBounds.x() >= 0) ||
+ (_position.x == 0 &&
+ predictedMousePosition.x() <= centerMouseLocation.x());
+
+ _anchored.right =
+ (nextBounds.x() >= 0) ||
+ (_position.x == currentBounds.x() &&
+ predictedMousePosition.x() >= centerMouseLocation.x()) ||
+ (_mousePosition.x() == _frameSize.width() - 1 && !_anchored.left);
+
+ _anchored.bottom = (nextBounds.y() >= 0) ||
+ (_position.y == 0 &&
+ predictedMousePosition.y() >= centerMouseLocation.y());
+
+ _anchored.top =
+ (nextBounds.y() >= 0) ||
+ (_position.y == currentBounds.y() &&
+ predictedMousePosition.y() <= centerMouseLocation.y()) ||
+ (_mousePosition.y() == _frameSize.height() - 1 && !_anchored.bottom);
+}
+
++ (float)positionDeltaFromScaling:(float)ratio
+ position:(float)position
+ length:(float)length
+ anchor:(float)anchor {
+ float newSize = length * ratio;
+ float scaleXBy = fabs(position - anchor) / length;
+ float delta = (newSize - length) * scaleXBy;
+ return delta;
+}
+
++ (int)positionDeltaFromTranslation:(int)translation
+ position:(int)position
+ freeSpace:(int)freeSpace
+ scaleingPositionDelta:(int)scaleingPositionDelta
+ isAnchoredLow:(BOOL)isAnchoredLow
+ isAnchoredHigh:(BOOL)isAnchoredHigh {
+ if (isAnchoredLow && isAnchoredHigh) {
+ // center the view
+ return (freeSpace / 2) - position;
+ } else if (isAnchoredLow) {
+ return 0;
+ } else if (isAnchoredHigh) {
+ return scaleingPositionDelta;
+ } else {
+ return translation + scaleingPositionDelta;
+ }
+}
+
++ (int)boundDeltaFromPosition:(float)position
+ delta:(int)delta
+ lowerBound:(int)lowerBound
+ upperBound:(int)upperBound {
+ int result = position + delta;
+
+ if (lowerBound < upperBound) { // the view is larger than the bounds
+ if (result > upperBound) {
+ result = upperBound;
+ } else if (result < lowerBound) {
+ result = lowerBound;
+ }
+ } else {
+ // the view is smaller than the bounds so we'll always be at the lowerBound
+ result = lowerBound;
+ }
+ return result - position;
+}
+
++ (int)boundMouseGivenNextPosition:(int)nextPosition
+ maxPosition:(int)maxPosition
+ centerPosition:(int)centerPosition
+ isAnchoredLow:(BOOL)isAnchoredLow
+ isAnchoredHigh:(BOOL)isAnchoredHigh {
+ if (nextPosition < 0) {
+ return 0;
+ }
+ if (nextPosition > maxPosition - 1) {
+ return maxPosition - 1;
+ }
+
+ if ((isAnchoredLow && nextPosition <= centerPosition) ||
+ (isAnchoredHigh && nextPosition >= centerPosition)) {
+ return nextPosition;
+ }
+
+ return centerPosition;
+}
+
++ (float)boundVelocity:(float)velocity
+ axisLength:(int)axisLength
+ mousePosition:(int)mousePosition {
+ if (velocity != 0) {
+ if (mousePosition <= 0 || mousePosition >= (axisLength - 1)) {
+ return 0;
+ }
+ }
+
+ return velocity;
+}
+
+- (BOOL)tickPanVelocity {
+ BOOL inMotion = ((_panVelocity.x != 0.0) || (_panVelocity.y != 0.0));
+
+ if (inMotion) {
+
+ uint32_t divisor = 50 / _position.z;
+ float reducer = .95;
+
+ if (_panVelocity.x != 0.0 && ABS(_panVelocity.x) < divisor) {
+ _panVelocity = CGPointMake(0.0, _panVelocity.y);
+ }
+
+ if (_panVelocity.y != 0.0 && ABS(_panVelocity.y) < divisor) {
+ _panVelocity = CGPointMake(_panVelocity.x, 0.0);
+ }
+
+ [self panAndZoom:CGPointMake(_panVelocity.x / divisor,
+ _panVelocity.y / divisor)
+ scaleBy:1.0];
+
+ _panVelocity.x *= reducer;
+ _panVelocity.y *= reducer;
+ }
+
+ return inMotion;
+}
+
+@end \ No newline at end of file
diff --git a/remoting/ios/ui/scene_view_unittest.mm b/remoting/ios/ui/scene_view_unittest.mm
new file mode 100644
index 0000000..d1dfabc
--- /dev/null
+++ b/remoting/ios/ui/scene_view_unittest.mm
@@ -0,0 +1,1219 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "remoting/ios/ui/scene_view.h"
+
+#import "base/compiler_specific.h"
+#import "testing/gtest_mac.h"
+
+namespace remoting {
+
+namespace {
+const int kClientWidth = 200;
+const int kClientHeight = 100;
+const webrtc::DesktopSize kClientSize(kClientWidth, kClientHeight);
+// Smaller then ClientSize
+const webrtc::DesktopSize kSmall(50, 75);
+// Inverted - The vertical is closer to an edge than the horizontal
+const webrtc::DesktopSize kSmallInversed(175, 50);
+// Larger then ClientSize
+const webrtc::DesktopSize kLarge(800, 125);
+const webrtc::DesktopSize kLargeInversed(225, 400);
+} // namespace
+
+class SceneViewTest : public ::testing::Test {
+ protected:
+ virtual void SetUp() OVERRIDE {
+ scene_ = [[SceneView alloc] init];
+ [scene_
+ setContentSize:CGSizeMake(kClientSize.width(), kClientSize.height())];
+ [scene_ setFrameSize:kLarge];
+ }
+
+ void MakeLarge() { [scene_ setFrameSize:kLarge]; }
+
+ SceneView* scene_;
+};
+
+TEST(SceneViewTest_Property, ContentSize) {
+ SceneView* scene = [[SceneView alloc] init];
+
+ [scene setContentSize:CGSizeMake(0, 0)];
+ EXPECT_EQ(0, scene.contentSize.width());
+ EXPECT_EQ(0, scene.contentSize.height());
+ float zeros[16] = {1.0f / 0.0f, 0, 0, 0, 0, 1.0f / 0.0f, 0, 0,
+ 0, 0, 1, 0, 0.0f / 0.0f, 0.0f / 0.0f, 0, 1};
+
+ ASSERT_TRUE(memcmp(zeros, scene.projectionMatrix.m, 16 * sizeof(float)) == 0);
+
+ [scene setContentSize:CGSizeMake(kClientSize.width(), kClientSize.height())];
+ EXPECT_EQ(kClientSize.width(), scene.contentSize.width());
+ EXPECT_EQ(kClientSize.height(), scene.contentSize.height());
+
+ EXPECT_TRUE(memcmp(GLKMatrix4MakeOrtho(
+ 0.0, kClientWidth, 0.0, kClientHeight, 1.0, -1.0).m,
+ scene.projectionMatrix.m,
+ 16 * sizeof(float)) == 0);
+}
+
+TEST(SceneViewTest_Property, FrameSizeInit) {
+ SceneView* scene = [[SceneView alloc] init];
+ [scene setContentSize:CGSizeMake(kClientSize.width(), kClientSize.height())];
+
+ [scene setFrameSize:webrtc::DesktopSize(1, 1)];
+ EXPECT_EQ(1, scene.frameSize.width());
+ EXPECT_EQ(1, scene.frameSize.height());
+
+ EXPECT_EQ(0, scene.position.x);
+ EXPECT_EQ(0, scene.position.y);
+ EXPECT_EQ(1, scene.position.z);
+
+ EXPECT_FALSE(scene.anchored.left);
+ EXPECT_FALSE(scene.anchored.right);
+ EXPECT_FALSE(scene.anchored.top);
+ EXPECT_FALSE(scene.anchored.bottom);
+
+ EXPECT_EQ(0, scene.mousePosition.x());
+ EXPECT_EQ(0, scene.mousePosition.y());
+}
+
+TEST(SceneViewTest_Property, FrameSizeLarge) {
+ SceneView* scene = [[SceneView alloc] init];
+ [scene setContentSize:CGSizeMake(kClientSize.width(), kClientSize.height())];
+ [scene setFrameSize:kLarge];
+ EXPECT_EQ(kLarge.width(), scene.frameSize.width());
+ EXPECT_EQ(kLarge.height(), scene.frameSize.height());
+
+ // Screen is positioned in the lower,left corner, zoomed until the vertical
+ // fits exactly, and then centered horizontally
+ // HOST
+ // CLIENT ------------------------------------------------
+ // ------------ | |
+ // | | | |
+ // | | | |
+ // | | | |
+ // ------------ ------------------------------------------------
+ // RESULT - ONSCREEN is completely covered, with some of the HOST off screen
+ // (-.-) the mouse cursor
+ // -----------------------------------------
+ // | ONSCREEN | OFFSCREEN |
+ // | -.- | |
+ // | | |
+ // -----------------------------------------
+ float scale = static_cast<float>(kClientSize.height()) /
+ static_cast<float>(kLarge.height());
+ // vertical fits exactly
+ EXPECT_EQ(scale, scene.position.z);
+
+ // sitting on both Axis
+ EXPECT_EQ(0, scene.position.x);
+ EXPECT_EQ(0, scene.position.y);
+
+ // bound on 3 sides, not on the right
+ EXPECT_TRUE(scene.anchored.left);
+ EXPECT_FALSE(scene.anchored.right);
+ EXPECT_TRUE(scene.anchored.top);
+ EXPECT_TRUE(scene.anchored.bottom);
+
+ // mouse is off center on the left horizontal
+ EXPECT_EQ(kClientSize.width() / (scale * 2), scene.mousePosition.x());
+ // mouse is centered vertical
+ EXPECT_EQ(kLarge.height() / 2, scene.mousePosition.y());
+}
+
+TEST(SceneViewTest_Property, FrameSizeLargeInversed) {
+ SceneView* scene = [[SceneView alloc] init];
+ [scene setContentSize:CGSizeMake(kClientSize.width(), kClientSize.height())];
+ [scene setFrameSize:kLargeInversed];
+ EXPECT_EQ(kLargeInversed.width(), scene.frameSize.width());
+ EXPECT_EQ(kLargeInversed.height(), scene.frameSize.height());
+
+ // Screen is positioned in the lower,left corner, zoomed until the vertical
+ // fits exactly, and then centered horizontally
+ // HOST
+ // ---------------
+ // | |
+ // | |
+ // | |
+ // | |
+ // | |
+ // | |
+ // | |
+ // CLIENT | |
+ // ------------- | |
+ // | | | |
+ // | | | |
+ // | | | |
+ // ------------- ---------------
+ // RESULT, entire HOST is on screen
+ // (-.-) the mouse cursor, XX is black backdrop
+ // -------------
+ // |XX| |XX|
+ // |XX| -.- |XX|
+ // |XX| |XX|
+ // -------------
+ float scale = static_cast<float>(kClientSize.height()) /
+ static_cast<float>(kLargeInversed.height());
+ // Vertical fits exactly
+ EXPECT_EQ(scale, scene.position.z);
+
+ // centered
+ EXPECT_EQ(
+ (kClientSize.width() - static_cast<int>(scale * kLargeInversed.width())) /
+ 2,
+ scene.position.x);
+ // sits on Axis
+ EXPECT_EQ(0, scene.position.y);
+
+ // bound on all 4 sides
+ EXPECT_TRUE(scene.anchored.left);
+ EXPECT_TRUE(scene.anchored.right);
+ EXPECT_TRUE(scene.anchored.top);
+ EXPECT_TRUE(scene.anchored.bottom);
+
+ // mouse is in centered both vertical and horizontal
+ EXPECT_EQ(kLargeInversed.width() / 2, scene.mousePosition.x());
+ EXPECT_EQ(kLargeInversed.height() / 2, scene.mousePosition.y());
+}
+
+TEST(SceneViewTest_Property, FrameSizeSmall) {
+ SceneView* scene = [[SceneView alloc] init];
+ [scene setContentSize:CGSizeMake(kClientSize.width(), kClientSize.height())];
+ [scene setFrameSize:kSmall];
+ EXPECT_EQ(kSmall.width(), scene.frameSize.width());
+ EXPECT_EQ(kSmall.height(), scene.frameSize.height());
+
+ // Screen is positioned in the lower,left corner, zoomed until the vertical
+ // fits exactly, and then centered horizontally
+ // CLIENT
+ // ---------------------------
+ // | | HOST
+ // | | -------
+ // | | | |
+ // | | | |
+ // | | | |
+ // | | | |
+ // | | | |
+ // --------------------------- -------
+ // RESULT, entire HOST is on screen
+ // (-.-) the mouse cursor, XX is black backdrop
+ // ---------------------------
+ // |XXXXXXXXX| |XXXXXXXXX|
+ // |XXXXXXXXX| |XXXXXXXXX|
+ // |XXXXXXXXX| |XXXXXXXXX|
+ // |XXXXXXXXX| -.- |XXXXXXXXX|
+ // |XXXXXXXXX| |XXXXXXXXX|
+ // |XXXXXXXXX| |XXXXXXXXX|
+ // |XXXXXXXXX| |XXXXXXXXX|
+ // ---------------------------
+ float scale = static_cast<float>(kClientSize.height()) /
+ static_cast<float>(kSmall.height());
+ // Vertical fits exactly
+ EXPECT_EQ(scale, scene.position.z);
+
+ // centered
+ EXPECT_EQ(
+ (kClientSize.width() - static_cast<int>(scale * kSmall.width())) / 2,
+ scene.position.x);
+ // sits on Axis
+ EXPECT_EQ(0, scene.position.y);
+
+ // bound on all 4 sides
+ EXPECT_TRUE(scene.anchored.left);
+ EXPECT_TRUE(scene.anchored.right);
+ EXPECT_TRUE(scene.anchored.top);
+ EXPECT_TRUE(scene.anchored.bottom);
+
+ // mouse is in centered both vertical and horizontal
+ EXPECT_EQ((kSmall.width() / 2) - 1, // -1 for pixel rounding
+ scene.mousePosition.x());
+ EXPECT_EQ(kSmall.height() / 2, scene.mousePosition.y());
+}
+
+TEST(SceneViewTest_Property, FrameSizeSmallInversed) {
+ SceneView* scene = [[SceneView alloc] init];
+ [scene setContentSize:CGSizeMake(kClientSize.width(), kClientSize.height())];
+ [scene setFrameSize:kSmallInversed];
+ EXPECT_EQ(kSmallInversed.width(), scene.frameSize.width());
+ EXPECT_EQ(kSmallInversed.height(), scene.frameSize.height());
+
+ // Screen is positioned in the lower,left corner, zoomed until the vertical
+ // fits exactly, and then centered horizontally
+ // CLIENT
+ // ---------------------------
+ // | |
+ // | |
+ // | | HOST
+ // | | ----------------------
+ // | | | |
+ // | | | |
+ // | | | |
+ // --------------------------- ----------------------
+ // RESULT - ONSCREEN is completely covered, with some of the HOST off screen
+ // (-.-) the mouse cursor
+ // --------------------------------------------
+ // | ONSCREEN | OFFSCREEN |
+ // | | |
+ // | | |
+ // | -.- | |
+ // | | |
+ // | | |
+ // | | |
+ // --------------------------------------------
+ float scale = static_cast<float>(kClientSize.height()) /
+ static_cast<float>(kSmallInversed.height());
+ // vertical fits exactly
+ EXPECT_EQ(scale, scene.position.z);
+
+ // sitting on both Axis
+ EXPECT_EQ(0, scene.position.x);
+ EXPECT_EQ(0, scene.position.y);
+
+ // bound on 3 sides, not on the right
+ EXPECT_TRUE(scene.anchored.left);
+ EXPECT_FALSE(scene.anchored.right);
+ EXPECT_TRUE(scene.anchored.top);
+ EXPECT_TRUE(scene.anchored.bottom);
+
+ // mouse is off center on the left horizontal
+ EXPECT_EQ(kClientSize.width() / (scale * 2), scene.mousePosition.x());
+ // mouse is centered vertical
+ EXPECT_EQ(kSmallInversed.height() / 2, scene.mousePosition.y());
+}
+
+TEST_F(SceneViewTest, ContainsTouchPoint) {
+ int midWidth = kClientWidth / 2;
+ int midHeight = kClientHeight / 2;
+ // left
+ EXPECT_FALSE([scene_ containsTouchPoint:CGPointMake(-1, midHeight)]);
+ EXPECT_TRUE([scene_ containsTouchPoint:CGPointMake(0, midHeight)]);
+ // right
+ EXPECT_FALSE(
+ [scene_ containsTouchPoint:CGPointMake(kClientWidth, midHeight)]);
+ EXPECT_TRUE(
+ [scene_ containsTouchPoint:CGPointMake(kClientWidth - 1, midHeight)]);
+ // top
+ EXPECT_FALSE(
+ [scene_ containsTouchPoint:CGPointMake(midWidth, kClientHeight)]);
+ EXPECT_TRUE(
+ [scene_ containsTouchPoint:CGPointMake(midWidth, kClientHeight - 1)]);
+ // bottom
+ EXPECT_FALSE([scene_ containsTouchPoint:CGPointMake(midWidth, -1)]);
+ EXPECT_TRUE([scene_ containsTouchPoint:CGPointMake(midWidth, 0)]);
+
+ [scene_ setMarginsFromLeft:10 right:10 top:10 bottom:10];
+
+ // left
+ EXPECT_FALSE([scene_ containsTouchPoint:CGPointMake(9, midHeight)]);
+ EXPECT_TRUE([scene_ containsTouchPoint:CGPointMake(10, midHeight)]);
+ // right
+ EXPECT_FALSE(
+ [scene_ containsTouchPoint:CGPointMake(kClientWidth - 10, midHeight)]);
+ EXPECT_TRUE(
+ [scene_ containsTouchPoint:CGPointMake(kClientWidth - 11, midHeight)]);
+ // top
+ EXPECT_FALSE(
+ [scene_ containsTouchPoint:CGPointMake(midWidth, kClientHeight - 10)]);
+ EXPECT_TRUE(
+ [scene_ containsTouchPoint:CGPointMake(midWidth, kClientHeight - 11)]);
+ // bottom
+ EXPECT_FALSE([scene_ containsTouchPoint:CGPointMake(midWidth, 9)]);
+ EXPECT_TRUE([scene_ containsTouchPoint:CGPointMake(midWidth, 10)]);
+}
+
+TEST_F(SceneViewTest,
+ UpdateMousePositionAndAnchorsWithTranslationNoMovement) {
+
+ webrtc::DesktopVector originalPosition = scene_.mousePosition;
+ AnchorPosition originalAnchors = scene_.anchored;
+
+ [scene_ updateMousePositionAndAnchorsWithTranslation:CGPointMake(0, 0)
+ scale:1];
+
+ webrtc::DesktopVector newPosition = scene_.mousePosition;
+
+ EXPECT_EQ(0, abs(originalPosition.x() - newPosition.x()));
+ EXPECT_EQ(0, abs(originalPosition.y() - newPosition.y()));
+
+ EXPECT_EQ(originalAnchors.right, scene_.anchored.right);
+ EXPECT_EQ(originalAnchors.top, scene_.anchored.top);
+ EXPECT_EQ(originalAnchors.left, scene_.anchored.left);
+ EXPECT_EQ(originalAnchors.bottom, scene_.anchored.bottom);
+
+ EXPECT_FALSE(scene_.tickPanVelocity);
+}
+
+TEST_F(SceneViewTest,
+ UpdateMousePositionAndAnchorsWithTranslationTowardLeftAndTop) {
+ // Translation is in a coordinate space where (0,0) is the bottom left of the
+ // view. Mouse position in in a coordinate space where (0,0) is the top left
+ // of the view. So |y| is moved in the negative direction.
+
+ webrtc::DesktopVector originalPosition = scene_.mousePosition;
+
+ [scene_ setPanVelocity:CGPointMake(1, 1)];
+ [scene_ updateMousePositionAndAnchorsWithTranslation:CGPointMake(2, -1)
+ scale:1];
+
+ webrtc::DesktopVector newPosition = scene_.mousePosition;
+
+ // We could do these checks as a single test, for a positive vs negative
+ // difference. But this style has a clearer meaning that the position moved
+ // toward or away from the origin.
+ EXPECT_LT(newPosition.x(), originalPosition.x());
+ EXPECT_LT(newPosition.y(), originalPosition.y());
+ EXPECT_EQ(2, abs(originalPosition.x() - newPosition.x()));
+ EXPECT_EQ(1, abs(originalPosition.y() - newPosition.y()));
+
+ EXPECT_TRUE(scene_.anchored.left);
+ EXPECT_TRUE(scene_.anchored.top);
+
+ EXPECT_FALSE(scene_.anchored.right);
+ EXPECT_FALSE(scene_.anchored.bottom);
+
+ EXPECT_TRUE(scene_.tickPanVelocity);
+
+ // move much further than the bounds allow
+ [scene_ setPanVelocity:CGPointMake(1, 1)];
+ [scene_
+ updateMousePositionAndAnchorsWithTranslation:CGPointMake(10000, -10000)
+ scale:1];
+
+ newPosition = scene_.mousePosition;
+
+ EXPECT_EQ(0, newPosition.x());
+ EXPECT_EQ(0, newPosition.y());
+
+ EXPECT_TRUE(scene_.anchored.left);
+ EXPECT_TRUE(scene_.anchored.top);
+
+ EXPECT_FALSE(scene_.anchored.right);
+ EXPECT_FALSE(scene_.anchored.bottom);
+
+ EXPECT_FALSE(scene_.tickPanVelocity);
+}
+
+TEST_F(SceneViewTest,
+ UpdateMousePositionAndAnchorsWithTranslationTowardLeftAndBottom) {
+ webrtc::DesktopVector originalPosition = scene_.mousePosition;
+
+ // see notes for Test
+ // UpdateMousePositionAndAnchorsWithTranslationTowardLeftAndTop
+ [scene_ setPanVelocity:CGPointMake(1, 1)];
+ [scene_ updateMousePositionAndAnchorsWithTranslation:CGPointMake(2, 1)
+ scale:1];
+ webrtc::DesktopVector newPosition = scene_.mousePosition;
+
+ EXPECT_LT(newPosition.x(), originalPosition.x());
+ EXPECT_GT(newPosition.y(), originalPosition.y());
+ EXPECT_EQ(2, abs(originalPosition.x() - newPosition.x()));
+ EXPECT_EQ(1, abs(originalPosition.y() - newPosition.y()));
+
+ EXPECT_TRUE(scene_.anchored.left);
+ EXPECT_TRUE(scene_.anchored.bottom);
+
+ EXPECT_FALSE(scene_.anchored.right);
+ EXPECT_FALSE(scene_.anchored.top);
+
+ EXPECT_TRUE(scene_.tickPanVelocity);
+
+ [scene_ setPanVelocity:CGPointMake(1, 1)];
+ [scene_ updateMousePositionAndAnchorsWithTranslation:CGPointMake(10000, 10000)
+ scale:1];
+ newPosition = scene_.mousePosition;
+
+ EXPECT_EQ(0, newPosition.x());
+ EXPECT_EQ(scene_.frameSize.height() - 1, newPosition.y());
+
+ EXPECT_TRUE(scene_.anchored.left);
+ EXPECT_TRUE(scene_.anchored.bottom);
+
+ EXPECT_FALSE(scene_.anchored.right);
+ EXPECT_FALSE(scene_.anchored.top);
+
+ EXPECT_FALSE(scene_.tickPanVelocity);
+}
+
+TEST_F(SceneViewTest,
+ UpdateMousePositionAndAnchorsWithTranslationTowardRightAndTop) {
+ webrtc::DesktopVector originalPosition = scene_.mousePosition;
+
+ // see notes for Test
+ // UpdateMousePositionAndAnchorsWithTranslationTowardLeftAndTop
+
+ // When moving to the right the mouse remains centered since the horizontal
+ // display space is larger than the view space
+ [scene_ setPanVelocity:CGPointMake(1, 1)];
+ [scene_ updateMousePositionAndAnchorsWithTranslation:CGPointMake(-2, -1)
+ scale:1];
+ webrtc::DesktopVector newPosition = scene_.mousePosition;
+
+ EXPECT_LT(newPosition.y(), originalPosition.y());
+ EXPECT_EQ(0, abs(originalPosition.x() - newPosition.x()));
+ EXPECT_EQ(1, abs(originalPosition.y() - newPosition.y()));
+
+ EXPECT_TRUE(scene_.anchored.top);
+
+ EXPECT_FALSE(scene_.anchored.left);
+ EXPECT_FALSE(scene_.anchored.right);
+ EXPECT_FALSE(scene_.anchored.bottom);
+
+ EXPECT_TRUE(scene_.tickPanVelocity);
+
+ [scene_ setPanVelocity:CGPointMake(1, 1)];
+ [scene_
+ updateMousePositionAndAnchorsWithTranslation:CGPointMake(-10000, -10000)
+ scale:1];
+ newPosition = scene_.mousePosition;
+
+ EXPECT_EQ(scene_.frameSize.width() - 1, newPosition.x());
+ EXPECT_EQ(0, newPosition.y());
+
+ EXPECT_TRUE(scene_.anchored.right);
+ EXPECT_TRUE(scene_.anchored.top);
+
+ EXPECT_FALSE(scene_.anchored.left);
+ EXPECT_FALSE(scene_.anchored.bottom);
+
+ EXPECT_FALSE(scene_.tickPanVelocity);
+}
+
+TEST_F(SceneViewTest,
+ UpdateMousePositionAndAnchorsWithTranslationTowardRightAndBottom) {
+ webrtc::DesktopVector originalPosition = scene_.mousePosition;
+
+ // see notes for Test
+ // UpdateMousePositionAndAnchorsWithTranslationTowardLeftAndTop
+
+ // When moving to the right the mouse remains centered since the horizontal
+ // display space is larger than the view space
+ [scene_ setPanVelocity:CGPointMake(1, 1)];
+ [scene_ updateMousePositionAndAnchorsWithTranslation:CGPointMake(-2, 1)
+ scale:1];
+ webrtc::DesktopVector newPosition = scene_.mousePosition;
+
+ EXPECT_GT(newPosition.y(), originalPosition.y());
+ EXPECT_EQ(0, abs(originalPosition.x() - newPosition.x()));
+ EXPECT_EQ(1, abs(originalPosition.y() - newPosition.y()));
+
+ EXPECT_TRUE(scene_.anchored.bottom);
+
+ EXPECT_FALSE(scene_.anchored.left);
+ EXPECT_FALSE(scene_.anchored.right);
+ EXPECT_FALSE(scene_.anchored.top);
+
+ EXPECT_TRUE(scene_.tickPanVelocity);
+
+ [scene_ setPanVelocity:CGPointMake(1, 1)];
+ [scene_
+ updateMousePositionAndAnchorsWithTranslation:CGPointMake(-10000, 10000)
+ scale:1];
+ newPosition = scene_.mousePosition;
+
+ EXPECT_EQ(scene_.frameSize.width() - 1, newPosition.x());
+ EXPECT_EQ(scene_.frameSize.height() - 1, newPosition.y());
+
+ EXPECT_TRUE(scene_.anchored.right);
+ EXPECT_TRUE(scene_.anchored.bottom);
+
+ EXPECT_FALSE(scene_.anchored.left);
+ EXPECT_FALSE(scene_.anchored.top);
+
+ EXPECT_FALSE(scene_.tickPanVelocity);
+}
+
+TEST(SceneViewTest_Static, PositionDeltaFromScaling) {
+
+ // Legend:
+ // * anchored point or end point
+ // | unanchored endpoint
+ // - onscreen
+ // # offscreen
+
+ // *---|
+ // *-------|
+ EXPECT_EQ(
+ 0,
+ [SceneView positionDeltaFromScaling:2.0F position:0 length:100 anchor:0]);
+ // *---|
+ // *-|
+ EXPECT_EQ(
+ 0,
+ [SceneView positionDeltaFromScaling:0.5F position:0 length:100 anchor:0]);
+ // |---*
+ // |-------*
+ EXPECT_EQ(100,
+ [SceneView positionDeltaFromScaling:2.0F
+ position:0
+ length:100
+ anchor:100]);
+ // |----*
+ // |--*
+ EXPECT_EQ(-50,
+ [SceneView positionDeltaFromScaling:0.5F
+ position:0
+ length:100
+ anchor:100]);
+ // |*---|
+ // |-*-------|
+ EXPECT_EQ(25,
+ [SceneView positionDeltaFromScaling:2.0F
+ position:0
+ length:100
+ anchor:25]);
+ // |-*--|
+ // |*-|
+ EXPECT_EQ(-12.5,
+ [SceneView positionDeltaFromScaling:0.5F
+ position:0
+ length:100
+ anchor:25]);
+ // |---*|
+ // |------*-|
+ EXPECT_EQ(75,
+ [SceneView positionDeltaFromScaling:2.0F
+ position:0
+ length:100
+ anchor:75]);
+ // |--*-|
+ // |-*|
+ EXPECT_EQ(-37.5,
+ [SceneView positionDeltaFromScaling:0.5F
+ position:0
+ length:100
+ anchor:75]);
+ // |-*-|
+ // |---*---|
+ EXPECT_EQ(50,
+ [SceneView positionDeltaFromScaling:2.0F
+ position:0
+ length:100
+ anchor:50]);
+ // |--*--|
+ // |*|
+ EXPECT_EQ(-25,
+ [SceneView positionDeltaFromScaling:0.5F
+ position:0
+ length:100
+ anchor:50]);
+ //////////////////////////////////
+ // Change position to 50, anchor is relatively the same
+ //////////////////////////////////
+ EXPECT_EQ(0,
+ [SceneView positionDeltaFromScaling:2.0F
+ position:50
+ length:100
+ anchor:50]);
+ EXPECT_EQ(0,
+ [SceneView positionDeltaFromScaling:0.5F
+ position:50
+ length:100
+ anchor:50]);
+ EXPECT_EQ(100,
+ [SceneView positionDeltaFromScaling:2.0F
+ position:50
+ length:100
+ anchor:150]);
+ EXPECT_EQ(-50,
+ [SceneView positionDeltaFromScaling:0.5F
+ position:50
+ length:100
+ anchor:150]);
+ EXPECT_EQ(25,
+ [SceneView positionDeltaFromScaling:2.0F
+ position:50
+ length:100
+ anchor:75]);
+ EXPECT_EQ(-12.5,
+ [SceneView positionDeltaFromScaling:0.5F
+ position:50
+ length:100
+ anchor:75]);
+ EXPECT_EQ(75,
+ [SceneView positionDeltaFromScaling:2.0F
+ position:50
+ length:100
+ anchor:125]);
+ EXPECT_EQ(-37.5,
+ [SceneView positionDeltaFromScaling:0.5F
+ position:50
+ length:100
+ anchor:125]);
+ EXPECT_EQ(50,
+ [SceneView positionDeltaFromScaling:2.0F
+ position:50
+ length:100
+ anchor:100]);
+ EXPECT_EQ(-25,
+ [SceneView positionDeltaFromScaling:0.5F
+ position:50
+ length:100
+ anchor:100]);
+
+ //////////////////////////////////
+ // Change position to -50, length to 200, anchor is relatively the same
+ //////////////////////////////////
+ EXPECT_EQ(0,
+ [SceneView positionDeltaFromScaling:2.0F
+ position:-50
+ length:200
+ anchor:-50]);
+ EXPECT_EQ(0,
+ [SceneView positionDeltaFromScaling:0.5F
+ position:-50
+ length:200
+ anchor:-50]);
+ EXPECT_EQ(200,
+ [SceneView positionDeltaFromScaling:2.0F
+ position:-50
+ length:200
+ anchor:150]);
+ EXPECT_EQ(-100,
+ [SceneView positionDeltaFromScaling:0.5F
+ position:-50
+ length:200
+ anchor:150]);
+ EXPECT_EQ(50,
+ [SceneView positionDeltaFromScaling:2.0F
+ position:-50
+ length:200
+ anchor:0]);
+ EXPECT_EQ(-25,
+ [SceneView positionDeltaFromScaling:0.5F
+ position:-50
+ length:200
+ anchor:0]);
+ EXPECT_EQ(150,
+ [SceneView positionDeltaFromScaling:2.0F
+ position:-50
+ length:200
+ anchor:100]);
+ EXPECT_EQ(-75,
+ [SceneView positionDeltaFromScaling:0.5F
+ position:-50
+ length:200
+ anchor:100]);
+ EXPECT_EQ(100,
+ [SceneView positionDeltaFromScaling:2.0F
+ position:-50
+ length:200
+ anchor:50]);
+ EXPECT_EQ(-50,
+ [SceneView positionDeltaFromScaling:0.5F
+ position:-50
+ length:200
+ anchor:50]);
+}
+
+TEST(SceneViewTest_Static, PositionDeltaFromTranslation) {
+ // Anchored on both sides. Center it by using 1/2 the free space, offset by
+ // the current position
+ EXPECT_EQ(50,
+ [SceneView positionDeltaFromTranslation:0
+ position:0
+ freeSpace:100
+ scaleingPositionDelta:0
+ isAnchoredLow:YES
+ isAnchoredHigh:YES]);
+ EXPECT_EQ(50,
+ [SceneView positionDeltaFromTranslation:100
+ position:0
+ freeSpace:100
+ scaleingPositionDelta:0
+ isAnchoredLow:YES
+ isAnchoredHigh:YES]);
+ EXPECT_EQ(-50,
+ [SceneView positionDeltaFromTranslation:0
+ position:100
+ freeSpace:100
+ scaleingPositionDelta:0
+ isAnchoredLow:YES
+ isAnchoredHigh:YES]);
+ EXPECT_EQ(50,
+ [SceneView positionDeltaFromTranslation:0
+ position:0
+ freeSpace:100
+ scaleingPositionDelta:100
+ isAnchoredLow:YES
+ isAnchoredHigh:YES]);
+ EXPECT_EQ(100,
+ [SceneView positionDeltaFromTranslation:0
+ position:0
+ freeSpace:200
+ scaleingPositionDelta:0
+ isAnchoredLow:YES
+ isAnchoredHigh:YES]);
+
+ // Anchored only on the left. Don't move it
+ EXPECT_EQ(0,
+ [SceneView positionDeltaFromTranslation:0
+ position:0
+ freeSpace:100
+ scaleingPositionDelta:0
+ isAnchoredLow:YES
+ isAnchoredHigh:NO]);
+ EXPECT_EQ(0,
+ [SceneView positionDeltaFromTranslation:100
+ position:0
+ freeSpace:100
+ scaleingPositionDelta:0
+ isAnchoredLow:YES
+ isAnchoredHigh:NO]);
+ EXPECT_EQ(0,
+ [SceneView positionDeltaFromTranslation:0
+ position:100
+ freeSpace:100
+ scaleingPositionDelta:0
+ isAnchoredLow:YES
+ isAnchoredHigh:NO]);
+ EXPECT_EQ(0,
+ [SceneView positionDeltaFromTranslation:0
+ position:0
+ freeSpace:200
+ scaleingPositionDelta:100
+ isAnchoredLow:YES
+ isAnchoredHigh:NO]);
+ EXPECT_EQ(0,
+ [SceneView positionDeltaFromTranslation:0
+ position:0
+ freeSpace:200
+ scaleingPositionDelta:0
+ isAnchoredLow:YES
+ isAnchoredHigh:NO]);
+ // Anchored only on the right. Move by the scaling delta
+ EXPECT_EQ(25,
+ [SceneView positionDeltaFromTranslation:0
+ position:0
+ freeSpace:100
+ scaleingPositionDelta:25
+ isAnchoredLow:NO
+ isAnchoredHigh:YES]);
+ EXPECT_EQ(50,
+ [SceneView positionDeltaFromTranslation:100
+ position:0
+ freeSpace:100
+ scaleingPositionDelta:50
+ isAnchoredLow:NO
+ isAnchoredHigh:YES]);
+ EXPECT_EQ(75,
+ [SceneView positionDeltaFromTranslation:0
+ position:100
+ freeSpace:100
+ scaleingPositionDelta:75
+ isAnchoredLow:NO
+ isAnchoredHigh:YES]);
+ EXPECT_EQ(100,
+ [SceneView positionDeltaFromTranslation:0
+ position:0
+ freeSpace:100
+ scaleingPositionDelta:100
+ isAnchoredLow:NO
+ isAnchoredHigh:YES]);
+ EXPECT_EQ(125,
+ [SceneView positionDeltaFromTranslation:0
+ position:0
+ freeSpace:200
+ scaleingPositionDelta:125
+ isAnchoredLow:NO
+ isAnchoredHigh:YES]);
+ // Not anchored, translate and move by the scaling delta
+ EXPECT_EQ(0,
+ [SceneView positionDeltaFromTranslation:0
+ position:0
+ freeSpace:100
+ scaleingPositionDelta:0
+ isAnchoredLow:NO
+ isAnchoredHigh:NO]);
+ EXPECT_EQ(25,
+ [SceneView positionDeltaFromTranslation:25
+ position:0
+ freeSpace:100
+ scaleingPositionDelta:0
+ isAnchoredLow:NO
+ isAnchoredHigh:NO]);
+ EXPECT_EQ(50,
+ [SceneView positionDeltaFromTranslation:50
+ position:100
+ freeSpace:100
+ scaleingPositionDelta:0
+ isAnchoredLow:NO
+ isAnchoredHigh:NO]);
+ EXPECT_EQ(175,
+ [SceneView positionDeltaFromTranslation:75
+ position:0
+ freeSpace:100
+ scaleingPositionDelta:100
+ isAnchoredLow:NO
+ isAnchoredHigh:NO]);
+ EXPECT_EQ(100,
+ [SceneView positionDeltaFromTranslation:100
+ position:0
+ freeSpace:200
+ scaleingPositionDelta:0
+ isAnchoredLow:NO
+ isAnchoredHigh:NO]);
+}
+
+TEST(SceneViewTest_Static, BoundDeltaFromPosition) {
+ // Entire entity fits in our view, lower bound is not less than the
+ // upperBound. The delta is bounded to the lowerBound.
+ EXPECT_EQ(200,
+ [SceneView boundDeltaFromPosition:0
+ delta:0
+ lowerBound:200
+ upperBound:100]);
+ EXPECT_EQ(100,
+ [SceneView boundDeltaFromPosition:100
+ delta:0
+ lowerBound:200
+ upperBound:100]);
+ EXPECT_EQ(200,
+ [SceneView boundDeltaFromPosition:0
+ delta:100
+ lowerBound:200
+ upperBound:100]);
+ EXPECT_EQ(150,
+ [SceneView boundDeltaFromPosition:50
+ delta:100
+ lowerBound:200
+ upperBound:200]);
+ // Entity does not fit in our view. The result would be out of bounds on the
+ // high bound. The delta is bounded to the upper bound and the delta from the
+ // position is returned.
+ EXPECT_EQ(100,
+ [SceneView boundDeltaFromPosition:0
+ delta:1000
+ lowerBound:0
+ upperBound:100]);
+ EXPECT_EQ(99,
+ [SceneView boundDeltaFromPosition:1
+ delta:1000
+ lowerBound:0
+ upperBound:100]);
+ EXPECT_EQ(-50,
+ [SceneView boundDeltaFromPosition:150
+ delta:1000
+ lowerBound:50
+ upperBound:100]);
+ EXPECT_EQ(100,
+ [SceneView boundDeltaFromPosition:100
+ delta:1000
+ lowerBound:0
+ upperBound:200]);
+ // Entity does not fit in our view. The result would be out of bounds on the
+ // low bound. The delta is bounded to the lower bound and the delta from the
+ // position is returned.
+ EXPECT_EQ(0,
+ [SceneView boundDeltaFromPosition:0
+ delta:-1000
+ lowerBound:0
+ upperBound:100]);
+ EXPECT_EQ(-20,
+ [SceneView boundDeltaFromPosition:20
+ delta:-1000
+ lowerBound:0
+ upperBound:100]);
+ EXPECT_EQ(21,
+ [SceneView boundDeltaFromPosition:29
+ delta:-1000
+ lowerBound:50
+ upperBound:100]);
+ EXPECT_EQ(1,
+ [SceneView boundDeltaFromPosition:-1
+ delta:-1000
+ lowerBound:0
+ upperBound:200]);
+ // Entity does not fit in our view. The result is in bounds. The delta is
+ // returned unchanged.
+ EXPECT_EQ(50,
+ [SceneView boundDeltaFromPosition:0
+ delta:50
+ lowerBound:0
+ upperBound:100]);
+ EXPECT_EQ(-10,
+ [SceneView boundDeltaFromPosition:20
+ delta:-10
+ lowerBound:0
+ upperBound:100]);
+ EXPECT_EQ(31,
+ [SceneView boundDeltaFromPosition:29
+ delta:31
+ lowerBound:50
+ upperBound:100]);
+ EXPECT_EQ(50,
+ [SceneView boundDeltaFromPosition:100
+ delta:50
+ lowerBound:0
+ upperBound:200]);
+}
+
+TEST(SceneViewTest_Static, BoundMouseGivenNextPosition) {
+ // Mouse would move off screen in the negative
+ EXPECT_EQ(0,
+ [SceneView boundMouseGivenNextPosition:-1
+ maxPosition:50
+ centerPosition:2
+ isAnchoredLow:YES
+ isAnchoredHigh:YES]);
+ EXPECT_EQ(0,
+ [SceneView boundMouseGivenNextPosition:-1
+ maxPosition:25
+ centerPosition:99
+ isAnchoredLow:NO
+ isAnchoredHigh:NO]);
+ EXPECT_EQ(0,
+ [SceneView boundMouseGivenNextPosition:-11
+ maxPosition:0
+ centerPosition:-52
+ isAnchoredLow:YES
+ isAnchoredHigh:YES]);
+ EXPECT_EQ(0,
+ [SceneView boundMouseGivenNextPosition:-11
+ maxPosition:-100
+ centerPosition:44
+ isAnchoredLow:NO
+ isAnchoredHigh:NO]);
+ EXPECT_EQ(0,
+ [SceneView boundMouseGivenNextPosition:-1
+ maxPosition:50
+ centerPosition:-20
+ isAnchoredLow:YES
+ isAnchoredHigh:YES]);
+
+ // Mouse would move off screen in the positive
+ EXPECT_EQ(49,
+ [SceneView boundMouseGivenNextPosition:50
+ maxPosition:50
+ centerPosition:2
+ isAnchoredLow:YES
+ isAnchoredHigh:YES]);
+ EXPECT_EQ(24,
+ [SceneView boundMouseGivenNextPosition:26
+ maxPosition:25
+ centerPosition:99
+ isAnchoredLow:NO
+ isAnchoredHigh:NO]);
+ EXPECT_EQ(-1,
+ [SceneView boundMouseGivenNextPosition:1
+ maxPosition:0
+ centerPosition:-52
+ isAnchoredLow:YES
+ isAnchoredHigh:YES]);
+ EXPECT_EQ(-101,
+ [SceneView boundMouseGivenNextPosition:0
+ maxPosition:-100
+ centerPosition:44
+ isAnchoredLow:NO
+ isAnchoredHigh:NO]);
+ EXPECT_EQ(49,
+ [SceneView boundMouseGivenNextPosition:60
+ maxPosition:50
+ centerPosition:-20
+ isAnchoredLow:YES
+ isAnchoredHigh:YES]);
+
+ // Mouse is not out of bounds, and not anchored. The Center is returned.
+ EXPECT_EQ(2,
+ [SceneView boundMouseGivenNextPosition:0
+ maxPosition:100
+ centerPosition:2
+ isAnchoredLow:NO
+ isAnchoredHigh:NO]);
+ EXPECT_EQ(99,
+ [SceneView boundMouseGivenNextPosition:25
+ maxPosition:100
+ centerPosition:99
+ isAnchoredLow:NO
+ isAnchoredHigh:NO]);
+ EXPECT_EQ(-52,
+ [SceneView boundMouseGivenNextPosition:99
+ maxPosition:100
+ centerPosition:-52
+ isAnchoredLow:NO
+ isAnchoredHigh:NO]);
+ EXPECT_EQ(44,
+ [SceneView boundMouseGivenNextPosition:120
+ maxPosition:200
+ centerPosition:44
+ isAnchoredLow:NO
+ isAnchoredHigh:NO]);
+ EXPECT_EQ(-20,
+ [SceneView boundMouseGivenNextPosition:180
+ maxPosition:200
+ centerPosition:-20
+ isAnchoredLow:NO
+ isAnchoredHigh:NO]);
+
+ // Mouse is not out of bounds, and anchored. The position closest
+ // to the anchor is returned.
+ EXPECT_EQ(0,
+ [SceneView boundMouseGivenNextPosition:0
+ maxPosition:100
+ centerPosition:2
+ isAnchoredLow:YES
+ isAnchoredHigh:NO]);
+ EXPECT_EQ(25,
+ [SceneView boundMouseGivenNextPosition:25
+ maxPosition:100
+ centerPosition:99
+ isAnchoredLow:YES
+ isAnchoredHigh:NO]);
+ EXPECT_EQ(-52,
+ [SceneView boundMouseGivenNextPosition:99
+ maxPosition:100
+ centerPosition:-52
+ isAnchoredLow:YES
+ isAnchoredHigh:NO]);
+ EXPECT_EQ(44,
+ [SceneView boundMouseGivenNextPosition:120
+ maxPosition:200
+ centerPosition:44
+ isAnchoredLow:YES
+ isAnchoredHigh:NO]);
+ EXPECT_EQ(-20,
+ [SceneView boundMouseGivenNextPosition:180
+ maxPosition:200
+ centerPosition:-20
+ isAnchoredLow:YES
+ isAnchoredHigh:NO]);
+ EXPECT_EQ(2,
+ [SceneView boundMouseGivenNextPosition:0
+ maxPosition:100
+ centerPosition:2
+ isAnchoredLow:NO
+ isAnchoredHigh:YES]);
+ EXPECT_EQ(99,
+ [SceneView boundMouseGivenNextPosition:25
+ maxPosition:100
+ centerPosition:99
+ isAnchoredLow:NO
+ isAnchoredHigh:YES]);
+ EXPECT_EQ(99,
+ [SceneView boundMouseGivenNextPosition:99
+ maxPosition:100
+ centerPosition:-52
+ isAnchoredLow:NO
+ isAnchoredHigh:YES]);
+ EXPECT_EQ(120,
+ [SceneView boundMouseGivenNextPosition:120
+ maxPosition:200
+ centerPosition:44
+ isAnchoredLow:NO
+ isAnchoredHigh:YES]);
+ EXPECT_EQ(180,
+ [SceneView boundMouseGivenNextPosition:180
+ maxPosition:200
+ centerPosition:-20
+ isAnchoredLow:NO
+ isAnchoredHigh:YES]);
+ EXPECT_EQ(0,
+ [SceneView boundMouseGivenNextPosition:0
+ maxPosition:100
+ centerPosition:2
+ isAnchoredLow:YES
+ isAnchoredHigh:YES]);
+ EXPECT_EQ(25,
+ [SceneView boundMouseGivenNextPosition:25
+ maxPosition:100
+ centerPosition:99
+ isAnchoredLow:YES
+ isAnchoredHigh:YES]);
+ EXPECT_EQ(99,
+ [SceneView boundMouseGivenNextPosition:99
+ maxPosition:100
+ centerPosition:-52
+ isAnchoredLow:YES
+ isAnchoredHigh:YES]);
+ EXPECT_EQ(120,
+ [SceneView boundMouseGivenNextPosition:120
+ maxPosition:200
+ centerPosition:44
+ isAnchoredLow:YES
+ isAnchoredHigh:YES]);
+ EXPECT_EQ(180,
+ [SceneView boundMouseGivenNextPosition:180
+ maxPosition:200
+ centerPosition:-20
+ isAnchoredLow:YES
+ isAnchoredHigh:YES]);
+}
+
+TEST(SceneViewTest_Static, BoundVelocity) {
+ // Outside bounds of the axis
+ EXPECT_EQ(0, [SceneView boundVelocity:5.0f axisLength:100 mousePosition:0]);
+ EXPECT_EQ(0, [SceneView boundVelocity:5.0f axisLength:100 mousePosition:99]);
+ EXPECT_EQ(0, [SceneView boundVelocity:5.0f axisLength:200 mousePosition:200]);
+ // Not outside bounds of the axis
+ EXPECT_EQ(5.0f,
+ [SceneView boundVelocity:5.0f axisLength:100 mousePosition:1]);
+ EXPECT_EQ(5.0f,
+ [SceneView boundVelocity:5.0f axisLength:100 mousePosition:98]);
+ EXPECT_EQ(5.0f,
+ [SceneView boundVelocity:5.0f axisLength:200 mousePosition:100]);
+}
+
+TEST_F(SceneViewTest, TickPanVelocity) {
+ // We are in the large frame, which can pan left and right but not up and
+ // down. Start by resizing it to allow panning up and down.
+
+ [scene_ panAndZoom:CGPointMake(0, 0) scaleBy:2.0f];
+
+ // Going up and right
+ [scene_ setPanVelocity:CGPointMake(1000, 1000)];
+ [scene_ tickPanVelocity];
+
+ webrtc::DesktopVector pos = scene_.mousePosition;
+ int loopLimit = 0;
+ bool didMove = false;
+ bool inMotion = true;
+
+ while (inMotion && loopLimit < 100) {
+ inMotion = [scene_ tickPanVelocity];
+ if (inMotion) {
+ ASSERT_TRUE(pos.x() <= scene_.mousePosition.x()) << " after " << loopLimit
+ << " iterations.";
+ ASSERT_TRUE(pos.y() <= scene_.mousePosition.y()) << " after " << loopLimit
+ << " iterations.";
+ didMove = true;
+ }
+ pos = scene_.mousePosition;
+ loopLimit++;
+ }
+
+ EXPECT_LT(1, loopLimit);
+ EXPECT_TRUE(!inMotion);
+ EXPECT_TRUE(didMove);
+
+ // Going down and left
+ [scene_ setPanVelocity:CGPointMake(-1000, -1000)];
+ [scene_ tickPanVelocity];
+
+ pos = scene_.mousePosition;
+ loopLimit = 0;
+ didMove = false;
+ inMotion = true;
+
+ while (inMotion && loopLimit < 100) {
+ inMotion = [scene_ tickPanVelocity];
+ if (inMotion) {
+ ASSERT_TRUE(pos.x() >= scene_.mousePosition.x()) << " after " << loopLimit
+ << " iterations.";
+ ASSERT_TRUE(pos.y() >= scene_.mousePosition.y()) << " after " << loopLimit
+ << " iterations.";
+ didMove = true;
+ }
+ pos = scene_.mousePosition;
+ loopLimit++;
+ }
+
+ EXPECT_LT(1, loopLimit);
+ EXPECT_TRUE(!inMotion);
+ EXPECT_TRUE(didMove);
+}
+
+} // namespace remoting \ No newline at end of file
diff --git a/remoting/ios/utility.h b/remoting/ios/utility.h
new file mode 100644
index 0000000..ac6a2a4
--- /dev/null
+++ b/remoting/ios/utility.h
@@ -0,0 +1,64 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef REMOTING_IOS_UTILITY_H_
+#define REMOTING_IOS_UTILITY_H_
+
+#import <Foundation/Foundation.h>
+
+#include "base/memory/scoped_ptr.h"
+#include "third_party/webrtc/modules/desktop_capture/desktop_frame.h"
+#include "third_party/webrtc/modules/desktop_capture/desktop_geometry.h"
+
+#import "remoting/ios/bridge/host_proxy.h"
+
+typedef struct {
+ scoped_ptr<webrtc::BasicDesktopFrame> image;
+ scoped_ptr<webrtc::DesktopVector> offset;
+} GLRegion;
+
+@interface Utility : NSObject
+
++ (BOOL)isPad;
+
++ (BOOL)isInLandscapeMode;
+
+// Return the resolution in respect to orientation
++ (CGSize)getOrientatedSize:(CGSize)size
+ shouldWidthBeLongestSide:(BOOL)shouldWidthBeLongestSide;
+
++ (void)showAlert:(NSString*)title message:(NSString*)message;
+
++ (NSString*)appVersionNumberDisplayString;
+
+// GL Binding Context requires some specific flags for the type of textures
+// being drawn
++ (void)bindTextureForIOS:(GLuint)glName;
+
+// Sometimes its necessary to read gl errors. This is called in various places
+// while working in the GL Context
++ (void)logGLErrorCode:(NSString*)funcName;
+
++ (void)drawSubRectToGLFromRectOfSize:(const webrtc::DesktopSize&)rectSize
+ subRect:(const webrtc::DesktopRect&)subRect
+ data:(const uint8_t*)data;
+
++ (void)moveMouse:(HostProxy*)controller at:(const webrtc::DesktopVector&)point;
+
++ (void)leftClickOn:(HostProxy*)controller
+ at:(const webrtc::DesktopVector&)point;
+
++ (void)middleClickOn:(HostProxy*)controller
+ at:(const webrtc::DesktopVector&)point;
+
++ (void)rightClickOn:(HostProxy*)controller
+ at:(const webrtc::DesktopVector&)point;
+
++ (void)mouseScroll:(HostProxy*)controller
+ at:(const webrtc::DesktopVector&)point
+ delta:(const webrtc::DesktopVector&)delta;
+
+@end
+
+#endif // REMOTING_IOS_UTILITY_H_
diff --git a/remoting/ios/utility.mm b/remoting/ios/utility.mm
new file mode 100644
index 0000000..2122997
--- /dev/null
+++ b/remoting/ios/utility.mm
@@ -0,0 +1,150 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "Utility.h"
+
+@implementation Utility
+
++ (BOOL)isPad {
+ return (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad);
+}
+
++ (BOOL)isInLandscapeMode {
+ UIInterfaceOrientation orientation =
+ [UIApplication sharedApplication].statusBarOrientation;
+
+ if ((orientation == UIInterfaceOrientationLandscapeLeft) ||
+ (orientation == UIInterfaceOrientationLandscapeRight)) {
+ return YES;
+ }
+ return NO;
+}
+
++ (CGSize)getOrientatedSize:(CGSize)size
+ shouldWidthBeLongestSide:(BOOL)shouldWidthBeLongestSide {
+ if (shouldWidthBeLongestSide && (size.height > size.width)) {
+ return CGSizeMake(size.height, size.width);
+ }
+ return size;
+}
+
++ (void)showAlert:(NSString*)title message:(NSString*)message {
+ UIAlertView* alert;
+ alert = [[UIAlertView alloc] init];
+ alert.title = title;
+ alert.message = message;
+ alert.delegate = nil;
+ [alert addButtonWithTitle:@"OK"];
+ [alert show];
+}
+
++ (NSString*)appVersionNumberDisplayString {
+ NSDictionary* infoDictionary = [[NSBundle mainBundle] infoDictionary];
+
+ NSString* majorVersion =
+ [infoDictionary objectForKey:@"CFBundleShortVersionString"];
+ NSString* minorVersion = [infoDictionary objectForKey:@"CFBundleVersion"];
+
+ return [NSString
+ stringWithFormat:@"Version %@ (%@)", majorVersion, minorVersion];
+}
+
++ (void)bindTextureForIOS:(GLuint)glName {
+ glBindTexture(GL_TEXTURE_2D, glName);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+}
+
++ (void)logGLErrorCode:(NSString*)funcName {
+ GLenum errorCode = 1;
+
+ while (errorCode != 0) {
+ errorCode = glGetError(); // I don't know why this is returning an error
+ // on the first call to this function, but if I
+ // don't read it, then stuff doesn't work...
+#if DEBUG
+ if (errorCode != 0) {
+ NSLog(@"glerror in %@: %X", funcName, errorCode);
+ }
+#endif // DEBUG
+ }
+}
+
++ (void)drawSubRectToGLFromRectOfSize:(const webrtc::DesktopSize&)rectSize
+ subRect:(const webrtc::DesktopRect&)subRect
+ data:(const uint8_t*)data {
+ DCHECK(rectSize.width() >= subRect.width());
+ DCHECK(rectSize.height() >= subRect.height());
+ DCHECK(rectSize.width() >= (subRect.left() + subRect.width()));
+ DCHECK(rectSize.height() >= (subRect.top() + subRect.height()));
+ DCHECK(data);
+
+ glTexSubImage2D(GL_TEXTURE_2D,
+ 0,
+ subRect.left(),
+ subRect.top(),
+ subRect.width(),
+ subRect.height(),
+ GL_RGBA,
+ GL_UNSIGNED_BYTE,
+ data);
+}
+
++ (void)moveMouse:(HostProxy*)controller
+ at:(const webrtc::DesktopVector&)point {
+ [controller mouseAction:point
+ wheelDelta:webrtc::DesktopVector(0, 0)
+ whichButton:0
+ buttonDown:NO];
+}
+
++ (void)leftClickOn:(HostProxy*)controller
+ at:(const webrtc::DesktopVector&)point {
+ [controller mouseAction:point
+ wheelDelta:webrtc::DesktopVector(0, 0)
+ whichButton:1
+ buttonDown:YES];
+ [controller mouseAction:point
+ wheelDelta:webrtc::DesktopVector(0, 0)
+ whichButton:1
+ buttonDown:NO];
+}
+
++ (void)middleClickOn:(HostProxy*)controller
+ at:(const webrtc::DesktopVector&)point {
+ [controller mouseAction:point
+ wheelDelta:webrtc::DesktopVector(0, 0)
+ whichButton:2
+ buttonDown:YES];
+ [controller mouseAction:point
+ wheelDelta:webrtc::DesktopVector(0, 0)
+ whichButton:2
+ buttonDown:NO];
+}
+
++ (void)rightClickOn:(HostProxy*)controller
+ at:(const webrtc::DesktopVector&)point {
+ [controller mouseAction:point
+ wheelDelta:webrtc::DesktopVector(0, 0)
+ whichButton:3
+ buttonDown:YES];
+ [controller mouseAction:point
+ wheelDelta:webrtc::DesktopVector(0, 0)
+ whichButton:3
+ buttonDown:NO];
+}
+
++ (void)mouseScroll:(HostProxy*)controller
+ at:(const webrtc::DesktopVector&)point
+ delta:(const webrtc::DesktopVector&)delta {
+ [controller mouseAction:point wheelDelta:delta whichButton:0 buttonDown:NO];
+}
+
+@end \ No newline at end of file