1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5var browserBridge;
6
7/**
8 * Class that keeps track of current burn process state.
9 * @param {Object} strings Localized state strings.
10 * @constructor
11 */
12function State(strings) {
13  this.setStrings(strings);
14  this.changeState(State.StatesEnum.DEVICE_NONE);
15}
16
17/**
18 * State Enum object.
19 */
20State.StatesEnum = {
21  DEVICE_NONE: {
22    cssState: 'device-detected-none',
23  },
24  DEVICE_USB: {
25    cssState: 'device-detected-usb warning',
26  },
27  DEVICE_SD: {
28    cssState: 'device-detected-sd warning',
29  },
30  DEVICE_MUL: {
31    cssState: 'device-detected-mul warning',
32  },
33  ERROR_NO_NETWORK: {
34    cssState: 'warning-no-conf',
35  },
36  ERROR_DEVICE_TOO_SMALL: {
37    cssState: 'warning-no-conf',
38  },
39  PROGRESS_DOWNLOAD: {
40    cssState: 'progress progress-canceble',
41  },
42  PROGRESS_UNZIP: {
43    cssState: 'progress progress-canceble',
44  },
45  PROGRESS_BURN: {
46    cssState: 'progress',
47  },
48  FAIL: {
49    cssState: 'error',
50  },
51  SUCCESS: {
52    cssState: 'success',
53  },
54};
55
56State.prototype = {
57  /**
58   * Sets the state strings.
59   * @param {Object} strings Localized state strings.
60   */
61  setStrings: function(strings) {
62    State.StatesEnum.DEVICE_NONE.statusText =
63        strings.getString('statusDevicesNone');
64    State.StatesEnum.DEVICE_NONE.warningText =
65        strings.getString('warningDevicesNone');
66    State.StatesEnum.DEVICE_USB.statusText =
67      strings.getString('statusDeviceUSB');
68    State.StatesEnum.DEVICE_SD.statusText = strings.getString('statusDeviceSD');
69    State.StatesEnum.DEVICE_MUL.statusText =
70        strings.getString('statusDevicesMultiple');
71    State.StatesEnum.ERROR_NO_NETWORK.statusText =
72        strings.getString('statusNoConnection');
73    State.StatesEnum.ERROR_NO_NETWORK.warningText =
74        strings.getString('warningNoConnection');
75    State.StatesEnum.ERROR_DEVICE_TOO_SMALL.statusText =
76        strings.getString('statusNoSpace');
77    State.StatesEnum.PROGRESS_DOWNLOAD.statusText =
78        strings.getString('statusDownloading');
79    State.StatesEnum.PROGRESS_UNZIP.statusText =
80        strings.getString('statusUnzip');
81    State.StatesEnum.PROGRESS_BURN.statusText = strings.getString('statusBurn');
82    State.StatesEnum.FAIL.statusText = strings.getString('statusError');
83    State.StatesEnum.SUCCESS.statusText = strings.getString('statusSuccess');
84    State.StatesEnum.SUCCESS.warningText = strings.getString('warningSuccess');
85  },
86
87  /**
88   * Changes the current state to new state.
89   * @param {Object} newState Specifies the new state object.
90   */
91  changeState: function(newState) {
92    if (newState == this.state)
93      return;
94    this.state = newState;
95
96    $('main-content').className = this.state.cssState;
97
98    $('status-text').textContent = this.state.statusText;
99
100    if (newState.warningText)
101      $('warning-text').textContent = this.state.warningText;
102
103    if (this.isInitialState() && this.state != State.StatesEnum.DEVICE_NONE) {
104      $('warning-button').textContent = loadTimeData.getString('confirmButton');
105    } else if (this.state == State.StatesEnum.FAIL) {
106      $('warning-button').textContent =
107          loadTimeData.getString('retryButton');
108    }
109  },
110
111  /**
112   * Reset to initial state.
113   * @param {Array} devices Array of device information.
114   */
115  gotoInitialState: function(devices) {
116    if (devices.length == 0) {
117      this.changeState(State.StatesEnum.DEVICE_NONE);
118    } else if (devices.length == 1) {
119      // If a device type is not specified for some reason, we should
120      // default to display a USB device.
121      var initialState = State.StatesEnum.DEVICE_USB;
122      if (devices[0].type == 'sd')
123        initialState = State.StatesEnum.DEVICE_SD;
124      this.changeState(initialState);
125    } else {
126      this.changeState(State.StatesEnum.DEVICE_MUL);
127    }
128  },
129
130  /**
131   * Returns true if the device is in initial state.
132   * @return {boolean} True if the device is in initial state else false.
133   */
134  isInitialState: function() {
135    return this.state == State.StatesEnum.DEVICE_NONE ||
136           this.state == State.StatesEnum.DEVICE_USB ||
137           this.state == State.StatesEnum.DEVICE_SD ||
138           this.state == State.StatesEnum.DEVICE_MUL;
139  },
140
141  /**
142   * Returns true if device state matches the given state name.
143   * @param {string} stateName Given state name.
144   * @return {boolean} True if the device state matches the given state name.
145   */
146  equals: function(stateName) {
147    return this.state == stateName;
148  }
149};
150
151/**
152 * Class that keeps track of available devices.
153 * @constructor
154 */
155function DeviceSelection() {
156  this.selectedDevice = undefined;
157  this.devices = [];
158}
159
160DeviceSelection.prototype = {
161  /**
162   * Shows the currently selected device.
163   */
164  showDeviceSelection: function() {
165    if (this.devices.length == 0) {
166      this.selectedDevice = undefined;
167    } else {
168      this.selectDevice(this.devices[0].devicePath);
169    }
170  },
171
172  /**
173   * Handles device selected event.
174   * @param {string} label Device label.
175   * @param {string} filePath File path.
176   * @param {string} devicePath Selected device path.
177   */
178  onDeviceSelected: function(label, filePath, devicePath) {
179    $('warning-button').onclick =
180        browserBridge.sendBurnImageMessage.bind(browserBridge, filePath,
181            devicePath);
182
183    this.selectedDevice = devicePath;
184
185    $('warning-text').textContent =
186        loadTimeData.getStringF('warningDevices', label);
187  },
188
189  /**
190   * Selects the specified device based on the specified path.
191   * @param {string} path Device path.
192   */
193  selectDevice: function(path) {
194    var element = $('radio-' + path);
195    element.checked = true;
196    element.onclick.apply(element);
197  },
198
199  /**
200   * Creates a new device element.
201   * @param {Object} device Specifies new device information.
202   * @return {HTMLLIElement} New device element.
203   */
204  createNewDeviceElement: function(device) {
205    var element = document.createElement('li');
206    var radioButton = document.createElement('input');
207    radioButton.type = 'radio';
208    radioButton.name = 'device';
209    radioButton.value = device.label;
210    radioButton.id = 'radio-' + device.devicePath;
211    radioButton.className = 'float-start';
212    var deviceLabelText = document.createElement('p');
213    deviceLabelText.textContent = device.label;
214    deviceLabelText.className = 'select-option float-start';
215    var newLine = document.createElement('div');
216    newLine.className = 'new-line';
217    element.appendChild(radioButton);
218    element.appendChild(deviceLabelText);
219    element.appendChild(newLine);
220    element.id = 'select-' + device.devicePath;
221    element.className = 'selection-element';
222    radioButton.onclick = this.onDeviceSelected.bind(this,
223        device.label, device.filePath, device.devicePath);
224    return element;
225  },
226
227  /**
228   * Updates the list of selected devices.
229   * @param {Array} devices List of devices.
230   */
231  devicesUpdated: function(newDevices) {
232    this.devices = newDevices;
233    var selectListDOM = $('device-selection');
234    selectListDOM.innerHTML = '';
235    if (this.devices.length > 0) {
236      for (var i = 0; i < this.devices.length; i++) {
237        var element = this.createNewDeviceElement(this.devices[i]);
238        selectListDOM.appendChild(element);
239      }
240      this.selectDevice(this.devices[0].devicePath);
241    } else {
242      this.selectedDevice = undefined;
243    }
244  },
245
246  /**
247   * Handles device added event.
248   * @param {Object} device Device information.
249   * @param {boolean} allowSelect True to update the selected device info.
250   */
251  deviceAdded: function(device, allowSelect) {
252    this.devices.push(device);
253    var selectListDOM = $('device-selection');
254    selectListDOM.appendChild(this.createNewDeviceElement(device));
255    if (allowSelect && this.devices.length == 1)
256      this.selectDevice(device.devicePath);
257  },
258
259  /**
260   * Handles device removed event.
261   * @param {string} devicePath Device path to be removed.
262   * @param {boolean} allowSelect True to update the selected device info.
263   */
264  deviceRemoved: function(devicePath, allowSelect) {
265    device = this.findDevice(devicePath);
266    if (!device)
267      return;
268    this.devices.splice(this.devices.indexOf(device), 1);
269
270    // Remove device selection element from DOM.
271    var deviceSelectElement = $('select-' + devicePath);
272    deviceSelectElement.parentNode.removeChild(deviceSelectElement);
273
274    // Update selected device element.
275    if (allowSelect) {
276      if (this.devices.length > 0) {
277        if (this.selectedDevice == devicePath)
278          this.selectDevice(this.devices[0].devicePath);
279      } else {
280        this.selectedDevice = undefined;
281      }
282    }
283  },
284
285  /**
286   * Finds device with given device path property.
287   * @param {string} devicePath Device path of device to find.
288   * @return {Object} Matching device information or undefined if not found.
289   */
290  findDevice: function(devicePath) {
291    for (var i = 0; i < this.devices.length; ++i) {
292      if (this.devices[i].devicePath == devicePath) {
293        return this.devices[i];
294      }
295    }
296    return undefined;
297  }
298};
299
300/**
301 * Class that handles communication with chrome.
302 * @constructor
303 */
304function BrowserBridge() {
305  this.currentState = new State(loadTimeData);
306  this.deviceSelection = new DeviceSelection();
307  // We will use these often so it makes sence making them class members to
308  // avoid frequent document.getElementById calls.
309  this.progressElement = $('progress-div');
310  this.progressText = $('progress-text');
311  this.progressTimeLeftText = $('pending-time');
312}
313
314BrowserBridge.prototype = {
315  sendCancelMessage: function() {
316    chrome.send('cancelBurnImage');
317  },
318
319  sendGetDevicesMessage: function() {
320    chrome.send('getDevices');
321  },
322
323  sendWebuiInitializedMessage: function() {
324    chrome.send('webuiInitialized');
325  },
326
327  /**
328   * Sends the burn image message to c++ code.
329   * @param {string} filePath Specifies the file path.
330   * @param {string} devicePath Specifies the device path.
331   */
332  sendBurnImageMessage: function(filePath, devicePath) {
333    chrome.send('burnImage', [devicePath, filePath]);
334  },
335
336  reportSuccess: function() {
337    this.currentState.changeState(State.StatesEnum.SUCCESS);
338  },
339
340  /**
341   * Update the device state to report a failure and display an error message to
342   * the user.
343   * @param {string} errorMessage Specifies the warning text message.
344   */
345  reportFail: function(errorMessage) {
346    this.currentState.changeState(State.StatesEnum.FAIL);
347    $('warning-text').textContent = errorMessage;
348    $('warning-button').onclick = this.onBurnRetry.bind(this);
349  },
350
351  /**
352   * Handles device added event.
353   * @param {Object} device Device information.
354   */
355  deviceAdded: function(device) {
356    var inInitialState = this.currentState.isInitialState();
357    this.deviceSelection.deviceAdded(device, inInitialState);
358    if (inInitialState)
359      this.currentState.gotoInitialState(this.deviceSelection.devices);
360  },
361
362  /**
363   * Handles device removed event.
364   * @param {string} devicePath Device path to be removed.
365   */
366  deviceRemoved: function(devicePath) {
367    var inInitialState = this.currentState.isInitialState();
368    this.deviceSelection.deviceRemoved(devicePath, inInitialState);
369    if (inInitialState)
370      this.currentState.gotoInitialState(this.deviceSelection.devices);
371  },
372
373  /**
374   * Gets device callbacks and update the current state.
375   * @param {Array} devices List of devices.
376   */
377  getDevicesCallback: function(devices) {
378    this.deviceSelection.devicesUpdated(devices);
379    this.currentState.gotoInitialState(this.deviceSelection.devices);
380    this.sendWebuiInitializedMessage();
381  },
382
383  /**
384   *  Updates the progress information based on the signal received.
385   *  @param {Object} updateSignal Specifies the signal information.
386   */
387  updateProgress: function(updateSignal) {
388    if (updateSignal.progressType == 'download' &&
389        !this.currentState.equals(State.StatesEnum.PROGRESS_DOWNLOAD)) {
390      this.currentState.changeState(State.StatesEnum.PROGRESS_DOWNLOAD);
391    } else if (updateSignal.progressType == 'unzip' &&
392        !this.currentState.equals(State.StatesEnum.PROGRESS_UNZIP)) {
393      this.currentState.changeState(State.StatesEnum.PROGRESS_UNZIP);
394    } else if (updateSignal.progressType == 'burn' &&
395        !this.currentState.equals(State.StatesEnum.PROGRESS_BURN)) {
396      this.currentState.changeState(State.StatesEnum.PROGRESS_BURN);
397    }
398
399    if (!(updateSignal.amountTotal > 0)) {
400      this.progressElement.removeAttribute('value');
401    } else {
402      this.progressElement.value = updateSignal.amountFinished;
403      this.progressElement.max = updateSignal.amountTotal;
404    }
405
406    this.progressText.textContent = updateSignal.progressText;
407    this.progressTimeLeftText.textContent = updateSignal.timeLeftText;
408  },
409
410  reportNoNetwork: function() {
411    this.currentState.changeState(State.StatesEnum.ERROR_NO_NETWORK);
412  },
413
414  reportNetworkDetected: function() {
415    if (this.currentState.equals(State.StatesEnum.ERROR_NO_NETWORK)) {
416      this.deviceSelection.showDeviceSelection();
417      this.currentState.gotoInitialState(this.deviceSelection.devices);
418    }
419  },
420
421  /**
422   *  Updates the current state to report device too small error.
423   *  @param {number} deviceSize Received device size.
424   */
425  reportDeviceTooSmall: function(deviceSize) {
426    this.currentState.changeState(State.StatesEnum.ERROR_DEVICE_TOO_SMALL);
427    $('warning-text').textContent =
428        loadTimeData.getStringF('warningNoSpace', deviceSize);
429  },
430
431  /**
432   * Processes click on 'Retry' button in FAIL state.
433   */
434  onBurnRetry: function() {
435    this.deviceSelection.showDeviceSelection();
436    this.currentState.gotoInitialState(this.deviceSelection.devices);
437  }
438};
439
440document.addEventListener('DOMContentLoaded', function() {
441  browserBridge = new BrowserBridge();
442
443  $('cancel-button').onclick =
444      browserBridge.sendCancelMessage.bind(browserBridge);
445  browserBridge.sendGetDevicesMessage();
446});
447