diff options
author | dcaiafa@chromium.org <dcaiafa@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-05-13 17:50:52 +0000 |
---|---|---|
committer | dcaiafa@chromium.org <dcaiafa@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-05-13 17:50:52 +0000 |
commit | 55e5286f8204297aa968ef4621eeb36dec24de80 (patch) | |
tree | 24ed0bff0888adffec464c54d549fad6ec10ae21 /remoting | |
parent | 74c6fd3e872933c7e9f99150438e243d77cfcfc1 (diff) | |
download | chromium_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')
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="<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="<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 |