1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5'use strict';
6
7const Cu = Components.utils;
8const Cc = Components.classes;
9const Ci = Components.interfaces;
10
11Cu.import("resource://gre/modules/XPCOMUtils.jsm");
12XPCOMUtils.defineLazyModuleGetter(this, 'Services',
13  'resource://gre/modules/Services.jsm');
14XPCOMUtils.defineLazyModuleGetter(this, 'Rect',
15  'resource://gre/modules/Geometry.jsm');
16
17this.EXPORTED_SYMBOLS = ['Utils', 'Logger', 'PivotContext', 'PrefCache'];
18
19this.Utils = {
20  _buildAppMap: {
21    '{3c2e2abc-06d4-11e1-ac3b-374f68613e61}': 'b2g',
22    '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}': 'browser',
23    '{aa3c5121-dab2-40e2-81ca-7ea25febc110}': 'mobile/android',
24    '{a23983c0-fd0e-11dc-95ff-0800200c9a66}': 'mobile/xul'
25  },
26
27  init: function Utils_init(aWindow) {
28    if (this._win)
29      // XXX: only supports attaching to one window now.
30      throw new Error('Only one top-level window could used with AccessFu');
31
32    this._win = Cu.getWeakReference(aWindow);
33  },
34
35  uninit: function Utils_uninit() {
36    if (!this._win) {
37      return;
38    }
39    delete this._win;
40  },
41
42  get win() {
43    if (!this._win) {
44      return null;
45    }
46    return this._win.get();
47  },
48
49  get AccRetrieval() {
50    if (!this._AccRetrieval) {
51      this._AccRetrieval = Cc['@mozilla.org/accessibleRetrieval;1'].
52        getService(Ci.nsIAccessibleRetrieval);
53    }
54
55    return this._AccRetrieval;
56  },
57
58  set MozBuildApp(value) {
59    this._buildApp = value;
60  },
61
62  get MozBuildApp() {
63    if (!this._buildApp)
64      this._buildApp = this._buildAppMap[Services.appinfo.ID];
65    return this._buildApp;
66  },
67
68  get OS() {
69    if (!this._OS)
70      this._OS = Services.appinfo.OS;
71    return this._OS;
72  },
73
74  get ScriptName() {
75    if (!this._ScriptName)
76      this._ScriptName =
77        (Services.appinfo.processType == 2) ? 'AccessFuContent' : 'AccessFu';
78    return this._ScriptName;
79  },
80
81  get AndroidSdkVersion() {
82    if (!this._AndroidSdkVersion) {
83      if (Services.appinfo.OS == 'Android') {
84        this._AndroidSdkVersion = Services.sysinfo.getPropertyAsInt32('version');
85      } else {
86        // Most useful in desktop debugging.
87        this._AndroidSdkVersion = 15;
88      }
89    }
90    return this._AndroidSdkVersion;
91  },
92
93  set AndroidSdkVersion(value) {
94    // When we want to mimic another version.
95    this._AndroidSdkVersion = value;
96  },
97
98  get BrowserApp() {
99    if (!this.win) {
100      return null;
101    }
102    switch (this.MozBuildApp) {
103      case 'mobile/android':
104        return this.win.BrowserApp;
105      case 'browser':
106        return this.win.gBrowser;
107      case 'b2g':
108        return this.win.shell;
109      default:
110        return null;
111    }
112  },
113
114  get CurrentBrowser() {
115    if (!this.BrowserApp) {
116      return null;
117    }
118    if (this.MozBuildApp == 'b2g')
119      return this.BrowserApp.contentBrowser;
120    return this.BrowserApp.selectedBrowser;
121  },
122
123  get CurrentContentDoc() {
124    let browser = this.CurrentBrowser;
125    return browser ? browser.contentDocument : null;
126  },
127
128  get AllMessageManagers() {
129    let messageManagers = [];
130
131    for (let i = 0; i < this.win.messageManager.childCount; i++)
132      messageManagers.push(this.win.messageManager.getChildAt(i));
133
134    let document = this.CurrentContentDoc;
135
136    if (document) {
137      let remoteframes = document.querySelectorAll('iframe');
138
139      for (let i = 0; i < remoteframes.length; ++i) {
140        let mm = this.getMessageManager(remoteframes[i]);
141        if (mm) {
142          messageManagers.push(mm);
143        }
144      }
145
146    }
147
148    return messageManagers;
149  },
150
151  get isContentProcess() {
152    delete this.isContentProcess;
153    this.isContentProcess =
154      Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
155    return this.isContentProcess;
156  },
157
158  getMessageManager: function getMessageManager(aBrowser) {
159    try {
160      return aBrowser.QueryInterface(Ci.nsIFrameLoaderOwner).
161         frameLoader.messageManager;
162    } catch (x) {
163      Logger.logException(x);
164      return null;
165    }
166  },
167
168  getViewport: function getViewport(aWindow) {
169    switch (this.MozBuildApp) {
170      case 'mobile/android':
171        return aWindow.BrowserApp.selectedTab.getViewport();
172      default:
173        return null;
174    }
175  },
176
177  getStates: function getStates(aAccessible) {
178    if (!aAccessible)
179      return [0, 0];
180
181    let state = {};
182    let extState = {};
183    aAccessible.getState(state, extState);
184    return [state.value, extState.value];
185  },
186
187  getAttributes: function getAttributes(aAccessible) {
188    let attributesEnum = aAccessible.attributes.enumerate();
189    let attributes = {};
190
191    // Populate |attributes| object with |aAccessible|'s attribute key-value
192    // pairs.
193    while (attributesEnum.hasMoreElements()) {
194      let attribute = attributesEnum.getNext().QueryInterface(
195        Ci.nsIPropertyElement);
196      attributes[attribute.key] = attribute.value;
197    }
198
199    return attributes;
200  },
201
202  getVirtualCursor: function getVirtualCursor(aDocument) {
203    let doc = (aDocument instanceof Ci.nsIAccessible) ? aDocument :
204      this.AccRetrieval.getAccessibleFor(aDocument);
205
206    return doc.QueryInterface(Ci.nsIAccessibleDocument).virtualCursor;
207  },
208
209  getPixelsPerCSSPixel: function getPixelsPerCSSPixel(aWindow) {
210    return aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
211      .getInterface(Ci.nsIDOMWindowUtils).screenPixelsPerCSSPixel;
212  }
213};
214
215this.Logger = {
216  DEBUG: 0,
217  INFO: 1,
218  WARNING: 2,
219  ERROR: 3,
220  _LEVEL_NAMES: ['DEBUG', 'INFO', 'WARNING', 'ERROR'],
221
222  logLevel: 1, // INFO;
223
224  test: false,
225
226  log: function log(aLogLevel) {
227    if (aLogLevel < this.logLevel)
228      return;
229
230    let message = Array.prototype.slice.call(arguments, 1).join(' ');
231    message = '[' + Utils.ScriptName + '] ' + this._LEVEL_NAMES[aLogLevel] +
232      ' ' + message + '\n';
233    dump(message);
234    // Note: used for testing purposes. If |this.test| is true, also log to
235    // the console service.
236    if (this.test) {
237      try {
238        Services.console.logStringMessage(message);
239      } catch (ex) {
240        // There was an exception logging to the console service.
241      }
242    }
243  },
244
245  info: function info() {
246    this.log.apply(
247      this, [this.INFO].concat(Array.prototype.slice.call(arguments)));
248  },
249
250  debug: function debug() {
251    this.log.apply(
252      this, [this.DEBUG].concat(Array.prototype.slice.call(arguments)));
253  },
254
255  warning: function warning() {
256    this.log.apply(
257      this, [this.WARNING].concat(Array.prototype.slice.call(arguments)));
258  },
259
260  error: function error() {
261    this.log.apply(
262      this, [this.ERROR].concat(Array.prototype.slice.call(arguments)));
263  },
264
265  logException: function logException(aException) {
266    try {
267      let args = [aException.message];
268      args.push.apply(args, aException.stack ? ['\n', aException.stack] :
269        ['(' + aException.fileName + ':' + aException.lineNumber + ')']);
270      this.error.apply(this, args);
271    } catch (x) {
272      this.error(x);
273    }
274  },
275
276  accessibleToString: function accessibleToString(aAccessible) {
277    let str = '[ defunct ]';
278    try {
279      str = '[ ' + Utils.AccRetrieval.getStringRole(aAccessible.role) +
280        ' | ' + aAccessible.name + ' ]';
281    } catch (x) {
282    }
283
284    return str;
285  },
286
287  eventToString: function eventToString(aEvent) {
288    let str = Utils.AccRetrieval.getStringEventType(aEvent.eventType);
289    if (aEvent.eventType == Ci.nsIAccessibleEvent.EVENT_STATE_CHANGE) {
290      let event = aEvent.QueryInterface(Ci.nsIAccessibleStateChangeEvent);
291      let stateStrings = event.isExtraState ?
292        Utils.AccRetrieval.getStringStates(0, event.state) :
293        Utils.AccRetrieval.getStringStates(event.state, 0);
294      str += ' (' + stateStrings.item(0) + ')';
295    }
296
297    return str;
298  },
299
300  statesToString: function statesToString(aAccessible) {
301    let [state, extState] = Utils.getStates(aAccessible);
302    let stringArray = [];
303    let stateStrings = Utils.AccRetrieval.getStringStates(state, extState);
304    for (var i=0; i < stateStrings.length; i++)
305      stringArray.push(stateStrings.item(i));
306    return stringArray.join(' ');
307  },
308
309  dumpTree: function dumpTree(aLogLevel, aRootAccessible) {
310    if (aLogLevel < this.logLevel)
311      return;
312
313    this._dumpTreeInternal(aLogLevel, aRootAccessible, 0);
314  },
315
316  _dumpTreeInternal: function _dumpTreeInternal(aLogLevel, aAccessible, aIndent) {
317    let indentStr = '';
318    for (var i=0; i < aIndent; i++)
319      indentStr += ' ';
320    this.log(aLogLevel, indentStr,
321             this.accessibleToString(aAccessible),
322             '(' + this.statesToString(aAccessible) + ')');
323    for (var i=0; i < aAccessible.childCount; i++)
324      this._dumpTreeInternal(aLogLevel, aAccessible.getChildAt(i), aIndent + 1);
325    }
326};
327
328/**
329 * PivotContext: An object that generates and caches context information
330 * for a given accessible and its relationship with another accessible.
331 */
332this.PivotContext = function PivotContext(aAccessible, aOldAccessible) {
333  this._accessible = aAccessible;
334  this._oldAccessible =
335    this._isDefunct(aOldAccessible) ? null : aOldAccessible;
336}
337
338PivotContext.prototype = {
339  get accessible() {
340    return this._accessible;
341  },
342
343  get oldAccessible() {
344    return this._oldAccessible;
345  },
346
347  /*
348   * This is a list of the accessible's ancestry up to the common ancestor
349   * of the accessible and the old accessible. It is useful for giving the
350   * user context as to where they are in the heirarchy.
351   */
352  get newAncestry() {
353    if (!this._newAncestry) {
354      let newLineage = [];
355      let oldLineage = [];
356
357      let parent = this._accessible;
358      while (parent && (parent = parent.parent))
359        newLineage.push(parent);
360
361      parent = this._oldAccessible;
362      while (parent && (parent = parent.parent))
363        oldLineage.push(parent);
364
365      this._newAncestry = [];
366
367      while (true) {
368        let newAncestor = newLineage.pop();
369        let oldAncestor = oldLineage.pop();
370
371        if (newAncestor == undefined)
372          break;
373
374        if (newAncestor != oldAncestor)
375          this._newAncestry.push(newAncestor);
376      }
377
378    }
379
380    return this._newAncestry;
381  },
382
383  /*
384   * Traverse the accessible's subtree in pre or post order.
385   * It only includes the accessible's visible chidren.
386   */
387  _traverse: function _traverse(aAccessible, preorder) {
388    let list = [];
389    let child = aAccessible.firstChild;
390    while (child) {
391      let state = {};
392      child.getState(state, {});
393      if (!(state.value & Ci.nsIAccessibleStates.STATE_INVISIBLE)) {
394        let traversed = _traverse(child, preorder);
395        // Prepend or append a child, based on traverse order.
396        traversed[preorder ? "unshift" : "push"](child);
397        list.push.apply(list, traversed);
398      }
399      child = child.nextSibling;
400    }
401    return list;
402  },
403
404  /*
405   * This is a flattened list of the accessible's subtree in preorder.
406   * It only includes the accessible's visible chidren.
407   */
408  get subtreePreorder() {
409    if (!this._subtreePreOrder)
410      this._subtreePreOrder = this._traverse(this._accessible, true);
411
412    return this._subtreePreOrder;
413  },
414
415  /*
416   * This is a flattened list of the accessible's subtree in postorder.
417   * It only includes the accessible's visible chidren.
418   */
419  get subtreePostorder() {
420    if (!this._subtreePostOrder)
421      this._subtreePostOrder = this._traverse(this._accessible, false);
422
423    return this._subtreePostOrder;
424  },
425
426  get bounds() {
427    if (!this._bounds) {
428      let objX = {}, objY = {}, objW = {}, objH = {};
429
430      this._accessible.getBounds(objX, objY, objW, objH);
431
432      this._bounds = new Rect(objX.value, objY.value, objW.value, objH.value);
433    }
434
435    return this._bounds.clone();
436  },
437
438  _isDefunct: function _isDefunct(aAccessible) {
439    try {
440      let extstate = {};
441      aAccessible.getState({}, extstate);
442      return !!(aAccessible.value & Ci.nsIAccessibleStates.EXT_STATE_DEFUNCT);
443    } catch (x) {
444      return true;
445    }
446  }
447};
448
449this.PrefCache = function PrefCache(aName, aCallback, aRunCallbackNow) {
450  this.name = aName;
451  this.callback = aCallback;
452
453  let branch = Services.prefs;
454  this.value = this._getValue(branch);
455
456  if (this.callback && aRunCallbackNow) {
457    try {
458      this.callback(this.name, this.value);
459    } catch (x) {
460      Logger.logException(x);
461    }
462  }
463
464  branch.addObserver(aName, this, true);
465};
466
467PrefCache.prototype = {
468  _getValue: function _getValue(aBranch) {
469    if (!this.type) {
470      this.type = aBranch.getPrefType(this.name);
471    }
472
473    switch (this.type) {
474      case Ci.nsIPrefBranch.PREF_STRING:
475        return aBranch.getCharPref(this.name);
476      case Ci.nsIPrefBranch.PREF_INT:
477        return aBranch.getIntPref(this.name);
478      case Ci.nsIPrefBranch.PREF_BOOL:
479        return aBranch.getBoolPref(this.name);
480      default:
481        return null;
482    }
483  },
484
485  observe: function observe(aSubject, aTopic, aData) {
486    this.value = this._getValue(aSubject.QueryInterface(Ci.nsIPrefBranch));
487    if (this.callback) {
488      try {
489        this.callback(this.name, this.value);
490      } catch (x) {
491        Logger.logException(x);
492      }
493    }
494  },
495
496  QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
497                                          Ci.nsISupportsWeakReference])
498};