Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(391)

Side by Side Diff: remoting/webapp/crd/js/desktop_remoting.js

Issue 1016373003: [Chromoting] Change Application.Delegate to proper subclass of Application. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: sync/merge Created 5 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « remoting/webapp/crd/js/crd_main.js ('k') | remoting/webapp/crd/js/window_frame.js » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright 2014 The Chromium Authors. All rights reserved. 1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 /** 5 /**
6 * @fileoverview 6 * @fileoverview
7 * This class implements the functionality that is specific to desktop 7 * This class implements the functionality that is specific to desktop
8 * remoting ("Chromoting" or CRD). 8 * remoting ("Chromoting" or CRD).
9 */ 9 */
10 10
11 'use strict'; 11 'use strict';
12 12
13 /** @suppress {duplicate} */ 13 /** @suppress {duplicate} */
14 var remoting = remoting || {}; 14 var remoting = remoting || {};
15 15
16 /** 16 /**
17 * @param {remoting.Application} app The main app that owns this delegate. 17 * @param {Array<string>} appCapabilities Array of application capabilities.
18 * @constructor 18 * @constructor
19 * @implements {remoting.Application.Delegate} 19 * @implements {remoting.ApplicationInterface}
20 * @extends {remoting.Application}
20 */ 21 */
21 remoting.DesktopRemoting = function(app) { 22 remoting.DesktopRemoting = function(appCapabilities) {
22 /** 23 base.inherits(this, remoting.Application, appCapabilities);
23 * TODO(garykac): Remove this reference to the Application. It's only
24 * needed to get the current mode when reporting errors. So we should be
25 * able to refactor and remove this reference cycle.
26 *
27 * @private {remoting.Application}
28 */
29 this.app_ = app;
30 app.setDelegate(this);
31 24
32 /** 25 /**
33 * Whether to refresh the JID and retry the connection if the current JID 26 * Whether to refresh the JID and retry the connection if the current JID
34 * is offline. 27 * is offline.
35 * 28 *
36 * @private {boolean} 29 * @private {boolean}
37 */ 30 */
38 this.refreshHostJidIfOffline_ = true; 31 this.refreshHostJidIfOffline_ = true;
39 32
40 /** @private {remoting.DesktopConnectedView} */ 33 /** @private {remoting.DesktopConnectedView} */
41 this.connectedView_ = null; 34 this.connectedView_ = null;
42
43 remoting.desktopDelegateForTesting = this;
44 }; 35 };
45 36
46 /** 37 /**
47 * Initialize the application and register all event handlers. After this 38 * @return {string} Application product name to be used in UI.
48 * is called, the app is running and waiting for user events. 39 * @override {remoting.ApplicationInterface}
49 *
50 * @return {void} Nothing.
51 */ 40 */
52 remoting.DesktopRemoting.prototype.init = function() { 41 remoting.DesktopRemoting.prototype.getApplicationName = function() {
42 return chrome.i18n.getMessage(/*i18n-content*/'PRODUCT_NAME');
43 };
44
45 /**
46 * @param {!remoting.Error} error The failure reason.
47 * @override {remoting.ApplicationInterface}
48 */
49 remoting.DesktopRemoting.prototype.signInFailed_ = function(error) {
50 remoting.showErrorMessage(error);
51 };
52
53 /**
54 * @override {remoting.ApplicationInterface}
55 */
56 remoting.DesktopRemoting.prototype.initApplication_ = function() {
53 remoting.initElementEventHandlers(); 57 remoting.initElementEventHandlers();
54 58
55 if (base.isAppsV2()) { 59 if (base.isAppsV2()) {
56 remoting.windowFrame = new remoting.WindowFrame( 60 remoting.windowFrame = new remoting.WindowFrame(
57 document.getElementById('title-bar')); 61 document.getElementById('title-bar'));
58 remoting.optionsMenu = remoting.windowFrame.createOptionsMenu(); 62 remoting.optionsMenu = remoting.windowFrame.createOptionsMenu();
59 63
60 var START_FULLSCREEN = 'start-fullscreen'; 64 var START_FULLSCREEN = 'start-fullscreen';
61 remoting.fullscreen = new remoting.FullscreenAppsV2(); 65 remoting.fullscreen = new remoting.FullscreenAppsV2();
62 remoting.fullscreen.addListener(function(isFullscreen) { 66 remoting.fullscreen.addListener(function(isFullscreen) {
(...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after
115 document.getElementById('startup-mode-box-it2me').hidden = false; 119 document.getElementById('startup-mode-box-it2me').hidden = false;
116 } 120 }
117 }; 121 };
118 this.isWindowed_(onIsWindowed); 122 this.isWindowed_(onIsWindowed);
119 } 123 }
120 124
121 remoting.ClientPlugin.factory.preloadPlugin(); 125 remoting.ClientPlugin.factory.preloadPlugin();
122 }; 126 };
123 127
124 /** 128 /**
125 * Start the application. Once start() is called, the delegate can assume that 129 * @param {string} token An OAuth access token.
126 * the user has consented to all permissions specified in the manifest. 130 * @override {remoting.ApplicationInterface}
127 *
128 * @param {remoting.SessionConnector} connector
129 * @param {string} token An OAuth access token. The delegate should not cache
130 * this token, but can assume that it will remain valid during application
131 * start-up.
132 */ 131 */
133 remoting.DesktopRemoting.prototype.start = function(connector, token) { 132 remoting.DesktopRemoting.prototype.startApplication_ = function(token) {
134 remoting.identity.getEmail().then( 133 remoting.identity.getEmail().then(
135 function(/** string */ email) { 134 function(/** string */ email) {
136 document.getElementById('current-email').innerText = email; 135 document.getElementById('current-email').innerText = email;
137 document.getElementById('get-started-it2me').disabled = false; 136 document.getElementById('get-started-it2me').disabled = false;
138 document.getElementById('get-started-me2me').disabled = false; 137 document.getElementById('get-started-me2me').disabled = false;
139 }); 138 });
140 }; 139 };
141 140
142 /** 141 /** @override {remoting.ApplicationInterface} */
143 * Report an authentication error to the user. This is called in lieu of start() 142 remoting.DesktopRemoting.prototype.exitApplication_ = function() {
144 * if the user cannot be authenticated or if they decline the app permissions. 143 this.closeMainWindow_();
145 *
146 * @param {!remoting.Error} error The failure reason.
147 */
148 remoting.DesktopRemoting.prototype.signInFailed = function(error) {
149 remoting.showErrorMessage(error);
150 }; 144 };
151 145
152 /** 146 /**
153 * @return {string} Application product name to be used in UI. 147 * @param {remoting.ConnectionInfo} connectionInfo
148 * @override {remoting.ApplicationInterface}
154 */ 149 */
155 remoting.DesktopRemoting.prototype.getApplicationName = function() { 150 remoting.DesktopRemoting.prototype.onConnected_ = function(connectionInfo) {
156 return chrome.i18n.getMessage(/*i18n-content*/'PRODUCT_NAME'); 151 this.initSession_(connectionInfo);
157 };
158 152
159 /**
160 * Called when a new session has been connected.
161 *
162 * @param {remoting.ConnectionInfo} connectionInfo
163 * @return {void} Nothing.
164 */
165 remoting.DesktopRemoting.prototype.handleConnected = function(connectionInfo) {
166 // Set the text on the buttons shown under the error message so that they are 153 // Set the text on the buttons shown under the error message so that they are
167 // easy to understand in the case where a successful connection failed, as 154 // easy to understand in the case where a successful connection failed, as
168 // opposed to the case where a connection never succeeded. 155 // opposed to the case where a connection never succeeded.
169 // TODO(garykac): Investigate to see if these need to be reverted to their 156 // TODO(garykac): Investigate to see if these need to be reverted to their
170 // original values in the onDisconnected method. 157 // original values in the onDisconnected_ method.
171 var button1 = document.getElementById('client-reconnect-button'); 158 var button1 = document.getElementById('client-reconnect-button');
172 l10n.localizeElementFromTag(button1, /*i18n-content*/'RECONNECT'); 159 l10n.localizeElementFromTag(button1, /*i18n-content*/'RECONNECT');
173 button1.removeAttribute('autofocus'); 160 button1.removeAttribute('autofocus');
174 var button2 = document.getElementById('client-finished-me2me-button'); 161 var button2 = document.getElementById('client-finished-me2me-button');
175 l10n.localizeElementFromTag(button2, /*i18n-content*/'OK'); 162 l10n.localizeElementFromTag(button2, /*i18n-content*/'OK');
176 button2.setAttribute('autofocus', 'autofocus'); 163 button2.setAttribute('autofocus', 'autofocus');
177 164
178 // Reset the refresh flag so that the next connection will retry if needed. 165 // Reset the refresh flag so that the next connection will retry if needed.
179 this.refreshHostJidIfOffline_ = true; 166 this.refreshHostJidIfOffline_ = true;
180 167
181 document.getElementById('access-code-entry').value = ''; 168 document.getElementById('access-code-entry').value = '';
182 remoting.setMode(remoting.AppMode.IN_SESSION); 169 remoting.setMode(remoting.AppMode.IN_SESSION);
183 if (!base.isAppsV2()) { 170 if (!base.isAppsV2()) {
184 remoting.toolbar.center(); 171 remoting.toolbar.center();
185 remoting.toolbar.preview(); 172 remoting.toolbar.preview();
186 } 173 }
187 174
188 this.connectedView_ = new remoting.DesktopConnectedView( 175 this.connectedView_ = new remoting.DesktopConnectedView(
189 document.getElementById('client-container'), connectionInfo); 176 document.getElementById('client-container'), connectionInfo);
190 177
191 // By default, under ChromeOS, remap the right Control key to the right 178 // By default, under ChromeOS, remap the right Control key to the right
192 // Win / Cmd key. 179 // Win / Cmd key.
193 if (remoting.platformIsChromeOS()) { 180 if (remoting.platformIsChromeOS()) {
194 connectionInfo.plugin().setRemapKeys('0x0700e4>0x0700e7'); 181 connectionInfo.plugin().setRemapKeys('0x0700e4>0x0700e7');
195 } 182 }
196 183
197 var sessionConnector = remoting.app.getSessionConnector(); 184 var sessionConnector = remoting.app.getSessionConnector();
198 if (connectionInfo.mode() === remoting.DesktopConnectedView.Mode.ME2ME) { 185 if (connectionInfo.mode() === remoting.DesktopConnectedView.Mode.ME2ME) {
199 if (remoting.app.hasCapability(remoting.ClientSession.Capability.CAST)) { 186 if (remoting.app.hasCapability(remoting.ClientSession.Capability.CAST)) {
200 sessionConnector.registerProtocolExtension( 187 this.sessionConnector_.registerProtocolExtension(
201 new remoting.CastExtensionHandler()); 188 new remoting.CastExtensionHandler());
202 } 189 }
203 sessionConnector.registerProtocolExtension( 190 this.sessionConnector_.registerProtocolExtension(
204 new remoting.GnubbyAuthHandler()); 191 new remoting.GnubbyAuthHandler());
205 } 192 }
206 if (connectionInfo.session().hasCapability( 193 if (connectionInfo.session().hasCapability(
207 remoting.ClientSession.Capability.VIDEO_RECORDER)) { 194 remoting.ClientSession.Capability.VIDEO_RECORDER)) {
208 var recorder = new remoting.VideoFrameRecorder(); 195 var recorder = new remoting.VideoFrameRecorder();
209 sessionConnector.registerProtocolExtension(recorder); 196 sessionConnector.registerProtocolExtension(recorder);
210 this.connectedView_.setVideoFrameRecorder(recorder); 197 this.connectedView_.setVideoFrameRecorder(recorder);
211 } 198 }
212 199
213 if (remoting.pairingRequested) { 200 if (remoting.pairingRequested) {
201 var that = this;
214 /** 202 /**
215 * @param {string} clientId 203 * @param {string} clientId
216 * @param {string} sharedSecret 204 * @param {string} sharedSecret
217 */ 205 */
218 var onPairingComplete = function(clientId, sharedSecret) { 206 var onPairingComplete = function(clientId, sharedSecret) {
219 var connector = remoting.app.getSessionConnector(); 207 var connector = that.sessionConnector_;
220 var host = remoting.hostList.getHostForId(connector.getHostId()); 208 var host = remoting.hostList.getHostForId(connector.getHostId());
221 host.options.pairingInfo.clientId = clientId; 209 host.options.pairingInfo.clientId = clientId;
222 host.options.pairingInfo.sharedSecret = sharedSecret; 210 host.options.pairingInfo.sharedSecret = sharedSecret;
223 host.options.save(); 211 host.options.save();
224 connector.updatePairingInfo(clientId, sharedSecret); 212 connector.updatePairingInfo(clientId, sharedSecret);
225 }; 213 };
226 // Use the platform name as a proxy for the local computer name. 214 // Use the platform name as a proxy for the local computer name.
227 // TODO(jamiewalch): Use a descriptive name for the local computer, for 215 // TODO(jamiewalch): Use a descriptive name for the local computer, for
228 // example, its Chrome Sync name. 216 // example, its Chrome Sync name.
229 var clientName = ''; 217 var clientName = '';
230 if (remoting.platformIsMac()) { 218 if (remoting.platformIsMac()) {
231 clientName = 'Mac'; 219 clientName = 'Mac';
232 } else if (remoting.platformIsWindows()) { 220 } else if (remoting.platformIsWindows()) {
233 clientName = 'Windows'; 221 clientName = 'Windows';
234 } else if (remoting.platformIsChromeOS()) { 222 } else if (remoting.platformIsChromeOS()) {
235 clientName = 'ChromeOS'; 223 clientName = 'ChromeOS';
236 } else if (remoting.platformIsLinux()) { 224 } else if (remoting.platformIsLinux()) {
237 clientName = 'Linux'; 225 clientName = 'Linux';
238 } else { 226 } else {
239 console.log('Unrecognized client platform. Using navigator.platform.'); 227 console.log('Unrecognized client platform. Using navigator.platform.');
240 clientName = navigator.platform; 228 clientName = navigator.platform;
241 } 229 }
242 connectionInfo.session().requestPairing(clientName, onPairingComplete); 230 connectionInfo.session().requestPairing(clientName, onPairingComplete);
243 } 231 }
244 }; 232 };
245 233
246 /** 234 /**
247 * Called when the current session has been disconnected. 235 * @override {remoting.ApplicationInterface}
248 *
249 * @return {void} Nothing.
250 */ 236 */
251 remoting.DesktopRemoting.prototype.handleDisconnected = function() { 237 remoting.DesktopRemoting.prototype.onDisconnected_ = function() {
252 var mode = this.connectedView_.getMode(); 238 var mode = this.connectedView_.getMode();
253 if (mode === remoting.DesktopConnectedView.Mode.IT2ME) { 239 if (mode === remoting.DesktopConnectedView.Mode.IT2ME) {
254 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_IT2ME); 240 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_IT2ME);
255 } else { 241 } else {
256 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_ME2ME); 242 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_ME2ME);
257 } 243 }
258 base.dispose(this.connectedView_); 244 base.dispose(this.connectedView_);
259 this.connectedView_ = null; 245 this.connectedView_ = null;
260 }; 246 };
261 247
262 /** 248 /**
263 * Called when the current session's connection has failed.
264 *
265 * @param {remoting.SessionConnector} connector
266 * @param {!remoting.Error} error 249 * @param {!remoting.Error} error
267 * @return {void} Nothing. 250 * @override {remoting.ApplicationInterface}
268 */ 251 */
269 remoting.DesktopRemoting.prototype.handleConnectionFailed = function( 252 remoting.DesktopRemoting.prototype.onConnectionFailed_ = function(error) {
270 connector, error) {
271 var that = this; 253 var that = this;
272 var onHostListRefresh = function(/** boolean */ success) { 254 var onHostListRefresh = function(/** boolean */ success) {
273 if (success) { 255 if (success) {
256 var connector = that.sessionConnector_;
274 var host = remoting.hostList.getHostForId(connector.getHostId()); 257 var host = remoting.hostList.getHostForId(connector.getHostId());
275 if (host) { 258 if (host) {
276 connector.retryConnectMe2Me(host); 259 connector.retryConnectMe2Me(host);
277 return; 260 return;
278 } 261 }
279 } 262 }
280 that.handleError(error); 263 that.onError_(error);
281 }; 264 };
282 265
283 var mode = this.app_.getSessionConnector().getConnectionMode(); 266 var mode = this.sessionConnector_.getConnectionMode();
284 if (error.hasTag(remoting.Error.Tag.HOST_IS_OFFLINE) && 267 if (error.hasTag(remoting.Error.Tag.HOST_IS_OFFLINE) &&
285 mode === remoting.DesktopConnectedView.Mode.ME2ME && 268 mode === remoting.DesktopConnectedView.Mode.ME2ME &&
286 this.refreshHostJidIfOffline_) { 269 this.refreshHostJidIfOffline_) {
287 this.refreshHostJidIfOffline_ = false; 270 this.refreshHostJidIfOffline_ = false;
288 271
289 // The plugin will be re-created when the host finished refreshing 272 // The plugin will be re-created when the host finished refreshing
290 remoting.hostList.refresh(onHostListRefresh); 273 remoting.hostList.refresh(onHostListRefresh);
291 } else { 274 } else {
292 this.handleError(error); 275 this.onError_(error);
293 } 276 }
294 }; 277 };
295 278
296 /** 279 /**
297 * Called when an error needs to be displayed to the user.
298 *
299 * @param {!remoting.Error} error The error to be localized and displayed. 280 * @param {!remoting.Error} error The error to be localized and displayed.
300 * @return {void} Nothing. 281 * @override {remoting.ApplicationInterface}
301 */ 282 */
302 remoting.DesktopRemoting.prototype.handleError = function(error) { 283 remoting.DesktopRemoting.prototype.onError_ = function(error) {
303 console.error('Connection failed: ' + error.toString()); 284 console.error('Connection failed: ' + error.toString());
304 var mode = this.connectedView_ ? this.connectedView_.getMode() 285 var mode = this.connectedView_ ? this.connectedView_.getMode()
305 : this.app_.getSessionConnector().getConnectionMode(); 286 : this.sessionConnector_.getConnectionMode();
306 base.dispose(this.connectedView_); 287 base.dispose(this.connectedView_);
307 this.connectedView_ = null; 288 this.connectedView_ = null;
308 289
309 if (error.hasTag(remoting.Error.Tag.AUTHENTICATION_FAILED)) { 290 if (error.hasTag(remoting.Error.Tag.AUTHENTICATION_FAILED)) {
310 remoting.setMode(remoting.AppMode.HOME); 291 remoting.setMode(remoting.AppMode.HOME);
311 remoting.handleAuthFailureAndRelaunch(); 292 remoting.handleAuthFailureAndRelaunch();
312 return; 293 return;
313 } 294 }
314 295
315 // Reset the refresh flag so that the next connection will retry if needed. 296 // Reset the refresh flag so that the next connection will retry if needed.
316 this.refreshHostJidIfOffline_ = true; 297 this.refreshHostJidIfOffline_ = true;
317 298
318 var errorDiv = document.getElementById('connect-error-message'); 299 var errorDiv = document.getElementById('connect-error-message');
319 l10n.localizeElementFromTag(errorDiv, error.getTag()); 300 l10n.localizeElementFromTag(errorDiv, error.getTag());
320 301
321 if (mode == remoting.DesktopConnectedView.Mode.IT2ME) { 302 if (mode == remoting.DesktopConnectedView.Mode.IT2ME) {
322 remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED_IT2ME); 303 remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED_IT2ME);
323 } else { 304 } else {
324 remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED_ME2ME); 305 remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED_ME2ME);
325 } 306 }
326 }; 307 };
327 308
328 /** 309 /**
329 * No cleanup required for desktop remoting.
330 */
331 remoting.DesktopRemoting.prototype.handleExit = function() {
332 };
333
334 /**
335 * Determine whether or not the app is running in a window. 310 * Determine whether or not the app is running in a window.
336 * @param {function(boolean):void} callback Callback to receive whether or not 311 * @param {function(boolean):void} callback Callback to receive whether or not
337 * the current tab is running in windowed mode. 312 * the current tab is running in windowed mode.
338 * @private 313 * @private
339 */ 314 */
340 remoting.DesktopRemoting.prototype.isWindowed_ = function(callback) { 315 remoting.DesktopRemoting.prototype.isWindowed_ = function(callback) {
341 /** @param {chrome.Window} win The current window. */ 316 /** @param {chrome.Window} win The current window. */
342 var windowCallback = function(win) { 317 var windowCallback = function(win) {
343 callback(win.type == 'popup'); 318 callback(win.type == 'popup');
344 }; 319 };
(...skipping 12 matching lines...) Expand all
357 } 332 }
358 } 333 }
359 334
360 /** 335 /**
361 * If an IT2Me client or host is active then prompt the user before closing. 336 * If an IT2Me client or host is active then prompt the user before closing.
362 * If a Me2Me client is active then don't bother, since closing the window is 337 * If a Me2Me client is active then don't bother, since closing the window is
363 * the more intuitive way to end a Me2Me session, and re-connecting is easy. 338 * the more intuitive way to end a Me2Me session, and re-connecting is easy.
364 * @private 339 * @private
365 */ 340 */
366 remoting.DesktopRemoting.prototype.promptClose_ = function() { 341 remoting.DesktopRemoting.prototype.promptClose_ = function() {
367 var sessionConnector = remoting.app.getSessionConnector(); 342 var sessionConnector = this.sessionConnector_;
368 if (sessionConnector && 343 if (sessionConnector &&
369 sessionConnector.getConnectionMode() == 344 sessionConnector.getConnectionMode() ==
370 remoting.DesktopConnectedView.Mode.IT2ME) { 345 remoting.DesktopConnectedView.Mode.IT2ME) {
371 switch (remoting.currentMode) { 346 switch (remoting.currentMode) {
372 case remoting.AppMode.CLIENT_CONNECTING: 347 case remoting.AppMode.CLIENT_CONNECTING:
373 case remoting.AppMode.HOST_WAITING_FOR_CODE: 348 case remoting.AppMode.HOST_WAITING_FOR_CODE:
374 case remoting.AppMode.HOST_WAITING_FOR_CONNECTION: 349 case remoting.AppMode.HOST_WAITING_FOR_CONNECTION:
375 case remoting.AppMode.HOST_SHARED: 350 case remoting.AppMode.HOST_SHARED:
376 case remoting.AppMode.IN_SESSION: 351 case remoting.AppMode.IN_SESSION:
377 return chrome.i18n.getMessage(/*i18n-content*/'CLOSE_PROMPT'); 352 return chrome.i18n.getMessage(/*i18n-content*/'CLOSE_PROMPT');
378 default: 353 default:
379 return null; 354 return null;
380 } 355 }
381 } 356 }
382 }; 357 };
383 358
384 /** @returns {remoting.DesktopConnectedView} */ 359 /** @returns {remoting.DesktopConnectedView} */
385 remoting.DesktopRemoting.prototype.getConnectedViewForTesting = function() { 360 remoting.DesktopRemoting.prototype.getConnectedViewForTesting = function() {
386 return this.connectedView_; 361 return this.connectedView_;
387 }; 362 };
388
389 /**
390 * Global instance of remoting.DesktopRemoting used for testing.
391 * @type {remoting.DesktopRemoting}
392 */
393 remoting.desktopDelegateForTesting = null;
OLDNEW
« no previous file with comments | « remoting/webapp/crd/js/crd_main.js ('k') | remoting/webapp/crd/js/window_frame.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698