├── .gitignore ├── AUTHORS ├── CONTRIBUTING ├── CONTRIBUTORS ├── LICENSE ├── OVERVIEW.md ├── README.md ├── docs ├── api.html ├── css │ └── style.css └── img │ └── chord_UI.png └── viewer ├── background.js ├── css └── style.css ├── device ├── deviceSpec.json └── deviceTemplate.html ├── img ├── app │ ├── calendar.png │ ├── gmail.png │ ├── maps.png │ └── photos.png ├── close.png ├── device │ ├── nexus5.png │ ├── nexus9.png │ └── wear.png ├── down.png ├── folder.png ├── folder_open.png ├── icon │ ├── Calendar.png │ ├── Gmail.png │ ├── maps.png │ ├── next.png │ ├── photos.png │ ├── play.png │ └── prev.png ├── left.png ├── listen.png ├── logo │ ├── chord-128.png │ └── chord-16.png ├── phonecall.jpg ├── right.png ├── rotateCCW.png ├── rotateCW.png ├── run.png ├── sample.png ├── shake.png ├── talk.png └── up.png ├── index.html ├── js ├── chord.js └── viewer.js ├── manifest.json ├── samples ├── bump │ ├── audio │ │ └── success.mp3 │ └── service.js ├── launchPad │ ├── panel.html │ └── service.js ├── oneAppLauncher │ └── service.js ├── photoLauncher │ ├── photo.jpg │ └── service.js └── slideshow │ ├── controller.html │ ├── img │ ├── 1.jpg │ ├── 2.jpg │ ├── 3.jpg │ └── 4.jpg │ └── service.js └── third_party ├── bootstrap ├── LICENSE └── bootstrap.min.css └── jQuery ├── LICENSE.txt └── jquery.min.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the official list of the Chord authors for copyright purposes. 2 | 3 | Google Inc. 4 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | Want to contribute? Great! First, read this page (including the small print at 2 | the end). 3 | 4 | ### Before you contribute Before we can use your code, you must sign the [Google 5 | Individual Contributor License Agreement] 6 | (https://cla.developers.google.com/about/google-individual) (CLA), which you can 7 | do online. The CLA is necessary mainly because you own the copyright to your 8 | changes, even after your contribution becomes part of our codebase, so we need 9 | your permission to use and distribute your code. We also need to be sure of 10 | various other things—for instance that you'll tell us if you know that your code 11 | infringes on other people's patents. You don't have to sign the CLA until after 12 | you've submitted your code for review and a member has approved it, but you must 13 | do it before we can put your code into our codebase. Before you start working on 14 | a larger contribution, you should get in touch with us first through the issue 15 | tracker with your idea so that we can help out and possibly guide you. 16 | Coordinating up front makes it much easier to avoid frustration later on. 17 | 18 | ### Code reviews All submissions, including submissions by project members, 19 | require review. We use Github pull requests for this purpose. 20 | 21 | ### The small print Contributions made by corporations are covered by a 22 | different agreement than the one above, the [Software Grant and Corporate 23 | Contributor License Agreement] (https://cla.developers.google.com/about/google- 24 | corporate). 25 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # People who have agreed to one of the CLAs and can contribute patches. 2 | 3 | Peggy Chi 4 | Yang Li -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /OVERVIEW.md: -------------------------------------------------------------------------------- 1 | This repository contains the following parts: 2 | 3 | ## Viewer 4 | 5 | This contains the source code of a Chrome packaged app. It shows a set of 6 | interactive device emulators and a log panel, which shows the execution log 7 | messages. 8 | 9 | * To load a Chord script: Select from the list of samples via the menu ![sample 10 | icon](/viewer/img/sample.png), or specify a directory you created under /viewer 11 | by the system dialog ![folder icon](/viewer/img/folder.png). 12 | 13 | * To test with emulators: Trigger events via the device UI or the event buttons on the right-hand side. 14 | 15 | ## Docs 16 | 17 | This contains the Chord API documentation. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chord: Scripting Cross-Device Interactions 2 | 3 | Chord is a framework for developers to create cross-device wearable interaction 4 | by scripting. This directory contains the implementation for the Chord framework 5 | and a viewer for a set of interactive emulators. 6 | 7 | *This project is previously named "Weave". We renamed this project to avoid 8 | confusion with the [Brillo and Weave 9 | platform](https://developers.google.com/brillo/) (Note: This is a different 10 | project!)* 11 | 12 | ## Goals 13 | 14 | Chord provides a set of high-level APIs, based on JavaScript, for developers to 15 | easily distribute UI output and combine user input and sensing events across 16 | devices. These high-level APIs as well as their underlying scripting concepts 17 | allow developers to focus on their target interaction behaviors and think about 18 | target devices regarding their capabilities and affordances, rather than low- 19 | level specifications. 20 | 21 | Chord also contributes an environment for developers to test cross-device 22 | behaviors, and when ready, deploy these behaviors to its runtime environment on 23 | users’ ad-hoc network of mobile and wearable devices. 24 | 25 | ## Requirements and Setup 26 | 27 | Chord is implemented as a Chrome packaged app. Please install 28 | [Chrome](https://www.google.com/chrome/browser/) and load the directory viewer/ 29 | (see [instructions to launch a Chrome app] 30 | (https://developer.chrome.com/apps/first_app#five)). 31 | 32 | ## Progress 33 | 34 | This current version enables developers to load chord scripts and interact with 35 | the emulators, including a smartphone, a smart watch, and a tablet. 36 | 37 | In our next update, developers will be able to connect live Android devices on 38 | the network, and test with devices. 39 | 40 | ## Publication 41 | 42 | This work has been published at [CHI 2015](http://chi2015.acm.org): 43 | 44 | * Pei-Yu (Peggy) Chi and Yang Li. 2015. Weave: Scripting Cross-Device Wearable 45 | Interaction. In *Proceedings of the 33rd Annual ACM Conference on Human Factors 46 | in Computing Systems (CHI 2015)*. ACM, New York, NY, USA, 3923-3932. 47 | [DOI=http://dx.doi.org/10.1145/2702123.2702451] 48 | (http://dx.doi.org/10.1145/2702123.2702451) 49 | 50 | ## Disclaimer 51 | 52 | This is not an official Google product. The application uses third party 53 | libraries listed under the directory third_party/. 54 | 55 | ## Contacts 56 | 57 | This package is active and maintained. If you have any questions, please send 58 | them to: 59 | 60 | [Peggy Chi](http://www.cs.berkeley.edu/~peggychi/) 61 | ([peggychi@cs.berkeley.edu](mailto:peggychi@cs.berkeley.edu)) and [Yang 62 | Li](http://yangl.org/) ([yangli@acm.org](mailto:yangli@acm.org)) 63 | 64 | ![Chord UI](/docs/img/chord_UI.png) 65 | -------------------------------------------------------------------------------- /docs/api.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Learning Chord: Scripting Cross-Device Interactions 8 | 9 | 10 | 22 |
23 | 26 | 27 | 28 | 31 |
32 |

chord.select(selector)

33 |

Return a ChordSelection object that includes all the matched device(s) on a local network based on the selector string.

34 |

selector is a list of filter criteria in string. Whitespace will be eliminated.

35 |

The following snippet selects any device that has a normal-size, low-privacy touchable display.

36 |
chord.select('.showable[size="normal"][privacy="low"].touchable')
37 |

The following snippet selects any Android watch.

38 |
chord.select(':watch[os="android"]')
39 |

Properties can always be specified:

40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
PropertyOptions
type":phone", ":watch", ":glass"
os"android", "iOS", "windows"
54 |

List of device capability options:

55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 |
ComponentKeywordSpecificationActionEvent (.on)
Display/LEDshowablesize [xlarge, large, normal, small]; glanceability [high, normal, low]; privacy [high, normal, low], interruption [high, normal, low].show, .play-
Speakerspeakableprivacy [high, normal, low].playlistenStart, listenEnd
Touch screentouchablesize [xlarge, large, normal, small]-tap, doubleTap, longTap, swipe, touchStart, touchMove, touchEnd
Accelerometershakablesize [xlarge, large, medium, small]-shake
Rotation sensorrotatablesize [xlarge, large, medium, small]-rotateCW, crotateCCW
Phonephoneable-.call-
106 |

* Future work: microphone (hearable), Camera (camerable), GPS (locationTrackable), health-related (stepTrackable)

107 |
108 | 109 |
110 |

chord.selectAll()

111 |

Return a ChordSelection object that includes all the device(s) on the Chord local network.

112 |

The followings are equal: selectAll(), select(), select(''), select('all')

113 |
114 | 115 |
116 |

chord.getDeviceByType(type)

117 |

chord.getDeviceByJoint(joint)

118 |

chord.getDeviceByID(id)

119 |

Return a ChordSelection object that includes matched device(s).

120 |
121 | 122 |
123 |

.not(selector)

124 |

To exclude the device(s) based on the selector string.

125 |
chord.select('.showable').not('.phoneable')
126 |
127 | 128 |
129 |

.not(ChordSelection)

130 |

To exclude the device(s) of the ChordSelection object.

131 |
chord.select('.showable').not(':glass')
132 |
133 | 134 |
135 |

.not(device)

136 |

To exclude the specific device object.

137 |
chord.select('.showable').not(event.getDevice())
138 |
139 | 140 | 141 | 144 | 145 |
146 |

.on(eventType, handler)

147 |

When an event occurs on the specified device, trigger the callback function.

148 |

The argument "event" in the callback function contains necessary information about the user event. For examples, event.getDevice() or event.getDevices() returns the source device(s) as a ChordSelection object that triggered the event; event.getValue() returns the corresponding value, such as the button value.

149 |
chord.select('.shakable.showable'))
150 | .on('shake', function(event) {
151 |   event.getDevice().show('shaken');
152 | });
153 |
154 | 155 | 156 | 159 | 160 |
161 |

.show(Panel)

162 |

Render a UI panel on the device.

163 |
chord.select('.showable').show(chord.getLayoutById('panel'));
164 |
165 | 166 |
167 |

.show(String)

168 |

Render a string on the device.

169 |
chord.select('.showable').show('Hello World.');
170 |
171 | 172 |
173 |

.play(filepath)

174 |

Play a media file of the filepath on the device.

175 |
chord.select('.speakable').play('audio/ring.mp3');
176 |
177 | 178 |
179 |

.call(num)

180 |

Make a phone call to the number.

181 |
chord.select('.phoneable').call('6501234567');
182 |
183 | 184 |
185 |

.startApp(appname)

186 |

Launch a device application.

187 |
chord.select('.showable').startApp('GoogleMaps');
188 |

Available build-in supports:

189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 |
AppValue
Google MapsGoogleMaps
GmailGmail
Google CalendarCalendar
207 |
208 | 209 | 210 | 213 | 214 |
215 |

chord.getLayoutById(panelID)

216 |

Return a Panel object by analyzing the HTML content defined by the developer.

217 |
chord.select('.showable').show(chord.getLayoutById('panel'));
218 |
219 | 220 | 221 | 224 | 225 |
226 |

By default, Chord sees each device as the same:

227 |

It picks *one* suitable device for the action.

228 |

An event handler or an action will be attached to the devices *individually*.

229 |
230 | 231 |

The following methods provide other device behaviors:

232 | 233 |
234 |

.all()

235 |

Return a ChordSelection object that each device will behave the same:

236 |

An action will be applied to all the devices (e.g., .show the same content on all displays).

237 |
chord.select('.showable').all().show("all on");
238 |

An event handler will be triggered only when the event occurs on all the devices (e.g., all devices .shake at the same time).

239 |
240 | 241 |
242 |

.combine()

243 |

Return a ChordSelection object that encapsulates the devices as one conceptual device:

244 |

An action will be distributed to devices. For example, the content will be distributed to .show on individual screens.

245 |

Developers can specify a new event condition using the same format of device selection. An event handler will be triggered only when the event occurs on the specified devices.

246 |
var combinedDevice = chord.selectAll().combine();
247 | combinedDevice.show(chord.getLayoutById('panel'));
248 | combinedDevice.on("swipeDown[joint='hand'], swipeUp[joint='watch']",
249 |   function(event) {
250 |     // pinch over two devices
251 |   });
252 |
253 | 254 | 255 | 258 | 259 |
260 |

ChordSelection

261 |

Include a list of devices, behaviors (devices to behave individually, all as a group, or combined), and methods (to apply to the devices based on the behavior). A ChordSelection object can be chainable to run multiple methods in sequence.

262 |
263 |

.size()

264 |

Return the number of devices in this selection.

265 |
266 |
267 |

.getDeviceName(), .getDeviceNames()

268 |

Return the device name(s) in string, separated by comma.

269 |
270 |
271 |

.getDeviceHasUIByID(id)

272 |

Return the device(s) that is showing an UI element with the specified id.

273 |
274 |
275 |

.updateUIAttr(elementID, attribute, value)

276 |

Replace the value of an attribute with the specified element id.

277 |
chord.select('.showable')
278 |   .show(chord.getLayoutById('panel'))
279 |   .updateUIAttr('imageView', 'src', 'img/new_image.png');
280 |
281 | 282 |
283 |

Device

284 |

Maintain the properties, status, and methods of one device.

285 |
286 |

.is(selector)

287 |

Return true is the device is capable of the selector option.

288 |
phoneableDevice.is('.phoneable')
289 |
290 |
291 |

.size()

292 |

Always return 1.

293 |
294 |
295 |

.getDeviceName()

296 |

Return the device name in string.

297 |
298 |
299 |

.updateUIAttr(elementID, attribute, value)

300 |

Replace the value of an attribute with the specified element id.

301 |
302 |
303 | 304 |
305 |

Log

306 |

Show log messages in the log panel, including 5 color options:

307 |
308 |

309 | 310 | Log.v(message) 311 |
312 | 313 | Log.d(message) 314 |
315 | 316 | Log.i(message) 317 |
318 | 319 | Log.w(message) 320 |
321 | 322 | Log.e(message) 323 | 324 |

325 |
326 |
327 | 328 |
329 | 330 |
331 | 332 | 333 | -------------------------------------------------------------------------------- /docs/css/style.css: -------------------------------------------------------------------------------- 1 | .main { 2 | max-width: 680px; 3 | } 4 | 5 | .pre-next { 6 | margin-bottom: 250px; 7 | margin-top: 50px; 8 | } 9 | 10 | .link { 11 | margin-right: 10px; 12 | } 13 | 14 | img { 15 | border: 1px solid lightgrey; 16 | } 17 | 18 | .fig { 19 | width: 600px; 20 | } 21 | 22 | .chordCode { 23 | color: #080; 24 | font-family: "source-code-pro", Consolas, monospace!important; 25 | } 26 | 27 | .func { 28 | color: orange; 29 | } 30 | 31 | .arg { 32 | color: #708; 33 | } 34 | 35 | .note { 36 | color: #05a; 37 | } 38 | 39 | p { 40 | font-size: 18px; 41 | } 42 | 43 | .method { 44 | margin-top: 30px; 45 | } 46 | 47 | .method .name { 48 | font-family: "source-code-pro", Consolas, monospace!important; 49 | } 50 | 51 | .method .detail { 52 | margin-left: 30px; 53 | } 54 | 55 | .method .codeEx { 56 | margin-left: 30px; 57 | width: 90%; 58 | } 59 | 60 | .method table { 61 | font-family: "source-code-pro", Consolas, monospace!important; 62 | margin-left: 30px; 63 | width: 80%; 64 | } 65 | 66 | .submethod { 67 | margin-left: 60px; 68 | } 69 | 70 | .log_v { 71 | color: grey; 72 | } 73 | .log_d { 74 | color: blue; 75 | } 76 | .log_i { 77 | color: green; 78 | } 79 | .log_w { 80 | color: DarkOrange; 81 | } 82 | .log_e { 83 | color: red; 84 | } 85 | -------------------------------------------------------------------------------- /docs/img/chord_UI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/docs/img/chord_UI.png -------------------------------------------------------------------------------- /viewer/background.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 The Chord Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Implements the background script that renders 19 | * a window when the user launches the Weave Chrome app. 20 | * @author peggychi@cs.berkeley.edu (Peggy Chi) 21 | */ 22 | 23 | chrome.app.runtime.onLaunched.addListener(function() { 24 | var screenWidth = window.screen.availWidth; 25 | var screenHeight = Math.min(1600, window.screen.availHeight); 26 | var windowWidth = Math.min(550, Math.round(screenWidth*0.4)); 27 | chrome.app.window.create('index.html', { 28 | 'outerBounds': { 29 | 'width': windowWidth, 30 | 'height': screenHeight, 31 | 'left': screenWidth - windowWidth, 32 | 'top': 0 33 | }, 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /viewer/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #f5f5f5!important; 3 | } 4 | 5 | /* Header */ 6 | 7 | .topbar { 8 | background-color: white!important; 9 | border-bottom: 1px solid #e5e5e5; 10 | height: 41px; 11 | position: relative; 12 | } 13 | 14 | #logo { 15 | background-image: url("../img/logo/chord-128.png"); 16 | background-position: center; 17 | background-repeat: no-repeat; 18 | background-size: contain; 19 | height: 40px; 20 | position: absolute; 21 | width: 40px; 22 | } 23 | 24 | h3 { 25 | font-size: 18px!important; 26 | line-height: 40px!important; 27 | margin-left: 50px!important; 28 | margin-top: 0!important; 29 | } 30 | 31 | .icon { 32 | background-position: center; 33 | background-repeat: no-repeat; 34 | background-size: 100%; 35 | display: inline-block; 36 | height: 23px; 37 | margin-left: 5px; 38 | position: relative; 39 | vertical-align: middle; 40 | width: 23px; 41 | } 42 | 43 | .sample { 44 | background-image: url("../img/sample.png"); 45 | } 46 | 47 | .folder { 48 | background-image: url("../img/folder.png"); 49 | } 50 | 51 | .folder-open { 52 | background-image: url("../img/folder_open.png"); 53 | } 54 | 55 | .run { 56 | background-image: url("../img/run.png"); 57 | } 58 | 59 | .disable { 60 | cursor: not-allowed; 61 | opacity: .65; 62 | } 63 | 64 | .path { 65 | color: grey; 66 | font-size: 12px; 67 | font-weight: normal; 68 | } 69 | 70 | /* Main device panel */ 71 | 72 | .device-panel { 73 | border-radius: 4px; 74 | color: #555; 75 | font-size: 14px; 76 | margin: 5px; 77 | overflow: hidden!important; 78 | padding: 0; 79 | } 80 | 81 | .devices { 82 | max-height: 80%; 83 | } 84 | 85 | .device-container { 86 | border: 1px solid #ccc!important; 87 | } 88 | 89 | .deviceType { 90 | font-weight: bold; 91 | } 92 | 93 | .panel { 94 | height: 210px!important; 95 | margin-bottom: 5px!important; 96 | padding-right: 0!important; 97 | } 98 | 99 | .panel .row { 100 | height: 210px !important; 101 | padding: 10px; 102 | } 103 | 104 | .panel .info .status { 105 | color: #a50; 106 | font-size: 12px; 107 | margin-top: 5px; 108 | min-height: 14px; 109 | top: -5px; 110 | } 111 | 112 | .deviceSkin { 113 | background-position: center; 114 | background-repeat: no-repeat; 115 | background-size: contain; 116 | height: 180px; 117 | width: 100%; 118 | } 119 | 120 | .UI { 121 | background-color: black; 122 | margin: 0 auto; 123 | position: relative; 124 | } 125 | 126 | .UI img { 127 | display: block; 128 | height: auto; 129 | margin-left: auto; 130 | margin-right: auto; 131 | max-height: 100%; 132 | max-width: 100%; 133 | width: auto; 134 | } 135 | 136 | .callStart { 137 | background-image: url("../img/phonecall.jpg"); 138 | background-position: center; 139 | background-repeat: no-repeat; 140 | background-size: 70%; 141 | } 142 | 143 | .phoneSkin { 144 | background-image: url("../img/device/nexus5.png"); 145 | } 146 | 147 | .phoneSkin .UI { 148 | height: 141px; 149 | top: 17px; 150 | width: 80px; 151 | } 152 | 153 | .phoneSkin .landscape { 154 | height: 80px; 155 | left: -1px; 156 | top: 46px; 157 | width: 141px; 158 | } 159 | 160 | .phoneSkin .activeUI { 161 | background-color: white!important; 162 | } 163 | 164 | .tabletSkin { 165 | background-image: url("../img/device/nexus9.png"); 166 | } 167 | 168 | .tabletSkin .UI { 169 | height: 141px; 170 | top: 19px; 171 | width: 106px; 172 | } 173 | 174 | .tabletSkin .landscape { 175 | height: 80px; 176 | left: -1px; 177 | top: 46px; 178 | width: 141px; 179 | } 180 | 181 | .tabletSkin .activeUI { 182 | background-color: white!important; 183 | } 184 | 185 | .watchSkin { 186 | background-image: url("../img/device/wear.png"); 187 | } 188 | 189 | .watchSkin .UI { 190 | height: 132px; 191 | top: 24px; 192 | width: 132px; 193 | } 194 | 195 | .watchSkin .UI * { 196 | background-color: white; 197 | background-position: right; 198 | background-repeat: no-repeat; 199 | background-size: 40px 40px; 200 | font-size: 14px!important; 201 | height: 45%; 202 | margin-left: 8px; 203 | padding-top: 30px; 204 | text-align: left; 205 | vertical-align: text-bottom; 206 | width: 90%; 207 | } 208 | 209 | .watchSkin .UI button { 210 | border: 0; 211 | margin-top: 50%; 212 | padding-top: 30px; 213 | } 214 | 215 | .watchSkin .UI img { 216 | border: 0; 217 | margin-top: 50%; 218 | padding-top: 30px; 219 | } 220 | 221 | .watchSkin .UI p { 222 | bottom: 0; 223 | padding-left: 4px; 224 | padding-top: 10px; 225 | position: absolute; 226 | } 227 | 228 | .watchSkin .UI .prev { 229 | background-image: url("../img/icon/prev.png"); 230 | } 231 | 232 | .watchSkin .UI .next { 233 | background-image: url("../img/icon/next.png"); 234 | } 235 | 236 | .watchSkin .UI .play { 237 | background-image: url("../img/icon/play.png"); 238 | } 239 | 240 | .watchSkin .UI .GoogleMaps { 241 | background-image: url("../img/icon/maps.png"); 242 | } 243 | 244 | .watchSkin .UI .Gmail { 245 | background-image: url("../img/icon/gmail.png"); 246 | } 247 | 248 | .watchSkin .UI .Calendar { 249 | background-image: url("../img/icon/calendar.png"); 250 | } 251 | 252 | .watchSkin .UI .Photos { 253 | background-image: url("../img/icon/photos.png"); 254 | } 255 | 256 | .watchSkin .activeUI { 257 | background-color: black!important; 258 | } 259 | 260 | .glassSkin { 261 | background-color: black; 262 | color: white; 263 | font-size: 40px!important; 264 | height: 126px; /* 640x360 = 16:9 */ 265 | margin: 0 auto; 266 | margin-top: 20px; 267 | width: 224px; 268 | } 269 | 270 | .glassSkin .UI { 271 | height: 126px; 272 | width: 224px; 273 | } 274 | 275 | .glassSkin .UI * { 276 | font-size: 20px!important; 277 | padding-top: 10px; 278 | text-align: center; 279 | } 280 | 281 | .glassSkin .UI button { 282 | padding-left: 10px; 283 | } 284 | 285 | .panel .events { 286 | margin-left: -25px!important; 287 | max-height: 200px; 288 | padding-left: 5px; 289 | } 290 | 291 | .manualEvts { 292 | padding: 5px; 293 | width: 150%; 294 | } 295 | 296 | .manualEvts button { 297 | float: left; 298 | font-size: 12px; 299 | margin-bottom: 3px; 300 | margin-left: 2px; 301 | position: relative; 302 | } 303 | 304 | .live { 305 | color: red; 306 | font-style: italic; 307 | } 308 | 309 | .evt { 310 | background-position: center; 311 | background-repeat: no-repeat; 312 | background-size: 70%; 313 | height: 27px; 314 | width: 36px; 315 | } 316 | 317 | .evt:hover:after { 318 | background: rgba(10, 10, 10, .7); 319 | border-radius: 5px; 320 | color: #fff; 321 | content: attr(evtname); 322 | left: -20px; 323 | max-width: 80px; 324 | padding: 5px; 325 | position: absolute; 326 | top: 28px; 327 | z-index: 100; 328 | } 329 | 330 | .evt_swipeLeft { 331 | background-image: url("../img/left.png")!important; 332 | } 333 | 334 | .evt_swipeRight { 335 | background-image: url("../img/right.png")!important; 336 | } 337 | 338 | .evt_swipeUp { 339 | background-image: url("../img/up.png")!important; 340 | } 341 | 342 | .evt_swipeDown { 343 | background-image: url("../img/down.png")!important; 344 | } 345 | 346 | .evt_rotateCW { 347 | background-image: url("../img/rotateCW.png")!important; 348 | } 349 | 350 | .evt_rotateCCW { 351 | background-image: url("../img/rotateCCW.png")!important; 352 | } 353 | 354 | .evt_listen { 355 | background-image: url("../img/listen.png")!important; 356 | } 357 | 358 | .evt_talk { 359 | background-image: url("../img/talk.png")!important; 360 | } 361 | 362 | .evt_shake { 363 | background-image: url("../img/shake.png")!important; 364 | } 365 | 366 | .device_delete { 367 | background-image: url("../img/close.png")!important; 368 | background-position: center!important; 369 | background-repeat: no-repeat!important; 370 | height: 30px; 371 | position: relative; 372 | right: 0; 373 | top: -210px; 374 | width: 30px; 375 | } 376 | 377 | button span { 378 | color: darkgrey; 379 | font-size: 12px; 380 | position: relative; 381 | vertical-align: middle; 382 | } 383 | 384 | .app { 385 | background-repeat: no-repeat!important; 386 | background-size: contain; 387 | height: 100%; 388 | width: 100%; 389 | } 390 | 391 | .appGmail { 392 | background-image: url("../img/app/gmail.png"); 393 | } 394 | 395 | .appGoogleMaps { 396 | background-image: url("../img/app/maps.png"); 397 | } 398 | 399 | .appPhotos { 400 | background-image: url("../img/app/photos.png"); 401 | } 402 | 403 | .appCalendar { 404 | background-image: url("../img/app/calendar.png"); 405 | } 406 | 407 | /* Log panel */ 408 | 409 | .log-panel { 410 | bottom: 10px; 411 | height: 140px; 412 | position: fixed; 413 | width: 100%; 414 | } 415 | 416 | .log-panel .panel { 417 | bottom: 0; 418 | height: 100%!important; 419 | margin: 5px; 420 | } 421 | 422 | .console { 423 | color: grey; 424 | font-family: "source-code-pro", Consolas, monospace!important; 425 | font-size: 12px; 426 | height: 100%; 427 | overflow-y: auto; 428 | padding: 6px!important; 429 | } 430 | 431 | .log_d { 432 | color: blue; 433 | } 434 | 435 | .log_i { 436 | color: green; 437 | } 438 | 439 | .log_w { 440 | color: DarkOrange; 441 | } 442 | 443 | .log_e { 444 | color: red; 445 | } 446 | 447 | /* Dialog boxes */ 448 | 449 | code { 450 | background-color: #f7f7f9; 451 | border: 1px solid #e1e1e8; 452 | color: #d14; 453 | float: right; 454 | padding: 2px 4px; 455 | position: relative; 456 | right: 20px; 457 | top: -40px; 458 | white-space: nowrap; 459 | } 460 | 461 | dialog { 462 | border: 1px solid rgba(0, 0, 0, .3); 463 | border-radius: 6px; 464 | box-shadow: 0 3px 7px rgba(0, 0, 0, .3); 465 | min-width: 180px; 466 | padding: 0; 467 | } 468 | 469 | dialog::backdrop { 470 | background-color: rgba(0, 0, 0, .7); 471 | bottom: 0; 472 | left: 0; 473 | position: fixed; 474 | right: 0; 475 | top: 0; 476 | } 477 | 478 | dialog .dlg-content { 479 | padding: 10pt; 480 | } 481 | 482 | dialog .dlg-content button { 483 | margin-left: 5pt; 484 | } 485 | 486 | dialog .dlg-close { 487 | background-image: url("../img/close.png")!important; 488 | height: 20px; 489 | position: absolute; 490 | right: 3px; 491 | top: 3px; 492 | width: 20px; 493 | } 494 | -------------------------------------------------------------------------------- /viewer/device/deviceSpec.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceCapabilities": { 3 | "phone_nexus5": { 4 | "showable": { 5 | "size": "normal", 6 | "shape": "16:9", 7 | "glanceability": "low", 8 | "interruption": "high", 9 | "privacy": "low" 10 | }, 11 | "shakable": { 12 | "size": "medium", 13 | "on": [ 14 | "shake" 15 | ] 16 | }, 17 | "rotatable": { 18 | "size": "medium", 19 | "on": [ 20 | "rotateLandscape", 21 | "rotatePortrait" 22 | ] 23 | }, 24 | "touchable": { 25 | "size": "normal", 26 | "on": [ 27 | "tap", 28 | "doubleTap", 29 | "longTap", 30 | "swipeLeft", 31 | "swipeRight", 32 | "swipeUp", 33 | "swipeDown" 34 | ] 35 | }, 36 | "keyable": { 37 | "method": "screen", 38 | "on": [ 39 | "keypress" 40 | ] 41 | }, 42 | "phoneable": { 43 | "privacy": "high", 44 | "on": [ 45 | "callStart", 46 | "callEnd" 47 | ] 48 | }, 49 | "speakable": { 50 | "privacy": "low", 51 | "on": [ 52 | "playStart", 53 | "playEnd" 54 | ] 55 | }, 56 | "hearable": { 57 | "on": [ 58 | "listen", 59 | "listenEnd" 60 | ] 61 | }, 62 | "recordable": { 63 | "on": [ 64 | "recordStart", 65 | "recordEnd" 66 | ] 67 | }, 68 | "locationTrackable": { 69 | "on": [ 70 | "locationChange" 71 | ] 72 | } 73 | }, 74 | "phone_nexus6": { 75 | "showable": { 76 | "size": "normal", 77 | "shape": "16:9", 78 | "glanceability": "low", 79 | "interruption": "high", 80 | "privacy": "low" 81 | }, 82 | "shakable": { 83 | "size": "medium", 84 | "on": [ 85 | "shake" 86 | ] 87 | }, 88 | "rotatable": { 89 | "size": "medium", 90 | "on": [ 91 | "rotateLandscape", 92 | "rotatePortrait" 93 | ] 94 | }, 95 | "touchable": { 96 | "size": "normal", 97 | "on": [ 98 | "tap", 99 | "doubleTap", 100 | "longTap", 101 | "swipeLeft", 102 | "swipeRight", 103 | "swipeUp", 104 | "swipeDown" 105 | ] 106 | }, 107 | "keyable": { 108 | "method": "screen", 109 | "on": [ 110 | "keypress" 111 | ] 112 | }, 113 | "phoneable": { 114 | "privacy": "high", 115 | "on": [ 116 | "callStart", 117 | "callEnd" 118 | ] 119 | }, 120 | "speakable": { 121 | "privacy": "low", 122 | "on": [ 123 | "playStart", 124 | "playEnd" 125 | ] 126 | }, 127 | "hearable": { 128 | "on": [ 129 | "listen", 130 | "listenEnd" 131 | ] 132 | }, 133 | "recordable": { 134 | "on": [ 135 | "recordStart", 136 | "recordEnd" 137 | ] 138 | }, 139 | "locationTrackable": { 140 | "on": [ 141 | "locationChange" 142 | ] 143 | } 144 | }, 145 | "phone_iphon6": { 146 | "showable": { 147 | "size": "normal", 148 | "shape": "16:9", 149 | "glanceability": "low", 150 | "interruption": "high", 151 | "privacy": "low" 152 | }, 153 | "shakable": { 154 | "size": "medium", 155 | "on": [ 156 | "shake" 157 | ] 158 | }, 159 | "rotatable": { 160 | "size": "medium", 161 | "on": [ 162 | "rotateLandscape", 163 | "rotatePortrait" 164 | ] 165 | }, 166 | "touchable": { 167 | "size": "normal", 168 | "on": [ 169 | "tap", 170 | "doubleTap", 171 | "longTap", 172 | "swipeLeft", 173 | "swipeRight", 174 | "swipeUp", 175 | "swipeDown" 176 | ] 177 | }, 178 | "keyable": { 179 | "method": "screen", 180 | "on": [ 181 | "keypress" 182 | ] 183 | }, 184 | "phoneable": { 185 | "privacy": "high", 186 | "on": [ 187 | "callStart", 188 | "callEnd" 189 | ] 190 | }, 191 | "speakable": { 192 | "privacy": "low", 193 | "on": [ 194 | "playStart", 195 | "playEnd" 196 | ] 197 | }, 198 | "hearable": { 199 | "on": [ 200 | "listen", 201 | "listenEnd" 202 | ] 203 | }, 204 | "recordable": { 205 | "on": [ 206 | "recordStart", 207 | "recordEnd" 208 | ] 209 | }, 210 | "locationTrackable": { 211 | "on": [ 212 | "locationChange" 213 | ] 214 | } 215 | }, 216 | "phone_lumia520": { 217 | "showable": { 218 | "size": "normal", 219 | "shape": "16:9", 220 | "glanceability": "low", 221 | "interruption": "high", 222 | "privacy": "low" 223 | }, 224 | "shakable": { 225 | "size": "medium", 226 | "on": [ 227 | "shake" 228 | ] 229 | }, 230 | "rotatable": { 231 | "size": "medium", 232 | "on": [ 233 | "rotateLandscape", 234 | "rotatePortrait" 235 | ] 236 | }, 237 | "touchable": { 238 | "size": "normal", 239 | "on": [ 240 | "tap", 241 | "doubleTap", 242 | "longTap", 243 | "swipeLeft", 244 | "swipeRight", 245 | "swipeUp", 246 | "swipeDown" 247 | ] 248 | }, 249 | "keyable": { 250 | "method": "screen", 251 | "on": [ 252 | "keypress" 253 | ] 254 | }, 255 | "phoneable": { 256 | "privacy": "high", 257 | "on": [ 258 | "callStart", 259 | "callEnd" 260 | ] 261 | }, 262 | "speakable": { 263 | "privacy": "low", 264 | "on": [ 265 | "playStart", 266 | "playEnd" 267 | ] 268 | }, 269 | "hearable": { 270 | "on": [ 271 | "listen", 272 | "listenEnd" 273 | ] 274 | }, 275 | "recordable": { 276 | "on": [ 277 | "recordStart", 278 | "recordEnd" 279 | ] 280 | }, 281 | "locationTrackable": { 282 | "on": [ 283 | "locationChange" 284 | ] 285 | } 286 | }, 287 | "phone_galaxy-s5": { 288 | "showable": { 289 | "size": "normal", 290 | "shape": "16:9", 291 | "glanceability": "low", 292 | "interruption": "high", 293 | "privacy": "low" 294 | }, 295 | "shakable": { 296 | "size": "medium", 297 | "on": [ 298 | "shake" 299 | ] 300 | }, 301 | "rotatable": { 302 | "size": "medium", 303 | "on": [ 304 | "rotateLandscape", 305 | "rotatePortrait" 306 | ] 307 | }, 308 | "touchable": { 309 | "size": "normal", 310 | "on": [ 311 | "tap", 312 | "doubleTap", 313 | "longTap", 314 | "swipeLeft", 315 | "swipeRight", 316 | "swipeUp", 317 | "swipeDown" 318 | ] 319 | }, 320 | "keyable": { 321 | "method": "screen", 322 | "on": [ 323 | "keypress" 324 | ] 325 | }, 326 | "phoneable": { 327 | "privacy": "high", 328 | "on": [ 329 | "callStart", 330 | "callEnd" 331 | ] 332 | }, 333 | "speakable": { 334 | "privacy": "low", 335 | "on": [ 336 | "playStart", 337 | "playEnd" 338 | ] 339 | }, 340 | "hearable": { 341 | "on": [ 342 | "listen", 343 | "listenEnd" 344 | ] 345 | }, 346 | "recordable": { 347 | "on": [ 348 | "recordStart", 349 | "recordEnd" 350 | ] 351 | }, 352 | "locationTrackable": { 353 | "on": [ 354 | "locationChange" 355 | ] 356 | } 357 | }, 358 | "phone_xperia-z3": { 359 | "showable": { 360 | "size": "normal", 361 | "shape": "16:9", 362 | "glanceability": "low", 363 | "interruption": "high", 364 | "privacy": "low" 365 | }, 366 | "shakable": { 367 | "size": "medium", 368 | "on": [ 369 | "shake" 370 | ] 371 | }, 372 | "rotatable": { 373 | "size": "medium", 374 | "on": [ 375 | "rotateLandscape", 376 | "rotatePortrait" 377 | ] 378 | }, 379 | "touchable": { 380 | "size": "normal", 381 | "on": [ 382 | "tap", 383 | "doubleTap", 384 | "longTap", 385 | "swipeLeft", 386 | "swipeRight", 387 | "swipeUp", 388 | "swipeDown" 389 | ] 390 | }, 391 | "keyable": { 392 | "method": "screen", 393 | "on": [ 394 | "keypress" 395 | ] 396 | }, 397 | "phoneable": { 398 | "privacy": "high", 399 | "on": [ 400 | "callStart", 401 | "callEnd" 402 | ] 403 | }, 404 | "speakable": { 405 | "privacy": "low", 406 | "on": [ 407 | "playStart", 408 | "playEnd" 409 | ] 410 | }, 411 | "hearable": { 412 | "on": [ 413 | "listen", 414 | "listenEnd" 415 | ] 416 | }, 417 | "recordable": { 418 | "on": [ 419 | "recordStart", 420 | "recordEnd" 421 | ] 422 | }, 423 | "locationTrackable": { 424 | "on": [ 425 | "locationChange" 426 | ] 427 | } 428 | }, 429 | "phone_galaxy-note4": { 430 | "showable": { 431 | "size": "normal", 432 | "shape": "16:9", 433 | "glanceability": "low", 434 | "interruption": "high", 435 | "privacy": "low" 436 | }, 437 | "shakable": { 438 | "size": "medium", 439 | "on": [ 440 | "shake" 441 | ] 442 | }, 443 | "rotatable": { 444 | "size": "medium", 445 | "on": [ 446 | "rotateLandscape", 447 | "rotatePortrait" 448 | ] 449 | }, 450 | "touchable": { 451 | "size": "normal", 452 | "on": [ 453 | "tap", 454 | "doubleTap", 455 | "longTap", 456 | "swipeLeft", 457 | "swipeRight", 458 | "swipeUp", 459 | "swipeDown" 460 | ] 461 | }, 462 | "keyable": { 463 | "method": "screen", 464 | "on": [ 465 | "keypress" 466 | ] 467 | }, 468 | "phoneable": { 469 | "privacy": "high", 470 | "on": [ 471 | "callStart", 472 | "callEnd" 473 | ] 474 | }, 475 | "speakable": { 476 | "privacy": "low", 477 | "on": [ 478 | "playStart", 479 | "playEnd" 480 | ] 481 | }, 482 | "hearable": { 483 | "on": [ 484 | "listen", 485 | "listenEnd" 486 | ] 487 | }, 488 | "recordable": { 489 | "on": [ 490 | "recordStart", 491 | "recordEnd" 492 | ] 493 | }, 494 | "locationTrackable": { 495 | "on": [ 496 | "locationChange" 497 | ] 498 | } 499 | }, 500 | "phone_galaxy-s6-edge": { 501 | "showable": { 502 | "size": "normal", 503 | "shape": "16:9", 504 | "glanceability": "low", 505 | "interruption": "high", 506 | "privacy": "low" 507 | }, 508 | "shakable": { 509 | "size": "medium", 510 | "on": [ 511 | "shake" 512 | ] 513 | }, 514 | "rotatable": { 515 | "size": "medium", 516 | "on": [ 517 | "rotateLandscape", 518 | "rotatePortrait" 519 | ] 520 | }, 521 | "touchable": { 522 | "size": "normal", 523 | "on": [ 524 | "tap", 525 | "doubleTap", 526 | "longTap", 527 | "swipeLeft", 528 | "swipeRight", 529 | "swipeUp", 530 | "swipeDown" 531 | ] 532 | }, 533 | "keyable": { 534 | "method": "screen", 535 | "on": [ 536 | "keypress" 537 | ] 538 | }, 539 | "phoneable": { 540 | "privacy": "high", 541 | "on": [ 542 | "callStart", 543 | "callEnd" 544 | ] 545 | }, 546 | "speakable": { 547 | "privacy": "low", 548 | "on": [ 549 | "playStart", 550 | "playEnd" 551 | ] 552 | }, 553 | "hearable": { 554 | "on": [ 555 | "listen", 556 | "listenEnd" 557 | ] 558 | }, 559 | "recordable": { 560 | "on": [ 561 | "recordStart", 562 | "recordEnd" 563 | ] 564 | }, 565 | "locationTrackable": { 566 | "on": [ 567 | "locationChange" 568 | ] 569 | } 570 | }, 571 | "phone_htc-one-m9": { 572 | "showable": { 573 | "size": "normal", 574 | "shape": "16:9", 575 | "glanceability": "low", 576 | "interruption": "high", 577 | "privacy": "low" 578 | }, 579 | "shakable": { 580 | "size": "medium", 581 | "on": [ 582 | "shake" 583 | ] 584 | }, 585 | "rotatable": { 586 | "size": "medium", 587 | "on": [ 588 | "rotateLandscape", 589 | "rotatePortrait" 590 | ] 591 | }, 592 | "touchable": { 593 | "size": "normal", 594 | "on": [ 595 | "tap", 596 | "doubleTap", 597 | "longTap", 598 | "swipeLeft", 599 | "swipeRight", 600 | "swipeUp", 601 | "swipeDown" 602 | ] 603 | }, 604 | "keyable": { 605 | "method": "screen", 606 | "on": [ 607 | "keypress" 608 | ] 609 | }, 610 | "phoneable": { 611 | "privacy": "high", 612 | "on": [ 613 | "callStart", 614 | "callEnd" 615 | ] 616 | }, 617 | "speakable": { 618 | "privacy": "low", 619 | "on": [ 620 | "playStart", 621 | "playEnd" 622 | ] 623 | }, 624 | "hearable": { 625 | "on": [ 626 | "listen", 627 | "listenEnd" 628 | ] 629 | }, 630 | "recordable": { 631 | "on": [ 632 | "recordStart", 633 | "recordEnd" 634 | ] 635 | }, 636 | "locationTrackable": { 637 | "on": [ 638 | "locationChange" 639 | ] 640 | } 641 | }, 642 | "tablet_nexus9": { 643 | "showable": { 644 | "size": "large", 645 | "shape": "16:9", 646 | "glanceability": "low", 647 | "interruption": "low", 648 | "privacy": "low" 649 | }, 650 | "shakable": { 651 | "size": "large", 652 | "on": [ 653 | "shake" 654 | ] 655 | }, 656 | "rotatable": { 657 | "size": "large", 658 | "on": [ 659 | "rotateLandscape", 660 | "rotatePortrait" 661 | ] 662 | }, 663 | "touchable": { 664 | "size": "large", 665 | "on": [ 666 | "tap", 667 | "doubleTap", 668 | "longTap", 669 | "swipeLeft", 670 | "swipeRight", 671 | "swipeUp", 672 | "swipeDown" 673 | ] 674 | }, 675 | "keyable": { 676 | "method": "screen", 677 | "on": [ 678 | "keypress" 679 | ] 680 | }, 681 | "speakable": { 682 | "privacy": "low", 683 | "on": [ 684 | "playStart", 685 | "playEnd" 686 | ] 687 | }, 688 | "hearable": { 689 | "on": [ 690 | "listen", 691 | "listenEnd" 692 | ] 693 | }, 694 | "recordable": { 695 | "on": [ 696 | "recordStart", 697 | "recordEnd" 698 | ] 699 | }, 700 | "locationTrackable": { 701 | "on": [ 702 | "locationChange" 703 | ] 704 | } 705 | }, 706 | "tablet_galaxy-tab4": { 707 | "showable": { 708 | "size": "large", 709 | "shape": "16:10", 710 | "glanceability": "low", 711 | "interruption": "low", 712 | "privacy": "low" 713 | }, 714 | "shakable": { 715 | "size": "large", 716 | "on": [ 717 | "shake" 718 | ] 719 | }, 720 | "rotatable": { 721 | "size": "large", 722 | "on": [ 723 | "rotateLandscape", 724 | "rotatePortrait" 725 | ] 726 | }, 727 | "touchable": { 728 | "size": "large", 729 | "on": [ 730 | "tap", 731 | "doubleTap", 732 | "longTap", 733 | "swipeLeft", 734 | "swipeRight", 735 | "swipeUp", 736 | "swipeDown" 737 | ] 738 | }, 739 | "keyable": { 740 | "method": "screen", 741 | "on": [ 742 | "keypress" 743 | ] 744 | }, 745 | "speakable": { 746 | "privacy": "low", 747 | "on": [ 748 | "playStart", 749 | "playEnd" 750 | ] 751 | }, 752 | "hearable": { 753 | "on": [ 754 | "listen", 755 | "listenEnd" 756 | ] 757 | }, 758 | "recordable": { 759 | "on": [ 760 | "recordStart", 761 | "recordEnd" 762 | ] 763 | }, 764 | "locationTrackable": { 765 | "on": [ 766 | "locationChange" 767 | ] 768 | } 769 | }, 770 | "tablet_surface3": { 771 | "showable": { 772 | "size": "large", 773 | "shape": "3:2", 774 | "glanceability": "low", 775 | "interruption": "low", 776 | "privacy": "low" 777 | }, 778 | "shakable": { 779 | "size": "large", 780 | "on": [ 781 | "shake" 782 | ] 783 | }, 784 | "rotatable": { 785 | "size": "large", 786 | "on": [ 787 | "rotateLandscape", 788 | "rotatePortrait" 789 | ] 790 | }, 791 | "touchable": { 792 | "size": "large", 793 | "on": [ 794 | "tap", 795 | "doubleTap", 796 | "longTap", 797 | "swipeLeft", 798 | "swipeRight", 799 | "swipeUp", 800 | "swipeDown" 801 | ] 802 | }, 803 | "keyable": { 804 | "method": "screen", 805 | "on": [ 806 | "keypress" 807 | ] 808 | }, 809 | "speakable": { 810 | "privacy": "low", 811 | "on": [ 812 | "playStart", 813 | "playEnd" 814 | ] 815 | }, 816 | "hearable": { 817 | "on": [ 818 | "listen", 819 | "listenEnd" 820 | ] 821 | }, 822 | "recordable": { 823 | "on": [ 824 | "recordStart", 825 | "recordEnd" 826 | ] 827 | }, 828 | "locationTrackable": { 829 | "on": [ 830 | "locationChange" 831 | ] 832 | } 833 | }, 834 | "watch_lg-g-watch": { 835 | "showable": { 836 | "size": "small", 837 | "shape": "square", 838 | "glanceability": "high", 839 | "interruption": "low", 840 | "privacy": "low" 841 | }, 842 | "shakable": { 843 | "size": "small", 844 | "on": [ 845 | "shake" 846 | ] 847 | }, 848 | "rotatable": { 849 | "size": "small", 850 | "on": [ 851 | "rotateCCW", 852 | "rotateCW" 853 | ] 854 | }, 855 | "touchable": { 856 | "size": "small", 857 | "on": [ 858 | "tap", 859 | "doubleTap", 860 | "longTap", 861 | "swipeLeft", 862 | "swipeRight", 863 | "swipeUp", 864 | "swipeDown" 865 | ] 866 | }, 867 | "keyable": { 868 | "method": "screen", 869 | "on": [ 870 | "keypress" 871 | ] 872 | }, 873 | "phoneable": { 874 | "privacy": "low", 875 | "on": [ 876 | "callStart", 877 | "callEnd" 878 | ] 879 | }, 880 | "speakable": { 881 | "privacy": "low", 882 | "on": [ 883 | "playStart", 884 | "playEnd" 885 | ] 886 | }, 887 | "hearable": { 888 | "on": [ 889 | "listen", 890 | "listenEnd" 891 | ] 892 | }, 893 | "locationTrackable": { 894 | "on": [ 895 | "locationChange" 896 | ] 897 | } 898 | }, 899 | "watch_moto360": { 900 | "showable": { 901 | "size": "small", 902 | "shape": "round", 903 | "glanceability": "high", 904 | "interruption": "low", 905 | "privacy": "low" 906 | }, 907 | "shakable": { 908 | "size": "small", 909 | "on": [ 910 | "shake" 911 | ] 912 | }, 913 | "rotatable": { 914 | "size": "small", 915 | "on": [ 916 | "rotateCCW", 917 | "rotateCW" 918 | ] 919 | }, 920 | "touchable": { 921 | "size": "small", 922 | "on": [ 923 | "tap", 924 | "doubleTap", 925 | "longTap", 926 | "swipeLeft", 927 | "swipeRight", 928 | "swipeUp", 929 | "swipeDown" 930 | ] 931 | }, 932 | "keyable": { 933 | "method": "screen", 934 | "on": [ 935 | "keypress" 936 | ] 937 | }, 938 | "phoneable": { 939 | "privacy": "low", 940 | "on": [ 941 | "callStart", 942 | "callEnd" 943 | ] 944 | }, 945 | "speakable": { 946 | "privacy": "low", 947 | "on": [ 948 | "playStart", 949 | "playEnd" 950 | ] 951 | }, 952 | "hearable": { 953 | "on": [ 954 | "listen", 955 | "listenEnd" 956 | ] 957 | }, 958 | "locationTrackable": { 959 | "on": [ 960 | "locationChange" 961 | ] 962 | } 963 | }, 964 | "watch_samsung-gear-llive": { 965 | "showable": { 966 | "size": "small", 967 | "shape": "square", 968 | "glanceability": "high", 969 | "interruption": "low", 970 | "privacy": "low" 971 | }, 972 | "shakable": { 973 | "size": "small", 974 | "on": [ 975 | "shake" 976 | ] 977 | }, 978 | "rotatable": { 979 | "size": "small", 980 | "on": [ 981 | "rotateCCW", 982 | "rotateCW" 983 | ] 984 | }, 985 | "touchable": { 986 | "size": "small", 987 | "on": [ 988 | "tap", 989 | "doubleTap", 990 | "longTap", 991 | "swipeLeft", 992 | "swipeRight", 993 | "swipeUp", 994 | "swipeDown" 995 | ] 996 | }, 997 | "keyable": { 998 | "method": "screen", 999 | "on": [ 1000 | "keypress" 1001 | ] 1002 | }, 1003 | "phoneable": { 1004 | "privacy": "low", 1005 | "on": [ 1006 | "callStart", 1007 | "callEnd" 1008 | ] 1009 | }, 1010 | "speakable": { 1011 | "privacy": "low", 1012 | "on": [ 1013 | "playStart", 1014 | "playEnd" 1015 | ] 1016 | }, 1017 | "hearable": { 1018 | "on": [ 1019 | "listen", 1020 | "listenEnd" 1021 | ] 1022 | }, 1023 | "locationTrackable": { 1024 | "on": [ 1025 | "locationChange" 1026 | ] 1027 | } 1028 | }, 1029 | "watch_sony-smartwatch3": { 1030 | "showable": { 1031 | "size": "small", 1032 | "shape": "square", 1033 | "glanceability": "high", 1034 | "interruption": "low", 1035 | "privacy": "low" 1036 | }, 1037 | "shakable": { 1038 | "size": "small", 1039 | "on": [ 1040 | "shake" 1041 | ] 1042 | }, 1043 | "rotatable": { 1044 | "size": "small", 1045 | "on": [ 1046 | "rotateCCW", 1047 | "rotateCW" 1048 | ] 1049 | }, 1050 | "touchable": { 1051 | "size": "small", 1052 | "on": [ 1053 | "tap", 1054 | "doubleTap", 1055 | "longTap", 1056 | "swipeLeft", 1057 | "swipeRight", 1058 | "swipeUp", 1059 | "swipeDown" 1060 | ] 1061 | }, 1062 | "keyable": { 1063 | "method": "screen", 1064 | "on": [ 1065 | "keypress" 1066 | ] 1067 | }, 1068 | "phoneable": { 1069 | "privacy": "low", 1070 | "on": [ 1071 | "callStart", 1072 | "callEnd" 1073 | ] 1074 | }, 1075 | "speakable": { 1076 | "privacy": "low", 1077 | "on": [ 1078 | "playStart", 1079 | "playEnd" 1080 | ] 1081 | }, 1082 | "hearable": { 1083 | "on": [ 1084 | "listen", 1085 | "listenEnd" 1086 | ] 1087 | }, 1088 | "locationTrackable": { 1089 | "on": [ 1090 | "locationChange" 1091 | ] 1092 | } 1093 | }, 1094 | "eyewear_google-glass": { 1095 | "showable": { 1096 | "size": "large", 1097 | "shape": "square", 1098 | "glanceability": "high", 1099 | "interruption": "high", 1100 | "privacy": "high" 1101 | }, 1102 | "shakable": { 1103 | "size": "large", 1104 | "on": [ 1105 | "shake" 1106 | ] 1107 | }, 1108 | "touchable": { 1109 | "size": "small", 1110 | "on": [ 1111 | "tap", 1112 | "doubleTap", 1113 | "longTap", 1114 | "swipeLeft", 1115 | "swipeRight", 1116 | "swipeUp", 1117 | "swipeDown" 1118 | ] 1119 | }, 1120 | "keyable": { 1121 | "method": "pad", 1122 | "on": [ 1123 | "keypress" 1124 | ] 1125 | }, 1126 | "phoneable": { 1127 | "privacy": "high", 1128 | "on": [ 1129 | "callStart", 1130 | "callEnd" 1131 | ] 1132 | }, 1133 | "speakable": { 1134 | "privacy": "high", 1135 | "on": [ 1136 | "playStart", 1137 | "playEnd" 1138 | ] 1139 | }, 1140 | "hearable": { 1141 | "on": [ 1142 | "listen", 1143 | "listenEnd" 1144 | ] 1145 | }, 1146 | "recordable": { 1147 | "on": [ 1148 | "recordStart", 1149 | "recordEnd" 1150 | ] 1151 | }, 1152 | "locationTrackable": { 1153 | "on": [ 1154 | "locationChange" 1155 | ] 1156 | } 1157 | }, 1158 | "keyboard_mac-keyboard": { 1159 | "keyable": { 1160 | "method": "key", 1161 | "on": [ 1162 | "keypress" 1163 | ] 1164 | } 1165 | }, 1166 | "wristband_jawbone-up4": { 1167 | "showable": { 1168 | "size": "small", 1169 | "shape": "long", 1170 | "glanceability": "high", 1171 | "interruption": "low", 1172 | "privacy": "low" 1173 | }, 1174 | "shakable": { 1175 | "size": "small", 1176 | "on": [ 1177 | "shake" 1178 | ] 1179 | }, 1180 | "touchable": { 1181 | "size": "small", 1182 | "on": [ 1183 | "tap" 1184 | ] 1185 | }, 1186 | "locationTrackable": { 1187 | "on": [ 1188 | "locationChange" 1189 | ] 1190 | } 1191 | }, 1192 | "wristband_fitbit-surge": { 1193 | "showable": { 1194 | "size": "small", 1195 | "shape": "square", 1196 | "glanceability": "high", 1197 | "interruption": "low", 1198 | "privacy": "low" 1199 | }, 1200 | "shakable": { 1201 | "size": "small", 1202 | "on": [ 1203 | "shake" 1204 | ] 1205 | }, 1206 | "touchable": { 1207 | "size": "small", 1208 | "on": [ 1209 | "tap" 1210 | ] 1211 | }, 1212 | "locationTrackable": { 1213 | "on": [ 1214 | "locationChange" 1215 | ] 1216 | } 1217 | }, 1218 | "lightbulb_philips-lightbulb": { 1219 | "showable": { 1220 | "size": "small", 1221 | "shape": "dot", 1222 | "glanceability": "high", 1223 | "interruption": "low", 1224 | "privacy": "low" 1225 | } 1226 | } 1227 | }, 1228 | "devices": { 1229 | "phone_nexus5": { 1230 | "id": "phone_nexus5", 1231 | "fullname": "Nexus 5", 1232 | "type": "phone", 1233 | "name": "Nexus5", 1234 | "os": "Android", 1235 | "joint": "hand", 1236 | "img": "nexus5.jpg" 1237 | }, 1238 | "phone_nexus6": { 1239 | "id": "phone_nexus6", 1240 | "fullname": "Nexus 6", 1241 | "type": "phone", 1242 | "name": "Nexus6", 1243 | "os": "Android", 1244 | "joint": "hand", 1245 | "img": "nexus6.png" 1246 | }, 1247 | "phone_iphon6": { 1248 | "id": "phone_iphon6", 1249 | "fullname": "iPhone6", 1250 | "type": "phone", 1251 | "name": "iPhon6", 1252 | "os": "iOS", 1253 | "joint": "hand", 1254 | "img": "iphone6.jpg" 1255 | }, 1256 | "phone_lumia520": { 1257 | "id": "phone_lumia520", 1258 | "fullname": "Nokia Lumia 520", 1259 | "type": "phone", 1260 | "name": "Lumia520", 1261 | "os": "Windows", 1262 | "joint": "hand", 1263 | "img": "nokia-lumia-520.jpg" 1264 | }, 1265 | "phone_galaxy-s5": { 1266 | "id": "phone_galaxy-s5", 1267 | "fullname": "Samsung Galaxy S5", 1268 | "type": "phone", 1269 | "name": "Galaxy-S5", 1270 | "os": "Android", 1271 | "joint": "hand", 1272 | "img": "galaxy-s-5.jpg" 1273 | }, 1274 | "phone_xperia-z3": { 1275 | "id": "phone_xperia-z3", 1276 | "fullname": "Sony Xperia Z3", 1277 | "type": "phone", 1278 | "name": "Xperia-Z3", 1279 | "os": "Android", 1280 | "joint": "hand", 1281 | "img": "sony-xperia-z3.jpg" 1282 | }, 1283 | "phone_galaxy-note4": { 1284 | "id": "phone_galaxy-note4", 1285 | "fullname": "Samsung Galaxy Note 4", 1286 | "type": "phone", 1287 | "name": "Galaxy-Note4", 1288 | "os": "Android", 1289 | "joint": "hand", 1290 | "img": "samsung-galaxy-note4.jpg" 1291 | }, 1292 | "phone_galaxy-s6-edge": { 1293 | "id": "phone_galaxy-s6-edge", 1294 | "fullname": "Samsung Galaxy S6 Edge", 1295 | "type": "phone", 1296 | "name": "Galaxy-S6-Edge", 1297 | "os": "Android", 1298 | "joint": "hand", 1299 | "img": "galaxy-s6-edge.jpg" 1300 | }, 1301 | "phone_htc-one-m9": { 1302 | "id": "phone_htc-one-m9", 1303 | "fullname": "HTC One M9", 1304 | "type": "phone", 1305 | "name": "HTC-One-M9", 1306 | "os": "Android", 1307 | "joint": "hand", 1308 | "img": "htc-one-m9.png" 1309 | }, 1310 | "tablet_nexus9": { 1311 | "id": "tablet_nexus9", 1312 | "fullname": "Nexus 9", 1313 | "type": "tablet", 1314 | "name": "Nexus9", 1315 | "os": "Android", 1316 | "joint": "hand", 1317 | "img": "nexus9.png" 1318 | }, 1319 | "tablet_galaxy-tab4": { 1320 | "id": "tablet_galaxy-tab4", 1321 | "fullname": "Samsung Galaxy Tab 4", 1322 | "type": "tablet", 1323 | "name": "Galaxy-Tab4", 1324 | "os": "Android", 1325 | "joint": "hand", 1326 | "img": "Galaxy-tab-4.jpg" 1327 | }, 1328 | "tablet_surface3": { 1329 | "id": "tablet_surface3", 1330 | "fullname": "Microsoft Surface 3", 1331 | "type": "tablet", 1332 | "name": "Surface3", 1333 | "os": "Windows", 1334 | "joint": "hand", 1335 | "img": "Surface3.jpg" 1336 | }, 1337 | "watch_lg-g-watch": { 1338 | "id": "watch_lg-g-watch", 1339 | "fullname": "LG G watch", 1340 | "type": "watch", 1341 | "name": "LG-G-watch", 1342 | "os": "Android", 1343 | "joint": "wrist", 1344 | "img": "wear-lg-g.png" 1345 | }, 1346 | "watch_moto360": { 1347 | "id": "watch_moto360", 1348 | "fullname": "Moto 360", 1349 | "type": "watch", 1350 | "name": "Moto360", 1351 | "os": "Android", 1352 | "joint": "wrist", 1353 | "img": "wear-moto-360.png" 1354 | }, 1355 | "watch_samsung-gear-llive": { 1356 | "id": "watch_samsung-gear-llive", 1357 | "fullname": "Samsung Gear Live", 1358 | "type": "watch", 1359 | "name": "Samsung-Gear-Llive", 1360 | "os": "Android", 1361 | "joint": "wrist", 1362 | "img": "wear-samsung-gear-live.png" 1363 | }, 1364 | "watch_sony-smartwatch3": { 1365 | "id": "watch_sony-smartwatch3", 1366 | "fullname": "Sony SmartWatch 3", 1367 | "type": "watch", 1368 | "name": "Sony-Smartwatch3", 1369 | "os": "Android", 1370 | "joint": "wrist", 1371 | "img": "wear-sony-smart-watch-3.png" 1372 | }, 1373 | "eyewear_google-glass": { 1374 | "id": "eyewear_google-glass", 1375 | "fullname": "Google Glass", 1376 | "type": "eyewear", 1377 | "name": "Google-Glass", 1378 | "os": "Android", 1379 | "joint": "head", 1380 | "img": "google-glass.jpg" 1381 | }, 1382 | "keyboard_mac-keyboard": { 1383 | "id": "keyboard_mac-keyboard", 1384 | "fullname": "Mac Keyboard", 1385 | "type": "keyboard", 1386 | "name": "Mac-Keyboard", 1387 | "joint": "hand", 1388 | "img": "mackeyboard.jpg" 1389 | }, 1390 | "wristband_jawbone-up4": { 1391 | "id": "wristband_jawbone-up4", 1392 | "fullname": "Jawbone up4", 1393 | "type": "wristband", 1394 | "name": "Jawbone-Up4", 1395 | "os": "Jawbone", 1396 | "joint": "wrist", 1397 | "img": "jawbone-up.png" 1398 | }, 1399 | "wristband_fitbit-surge": { 1400 | "id": "wristband_fitbit-surge", 1401 | "fullname": "Fitbit Surge", 1402 | "type": "wristband", 1403 | "name": "Fitbit-Surge", 1404 | "os": "Fitbit", 1405 | "joint": "wrist", 1406 | "img": "fitbit-surge.jpg" 1407 | }, 1408 | "lightbulb_philips-lightbulb": { 1409 | "id": "lightbulb_philips-lightbulb", 1410 | "fullname": "Philips Lightbulb", 1411 | "type": "lightbulb", 1412 | "name": "Philips-Lightbulb", 1413 | "joint": "", 1414 | "img": "philips-lightbulb.jpg" 1415 | } 1416 | } 1417 | } -------------------------------------------------------------------------------- /viewer/device/deviceTemplate.html: -------------------------------------------------------------------------------- 1 |
3 |
4 |
5 |
DEVICE_TYPE
6 |
DEVICE_NAME
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | 20 |
-------------------------------------------------------------------------------- /viewer/img/app/calendar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/app/calendar.png -------------------------------------------------------------------------------- /viewer/img/app/gmail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/app/gmail.png -------------------------------------------------------------------------------- /viewer/img/app/maps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/app/maps.png -------------------------------------------------------------------------------- /viewer/img/app/photos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/app/photos.png -------------------------------------------------------------------------------- /viewer/img/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/close.png -------------------------------------------------------------------------------- /viewer/img/device/nexus5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/device/nexus5.png -------------------------------------------------------------------------------- /viewer/img/device/nexus9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/device/nexus9.png -------------------------------------------------------------------------------- /viewer/img/device/wear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/device/wear.png -------------------------------------------------------------------------------- /viewer/img/down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/down.png -------------------------------------------------------------------------------- /viewer/img/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/folder.png -------------------------------------------------------------------------------- /viewer/img/folder_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/folder_open.png -------------------------------------------------------------------------------- /viewer/img/icon/Calendar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/icon/Calendar.png -------------------------------------------------------------------------------- /viewer/img/icon/Gmail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/icon/Gmail.png -------------------------------------------------------------------------------- /viewer/img/icon/maps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/icon/maps.png -------------------------------------------------------------------------------- /viewer/img/icon/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/icon/next.png -------------------------------------------------------------------------------- /viewer/img/icon/photos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/icon/photos.png -------------------------------------------------------------------------------- /viewer/img/icon/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/icon/play.png -------------------------------------------------------------------------------- /viewer/img/icon/prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/icon/prev.png -------------------------------------------------------------------------------- /viewer/img/left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/left.png -------------------------------------------------------------------------------- /viewer/img/listen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/listen.png -------------------------------------------------------------------------------- /viewer/img/logo/chord-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/logo/chord-128.png -------------------------------------------------------------------------------- /viewer/img/logo/chord-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/logo/chord-16.png -------------------------------------------------------------------------------- /viewer/img/phonecall.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/phonecall.jpg -------------------------------------------------------------------------------- /viewer/img/right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/right.png -------------------------------------------------------------------------------- /viewer/img/rotateCCW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/rotateCCW.png -------------------------------------------------------------------------------- /viewer/img/rotateCW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/rotateCW.png -------------------------------------------------------------------------------- /viewer/img/run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/run.png -------------------------------------------------------------------------------- /viewer/img/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/sample.png -------------------------------------------------------------------------------- /viewer/img/shake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/shake.png -------------------------------------------------------------------------------- /viewer/img/talk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/talk.png -------------------------------------------------------------------------------- /viewer/img/up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/img/up.png -------------------------------------------------------------------------------- /viewer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Chord: Designing Cross-Device Interactions 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |

17 | Chord 18 |
19 |
20 |
21 | (select a folder) 22 |

23 |
24 | 25 |
26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 |
34 | log 35 |
36 | 37 | 38 |
39 |
40 |
41 | 42 | 43 | -------------------------------------------------------------------------------- /viewer/js/chord.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 The Chord Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Implements the Chord framework that handles 19 | * the device selection, actions, and events. 20 | * @author peggychi@cs.berkeley.edu (Peggy Chi) 21 | */ 22 | 23 | chord = (function() { 24 | /** 25 | * Whether this Chord object is running on server or web. 26 | * @type {boolean} 27 | */ 28 | var isWebUI = typeof window !== 'undefined'; 29 | /** 30 | * The Chord engine. 31 | * @type {!Object} 32 | */ 33 | var chord = { 34 | /** 35 | * On-network device list. 36 | * @type {!Array} 37 | */ 38 | devices: [], 39 | /** 40 | * Global device list. 41 | * @type {!Array} 42 | */ 43 | fullDevices: [], 44 | /** 45 | * Device templates by type. 46 | * @type {!Object} 47 | */ 48 | deviceTemplates: {}, 49 | /** 50 | * Emulator types and numbers. 51 | * @type {!Object} 52 | */ 53 | numEmulatedDevices: {}, 54 | /** 55 | * Device specification. 56 | * @type {!Object} 57 | */ 58 | deviceList: {}, 59 | /** 60 | * Device capability specification. 61 | * @type {!Object} 62 | */ 63 | deviceCapabilities: {}, 64 | /** 65 | * Device capability list. 66 | * @type {!Array} 67 | */ 68 | capabilityList: [], 69 | /** 70 | * The map between device actions and capabilities. 71 | * @type {!Object} 72 | */ 73 | actionCapabilityMap: {}, 74 | /** 75 | * Available device types. 76 | * @type {!Object} 77 | */ 78 | deviceType: {}, 79 | /** 80 | * Device property list. 81 | * @type {!Array} 82 | */ 83 | propertyList: ['name', 'type', 'joint', 'os'], 84 | /** 85 | * Application launch option. 86 | * @type {!Object} 87 | */ 88 | launchOption: { 89 | default: 'default', 90 | notification: 'notification' 91 | }, 92 | /** 93 | * HTML layouts. 94 | * @type {!Object} 95 | */ 96 | layouts: {}, 97 | /** 98 | * Current user ID. 99 | * @type {string} 100 | */ 101 | userId: null, 102 | /** 103 | * Current username. 104 | * @type {string} 105 | */ 106 | username: null, 107 | /** 108 | * Existing user IDs. 109 | * @type {Array} 110 | */ 111 | allUsers: null, 112 | /** 113 | * Path to the script directory. 114 | * @type {string} 115 | */ 116 | scriptDir: '', 117 | 118 | /** @constructor */ 119 | init: function() { 120 | }, 121 | 122 | /** 123 | * Selects a set of devices that match the selector. 124 | * @param {string} selectorStr The device selection criteria. 125 | * @param {boolean=} skipReport 126 | * Whether this should not report to the server. 127 | * @return {!ChordSelection} A collection of devices. 128 | */ 129 | select: function(selector, report) { 130 | if (typeof(selector) !== 'object') { 131 | selector = this.parseSelector(selector.toLowerCase()); 132 | } 133 | var selection = new ChordSelection(selector, this.getDevices(selector)); 134 | return selection; 135 | }, 136 | 137 | /** 138 | * Selects all the devices on the network. 139 | * @return {!ChordSelection} A collection of devices. 140 | */ 141 | selectAll: function() { 142 | return this.select('*'); 143 | }, 144 | 145 | /** 146 | * Changes all the devices on the network to behave the same. 147 | * @param {Object=} option Details options to manipulate the mode. 148 | * @return {!ChordSelection} A collection of devices in the 'all' mode. 149 | */ 150 | all: function(option) { 151 | var devices = this.selectAll(); 152 | devices.setMode(SelectionMode.all); 153 | if (option !== undefined) { 154 | devices.setOption(option); 155 | } 156 | return devices; 157 | }, 158 | 159 | /** 160 | * Combines devices on the network as one virtual device. 161 | * @param {Object=} option Details options to manipulate the mode. 162 | * @return {!ChordSelection} A collection of devices in the 'combine' mode. 163 | */ 164 | combine: function(option) { 165 | var devices = this.selectAll(); 166 | devices.setMode(SelectionMode.combine); 167 | if (option !== undefined) { 168 | devices.setOption(option); 169 | } 170 | return devices; 171 | }, 172 | 173 | /** 174 | * Retrieves a device by its ID. 175 | * @param {string} id The device id to be found. 176 | * @return {Device} The device of the specific ID. 177 | */ 178 | getDeviceById: function(id) { 179 | var idx = this.getDeviceIdx(id); 180 | return (idx >= 0) ? this.devices[this.getDeviceIdx(id)] : null; 181 | }, 182 | 183 | /** 184 | * Retrieves a set of device by joint. 185 | * @param {string} joint The joint of interest. 186 | * @return {!ChordSelection} The devices that match the joint. 187 | */ 188 | getDeviceByJoint: function(joint) { 189 | var matchedDevices = []; 190 | for (var i = 0, device; device = this.devices[i]; i++) { 191 | if (device.joint === joint) { 192 | matchedDevices.push(device); 193 | } 194 | } 195 | return new ChordSelection(null, matchedDevices); 196 | }, 197 | 198 | /** 199 | * Retrieves a set of device by type. 200 | * @param {string} type The device type of interest. 201 | * @return {!ChordSelection} The devices that match the type. 202 | */ 203 | getDeviceByType: function(type) { 204 | var matchedDevices = []; 205 | for (var i = 0, device; device = this.devices[i]; i++) { 206 | if (device.type === type) { 207 | matchedDevices.push(device); 208 | } 209 | } 210 | return new ChordSelection(null, matchedDevices); 211 | }, 212 | 213 | /** 214 | * Retrieves a list of devices that match the selector. 215 | * @param {!Object} selector The device selector. 216 | * @return {!Array} A list of devices that match the selector. 217 | */ 218 | getDevices: function(selector) { 219 | var matchedDevices = []; 220 | for (var i = 0, device; device = this.devices[i]; i++) { 221 | if (this.deviceSupports(device, selector)) { 222 | matchedDevices.push(device); 223 | } 224 | } 225 | return matchedDevices; // TODO: sorting 226 | }, 227 | 228 | /** 229 | * Retrieves the index of a device by its ID. 230 | * @param {string} id The device id to be found. 231 | * @return {number} The device index on the device list, 232 | * or -1 if the device doesn't exist. 233 | */ 234 | getDeviceIdx: function(id) { 235 | for (var i = this.devices.length - 1; i >= 0; i--) { 236 | if (this.devices[i].id === id) { 237 | return i; 238 | } 239 | } 240 | return -1; 241 | }, 242 | 243 | /** 244 | * Returns the current username. 245 | * @return {string} The current username. 246 | */ 247 | getUserName: function() { 248 | return this.username; 249 | }, 250 | 251 | /** 252 | * Return the full list of usernames. 253 | * @return {!Array} Full list of usernames. 254 | */ 255 | getUsers: function() { 256 | return this.allUsers; 257 | }, 258 | 259 | /** 260 | * Parses the device selector string. 261 | * @param {string} selectorStr The device selector. 262 | * @return {!Object} The structured selector by capabilities and criteria. 263 | */ 264 | parseSelector: function(selectorStr) { 265 | var finalSelector = {'selectorStr': selectorStr}; 266 | if ([undefined, 'none'].indexOf(selectorStr) >= 0) { 267 | return finalSelector; 268 | } 269 | else if (['all', 'any'].indexOf(selectorStr) >= 0) { 270 | finalSelector['*'] = {}; 271 | return finalSelector; 272 | } 273 | // Break into the top level nodes. 274 | // e.g. '.showable[size="small"][joint="wrist"],.touchable' 275 | // => '.showable[size="small"][joint="wrist"]' and ',.touchable' 276 | selectorStr = selectorStr.replace(/ /g, ''); // remove all spaces 277 | var selectList = []; 278 | var orGroups = selectorStr.split(/(?=,\.|,\*|,\#|,:)/g); 279 | for (var i = 0; i < orGroups.length; i++) { 280 | var capGroup = orGroups[i]; 281 | if (capGroup[0] === ',') { 282 | capGroup = capGroup.substring(1, capGroup.length); 283 | } 284 | var subCapGroups = capGroup.split('.'); 285 | var selector = {}; 286 | for (var j = 0; j < subCapGroups.length; j++) { 287 | var subCapGroup = subCapGroups[j]; 288 | if (subCapGroup !== '') { 289 | var strings = subCapGroup.match(/[*\w\-:]+/g); 290 | var subCap = strings[0]; // showable 291 | var ruleSet = {}; 292 | var attributes = subCapGroup.match(/\w+="[\w,]+"/g); // size="small" 293 | for (var k = 0; attributes && k < attributes.length; k++) { 294 | var attr = attributes[k].replace(/\"/g, '').split('='); 295 | var val = attr[1]; 296 | ruleSet[attr[0]] = {}; // size 297 | if (val.indexOf(',') >= 0) { 298 | ruleSet[attr[0]]['or'] = val.split(','); // small, medium 299 | } else { 300 | ruleSet[attr[0]]['and'] = [val]; // small 301 | } 302 | } 303 | if (subCapGroup.indexOf('#') >= 0) { 304 | ruleSet['name'] = subCapGroup.split('#')[1]; // #moto360 305 | subCap = '*'; 306 | } 307 | if (!(subCap.slice(0, 1) === ':')) { 308 | selector[subCap] = ruleSet; 309 | } else { 310 | ruleSet['type'] = {and: [subCap.replace(/:/g, '')]}; 311 | selector['*'] = ruleSet; 312 | } 313 | } 314 | } 315 | selectList.push(selector); 316 | } 317 | finalSelector[orGroups.length > 1 ? 'or' : 'and'] = selectList; 318 | return finalSelector; 319 | }, 320 | 321 | /** 322 | * Determines if the device fulfills the selector criteria. 323 | * @param {!Device} device The device to be examined. 324 | * @param {!Object} selector The device selector. 325 | * @return {boolean} Whether the device fulfills the selector criteria. 326 | */ 327 | deviceSupports: function(device, selector) { 328 | if (typeof selector === 'string') { 329 | selector = parseSelector(selector); 330 | } 331 | var list = selector.and || selector.or; 332 | var matched = true; 333 | for (var i = 0, l; l = list[i]; i++) { 334 | for (var key in l) { // showable 335 | var conditions = l[key]; // [shape="round"][size="small"] 336 | var totalCondition = Object.keys(conditions).length; 337 | var matchedCondition = 0; 338 | if ((key === '*' && !deviceHasProperties(device, conditions)) || 339 | (key !== '*' && device.capability[key] === undefined) || 340 | (key !== '*' && !(totalCondition === 0 || 341 | deviceSupportConstraints(device, conditions, 342 | device.capability[key])))) { 343 | matched = false; 344 | if (selector.and) break; 345 | } else { 346 | matched = true; 347 | } 348 | } 349 | if (!matched && selector.and) return false; 350 | if (matched && selector.or) return true; 351 | } 352 | return matched; 353 | 354 | function deviceHasProperties(device, selector) { 355 | var matched = true; 356 | for (var cap in selector) { 357 | var vals = selector[cap].and || selector[cap].or || [selector[cap]]; 358 | for (var i = 0, v; v = vals[i]; i++) { 359 | if (device[cap] !== v) { 360 | if (selector[cap].and || selector[cap].or === undefined) { 361 | return false; 362 | } 363 | else if (i === 0) { 364 | matched = false; 365 | } 366 | } else { 367 | matched = true; 368 | } 369 | } 370 | } 371 | return matched; 372 | } 373 | 374 | /** 375 | * Determines if the device fulfills the general selector criteria. 376 | * @param {!Device} device The device to be examined. 377 | * @param {!Object} selector The device selector. 378 | * @param {Object=} cap The device capability. 379 | * @return {boolean} Whether the device fulfills the selector criteria. 380 | */ 381 | function deviceSupportConstraints(device, selector, capability) { 382 | var matched = true; 383 | for (var cap in selector) { // shape 384 | var val = selector[cap]; // or: round,square 385 | var values = val.and || val.or; 386 | for (var i = 0, v; v = values[i]; i++) { 387 | if (capability === undefined || capability[cap] !== v) { 388 | if (val.and) return false; 389 | else matched = false; 390 | } 391 | else { 392 | matched = true; 393 | } 394 | } 395 | } 396 | return matched; 397 | } 398 | }, 399 | 400 | /** 401 | * Creates a Chord-capable device. 402 | * @param {string} id The device id. 403 | * @param {string} type The device type. 404 | * @param {string} name The device name. 405 | * @param {!boolean} live 406 | * Whether it should a physical device on the network. 407 | * @return {!Device} The Chord-capable device. 408 | */ 409 | createDevice: function(id, type, name, live) { 410 | var device = this.deviceTemplates[type].cloneDevice({}); 411 | device.id = id; 412 | return device; 413 | }, 414 | 415 | /** 416 | * Removes a device from the device list by its ID. 417 | * @param {string} id The device id to be removed. 418 | * @return {!boolean} Whether the deletion is successful. 419 | */ 420 | deleteDevice: function(id) { 421 | var i = this.getDeviceIdx(id); 422 | if (i >= 0) { 423 | this.devices.splice(i, 1); 424 | return true; 425 | } 426 | return false; 427 | }, 428 | 429 | /** 430 | * Resets all the devices. 431 | */ 432 | resetDevices: function() { 433 | for (var i = this.devices.length - 1; i >= 0; i--) { 434 | this.devices[i].reset(); 435 | } 436 | }, 437 | 438 | /** 439 | * Creates Chord-capable device objects. 440 | * @param {!Object} capabilities Device capabilities by types. 441 | * @param {!Object} deviceList A list of chord-capable devices. 442 | */ 443 | createDevices: function(capabilities, deviceList) { 444 | var self = this; 445 | this.deviceList = deviceList; 446 | this.deviceCapabilities = capabilities; 447 | createDeviceList(capabilities, deviceList); 448 | 449 | /** 450 | * Creates a list of devices, each with capabilities defined. 451 | * @param {!Object} capabilities Device capabilities by types. 452 | * @param {!Object} deviceList A list of chord-capable devices. 453 | */ 454 | function createDeviceList(capabilities, deviceList) { 455 | self.capabilityList = []; 456 | self.deviceType = {}; 457 | for (var deviceId in deviceList) { 458 | var info = deviceList[deviceId]; 459 | var device = new Device(info.type, info.name, info.fullname, 460 | info.joint, info.id, capabilities[deviceId], false); 461 | // add a new device type 462 | if (self.deviceType[info.type] === undefined) { 463 | self.deviceType[info.type] = info.type; 464 | } 465 | // update info 466 | for (var key in info) { 467 | if (self.propertyList.indexOf(key) >= 0) { 468 | device[key.toLowerCase()] = info[key].toLowerCase(); 469 | } 470 | } 471 | // find possible actions 472 | for (var cap in device.capability) { 473 | if (cap.indexOf('.') < 0 && self.capabilityList.indexOf(cap) < 0) { 474 | self.capabilityList.push(cap); 475 | } 476 | var actions = device.capability[cap].on; 477 | if (actions !== undefined) { 478 | for (var i = 0; i < actions.length; i++) { 479 | self.actionCapabilityMap[actions[i]] = cap; 480 | } 481 | } 482 | } 483 | if (self.deviceTemplates[device.type] === undefined) { 484 | self.deviceTemplates[device.type] = device; 485 | } 486 | self.fullDevices.push(device); 487 | } 488 | } 489 | }, 490 | 491 | /** 492 | * Sets up a full list of Chord-capable devices. 493 | * @param {!Object} capabilities Device capabilities by types. 494 | * @param {!Object} deviceList A list of chord-capable devices. 495 | */ 496 | setup: function(capabilities, deviceList) { 497 | this.createDevices(capabilities, deviceList); 498 | this.devices = this.fullDevices; 499 | }, 500 | 501 | /** 502 | * Sets up devices for the web UI with emulators. 503 | * @param {!Object} capabilities Device capabilities by types. 504 | * @param {!Object} deviceList A list of chord-capable devices. 505 | */ 506 | setupWeb: function(capabilities, deviceList) { 507 | this.createDevices(capabilities, deviceList); 508 | this.createEmulatedDevices(); 509 | // this.devices = this.fullDevices; // TODO: get this back, debugging only 510 | }, 511 | 512 | /** 513 | * Creates emulators. 514 | */ 515 | createEmulatedDevices: function(callback) { 516 | this.devices = []; 517 | var emulators = Object.keys(this.numEmulatedDevices); 518 | if (Object.keys(this.numEmulatedDevices).length > 0) { 519 | for (var deviceType in this.numEmulatedDevices) { 520 | var val = this.numEmulatedDevices[deviceType]; 521 | if (typeof(val) !== 'function') { 522 | for (var i = 0; i < val; i++) { 523 | var device = this.createDevice( 524 | deviceType + i, deviceType, null, false); 525 | this.devices.push(device); 526 | } 527 | } 528 | } 529 | callback(); 530 | } 531 | }, 532 | 533 | /** 534 | * Sets numbers of emulators. 535 | * @param {!Object} numEmulatedDevices 536 | * Specifies numbers of emulated devices. 537 | * @param {boolean=} keeplive 538 | * Whether to include live devices on the network. 539 | */ 540 | setEmulatedDevices: function(numEmulatedDevices, keeplive, callback) { 541 | if (keeplive) { 542 | for (var i = this.devices.length - 1; i >= 0; i--) { 543 | if (!this.devices[i].live) { // remove device emulators 544 | this.devices.splice(i, 1); 545 | } 546 | } 547 | } 548 | this.numEmulatedDevices = numEmulatedDevices; 549 | if (this.fullDevices.length > 0) { 550 | this.createEmulatedDevices(callback); 551 | } 552 | }, 553 | 554 | /** 555 | * Adds a new emulator to the current emulator list. 556 | * @param {string} id The device id. 557 | * @param {string} type The device type. 558 | * @param {string} name The device name. 559 | */ 560 | addEmulatedDevices: function(id, type, name) { 561 | if (id === null) id = Math.random().toString(36).substring(7); 562 | var device = this.createDevice(id, type, name, false); 563 | this.devices.unshift(device); 564 | Log.v('Created an emulated device (' + type + ')'); 565 | }, 566 | 567 | /** 568 | * Updates the available device list. 569 | * @param {!boolean} newAdded Whether this is a new device to be added. 570 | * @param {string} id The device id to be added. 571 | * @param {string} type The device type to be added. 572 | * @param {string} name The device name to be added. 573 | * @return {!boolean} Whether the update is successful. 574 | */ 575 | updateNetworkDevices: function(newAdded, id, type, name) { 576 | if (newAdded) { 577 | for (var i = 0, d; d = this.devices[i]; i++) { 578 | if (d.id === id) { 579 | Log.e('Redundant device; ignore registration'); 580 | return true; 581 | } 582 | } 583 | // prepend to the array 584 | var device = this.createDevice(id, type, name, true); 585 | this.devices.unshift(device); 586 | Log.v('A network device was added.'); 587 | return true; 588 | } else { 589 | var success = this.deleteDevice(id); 590 | if (success) Log.v('A network device went offline'); 591 | return success; 592 | } 593 | }, 594 | 595 | /** 596 | * Records a list of HTML layouts. 597 | * @param {!Array} layouts A list of HTML by layout IDs. 598 | */ 599 | loadLayouts: function(layouts) { 600 | this.layouts = layouts; 601 | return; 602 | }, 603 | 604 | /** 605 | * Retrieves a HTML layout by ID. 606 | * @param {string} id The layout ID. 607 | * @return {string} The HTML layout of the id. 608 | */ 609 | getLayoutById: function(id) { 610 | var layout = this.layouts[id]; 611 | return layout === undefined ? null : layout; 612 | }, 613 | 614 | /** 615 | * Sets the path to the script directory. 616 | * @param {string} path The directory path. 617 | */ 618 | setDir: function(path) { 619 | this.scriptDir = path; 620 | } 621 | }; 622 | 623 | /** 624 | * The selection mode. 625 | * @enum {number} 626 | */ 627 | var SelectionMode = { 628 | default: 0, 629 | all: 1, 630 | combine: 2 631 | }; 632 | 633 | /** 634 | * The class representing a Chord device selection set. 635 | * @param {!Object} selector The device selector. 636 | * @param {!Array} devices List of devices. 637 | * @constructor 638 | */ 639 | var ChordSelection = function(selector, devices) { 640 | if (!(this instanceof ChordSelection)) 641 | return new ChordSelection(selector, devices); 642 | var devices = devices; 643 | var selector = selector; 644 | var mode = SelectionMode.default; 645 | var option = {timeRange: 1000}; // default: 1 second 646 | var numElements = {}; 647 | var UIelements = []; 648 | var id = Math.random().toString(36).substring(5); 649 | 650 | /** 651 | * Detects available Chord-capable devices in the device set. 652 | * @param {string} action The action. 653 | * @return {!boolean} Whether there is any device available. 654 | */ 655 | function canRunService(action) { 656 | if (devices.length <= 0) { 657 | Log.e('No device available to run "' + action + '"'); 658 | return false; 659 | } 660 | return true; 661 | } 662 | 663 | /** 664 | * Retrieves one device from the list of devices. 665 | * @return {!Device} A device from this selection. 666 | */ 667 | function getOneDevice() { 668 | var liveDevices = []; // priority: live device 669 | for (var i = 0, device; device = devices[i]; i++) { 670 | if (device.live) { 671 | liveDevices.push(i); 672 | } 673 | } 674 | var idx = function(candidates) { 675 | // randomly choose from a list of live (if any) or all devices 676 | return Math.floor(Math.random() * candidates.length); 677 | }((liveDevices.length > 0) ? 678 | liveDevices : 679 | Array.apply(null, 680 | {length: devices.length - 1}).map(Number.call, Number)); 681 | return devices[idx]; 682 | } 683 | 684 | /** 685 | * Sets this selection to the all mode. 686 | * @param {Object=} option Details options to manipulate the mode. 687 | * @return {!ChordSelection} This collection of devices in 'all' mode. 688 | */ 689 | this.all = function(option) { 690 | this.setMode(SelectionMode.all); 691 | if (option !== undefined) { 692 | this.setOption(option); 693 | } 694 | return this; 695 | }; 696 | 697 | /** 698 | * Sets this selection to the combine mode. 699 | * @param {Object=} option Details options to manipulate the mode. 700 | * @return {!ChordSelection} This collection of devices in 'combine' mode. 701 | */ 702 | this.combine = function(option) { 703 | this.setMode(SelectionMode.combine); 704 | if (option !== undefined) { 705 | this.setOption(option); 706 | } 707 | return this; 708 | }; 709 | 710 | /** 711 | * Excludes devices from the device list. 712 | * @param {Device or ChordSelection} exclusion Device(s) to exclude. 713 | * @return {!ChordSelection} This collection of devices excluding exclusion. 714 | */ 715 | this.not = function(exclusion) { 716 | if (exclusion instanceof Device) { // directly remove the device 717 | removeByIdx(devices.indexOf(exclusion)); 718 | } else if (exclusion instanceof ChordSelection) { 719 | // remove a list of devices 720 | var exludeDevices = exclusion.getDevices(); 721 | for (var i = 0, excluded; excluded = exludeDevices[i]; i++) { 722 | removeByIdx(devices.indexOf(excluded)); 723 | } 724 | } else if (typeof exclusion === 'string') { // remove by selector 725 | var selector = chord.parseSelector(exclusion); 726 | var exludeDevices = new ChordSelection(selector, 727 | chord.getDevices(selector)); 728 | this.not(exludeDevices); 729 | } 730 | return this; 731 | 732 | /** 733 | * Removes a device by its index in the device list. 734 | * @param {Number} idx Index of the device to remove. 735 | */ 736 | function removeByIdx(idx) { 737 | if (idx > -1) { 738 | devices.splice(idx, 1); 739 | } 740 | } 741 | }; 742 | 743 | /** 744 | * Merges device selections. 745 | * @param {ChordSelection} otherSelection Device(s) to append. 746 | * @return {!ChordSelection} This collection of devices. 747 | */ 748 | this.append = function(otherSelection) { 749 | devices = devices.concat(otherSelection.getDevices()); 750 | return this; 751 | }; 752 | 753 | /** 754 | * Sets to a new mode. 755 | * @param {Number} newMode The new selection mode. 756 | * @return {!ChordSelection} This collection of devices. 757 | */ 758 | this.setMode = function(newMode) { 759 | mode = newMode; 760 | return this; 761 | }; 762 | 763 | /** 764 | * Sets new selection option. 765 | * @param {Object} newOption The selection option. 766 | * @return {!ChordSelection} This collection of devices. 767 | */ 768 | this.setOption = function(newOption) { 769 | option = newOption; 770 | return this; 771 | }; 772 | 773 | /** 774 | * Retrieves the list of devices of this selection. 775 | * @return {!Array} A device list. 776 | */ 777 | this.getDevices = function() { 778 | return devices; 779 | }; 780 | 781 | /** 782 | * Retrieves the number of devices of this selection. 783 | * @return {number} Number of devices. 784 | */ 785 | this.size = function() { 786 | if (devices === undefined) return 0; 787 | return devices.length; 788 | }; 789 | 790 | /** 791 | * Retrieves the name(s) of device(s) of this selection. 792 | * @return {string} Name(s) of devices. 793 | */ 794 | this.getDeviceName = function() { 795 | return this.getDeviceNames(); 796 | }; 797 | 798 | /** 799 | * Retrieves the name(s) of device(s) of this selection. 800 | * @return {string} Name(s) of devices. 801 | */ 802 | this.getDeviceNames = function() { 803 | var names = []; 804 | for (var i = 0, device; device = devices[i]; i++) { 805 | names.push(device.name); 806 | } 807 | return names.join(', '); 808 | }; 809 | 810 | /** 811 | * Retrieves the id(s) of device(s) of this selection. 812 | * @return {string} Id(s) of devices. 813 | */ 814 | this.getdeviceIds = function() { 815 | var ids = []; 816 | for (var i = 0, device; device = devices[i]; i++) { 817 | ids.push(device.id); 818 | } 819 | return ids; 820 | }; 821 | 822 | /** 823 | * Retrieves the id of this selection. 824 | * @return {string} Id of this selection. 825 | */ 826 | this.getId = function() { 827 | return id; 828 | }; 829 | 830 | /** 831 | * Retrieves the selector of this selection. 832 | * @return {string} Selector of this selection. 833 | */ 834 | this.getSelector = function() { 835 | return selector; 836 | }; 837 | 838 | /** 839 | * Retrieves the mode of this selection. 840 | * @return {!Object} Mode of this selection. 841 | */ 842 | this.getMode = function() { 843 | return mode; 844 | }; 845 | 846 | /** 847 | * Retrieves the option set for this selection. 848 | * @return {!Object} Option of this selection. 849 | */ 850 | this.getOption = function() { 851 | return option; 852 | }; 853 | 854 | 855 | this.getDeviceHasUIById = function(id) { 856 | var matchedDevices = []; 857 | for (var i = 0, device; device = devices[i]; i++) { 858 | if (device.UIelements !== undefined && device.UIelements.length > 0) { 859 | for (var k = 0, element; element = device.UIelements[k]; k++) { 860 | for (var j = 0, item; item = element.members[j]; j++) { 861 | if (item.id === id) { 862 | matchedDevices.push(device); 863 | } 864 | } 865 | } 866 | } 867 | } 868 | if (matchedDevices.length === 1) { 869 | return matchedDevices[0]; 870 | } else { 871 | return new ChordSelection(null, matchedDevices); 872 | } 873 | }; 874 | 875 | 876 | this.updateUIAttr = function(id, attr, value) { 877 | var devices = this.getDeviceHasUIById(id); 878 | if ((devices instanceof Device)) { 879 | devices.updateUIAttr(id, attr, value); 880 | } else { 881 | for (var i = 0, device; device = devices[i]; i++) { 882 | device.updateUIAttr(id, attr, value); 883 | } 884 | } 885 | }; 886 | 887 | 888 | this.on = function(eventType, fn) { 889 | var selector = chord.parseSelector(eventType); // parse selector 890 | if (canRunService(eventType)) { 891 | if (mode !== SelectionMode.combine) { // default or all mode 892 | var eventManager = new EventManager(this, eventType, fn); 893 | for (var i = 0, device; device = devices[i]; i++) { 894 | device.on(eventType, function(event, manager) { 895 | manager.eventTriggered(event); 896 | }, eventManager); 897 | } 898 | } else { // .combine 899 | var eventManager = new EventManager(this, eventType, fn); 900 | for (var key in selector) { 901 | var action = (key.indexOf(':') >= 0) ? 902 | key.substring(0, key.indexOf(':')) : key; 903 | var capability = chord.actionCapabilityMap[action]; 904 | if (capability !== undefined) { 905 | for (var i = 0, device; device = devices[i]; i++) { 906 | var newSelector = {}; 907 | newSelector[capability] = selector[key]; 908 | if (chord.deviceSupports(device, newSelector)) { 909 | device.on(key, function(event, manager) { 910 | manager.eventTriggered(event); 911 | }, eventManager); 912 | } 913 | } 914 | } 915 | } 916 | } 917 | } 918 | return this; 919 | }; 920 | 921 | this.when = function(eventType) { 922 | return this; 923 | }; 924 | 925 | this.run = function(fn, data) { 926 | data !== undefined ? fn(this, data) : fn(this); // run the function 927 | return this; 928 | }; 929 | 930 | this.show = function(html, fn) { 931 | if (html && canRunService('show')) { 932 | html = resetDir(html); 933 | var analysis = UiManager.getUIElements(html); 934 | this.UIelements = analysis[0]; 935 | this.numElements = analysis[1]; 936 | switch (mode) { 937 | case SelectionMode.default: // pick one to show 938 | getOneDevice().show(html, id, fn, 939 | this.UIelements, this.numElements); 940 | break; 941 | case SelectionMode.all: // show the same content to all 942 | for (var i = 0, device; device = devices[i]; i++) { 943 | device.show(html, id, fn, this.UIelements, this.numElements); 944 | } 945 | break; 946 | case SelectionMode.combine: // distribute the UIs 947 | var image = this.numElements['IMG_row']; 948 | if (image !== undefined) { // assign image 949 | for (var i = 0, device; device = devices[i]; i++) { 950 | if ((device.UIelements === undefined || 951 | device.UIelements.length === 0) && 952 | device.capability.showable !== undefined && 953 | device.capability.showable.size !== 'small') { 954 | // available to show 955 | var toShow = this.UIelements[image[0]]; 956 | var html = UiManager.renderHTML( 957 | toShow.type, toShow.members); 958 | device.show(html, id, fn, [toShow], this.numElements); 959 | break; 960 | } 961 | } 962 | } 963 | // assign the rest 964 | var html = ''; 965 | for (var i = 0, group; group = this.UIelements[i]; i++) { 966 | if (group.type !== 'IMG') { 967 | html += UiManager.renderHTML(group.type, group.members); 968 | } 969 | } 970 | for (var i = 0, device; device = devices[i]; i++) { 971 | if (device.UIelements === undefined || 972 | device.UIelements.length === 0) { 973 | device.show(html, id); 974 | break; 975 | } 976 | } 977 | break; 978 | } 979 | } 980 | return this; 981 | 982 | function resetDir(html) { // redirect the path; TODO: examine path 983 | return html.replace('src="', 'src="' + chord.scriptDir); 984 | } 985 | }; 986 | 987 | this.play = function(filepath) { 988 | if (devices === undefined || devices.length === 0) { 989 | console.log('[chord] no device available'); 990 | return; 991 | } 992 | switch (mode) { 993 | case SelectionMode.default: // pick one to play 994 | getOneDevice().play(filepath, id); 995 | break; 996 | case SelectionMode.all: // play the same content to all 997 | for (var i = 0, device; device = devices[i]; i++) { 998 | device.play(filepath, id); 999 | } 1000 | break; 1001 | case SelectionMode.combine: // pick one to play 1002 | getOneDevice().play(filepath, id); 1003 | break; 1004 | } 1005 | return this; 1006 | }; 1007 | 1008 | this.call = function(calleeNum, fn) { // only one device to make a call 1009 | if (canRunService()) { 1010 | getOneDevice().call(calleeNum, id, fn); 1011 | } 1012 | return this; 1013 | }; 1014 | 1015 | this.wakeup = function() { 1016 | if (canRunService('wakeup')) { 1017 | if (mode === SelectionMode.default) { // pick one 1018 | getOneDevice().wakeup(); 1019 | } else { // show to all or distribute 1020 | for (var i = 0, device; device = devices[i]; i++) { 1021 | device.wakeup(); 1022 | } 1023 | } 1024 | } 1025 | return this; 1026 | }; 1027 | 1028 | this.startApp = function(appName) { 1029 | var suc = false; 1030 | if (canRunService('startApp')) { 1031 | switch (mode) { 1032 | case SelectionMode.default: // pick one 1033 | getOneDevice().startApp(appName, id); 1034 | suc = true; 1035 | break; 1036 | case SelectionMode.all: // show to all 1037 | for (var i = 0, device; device = devices[i]; i++) { 1038 | device.startApp(appName, id); 1039 | } 1040 | suc = true; 1041 | break; 1042 | case SelectionMode.combine: // distribute control panel 1043 | for (var i = 0, device; device = devices[i] && !suc; i++) { 1044 | if (device.capability['showable'].size === 'normal') { 1045 | device.startApp(appName, id); 1046 | suc = true; 1047 | } 1048 | } 1049 | break; 1050 | } 1051 | } 1052 | if (!suc) { 1053 | Log.e('Unable to start the app ' + appName); 1054 | } 1055 | return this; 1056 | }; 1057 | 1058 | this.killApp = function(appName) { 1059 | if (canRunService('killApp')) { 1060 | if (mode === SelectionMode.default) { 1061 | getOneDevice().killApp(appName); 1062 | } else { 1063 | for (var i = 0, device; device = devices[i]; i++) { 1064 | device.killApp(appName); 1065 | } 1066 | } 1067 | } 1068 | return this; 1069 | }; 1070 | }; 1071 | 1072 | /** 1073 | * The class representing a Chord device. 1074 | * @param {string} type The device type. 1075 | * @param {string} name The device name. 1076 | * @param {string} joint Human joint that the device will be operated. 1077 | * @param {string} id The device id. 1078 | * @param {!Object} capabilities Device capabilities. 1079 | * @param {!boolean} live Whether it should a physical device on the network. 1080 | * @constructor 1081 | */ 1082 | var Device = function(type, name, fullname, joint, id, capabilities, live) { 1083 | if (!(this instanceof Device)) 1084 | return new Device(type, name, fullname, joint, id, capabilities, live); 1085 | this.type = type; 1086 | this.name = name; 1087 | this.fullname = fullname; 1088 | this.id = id; 1089 | this.selectionId = null; // the selection id this device attaches to 1090 | this.joint = joint; 1091 | this.live = live; // on network 1092 | this.capability = capabilities; 1093 | this.callbacks = null; 1094 | // UI-related 1095 | this.html = ''; 1096 | this.UIelements = null; 1097 | this.UIcards = []; 1098 | this.UIcardIdx = { 1099 | row: 0, 1100 | col: 0 1101 | }; 1102 | this.panelSetup = { 1103 | id: 'rootPanel', 1104 | maxW: 95, 1105 | maxH: 70, 1106 | minH: 15, 1107 | minW: 20, 1108 | fontSize: 40 1109 | }; 1110 | this.UI = null; 1111 | this.emulator = null; 1112 | this.init(); 1113 | }; 1114 | 1115 | Device.fn = Device.prototype = { 1116 | init: function() { 1117 | this.initCallback(); 1118 | } 1119 | }; 1120 | 1121 | Device.fn.initCallback = function() { 1122 | this.callbacks = {}; 1123 | for (var cap in this.capability) { // add automatic emulator update 1124 | if (this.capability[cap].on != undefined) { 1125 | for (var idx = 0, evt; evt = this.capability[cap].on[idx]; idx++) { 1126 | switch (evt) { 1127 | case 'rotateCCW': 1128 | this.on(evt, function(event) { 1129 | event.getDevice().emulator.rotateCCW(event.getDevice()); 1130 | }); 1131 | break; 1132 | case 'rotateCW': 1133 | this.on(evt, function(event) { 1134 | event.getDevice().emulator.rotateCW(event.getDevice()); 1135 | }); 1136 | break; 1137 | } 1138 | } 1139 | } 1140 | } 1141 | if (this.type === chord.deviceType.watch) { 1142 | var self = this; 1143 | this.on('pageChange', function(event) { 1144 | var info = event.getValue().split(','); 1145 | self.UIcardIdx.row = parseInt(info[0]); 1146 | self.UIcardIdx.col = parseInt(info[1]); 1147 | self.UI = self.emulator.showUI(self, 1148 | self.UIcards[self.UIcardIdx.row][self.UIcardIdx.col]); 1149 | }); 1150 | } 1151 | }; 1152 | 1153 | Device.fn.attr = function(attr, val) { 1154 | if (typeof val !== 'undefined') { 1155 | this[attr] = val; 1156 | } else { 1157 | return this[attr]; 1158 | } 1159 | }; 1160 | 1161 | Device.fn.is = function(capability) { 1162 | return this.capability[capability] !== undefined; 1163 | }; 1164 | 1165 | Device.fn.addEmulator = function(callback) { 1166 | this.emulator = callback; 1167 | }; 1168 | 1169 | Device.fn.getDeviceName = function() { 1170 | return this.name; 1171 | }; 1172 | 1173 | Device.fn.size = function() { 1174 | return 1; 1175 | }; 1176 | 1177 | Device.fn.renderUI = function(html, numElements) { 1178 | if (this.type === chord.deviceType.watch || 1179 | this.type === chord.deviceType.glass) { 1180 | return this.UIelements; 1181 | } else { 1182 | return wrapHtml(html, this.panelSetup, numElements); 1183 | } 1184 | 1185 | function wrapHtml(html, panelSetup, numElements) { 1186 | if (html === '') return html; 1187 | var style = ''; 1216 | return '
' + style + html + '
'; 1217 | }; 1218 | }; 1219 | 1220 | Device.fn.updateUIAttr = function(id, attr, value) { 1221 | var html = '', found = false; 1222 | for (var i = 0, element; element = this.UIelements[i]; i++) { 1223 | for (var j = 0, item; item = element.members[j]; j++) { 1224 | if (item.id === id) { 1225 | this.UIelements[i].members[j][String(attr)] = chord.scriptDir + value; 1226 | found = true; 1227 | break; 1228 | } 1229 | } 1230 | html += UiManager.renderHTML( 1231 | this.UIelements[i].type, this.UIelements[i].members); 1232 | } 1233 | if (found) { 1234 | this.show(html); 1235 | } 1236 | }; 1237 | 1238 | Device.fn.on = function(evt, fn, manager) { 1239 | if (typeof fn == 'function') { // add listener 1240 | if (this.callbacks[evt] === undefined) { 1241 | this.callbacks[evt] = []; 1242 | if (this.live) { 1243 | chordServer.on(this.id, evt); 1244 | } 1245 | } 1246 | // event bubbling: single-device event has higher priority 1247 | this.callbacks[evt].push({fn: fn, manager: manager}); 1248 | this.callbacks[evt].sort(function compare(a, b) { 1249 | if (a.manager === undefined || b.manager === undefined) 1250 | return -1; 1251 | if (a.manager.deviceNum() < b.manager.deviceNum()) 1252 | return -1; 1253 | if (a.manager.deviceNum() > b.manager.deviceNum()) 1254 | return 1; 1255 | return 0; 1256 | }); 1257 | // UI event 1258 | if (this.UI !== null && evt === 'tap:button') { 1259 | this.UI.find('button').click({device: this}, function(event) { 1260 | event.data.device.onUI('tap:button', $(this).attr('value')); 1261 | }); 1262 | } 1263 | } else { // event triggerred 1264 | if (isWebUI) { 1265 | this.emulator.applyClass(this.id, evt); 1266 | } 1267 | var newEvent = (typeof fn === 'string' || fn === undefined) ? 1268 | new SingleDeviceEvent(this, evt, fn === '' ? null : fn) : fn; 1269 | if (this.callbacks[evt] !== undefined) { 1270 | for (var i = 0, listener; listener = this.callbacks[evt][i]; i++) { 1271 | // propagation 1272 | if (listener.manager !== undefined) { // listener by EventManager 1273 | listener.fn(newEvent, listener.manager); 1274 | } else { 1275 | listener.fn(newEvent); // listener that directly hooks 1276 | } 1277 | } 1278 | } 1279 | if (evt.lastIndexOf('swipe', 0) === 0 && this.UIcards.length !== 0) { 1280 | // for UI simulation 1281 | if (this.type === chord.deviceType.watch) { 1282 | switch (evt) { 1283 | case 'swipeLeft': 1284 | if (this.UIcardIdx.col > 0) { 1285 | this.UIcardIdx.col--; 1286 | } 1287 | break; 1288 | case 'swipeRight': 1289 | if (this.UIcardIdx.col + 1 < 1290 | this.UIcards[this.UIcardIdx.row].length) { 1291 | this.UIcardIdx.col++; 1292 | } 1293 | break; 1294 | case 'swipeUp': 1295 | if (this.UIcardIdx.row > 0) { 1296 | this.UIcardIdx.row--; 1297 | } 1298 | break; 1299 | case 'swipeDown': 1300 | if (this.UIcardIdx.row + 1 < this.UIcards.length) { 1301 | this.UIcardIdx.row++; 1302 | } 1303 | break; 1304 | } 1305 | this.UI = this.emulator.showUI(this, 1306 | this.UIcards[this.UIcardIdx.row][this.UIcardIdx.col]); 1307 | } 1308 | else if (this.type === chord.deviceType.glass) { 1309 | switch (evt) { 1310 | case 'swipeLeft': 1311 | if (this.UIcardIdx.col > 0) { 1312 | this.UIcardIdx.col--; 1313 | } 1314 | break; 1315 | case 'swipeRight': 1316 | if (this.UIcardIdx.col + 1 < 1317 | this.UIcards[this.UIcardIdx.row].length) { 1318 | this.UIcardIdx.col++; 1319 | } 1320 | break; 1321 | case 'swipeUp': 1322 | break; 1323 | case 'swipeDown': 1324 | break; 1325 | } 1326 | this.UI = this.emulator.showUI(this, 1327 | this.UIcards[this.UIcardIdx.col]); 1328 | } 1329 | if (this.UI !== null && this.callbacks['tap:button'] !== undefined) { 1330 | this.UI.find('button').click({device: this}, function(event) { 1331 | event.data.device.onUI('tap:button', $(this).attr('value')); 1332 | }); 1333 | } 1334 | } 1335 | } 1336 | return this; 1337 | }; 1338 | 1339 | Device.fn.onUI = function(type, value) { 1340 | switch (type) { 1341 | case 'tap:button': 1342 | var deviceEvent = new SingleDeviceEvent(this, type, value); 1343 | this.on('tap:button', deviceEvent); 1344 | break; 1345 | } 1346 | }; 1347 | 1348 | Device.fn.run = function(fn, data) { 1349 | data !== undefined ? fn(this, data) : fn(this); // run the function 1350 | return this; 1351 | }; 1352 | 1353 | Device.fn.show = function(html, selectionId, fn, UIelements, numElements) { 1354 | var self = this; 1355 | this.selectionId = selectionId; 1356 | this.html = html; 1357 | if (UIelements === undefined || numElements === undefined) { 1358 | var analysis = UiManager.getUIElements(html); 1359 | UIelements = analysis[0]; 1360 | numElements = analysis[1]; 1361 | } 1362 | this.UIelements = UIelements; 1363 | this.wakeup(); 1364 | this.UI = this.emulator.showUI(this, 1365 | renderEmulatorUI(this.html, this.type, UIelements)); 1366 | if (this.live) { 1367 | chordServer.show(this.id, 1368 | this.renderUI(html, UIelements, numElements)); 1369 | } 1370 | if (fn !== undefined) { 1371 | fn(this); // when shown, callback if user specifies 1372 | } 1373 | return this; 1374 | 1375 | function renderEmulatorUI(html, type, UIelements) { 1376 | if (html === '') return ''; 1377 | if (type === chord.deviceType.watch || 1378 | type === chord.deviceType.glass) { // return cards 1379 | self.UIcards = renderEmulatorCards(UIelements, type); 1380 | self.UIcardIdx = {row: 0, col: 0}; 1381 | if (type === chord.deviceType.watch) { 1382 | return self.UIcards[self.UIcardIdx.row][self.UIcardIdx.col]; 1383 | } 1384 | else if (type === chord.deviceType.glass) { 1385 | return self.UIcards[self.UIcardIdx.col]; 1386 | } 1387 | } else { // return html UI 1388 | return html; 1389 | } 1390 | 1391 | function renderEmulatorCards(UIelements, type) { 1392 | var UIcards = []; 1393 | for (var i = 0, el; el = UIelements[i]; i++) { 1394 | var newRow = []; 1395 | for (var j = 0, member; member = el.members[j]; j++) { 1396 | var newCard = renderEmulatorCardHTML(el.type, member); 1397 | if (type === chord.deviceType.watch) { 1398 | newRow.push(newCard); 1399 | } 1400 | else if (type === chord.deviceType.glass) { 1401 | UIcards.push(newCard); 1402 | } 1403 | } 1404 | if (newRow.length !== 0) { 1405 | UIcards.push(newRow); 1406 | } 1407 | } 1408 | return UIcards; 1409 | } 1410 | 1411 | function renderEmulatorCardHTML(tag, member) { 1412 | return '<' + tag + ' value="' + (member.val || '') + '"' + ' class="' + 1413 | (member.val || '') + '" src="' + (member.src || '') + 1414 | '">' + member.html + ''; 1415 | } 1416 | }; 1417 | }; 1418 | 1419 | Device.fn.play = function(filepath, selectionId, fn) { 1420 | this.selectionId = selectionId; 1421 | this.wakeup(); 1422 | if (isWebUI) { 1423 | if (!filepath.startsWith('http')) { 1424 | filepath = chord.scriptDir + filepath; 1425 | } 1426 | var audioElement = document.createElement('audio'); 1427 | audioElement.setAttribute('src', filepath); 1428 | audioElement.setAttribute('autoplay', 'autoplay'); 1429 | } 1430 | if (this.live) { 1431 | chordServer.play(this.id, filepath); 1432 | } 1433 | // when shown, callback if user specifies 1434 | if (fn !== undefined) { 1435 | fn(this); 1436 | } 1437 | return this; 1438 | }; 1439 | 1440 | Device.fn.call = function(calleeNum, selectionId, fn) { 1441 | this.selectionId = selectionId; 1442 | if (isWebUI) { 1443 | this.wakeup(); 1444 | this.emulator.call(this, calleeNum); 1445 | } 1446 | if (this.live) { 1447 | chordServer.call(this.id, calleeNum); 1448 | } 1449 | // when shown, callback if user specifies 1450 | if (fn !== undefined) { 1451 | fn(this); 1452 | } 1453 | return this; 1454 | }; 1455 | 1456 | Device.fn.wakeup = function() { 1457 | if (isWebUI) { 1458 | this.emulator.wakeup(this); 1459 | } 1460 | if (this.live) { 1461 | chordServer.wakeup(this.id); 1462 | } 1463 | return this; 1464 | }; 1465 | 1466 | Device.fn.reset = function() { 1467 | this.initCallback(); 1468 | this.emulator.reset(this.id); 1469 | this.selectionId = null; 1470 | if (this.live) { 1471 | chordServer.reset(this.id); 1472 | } 1473 | return this; 1474 | }; 1475 | 1476 | Device.fn.startApp = function(appName, selectionId) { 1477 | if (isWebUI) { 1478 | this.emulator.startApp(this, appName); 1479 | } else if (this.live) { 1480 | chordServer.startApp(this.id, appName); 1481 | } 1482 | return this; 1483 | }; 1484 | 1485 | Device.fn.killApp = function(appName) { 1486 | if (isWebUI) { 1487 | this.emulator.killApp(this, appName); 1488 | } else if (this.live) { 1489 | chordServer.killApp(this.id, appName); 1490 | } 1491 | return this; 1492 | }; 1493 | 1494 | Device.fn.cloneDevice = function() { 1495 | var copy = new Device(this.type, this.name, this.fullname, this.joint, 1496 | this.id, this.capabilities, this.live); 1497 | for (var prop in this) { 1498 | copy[prop] = this[prop]; 1499 | } 1500 | return copy; 1501 | }; 1502 | 1503 | /** 1504 | * The class representing a Chord event. 1505 | * @param {!Array} devices The devices in the event. 1506 | * @param {!SelectionMode} mode The device mode. 1507 | * @param {string} eventType The triggered event type. 1508 | * @param {!Object} vals The values related to the event. 1509 | * @constructor 1510 | */ 1511 | var Event = function(devices, mode, eventType, vals) { 1512 | this.devices = devices; 1513 | this.eventType = eventType; 1514 | this.timestamp = new Date(); // current time 1515 | this.vals = vals; 1516 | this.selection = new ChordSelection(null, this.devices); 1517 | this.selection.setMode(mode); 1518 | }; 1519 | 1520 | Event.prototype.getDevices = function() { 1521 | return this.selection; 1522 | }; 1523 | 1524 | Event.prototype.getDevice = function() { 1525 | return this.getDevices(); 1526 | }; 1527 | 1528 | Event.prototype.getEventType = function() { 1529 | return this.eventType; 1530 | }; 1531 | 1532 | Event.prototype.getTimestamp = function() { 1533 | return this.timestamp; 1534 | }; 1535 | 1536 | Event.prototype.getValues = function() { 1537 | return this.vals; 1538 | }; 1539 | 1540 | Event.prototype.getValue = function() { 1541 | return this.getValues(); 1542 | }; 1543 | 1544 | /** 1545 | * The class representing a Chord single-device event. 1546 | * @param {!Device} device The single device in the event. 1547 | * @param {string} eventType The triggered event type. 1548 | * @param {!Object} val The values related to the event. 1549 | * @constructor 1550 | */ 1551 | var SingleDeviceEvent = function(device, eventType, val) { 1552 | Event.call(this, [device], SelectionMode.default, eventType, [val]); 1553 | }; 1554 | 1555 | SingleDeviceEvent.prototype = new Event(); 1556 | 1557 | SingleDeviceEvent.prototype.getDevice = function() { 1558 | return this.devices.length > 0 ? this.devices[0] : null; 1559 | }; 1560 | 1561 | SingleDeviceEvent.prototype.getValue = function() { 1562 | return this.devices.length > 0 ? this.vals[0] : null; 1563 | }; 1564 | 1565 | /** 1566 | * The class representing a Chord event manager. 1567 | * @param {!ChordSelection} selection The devices to attach to the event. 1568 | * @param {string} eventType The triggered event type. 1569 | * @param {!function} fn The callback function. 1570 | * @constructor 1571 | */ 1572 | var EventManager = function(selection, eventType, fn) { 1573 | if (!(this instanceof EventManager)) { 1574 | return new EventManager(selection, eventType, fn); 1575 | } 1576 | this.parent = selection; // ChordSelection source 1577 | this.eventType = eventType; 1578 | this.callbackFunc = fn; // developer callback function 1579 | 1580 | this.timestamps = []; // corresponding timestamps triggered 1581 | this.vals = []; // corresponding event values 1582 | 1583 | this.deviceIds = []; // id of devices 1584 | var devices = this.parent.getDevices(); 1585 | for (var i = 0, device; device = devices[i]; i++) { 1586 | this.deviceIds.push(device.id); 1587 | this.timestamps.push(null); 1588 | this.vals.push(null); 1589 | } 1590 | this.checkTimestamps = function(event) { 1591 | var idx = $.inArray(event.getDevice().id, this.deviceIds); 1592 | if (idx < 0) { 1593 | return false; 1594 | } 1595 | this.timestamps[idx] = event.timestamp; // mark the timestamp 1596 | this.vals[idx] = event.val; // mark the value 1597 | for (var i = 0; i < this.timestamps.length; i++) { 1598 | var t = this.timestamps[i]; 1599 | if (t === null || event.timestamp - t > 1600 | this.parent.getOption().timeRange) { 1601 | return false; 1602 | } 1603 | } 1604 | return true; 1605 | }; 1606 | }; 1607 | 1608 | EventManager.fn = EventManager.prototype = { 1609 | init: function() {} 1610 | }; 1611 | 1612 | EventManager.fn.deviceNum = function() { 1613 | return this.parent.getMode() === SelectionMode.default ? 1614 | 1 : this.parent.size(); 1615 | }; 1616 | 1617 | EventManager.fn.eventTriggered = function(event) { 1618 | if (this.parent.getMode() === SelectionMode.default) { 1619 | // individual device event 1620 | this.callbackFunc(event); 1621 | return true; 1622 | } 1623 | else if (this.parent.getMode() === SelectionMode.all) { // all sync 1624 | if (this.checkTimestamps(event)) { 1625 | // if all within timerange, 1626 | // callback the developer function with multi-device info 1627 | this.callbackFunc(new Event(this.parent.getDevices(), 1628 | this.parent.getMode(), this.eventType, this.vals)); 1629 | } 1630 | return true; 1631 | } 1632 | else if (this.parent.getMode() === SelectionMode.combine) { // combine 1633 | if (this.checkTimestamps(event) || event.eventType === 'tap:button') { 1634 | this.callbackFunc(new Event(this.parent.getDevices(), 1635 | this.parent.getMode(), this.eventType, event.getValues())); 1636 | } 1637 | return true; 1638 | } 1639 | return false; 1640 | }; 1641 | 1642 | /** 1643 | * The class representing a Chord UI manager. 1644 | * @constructor 1645 | */ 1646 | var UiManager = { 1647 | getUIElements: function(html) { 1648 | var numElements = {total: 0, maxBtn: 0}; 1649 | var UIelements = []; 1650 | if (html !== '') { 1651 | // parse HTML to generate UI elements 1652 | var pageObj = {}; 1653 | var preload = $('
').append(html).children(); 1654 | if (preload.prop('tagName') === undefined) { // string only 1655 | var groupName = 'P', group = []; 1656 | group.push({val: null, html: html, id: null, src: null}); 1657 | } else { 1658 | var allElements = (preload.prop('tagName') === 'DIV') ? 1659 | preload.find('*') : preload; 1660 | numElements.total = allElements.length; 1661 | var groupName = null, group = null; 1662 | for (var i = 0, el; el = allElements[i]; i++) { 1663 | var tagName = $(el).prop('tagName'); 1664 | if (tagName !== groupName) { 1665 | if (groupName) { 1666 | UIelements = this.addNewUIgroup(UIelements, 1667 | numElements, groupName, group); 1668 | } 1669 | groupName = tagName; 1670 | group = []; 1671 | } 1672 | var val = $(el).attr('value') || null; 1673 | var id = $(el).attr('id') || null; 1674 | var src = $(el).attr('src') || null; 1675 | if (src) { 1676 | src += chord.scriptDir; 1677 | } 1678 | group.push({val: val, html: $(el).html(), id: id, src: src}); 1679 | } 1680 | } 1681 | UIelements = this.addNewUIgroup(UIelements, 1682 | numElements, groupName, group); 1683 | } 1684 | return [UIelements, numElements]; 1685 | }, 1686 | 1687 | addNewUIgroup: function(UIelements, numElements, groupName, group) { 1688 | if (groupName !== null && group !== null) { 1689 | UIelements.push({type: groupName, members: group}); 1690 | var rowName = groupName + '_row'; 1691 | if (numElements[groupName + '_row'] === undefined) { 1692 | numElements[groupName + '_row'] = []; 1693 | } 1694 | numElements[groupName + '_row'].push(UIelements.length - 1); 1695 | if (groupName === 'BUTTON') { 1696 | numElements.maxBtn = Math.max(numElements.maxBtn, group.length); 1697 | } 1698 | } 1699 | return UIelements; 1700 | }, 1701 | 1702 | renderHTML: function(tag, items) { 1703 | var html = ''; 1704 | for (var j = 0, item; item = items[j]; j++) { 1705 | html += '<' + tag; 1706 | if (item.id !== null) html += ' id="' + item.id + '"'; 1707 | if (item.src !== null) html += ' src="' + item.src + '"'; 1708 | if (item.val !== null) html += ' value="' + item.val + '"'; 1709 | html += '>' + item.html + ''; 1710 | } 1711 | return html; 1712 | } 1713 | }; 1714 | 1715 | /** 1716 | * The class representing a Chord server module for web Authoring UI 1717 | * to communicate with the backend server to update live devices. 1718 | * @constructor 1719 | */ 1720 | var ChordWebServer = function() { 1721 | if (!(this instanceof ChordWebServer)) { 1722 | return new ChordWebServer(); 1723 | } 1724 | this.host = '127.0.0.1'; 1725 | this.port = 9999; 1726 | this.socket = null; 1727 | this.callback = {}; 1728 | }; 1729 | 1730 | ChordWebServer.prototype.addCallback = function(type, fn) { 1731 | this.callback[type] = fn; 1732 | }; 1733 | 1734 | ChordWebServer.prototype.on = function(deviceId, evt) { 1735 | this.socket.emit('on', deviceId, evt); 1736 | }; 1737 | 1738 | ChordWebServer.prototype.show = function(deviceId, selectionId, content) { 1739 | this.socket.emit('show', deviceId, content); 1740 | }; 1741 | 1742 | ChordWebServer.prototype.play = function(deviceId, media) { 1743 | this.socket.emit('play', deviceId, media); 1744 | }; 1745 | 1746 | ChordWebServer.prototype.call = function(deviceId, calleeNum) { 1747 | this.socket.emit('call', deviceId, calleeNum); 1748 | }; 1749 | 1750 | ChordWebServer.prototype.wakeup = function(deviceId) { 1751 | this.socket.emit('wakeup', deviceId); 1752 | }; 1753 | 1754 | ChordWebServer.prototype.reset = function(deviceId) { 1755 | this.socket.emit('reset', deviceId); 1756 | }; 1757 | 1758 | ChordWebServer.prototype.startApp = function(deviceId, appName) { 1759 | this.socket.emit('startApp', deviceId, appName); 1760 | }; 1761 | 1762 | ChordWebServer.prototype.killApp = function(deviceId, appName) { 1763 | this.socket.emit('killApp', deviceId, appName); 1764 | }; 1765 | 1766 | /** 1767 | * The Chord web server. 1768 | * @type {Object} 1769 | */ 1770 | var chordServer = new ChordWebServer(); 1771 | 1772 | chord.init(); 1773 | if (isWebUI) { 1774 | window.chord = chord; 1775 | window.chordServer = chordServer; 1776 | } 1777 | return chord; 1778 | })(); 1779 | -------------------------------------------------------------------------------- /viewer/js/viewer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 The Chord Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Manages the Chord viewer, 19 | * including a test panel with a set of emulators and a log panel. 20 | * @author peggychi@cs.berkeley.edu (Peggy Chi) 21 | */ 22 | 23 | /** 24 | * The file paths to device spec, script, and info. 25 | * @constructor 26 | */ 27 | var FILEPATH = { 28 | DEVICE_INFO_URL: '/device/deviceSpec.json', 29 | DEVICE_TEMPLATE: '/device/deviceTemplate.html', 30 | SAMPLE_DIR: 'samples/', 31 | SAMPLES: { 32 | 'One App Launcher': 'oneAppLauncher', 33 | 'Launch Pad': 'launchPad', 34 | 'Photo Launcher': 'photoLauncher', 35 | 'Photo Slideshow': 'slideshow', 36 | 'Multi-device Bump': 'bump' 37 | }, 38 | SCRIPT_NAME: 'service.js', 39 | VIEWER: 'viewer/', 40 | LAYOUT: { 41 | launchPad: ['panel.html'], 42 | slideshow: ['controller.html'] 43 | } 44 | }; 45 | 46 | // retrieve device info when UI is ready 47 | $(document).ready(function() { 48 | window.requestFileSystem = window.requestFileSystem || 49 | window.webkitRequestFileSystem; 50 | window.directoryEntry = window.directoryEntry || window.webkitDirectoryEntry; 51 | $.getJSON(FILEPATH.DEVICE_INFO_URL, function(data) { // load device specs 52 | chord.setup(data.deviceCapabilities, data.devices); 53 | viewer.init('dialog'); 54 | emulatorManager.init('devices', 'phone-watch-tablet'); 55 | scriptManager.init('.icon.sample', '.icon.folder', '.run'); 56 | window.Log = emulatorManager.showSystemLog; 57 | Log.v('Chord plugin ready'); 58 | }); 59 | scriptManager.loadDir(FILEPATH.SAMPLES['Photo Slideshow']); 60 | }); 61 | 62 | /** 63 | * The viewer object handling UI interactions. 64 | * @constructor 65 | */ 66 | var viewer = { 67 | dialog: null, 68 | dialogId: '', 69 | pathToShow: '.path', 70 | runIcon: '.run', 71 | 72 | /** 73 | * Initializes the viewer. 74 | * @param {string} dialogId Id of the dialog element. 75 | */ 76 | init: function(dialogId) { 77 | this.dialogId = dialogId; 78 | this.dialog = document.querySelector('#' + dialogId); 79 | $('#' + dialogId + ' .dlg-close').click(function() { 80 | dialog.close(); 81 | }); 82 | }, 83 | 84 | /** 85 | * Shows a dialog given content. 86 | * @param {string} message Id of the device panel container. 87 | * @param {Object} callbacks Classes and callbacks listening to a click event. 88 | */ 89 | showDialog: function(message, callbacks) { 90 | var dialogObj = $('#' + this.dialogId + ' .dlg-content').html(message); 91 | this.dialog.showModal(); 92 | for (var evt in callbacks) { 93 | dialogObj.find(evt).click(callbacks[evt]); 94 | } 95 | } 96 | }; 97 | 98 | /** 99 | * The emulatorManager object handling the emulators and events. 100 | * @constructor 101 | */ 102 | var emulatorManager = { 103 | deviceTemplate: '', 104 | deviceIds: [], 105 | devicePreset: 'phone-watch-glass', 106 | rootId: '', 107 | shownClasses: [], 108 | uiClass: { 109 | active: 'activeUI' 110 | }, 111 | targetEvent: { 112 | toSimulate: [], 113 | toExlude: ['tap', 'doubleTap', 'longTap', 'listenEnd', 114 | 'rotatePortrait', 'rotateLandscape'], 115 | typeToSimulate: ['shakable', 'rotatable', 'touchable', 'hearable'] 116 | }, 117 | 118 | /** 119 | * Initializes the emulators. 120 | * @param {string} rootId Id of the device panel container. 121 | * @param {string} preset The emulator set. 122 | */ 123 | init: function(rootId, preset) { 124 | var self = this; 125 | this.rootId = rootId; 126 | if (preset) { // emulator preset 127 | this.devicePreset = preset; 128 | } 129 | $.get(FILEPATH.DEVICE_TEMPLATE, function(content) { 130 | self.deviceTemplate = content; 131 | self.updateDevicePreset(self.devicePreset, function() { 132 | setUpEvents(chord.actionCapabilityMap); 133 | self.showDevices(); 134 | /** 135 | * Sets up input events to be simulated. 136 | * @param {Object} eventList The event list. 137 | */ 138 | function setUpEvents(eventList) { 139 | self.targetEvent.toSimulate = []; 140 | for (var evt in eventList) { 141 | if (self.targetEvent.typeToSimulate.indexOf(eventList[evt]) >= 0 142 | && self.targetEvent.toExlude.indexOf(evt) < 0) { 143 | self.targetEvent.toSimulate.push(evt); 144 | } 145 | } 146 | } 147 | }); 148 | }); 149 | this.showSystemLog.init(); // log 150 | }, 151 | 152 | /** 153 | * Updates the emulators. 154 | * @param {string} preset The emulator set. 155 | * @param {Object} callback Callback function to execute when finished. 156 | */ 157 | updateDevicePreset: function(preset, callback) { 158 | this.devicePreset = preset; 159 | var numEmulatedDevices = { 160 | phone: 0, 161 | watch: 0, 162 | glass: 0, 163 | tablet: 0 164 | }; 165 | switch (this.devicePreset) { 166 | case 'phone-watch-glass': 167 | numEmulatedDevices.phone = 1; 168 | numEmulatedDevices.watch = 1; 169 | numEmulatedDevices.glass = 1; 170 | break; 171 | case 'phone-watch-tablet': 172 | numEmulatedDevices.phone = 1; 173 | numEmulatedDevices.watch = 1; 174 | numEmulatedDevices.tablet = 1; 175 | break; 176 | case 'phone-watch': 177 | numEmulatedDevices.phone = 1; 178 | numEmulatedDevices.watch = 1; 179 | break; 180 | case 'one-phone': 181 | numEmulatedDevices.phone = 1; 182 | break; 183 | case 'two-phones': 184 | numEmulatedDevices.phone = 2; 185 | break; 186 | case 'glass': 187 | numEmulatedDevices.glass = 1; 188 | break; 189 | } 190 | chord.setEmulatedDevices(numEmulatedDevices, false, callback); 191 | }, 192 | 193 | /** 194 | * Renders the emulators. 195 | */ 196 | showDevices: function() { 197 | var IDs = [], exists = []; // update the list deviceIds 198 | $('#' + this.rootId + ' > .device-container').each(function() { 199 | IDs.push(this.id); 200 | exists.push(false); 201 | }); 202 | this.deviceIds = IDs; 203 | for (d in chord.devices) { // update panel 204 | var device = chord.devices[d]; 205 | var idx = this.deviceIds.indexOf(device.id); 206 | if (idx < 0) { 207 | this.addDevicePanel(device, !device.live); 208 | chord.devices[d].addEmulator(this); 209 | this.deviceIds.push(device.id); 210 | } 211 | else exists[idx] = true; 212 | } 213 | for (var i = 0; i < exists.length; i++) { 214 | if (!exists[i]) { // remove inactive devices 215 | $('#' + this.rootId + ' > .device-container[id=' 216 | + this.deviceIds[i] + ']') 217 | .fadeOut(1000, function() { $(this).remove(); }); 218 | } 219 | } 220 | }, 221 | 222 | /** 223 | * Updates the emulators. 224 | * @param {Object} device A Chord device. 225 | * @param {boolean} isEmulator Is an emulator. 226 | */ 227 | addDevicePanel: function(device, isEmulator) { 228 | var self = this; 229 | var panelHTML = this.deviceTemplate; 230 | panelHTML = panelHTML.replace(/DEVICE_ID/g, device.id) 231 | .replace(/DEVICE_TYPE/g, device.type) 232 | .replace(/DEVICE_NAME/g, device.name); 233 | $('#' + this.rootId).prepend($(panelHTML).fadeIn('slow')); 234 | var div = $('#' + device.id + ' > div .manualEvts'); 235 | var removeDevice = function(deviceId, type) { 236 | var success = chord.deleteDevice(deviceId); 237 | if (success) { 238 | Log.w('Delete device: ' + type); 239 | self.showDevices(); 240 | } 241 | return success; 242 | }; 243 | $('#' + device.id + ' .device_delete').click(function() { // remove device 244 | var id_toDelete = $(this).parent().attr('id'); 245 | var type_toDelete = $(this).parent().attr('dType'); 246 | viewer.showDialog('

Remove ' + 247 | type_toDelete + ' ' + id_toDelete + '?

' + 248 | '' + 249 | '', { 250 | '.btn-default': function() { 251 | dialog.close(); 252 | }, '.btn-danger': function() { 253 | dialog.close(); 254 | removeDevice(id_toDelete, type_toDelete); 255 | } 256 | }); 257 | }); 258 | if (isEmulator) { // add events for simulations 259 | for (cap in device.capability) { 260 | var evts = device.capability[cap].on; 261 | if (evts !== undefined) { 262 | for (var idx = 0, evt; evt = evts[idx]; idx++) { 263 | if (self.targetEvent.toSimulate.indexOf(evt) >= 0) { 264 | var evtClass = ' evt evt_' + evt; 265 | $('') 267 | .appendTo(div) 268 | .click(function() { 269 | simulateEvent($(this).attr('evtName'), device.id); 270 | }); 271 | } 272 | } 273 | } 274 | } 275 | } else { 276 | $('#' + device.id).removeClass('panel-info'); 277 | $('#' + device.id).addClass('panel-primary'); 278 | div.append('Live'); 279 | } 280 | 281 | /** 282 | * Simulates an input event update to an emulator. 283 | * @param {string} eventType The event type. 284 | * @param {string} deviceId The device ID. 285 | */ 286 | function simulateEvent(eventType, deviceId) { 287 | chord.getDeviceById(deviceId).on(eventType); 288 | } 289 | }, 290 | 291 | /** 292 | * Adds a new emulator. 293 | * @param {string} id The new device id. 294 | * @param {string} type The new device type. 295 | * @param {string} name The new device name. 296 | */ 297 | addManulDevice: function(id, type, name) { 298 | chord.addEmulatedDevices(id, type, name); 299 | this.showDevices(); 300 | }, 301 | 302 | /** 303 | * Updates the device status. 304 | * @param {string} deviceId The device id. 305 | * @param {string} msg The status update message. 306 | */ 307 | updateStatus: function(deviceId, msg) { 308 | $('#' + deviceId).find('.status').html(msg); 309 | Log.v('[' + deviceId + '] ' + msg); 310 | }, 311 | 312 | /** 313 | * Applies the event class. 314 | * @param {string} deviceId The device id. 315 | * @param {string} evt The event name. 316 | */ 317 | applyClass: function(deviceId, evt) { 318 | this.getUI(deviceId).addClass(evt); 319 | this.updateStatus(deviceId, 'received event ' + evt); 320 | this.shownClasses.push(evt); 321 | }, 322 | 323 | /** 324 | * Activates the device emulator view. 325 | * @param {Object} device The device. 326 | */ 327 | wakeup: function(device) { 328 | this.getUI(device.id).toggleClass(this.uiClass.active, true); 329 | this.shownClasses.push(this.uiClass.active); 330 | }, 331 | 332 | /** 333 | * Resets the device emulator view. 334 | * @param {string} deviceId The device id. 335 | */ 336 | reset: function(deviceId) { 337 | this.getUI(deviceId).removeClass(this.shownClasses.join(' ')).html(''); 338 | this.updateStatus(deviceId, 'reset'); 339 | }, 340 | 341 | /** 342 | * Resets all the device emulators. 343 | */ 344 | resetAll: function() { 345 | this.shownClasses = []; 346 | }, 347 | 348 | /** 349 | * Retrieves the jQuery element of the emulator. 350 | * @param {string} deviceId The device id. 351 | */ 352 | getUI: function(deviceId) { 353 | return $('#' + deviceId).find('.UI'); 354 | }, 355 | 356 | /** 357 | * Shows the device emulator view. 358 | * @param {Object} device The device. 359 | * @param {string} uiContent The html content. 360 | */ 361 | showUI: function(device, uiContent) { 362 | this.updateStatus(device.id, 'UI updated'); 363 | var UI = this.getUI(device.id); 364 | if (typeof uiContent === 'object') { // tmp: watch 365 | UI = UI.html(''); 366 | UI = UI.html(''); 367 | } else { 368 | UI = UI.html(uiContent); 369 | UI.find('*').css('font-size', '12px'); 370 | } 371 | return UI; 372 | }, 373 | 374 | /** 375 | * Launches an application on the device emulator. 376 | * @param {Object} device The device. 377 | * @param {string} appName The app name. 378 | */ 379 | startApp: function(device, appName) { 380 | this.wakeup(device); 381 | this.showUI(device, '
'); 382 | this.updateStatus(device.id, 'Start app ' + appName); 383 | }, 384 | 385 | /** 386 | * Kills an application on the device emulator. 387 | * @param {Object} device The device. 388 | * @param {string} appName The app name. 389 | */ 390 | killApp: function(device, appName) { 391 | this.showUI(device, ''); 392 | this.updateStatus(device.id, 'Kill app ' + appName); 393 | }, 394 | 395 | /** 396 | * Makes a phone call on the device emulator. 397 | * @param {Object} device The device. 398 | * @param {string} calleeNum The number to call. 399 | */ 400 | call: function(device, calleeNum) { 401 | this.showUI(device, 'Calling ' + calleeNum + '...'); 402 | this.applyClass(device.id, 'callStart'); 403 | }, 404 | 405 | /** 406 | * Rotates counter-clockwise the device emulator. 407 | * @param {Object} device The device. 408 | */ 409 | rotateCCW: function(device) { 410 | this.rotateDevice(device.id, -90); 411 | }, 412 | 413 | /** 414 | * Rotates clockwise the device emulator. 415 | * @param {Object} device The device. 416 | */ 417 | rotateCW: function(device) { 418 | this.rotateDevice(device.id, 90); 419 | }, 420 | 421 | /** 422 | * Rotates the device emulator. 423 | * @param {string} deviceId The device id. 424 | * @param {double} angle The rotate angle. 425 | */ 426 | rotateDevice: function(deviceId, angle) { 427 | var view = $('#' + deviceId).find('.deviceSkin'); 428 | var subview = this.getUI(deviceId); 429 | if (view.attr('orientation') !== undefined) { 430 | angle += parseInt(view.attr('orientation')); 431 | } 432 | view.css('-webkit-transform', 'rotate(' + angle + 'deg)'); 433 | (angle % 180 === 0) ? subview.removeClass('landscape') : 434 | subview.addClass('landscape'); 435 | subview.css('-webkit-transform', 'rotate(' + (-angle) + 'deg)'); 436 | view.attr('orientation', (angle % 360).toString()); 437 | }, 438 | 439 | /** 440 | * Handles log messages on the log panel. 441 | */ 442 | showSystemLog: { 443 | v: function(msg) { this.log('log_v', msg); }, 444 | d: function(msg) { this.log('log_d', msg); }, 445 | i: function(msg) { this.log('log_i', msg); }, 446 | w: function(msg) { this.log('log_w', msg); }, 447 | e: function(msg) { this.log('log_e', msg); }, 448 | 449 | /** 450 | * Prepends the log message to the log panel. 451 | * @param {string} c The class name of the message. 452 | * @param {string} msg The log message. 453 | */ 454 | log: function(c, msg) { 455 | $('
' + this.getDate( 456 | new Date()) + ' ' + msg + '
') 457 | .prependTo('#log-console'); 458 | }, 459 | 460 | /** 461 | * Renders the date to a readable form. 462 | * @param {Object} d The date. 463 | */ 464 | getDate: function(d) { // "07-24 15:46:10.006" 465 | var dd = attachZero(d.getDate(), 10); 466 | var mm = attachZero(d.getMonth() + 1, 10); 467 | var h = attachZero(d.getHours(), 10); 468 | var m = attachZero(d.getMinutes(), 10); 469 | var s = attachZero(d.getSeconds(), 10); 470 | var ms = attachZero(d.getMilliseconds(), 100); 471 | if (ms.length < 3) { 472 | ms = '0' + ms; 473 | } 474 | return [mm, '-', dd, ' ', h, ':', m, ':', s, '.', ms].join(''); 475 | 476 | /** 477 | * Prepends zero(s) to a number. 478 | * @param {int} num The date element. 479 | * @param {int} range The lenght of target number. 480 | */ 481 | function attachZero(num, range) { 482 | return (num < range) ? '0' + num : num; 483 | } 484 | }, 485 | 486 | /** 487 | * Initializes the log. 488 | */ 489 | init: function() { 490 | if (this.cachedLog.length > 0) { 491 | for (var i = 0, log; log = this.cachedLog[i]; i++) { 492 | Log.v(log); 493 | } 494 | } 495 | this.cachedLog = []; 496 | }, 497 | cachedLog: [] 498 | } 499 | }; 500 | 501 | /** 502 | * The scriptEntry object storing info of user's scripts. 503 | * @constructor 504 | */ 505 | var scriptEntry = { 506 | dir: null, 507 | dirPath: '', 508 | entries: [], 509 | layouts: [] 510 | }; 511 | 512 | /** 513 | * The scriptManager object handling sample and user's scripts. 514 | * @constructor 515 | */ 516 | var scriptManager = { 517 | /** 518 | * Initializes the menu UI callbacks for loading scripts. 519 | * @param {string} sampleId The sample icon class name. 520 | * @param {string} openDirId The open directory icon class name. 521 | * @param {string} runId The run icon class name. 522 | */ 523 | init: function(sampleId, openDirId, runId) { 524 | var listOfSamples = ''; 525 | for (var sampleName in FILEPATH.SAMPLES) { 526 | listOfSamples += '

' + 527 | sampleName + '

'; 528 | } 529 | $(sampleId).click(function() { // run a sample from the list 530 | viewer.showDialog(listOfSamples, { 531 | 'p': function() { 532 | scriptManager.loadDir($(this).attr('path')); 533 | dialog.close(); 534 | $('.folder-open').removeClass('folder-open').addClass('folder'); 535 | } 536 | }); 537 | }); 538 | $(openDirId).click(function() { // open system dialog 539 | chrome.fileSystem.chooseEntry({ 540 | type: 'openDirectory' 541 | }, function (dir) { 542 | if (!dir || !dir.isDirectory) { 543 | Log.e('unable to load the script directory: ' + dir); 544 | return; 545 | } 546 | scriptManager.loadDir(dir); 547 | }); 548 | }); 549 | $(runId).click(function() { // run button 550 | scriptEntry.dir ? 551 | scriptManager.loadDir(scriptEntry.dir) : 552 | Log.e('specify a directory to run script'); 553 | }); 554 | }, 555 | 556 | /** 557 | * Loads a script directory. 558 | * @param {Object, string} dir The directory. 559 | */ 560 | loadDir: function(dir) { 561 | scriptEntry = { 562 | dir: null, 563 | dirPath: '', 564 | entries: [], 565 | layouts: [] 566 | }; 567 | if (typeof dir === 'string') { 568 | scriptEntry.dir = dir; 569 | if (FILEPATH.LAYOUT[dir] !== undefined) { // html layouts 570 | scriptEntry.layouts = FILEPATH.LAYOUT[dir]; 571 | } 572 | retrieveScript(FILEPATH.SAMPLE_DIR + dir + '/', scriptEntry.layouts); 573 | } else { 574 | var reader = dir.createReader(); 575 | var readEntries = function() { 576 | reader.readEntries(function(entries) { 577 | if (entries.length) { 578 | entries.forEach(function(item) { 579 | if (item.isFile) { 580 | scriptEntry.entries.push(item.name); 581 | if (item.name.endsWith('.html')) { 582 | scriptEntry.layouts.push(item.name); 583 | } 584 | } 585 | }); 586 | readEntries(); 587 | } else { 588 | retrieveScriptByDir(); 589 | } 590 | }, errorHandler); 591 | } 592 | readEntries(); 593 | } 594 | 595 | /** 596 | * Retrieve the main Chord script. 597 | * @param {string} dirPath The directory path to the script. 598 | * @param {!Array} entries The file entries under dirPath. 599 | */ 600 | function retrieveScript(dirPath, layouts) { 601 | // retrieve layouts 602 | var layoutContent = {}; 603 | if (layouts.length === 0) { 604 | readyToLoadSample(null); 605 | } else { 606 | for (var i = 0, layoutName; layoutName = layouts[i]; i++) { 607 | $.get(dirPath + layoutName, function(content) { 608 | var layoutName = this.url.substring(this.url.lastIndexOf('/') + 1, 609 | this.url.lastIndexOf('.')); 610 | layoutContent[layoutName] = content; 611 | if (Object.keys(layoutContent).length === layouts.length) { 612 | readyToLoadSample(layoutContent); 613 | } 614 | }); 615 | } 616 | } 617 | 618 | function readyToLoadSample(layouts) { 619 | chord.loadLayouts(layouts); 620 | // set directory path 621 | scriptEntry.dirPath = dirPath; 622 | chord.setDir(dirPath); 623 | // load sample to run Chord 624 | loadSample(dirPath + FILEPATH.SCRIPT_NAME); 625 | // update viewer UI 626 | $(viewer.pathToShow).html(dirPath); 627 | $(viewer.runIcon).removeClass('disable'); 628 | $('.folder').removeClass('folder').addClass('folder-open'); 629 | } 630 | } 631 | 632 | /** 633 | * Retrieve the main Chord script by user specified directory. 634 | */ 635 | function retrieveScriptByDir() { 636 | if (scriptEntry.entries.length === 0 || 637 | scriptEntry.entries.indexOf(FILEPATH.SCRIPT_NAME) < 0) { 638 | Log.e('no main Chord script found'); 639 | Log.e('did you name your script to "' + FILEPATH.SCRIPT_NAME + '"?'); 640 | } else { 641 | scriptEntry.dir = dir; 642 | chrome.fileSystem.getDisplayPath(dir, function(path) { 643 | path = path.substring(path.lastIndexOf(FILEPATH.VIEWER) + 644 | FILEPATH.VIEWER.length, path.length) + '/'; 645 | retrieveScript(path, scriptEntry.layouts); 646 | }); 647 | } 648 | } 649 | 650 | /** 651 | * Loads a sample script. 652 | * @param {string} path The file path to the script. 653 | */ 654 | function loadSample(scriptpath) { 655 | $.get(scriptpath, function(content) { 656 | runScript(content); 657 | }).fail(function() { 658 | Log.e('failed to load sample: ' + scriptpath); 659 | }); 660 | } 661 | 662 | /** 663 | * Executes the Chord script by writing and retrieving from local storage. 664 | * @param {string} code The Chord script. 665 | */ 666 | function runScript(code) { 667 | chord.resetDevices(); 668 | emulatorManager.resetAll(); 669 | window.requestFileSystem(window.TEMPORARY, 1024*1024, 670 | onInitFs, errorHandler); 671 | function onInitFs(fs) { 672 | fs.root.getFile('chord.js', {create: true}, function(fileEntry) { 673 | fileEntry.createWriter(function(fileWriter) { 674 | fileWriter.truncate(0); 675 | fileWriter.onwriteend = function(e) { 676 | if (fileWriter.length === 0) { 677 | var blob = new Blob([code], {type: 'text/plain'}); 678 | fileWriter.write(blob); 679 | loadScript(fileEntry.toURL()); 680 | } 681 | }; 682 | fileWriter.onerror = function(e) { 683 | Log.e('failed to load script: ' + e.toString()); 684 | }; 685 | }, errorHandler); 686 | }, errorHandler); 687 | } 688 | 689 | /** 690 | * Loads the script. 691 | * @param {string} path The path to the script. 692 | */ 693 | function loadScript(path) { 694 | $.getScript(path, function() { 695 | Log.d('script loaded: ' + scriptEntry.dirPath + FILEPATH.SCRIPT_NAME); 696 | service(); 697 | }).fail(errorHandler); 698 | } 699 | } 700 | 701 | /** 702 | * Handles file I/O error. 703 | */ 704 | function errorHandler() { 705 | Log.e('failed to load file'); 706 | } 707 | } 708 | }; 709 | -------------------------------------------------------------------------------- /viewer/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Chord Plugin", 4 | "version": "0.1", 5 | "icons": { "16": "img/logo/chord-16.png", "128": "img/logo/chord-128.png" }, 6 | "permissions": [ 7 | {"socket": [ 8 | "tcp-connect", 9 | "tcp-listen"]}, 10 | {"fileSystem": [ 11 | "write", "retainEntries", "directory"]} 12 | ], 13 | "app": { 14 | "background": { 15 | "scripts": ["background.js"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /viewer/samples/bump/audio/success.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/samples/bump/audio/success.mp3 -------------------------------------------------------------------------------- /viewer/samples/bump/service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 The Chord Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Bump: shake 2+ devices to join as a group. 19 | */ 20 | 21 | chord.launchMethod = chord.launchOption.default; 22 | 23 | function service() { 24 | chord.select('.showable.shakable.speakable') 25 | .all({minNumOfDevices: 2}) // at least 2 devices 26 | .show('Bump to join') 27 | .on('shake', function(event) { 28 | event.getDevices().show('Welcome to the group!') 29 | .play('audio/success.mp3'); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /viewer/samples/launchPad/panel.html: -------------------------------------------------------------------------------- 1 |
2 |

Launch Pad Options

3 | 4 | 5 | 6 | 7 |
8 | -------------------------------------------------------------------------------- /viewer/samples/launchPad/service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 The Chord Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Cross-device launch pad for multiple apps. 19 | */ 20 | 21 | chord.launchMethod = chord.launchOption.default; 22 | 23 | function service() { 24 | chord.select('.showable[size="small"].touchable') 25 | .show(chord.getLayoutById('panel')) 26 | .on('tap:button', function(event) { 27 | chord.select('.showable[size="normal"]') 28 | .not(event.getDevice()) 29 | .startApp(event.getValue()); // e.g. 'GoogleMaps' 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /viewer/samples/oneAppLauncher/service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 The Chord Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Cross-device one app launcher. 19 | */ 20 | 21 | chord.launchMethod = chord.launchOption.default; 22 | 23 | function service() { 24 | chord.select('.showable[size="small"].touchable') 25 | .show('') 26 | .on('tap:button', function(event) { 27 | event.getDevice().show('launching app...'); 28 | chord.select('.showable[size="normal"]') 29 | .show('retrieving calendar data...') 30 | .startApp('Calendar'); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /viewer/samples/photoLauncher/photo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/samples/photoLauncher/photo.jpg -------------------------------------------------------------------------------- /viewer/samples/photoLauncher/service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 The Chord Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Cross-device photo launcher. 19 | */ 20 | 21 | chord.launchMethod = chord.launchOption.default; 22 | 23 | function service() { 24 | var photo = { 25 | path: '', 26 | caption: 'Credit: WallpaperCave' 27 | }; 28 | chord.select('.shakable[size="small"]') 29 | .show('Shake to view photo') 30 | .on('shake', function(event) { 31 | event.getDevice() 32 | .show(photo.caption); 33 | chord.select(':tablet[os="android"]') 34 | .show(photo.path); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /viewer/samples/slideshow/controller.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | -------------------------------------------------------------------------------- /viewer/samples/slideshow/img/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/samples/slideshow/img/1.jpg -------------------------------------------------------------------------------- /viewer/samples/slideshow/img/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/samples/slideshow/img/2.jpg -------------------------------------------------------------------------------- /viewer/samples/slideshow/img/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/samples/slideshow/img/3.jpg -------------------------------------------------------------------------------- /viewer/samples/slideshow/img/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chord/60277ccc300300c54b3b0e3d5c4b8d719ca6525b/viewer/samples/slideshow/img/4.jpg -------------------------------------------------------------------------------- /viewer/samples/slideshow/service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 The Chord Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Cross-device slideshow. 19 | */ 20 | 21 | chord.launchMethod = chord.launchOption.default; 22 | 23 | function service() { 24 | var photoIdx = 1, numOfPhotos = 4; 25 | var img = ''; 26 | 27 | // photo viewer 28 | var viewer = chord.select('.showable[size="normal"]') 29 | .show(img); 30 | 31 | // remote control 32 | chord.select('.showable[size="small"].touchable') 33 | .show(chord.getLayoutById('controller')) 34 | .on('tap:button', function(event) { 35 | if (event.getValue() === 'prev') { 36 | photoIdx--; 37 | if (photoIdx < 1) { 38 | photoIdx = numOfPhotos; 39 | } 40 | } else if (event.getValue() === 'next') { 41 | photoIdx++; 42 | if (photoIdx > numOfPhotos) { 43 | photoIdx = 1; 44 | } 45 | } 46 | updatePhoto(viewer, photoIdx); 47 | }); 48 | 49 | function updatePhoto(viewer, photoIdx) { 50 | viewer.updateUIAttr('imgView', 'src', 'img/' + photoIdx + '.jpg'); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /viewer/third_party/bootstrap/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2015 Twitter, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /viewer/third_party/jQuery/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright jQuery Foundation and other contributors, https://jquery.org/ 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals. For exact contribution history, see the revision history 5 | available at https://github.com/jquery/jquery 6 | 7 | The following license applies to all parts of this software except as 8 | documented below: 9 | 10 | ==== 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | ==== 32 | 33 | All files located in the node_modules and external directories are 34 | externally maintained libraries used by this software which have their 35 | own licenses; we recommend you read them, as their terms may differ from 36 | the terms above. --------------------------------------------------------------------------------