Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Narvaez <dwnarvaez@gmail.com>2013-02-06 13:49:14 (GMT)
committer Daniel Narvaez <dwnarvaez@gmail.com>2013-02-06 13:49:14 (GMT)
commit821413607a0718156f9d25d895e89b1c3d37aa8b (patch)
tree01c285af734ed5bba64b73b489e1e0226a94a262
parentc110fb485b3af0066c6df7aeac8c055e9d767efa (diff)
Copy various bits from gaia
-rw-r--r--Makefile680
-rw-r--r--apps/system/blank.html7
-rw-r--r--apps/system/camera/index.html66
-rw-r--r--apps/system/camera/js/camera.js982
-rw-r--r--apps/system/camera/js/filmstrip.js568
-rw-r--r--apps/system/camera/locales/camera.ar.properties19
-rw-r--r--apps/system/camera/locales/camera.en-US.properties19
-rw-r--r--apps/system/camera/locales/camera.fr.properties19
-rw-r--r--apps/system/camera/locales/camera.zh-TW.properties20
-rw-r--r--apps/system/camera/locales/locales.ini11
-rw-r--r--apps/system/camera/resources/sounds/shutter.oggbin0 -> 15807 bytes
-rw-r--r--apps/system/camera/style/VideoPlayer.css152
-rw-r--r--apps/system/camera/style/camera.css314
-rw-r--r--apps/system/camera/style/filmstrip.css187
-rw-r--r--apps/system/camera/style/icons/60/Camera.pngbin0 -> 5909 bytes
-rw-r--r--apps/system/camera/style/icons/Camera.pngbin0 -> 5909 bytes
-rw-r--r--apps/system/camera/style/images/actionicon_cancel.pngbin0 -> 1423 bytes
-rw-r--r--apps/system/camera/style/images/camera.pngbin0 -> 1654 bytes
-rw-r--r--apps/system/camera/style/images/delete.pngbin0 -> 472 bytes
-rw-r--r--apps/system/camera/style/images/flash_auto.pngbin0 -> 1880 bytes
-rw-r--r--apps/system/camera/style/images/flash_off.pngbin0 -> 1613 bytes
-rw-r--r--apps/system/camera/style/images/flash_on.pngbin0 -> 1673 bytes
-rw-r--r--apps/system/camera/style/images/flash_torch.pngbin0 -> 1880 bytes
-rw-r--r--apps/system/camera/style/images/grid.pngbin0 -> 1127 bytes
-rw-r--r--apps/system/camera/style/images/hud_button_underlay.pngbin0 -> 548 bytes
-rw-r--r--apps/system/camera/style/images/hud_button_underlay_focus.pngbin0 -> 954 bytes
-rw-r--r--apps/system/camera/style/images/play_overlay.pngbin0 -> 2498 bytes
-rw-r--r--apps/system/camera/style/images/share.pngbin0 -> 855 bytes
-rw-r--r--apps/system/camera/style/images/stop.pngbin0 -> 3233 bytes
-rw-r--r--apps/system/camera/style/images/toggle_back.pngbin0 -> 2106 bytes
-rw-r--r--apps/system/camera/style/images/toggle_front.pngbin0 -> 2249 bytes
-rw-r--r--apps/system/camera/style/images/ui/gradient.pngbin0 -> 3713 bytes
-rw-r--r--apps/system/camera/style/images/ui/pattern.pngbin0 -> 6851 bytes
-rw-r--r--apps/system/camera/style/images/video.pngbin0 -> 1360 bytes
-rw-r--r--apps/system/camera/style/images/video_pause_button.pngbin0 -> 1722 bytes
-rw-r--r--apps/system/camera/style/images/video_play_button.pngbin0 -> 3862 bytes
-rw-r--r--apps/system/camera/style/images/video_play_focus.pngbin0 -> 1373 bytes
-rw-r--r--apps/system/camera/style/images/video_play_normal.pngbin0 -> 1361 bytes
-rw-r--r--apps/system/camera/test/unit/_proxy.html49
-rw-r--r--apps/system/camera/test/unit/_sandbox.html28
-rw-r--r--apps/system/emergency-call/index.html198
-rw-r--r--apps/system/emergency-call/js/dialer.js80
-rw-r--r--apps/system/emergency-call/js/keypad.js461
-rw-r--r--apps/system/emergency-call/style/dialer.css250
-rw-r--r--apps/system/emergency-call/style/images/ActionIcon_40x40_bluetooth.pngbin0 -> 1842 bytes
-rw-r--r--apps/system/emergency-call/style/images/ActionIcon_40x40_bluetooth_active.pngbin0 -> 1866 bytes
-rw-r--r--apps/system/emergency-call/style/images/ActionIcon_40x40_hangup.pngbin0 -> 1409 bytes
-rw-r--r--apps/system/emergency-call/style/images/ActionIcon_40x40_pickup.pngbin0 -> 1404 bytes
-rw-r--r--apps/system/emergency-call/style/images/asterisk.pngbin0 -> 1172 bytes
-rw-r--r--apps/system/emergency-call/style/images/dialer_icon_delete.pngbin0 -> 1419 bytes
-rw-r--r--apps/system/emergency-call/style/images/sharp.pngbin0 -> 1176 bytes
-rw-r--r--apps/system/emergency-call/style/keypad.css301
-rw-r--r--apps/system/index.html896
-rw-r--r--apps/system/js/accessibility.js19
-rw-r--r--apps/system/js/activities.js86
-rw-r--r--apps/system/js/airplane_mode.js138
-rw-r--r--apps/system/js/app_install_manager.js443
-rw-r--r--apps/system/js/applications.js99
-rw-r--r--apps/system/js/attention_screen.js289
-rw-r--r--apps/system/js/authentication_dialog.js178
-rw-r--r--apps/system/js/background_service.js196
-rw-r--r--apps/system/js/battery_manager.js277
-rw-r--r--apps/system/js/bluetooth.js136
-rw-r--r--apps/system/js/bluetooth_transfer.js511
-rw-r--r--apps/system/js/bootstrap.js82
-rw-r--r--apps/system/js/call_forwarding.js45
-rw-r--r--apps/system/js/captive_portal.js73
-rw-r--r--apps/system/js/cards_view.js676
-rw-r--r--apps/system/js/context_menu.js24
-rw-r--r--apps/system/js/cost_control.js89
-rw-r--r--apps/system/js/crash_reporter.js140
-rw-r--r--apps/system/js/gridview.js40
-rw-r--r--apps/system/js/hardware_buttons.js318
-rw-r--r--apps/system/js/icc_cache.js86
-rw-r--r--apps/system/js/identity.js98
-rw-r--r--apps/system/js/keyboard_manager.js86
-rw-r--r--apps/system/js/list_menu.js180
-rw-r--r--apps/system/js/lockscreen.js1019
-rw-r--r--apps/system/js/modal_dialog.js443
-rw-r--r--apps/system/js/mouse2touch.js70
-rw-r--r--apps/system/js/notifications.js410
-rw-r--r--apps/system/js/operator_variant/operator_variant.js168
-rw-r--r--apps/system/js/payment.js151
-rw-r--r--apps/system/js/permission_manager.js203
-rw-r--r--apps/system/js/popup_manager.js313
-rw-r--r--apps/system/js/quick_settings.js279
-rw-r--r--apps/system/js/remote_debugger.js41
-rw-r--r--apps/system/js/screen_manager.js503
-rw-r--r--apps/system/js/screenshot.js115
-rw-r--r--apps/system/js/sim_lock.js110
-rw-r--r--apps/system/js/simcard_dialog.js355
-rw-r--r--apps/system/js/sleep_menu.js276
-rw-r--r--apps/system/js/sound_manager.js242
-rw-r--r--apps/system/js/source_view.js67
-rw-r--r--apps/system/js/statusbar.js618
-rw-r--r--apps/system/js/storage.js60
-rw-r--r--apps/system/js/system_banner.js33
-rw-r--r--apps/system/js/system_dialog.js113
-rw-r--r--apps/system/js/trusted_ui.js334
-rw-r--r--apps/system/js/ttlview.js50
-rw-r--r--apps/system/js/updatable.js269
-rw-r--r--apps/system/js/update_manager.js623
-rw-r--r--apps/system/js/utility_tray.js148
-rw-r--r--apps/system/js/value_selector/date_picker.js568
-rw-r--r--apps/system/js/value_selector/input_parser.js160
-rw-r--r--apps/system/js/value_selector/spin_date_picker.js341
-rw-r--r--apps/system/js/value_selector/value_picker.js222
-rw-r--r--apps/system/js/value_selector/value_selector.js526
-rw-r--r--apps/system/js/voicemail.js93
-rw-r--r--apps/system/js/wifi.js223
-rw-r--r--apps/system/js/window.js152
-rw-r--r--apps/system/js/window_manager.js2011
-rw-r--r--apps/system/js/wrapper.js198
-rw-r--r--apps/system/locales/locales.ini11
-rw-r--r--apps/system/locales/system.ar.properties290
-rw-r--r--apps/system/locales/system.en-US.properties296
-rw-r--r--apps/system/locales/system.fr.properties289
-rw-r--r--apps/system/locales/system.zh-TW.properties288
-rw-r--r--apps/system/manifest.webapp68
-rw-r--r--apps/system/payment.html17
-rw-r--r--apps/system/resources/images/backgrounds/default.pngbin0 -> 710769 bytes
-rw-r--r--apps/system/resources/sounds/unlock.oggbin0 -> 17084 bytes
-rw-r--r--apps/system/style/accessibility/accessibility.css3
-rw-r--r--apps/system/style/app_install_manager/app_install_manager.css98
-rw-r--r--apps/system/style/app_install_manager/images/downloads.pngbin0 -> 2417 bytes
-rw-r--r--apps/system/style/attention_screen.css57
-rw-r--r--apps/system/style/battery_manager/battery_manager.css55
-rw-r--r--apps/system/style/battery_manager/images/battery_empty_small.pngbin0 -> 3002 bytes
-rw-r--r--apps/system/style/battery_manager/images/header_bg.pngbin0 -> 9675 bytes
-rw-r--r--apps/system/style/bb/value_selector.css221
-rw-r--r--apps/system/style/bb/value_selector/images/icons/checked.pngbin0 -> 177 bytes
-rw-r--r--apps/system/style/bb/value_selector/images/ui/alpha.pngbin0 -> 993 bytes
-rw-r--r--apps/system/style/bb/value_selector/images/ui/button_reset.pngbin0 -> 92 bytes
-rw-r--r--apps/system/style/bb/value_selector/images/ui/button_special_disabled.pngbin0 -> 102 bytes
-rw-r--r--apps/system/style/bb/value_selector/images/ui/button_submit.pngbin0 -> 99 bytes
-rw-r--r--apps/system/style/bb/value_selector/images/ui/gradient.pngbin0 -> 3713 bytes
-rw-r--r--apps/system/style/bb/value_selector/images/ui/pattern.pngbin0 -> 6851 bytes
-rw-r--r--apps/system/style/bb/value_selector/images/ui/shadow-invert.pngbin0 -> 86 bytes
-rw-r--r--apps/system/style/bb/value_selector/images/ui/shadow.pngbin0 -> 85 bytes
-rw-r--r--apps/system/style/bluetooth_transfer/bluetooth_transfer.css53
-rw-r--r--apps/system/style/bluetooth_transfer/images/icon_bluetooth.pngbin0 -> 1341 bytes
-rw-r--r--apps/system/style/bluetooth_transfer/images/transfer.pngbin0 -> 1554 bytes
-rw-r--r--apps/system/style/cards_view/cards_view.css115
-rw-r--r--apps/system/style/cards_view/close.pngbin0 -> 1046 bytes
-rw-r--r--apps/system/style/cost_control/cost_control.css14
-rw-r--r--apps/system/style/crash_reporter/crash_reporter.css43
-rw-r--r--apps/system/style/fake-notification.css9
-rw-r--r--apps/system/style/gridview/gridview.css11
-rw-r--r--apps/system/style/gridview/images/grid.pngbin0 -> 6028 bytes
-rw-r--r--apps/system/style/icons/voicemail.pngbin0 -> 812 bytes
-rw-r--r--apps/system/style/list_menu/images/header-left-arrow.pngbin0 -> 313 bytes
-rw-r--r--apps/system/style/list_menu/images/header-right-arrow.pngbin0 -> 299 bytes
-rw-r--r--apps/system/style/list_menu/list_menu.css35
-rw-r--r--apps/system/style/lockscreen/images/handle.pngbin0 -> 1148 bytes
-rw-r--r--apps/system/style/lockscreen/images/icon-camera.pngbin0 -> 573 bytes
-rw-r--r--apps/system/style/lockscreen/images/icon-clear.pngbin0 -> 1240 bytes
-rw-r--r--apps/system/style/lockscreen/images/icon-unlock.pngbin0 -> 463 bytes
-rw-r--r--apps/system/style/lockscreen/images/mask.pngbin0 -> 18182 bytes
-rw-r--r--apps/system/style/lockscreen/images/mute.pngbin0 -> 1529 bytes
-rw-r--r--apps/system/style/lockscreen/lockscreen.css623
-rw-r--r--apps/system/style/modal_dialog/images/error_bk.pngbin0 -> 67183 bytes
-rw-r--r--apps/system/style/modal_dialog/modal_dialog.css112
-rw-r--r--apps/system/style/modal_dialog/prompt.css50
-rw-r--r--apps/system/style/notifications/images/grey-noise-bg.pngbin0 -> 943 bytes
-rw-r--r--apps/system/style/notifications/notifications.css200
-rw-r--r--apps/system/style/notifications/ringtones/notifier_exclamation.oggbin0 -> 32175 bytes
-rwxr-xr-xapps/system/style/permission_manager/images/PermissionsDialogIcons_Camera.pngbin0 -> 1814 bytes
-rwxr-xr-xapps/system/style/permission_manager/images/PermissionsDialogIcons_Contacts.pngbin0 -> 1598 bytes
-rwxr-xr-xapps/system/style/permission_manager/images/PermissionsDialogIcons_DeviceStorage.pngbin0 -> 1905 bytes
-rwxr-xr-xapps/system/style/permission_manager/images/PermissionsDialogIcons_FMRadio.pngbin0 -> 1783 bytes
-rwxr-xr-xapps/system/style/permission_manager/images/PermissionsDialogIcons_Geolocation.pngbin0 -> 2060 bytes
-rwxr-xr-xapps/system/style/permission_manager/images/PermissionsDialogIcons_WifiInformation.pngbin0 -> 2119 bytes
-rw-r--r--apps/system/style/permission_manager/permission_manager.css86
-rw-r--r--apps/system/style/pinlock/pinlock.css92
-rw-r--r--apps/system/style/popup_manager/popup_manager.css112
-rw-r--r--apps/system/style/quick_settings/images/airplane-mode-off.pngbin0 -> 316 bytes
-rw-r--r--apps/system/style/quick_settings/images/airplane-mode-on.pngbin0 -> 318 bytes
-rw-r--r--apps/system/style/quick_settings/images/background.pngbin0 -> 14554 bytes
-rw-r--r--apps/system/style/quick_settings/images/bluetooth-off.pngbin0 -> 1329 bytes
-rw-r--r--apps/system/style/quick_settings/images/bluetooth-on.pngbin0 -> 1328 bytes
-rw-r--r--apps/system/style/quick_settings/images/data-2g-off.pngbin0 -> 1328 bytes
-rw-r--r--apps/system/style/quick_settings/images/data-2g-on.pngbin0 -> 1331 bytes
-rw-r--r--apps/system/style/quick_settings/images/data-3g-off.pngbin0 -> 1329 bytes
-rw-r--r--apps/system/style/quick_settings/images/data-3g-on.pngbin0 -> 1333 bytes
-rw-r--r--apps/system/style/quick_settings/images/data-4g-off.pngbin0 -> 1313 bytes
-rw-r--r--apps/system/style/quick_settings/images/data-4g-on.pngbin0 -> 1318 bytes
-rw-r--r--apps/system/style/quick_settings/images/data-e-off.pngbin0 -> 1119 bytes
-rw-r--r--apps/system/style/quick_settings/images/data-e-on.pngbin0 -> 1119 bytes
-rw-r--r--apps/system/style/quick_settings/images/data-h+-off.pngbin0 -> 1177 bytes
-rw-r--r--apps/system/style/quick_settings/images/data-h+-on.pngbin0 -> 1164 bytes
-rw-r--r--apps/system/style/quick_settings/images/data-h-off.pngbin0 -> 1103 bytes
-rw-r--r--apps/system/style/quick_settings/images/data-h-on.pngbin0 -> 1103 bytes
-rw-r--r--apps/system/style/quick_settings/images/data-o-off.pngbin0 -> 1377 bytes
-rw-r--r--apps/system/style/quick_settings/images/data-o-on.pngbin0 -> 1377 bytes
-rw-r--r--apps/system/style/quick_settings/images/data-off.pngbin0 -> 1191 bytes
-rw-r--r--apps/system/style/quick_settings/images/power-save-off.pngbin0 -> 1451 bytes
-rw-r--r--apps/system/style/quick_settings/images/power-save-on.pngbin0 -> 1423 bytes
-rw-r--r--apps/system/style/quick_settings/images/settings-off.pngbin0 -> 1785 bytes
-rw-r--r--apps/system/style/quick_settings/images/settings-on.pngbin0 -> 1923 bytes
-rw-r--r--apps/system/style/quick_settings/images/wifi-off.pngbin0 -> 1598 bytes
-rw-r--r--apps/system/style/quick_settings/images/wifi-on.pngbin0 -> 1584 bytes
-rw-r--r--apps/system/style/quick_settings/quick_settings.css129
-rw-r--r--apps/system/style/shared/progress.gifbin0 -> 2225 bytes
-rw-r--r--apps/system/style/simcard.css52
-rw-r--r--apps/system/style/sleep_menu/images/airplane.pngbin0 -> 1755 bytes
-rw-r--r--apps/system/style/sleep_menu/images/power-off.pngbin0 -> 2166 bytes
-rw-r--r--apps/system/style/sleep_menu/images/restart.pngbin0 -> 2323 bytes
-rw-r--r--apps/system/style/sleep_menu/images/vibration.pngbin0 -> 3376 bytes
-rw-r--r--apps/system/style/sleep_menu/sleep_menu.css125
-rw-r--r--apps/system/style/sound_manager/images/VolumeOverlay_Landscape_SegmentOff.pngbin0 -> 942 bytes
-rw-r--r--apps/system/style/sound_manager/images/VolumeOverlay_Landscape_SegmentOff_left.pngbin0 -> 1001 bytes
-rw-r--r--apps/system/style/sound_manager/images/VolumeOverlay_Landscape_SegmentOff_right.pngbin0 -> 1002 bytes
-rw-r--r--apps/system/style/sound_manager/images/VolumeOverlay_Landscape_SegmentOn.pngbin0 -> 942 bytes
-rw-r--r--apps/system/style/sound_manager/images/VolumeOverlay_Landscape_SegmentOn_left.pngbin0 -> 1001 bytes
-rw-r--r--apps/system/style/sound_manager/images/VolumeOverlay_Landscape_SegmentOn_right.pngbin0 -> 1002 bytes
-rw-r--r--apps/system/style/sound_manager/images/header_bg.pngbin0 -> 9675 bytes
-rw-r--r--apps/system/style/sound_manager/images/speaker_loud_icon.pngbin0 -> 3271 bytes
-rw-r--r--apps/system/style/sound_manager/images/speaker_mute_icon.pngbin0 -> 3234 bytes
-rw-r--r--apps/system/style/sound_manager/images/speaker_regular_icon.pngbin0 -> 3027 bytes
-rw-r--r--apps/system/style/sound_manager/images/vibration.pngbin0 -> 3376 bytes
-rw-r--r--apps/system/style/sound_manager/images/vibration_disabled_icon.pngbin0 -> 2930 bytes
-rw-r--r--apps/system/style/sound_manager/images/vibration_enabled_icon.pngbin0 -> 2910 bytes
-rw-r--r--apps/system/style/sound_manager/images/volume-off.pngbin0 -> 3024 bytes
-rw-r--r--apps/system/style/sound_manager/images/volume-on.pngbin0 -> 1927 bytes
-rw-r--r--apps/system/style/sound_manager/images/volume_center_active.pngbin0 -> 934 bytes
-rw-r--r--apps/system/style/sound_manager/images/volume_center_disabled.pngbin0 -> 934 bytes
-rw-r--r--apps/system/style/sound_manager/images/volume_left_active.pngbin0 -> 997 bytes
-rw-r--r--apps/system/style/sound_manager/images/volume_left_disabled.pngbin0 -> 997 bytes
-rw-r--r--apps/system/style/sound_manager/images/volume_right_active.pngbin0 -> 998 bytes
-rw-r--r--apps/system/style/sound_manager/images/volume_right_disabled.pngbin0 -> 998 bytes
-rw-r--r--apps/system/style/sound_manager/sound_manager.css184
-rw-r--r--apps/system/style/statusbar/images/call-forwarding.pngbin0 -> 1272 bytes
-rw-r--r--apps/system/style/statusbar/images/icons.pngbin0 -> 9712 bytes
-rw-r--r--apps/system/style/statusbar/images/network-activity.gifbin0 -> 1700 bytes
-rw-r--r--apps/system/style/statusbar/images/signal-searching.gifbin0 -> 3459 bytes
-rw-r--r--apps/system/style/statusbar/images/system-downloads.gifbin0 -> 2033 bytes
-rw-r--r--apps/system/style/statusbar/images/wifi-connecting.gifbin0 -> 3765 bytes
-rw-r--r--apps/system/style/statusbar/statusbar.css412
-rw-r--r--apps/system/style/system/keyboard.css28
-rw-r--r--apps/system/style/system/system.css579
-rw-r--r--apps/system/style/themes/default/banner.css36
-rw-r--r--apps/system/style/themes/default/buttons.css232
-rw-r--r--apps/system/style/themes/default/core.css101
-rw-r--r--apps/system/style/themes/default/images/noise.pngbin0 -> 9317 bytes
-rw-r--r--apps/system/style/themes/default/images/notifications.pngbin0 -> 3847 bytes
-rw-r--r--apps/system/style/themes/default/images/notifications/close.pngbin0 -> 2855 bytes
-rw-r--r--apps/system/style/themes/default/images/tasks/close-active.pngbin0 -> 2901 bytes
-rw-r--r--apps/system/style/themes/default/images/tasks/close.pngbin0 -> 2855 bytes
-rw-r--r--apps/system/style/themes/default/images/ui/affirmative.pngbin0 -> 1020 bytes
-rw-r--r--apps/system/style/themes/default/images/ui/default.pngbin0 -> 1014 bytes
-rw-r--r--apps/system/style/themes/default/images/ui/gradient.pngbin0 -> 6737 bytes
-rw-r--r--apps/system/style/themes/default/images/ui/pattern.pngbin0 -> 13206 bytes
-rw-r--r--apps/system/style/themes/default/images/ui/time_pattern.pngbin0 -> 174 bytes
-rw-r--r--apps/system/style/themes/default/menus.css70
-rw-r--r--apps/system/style/themes/default/system.css27
-rw-r--r--apps/system/style/trusted_ui/trusted_ui.css82
-rw-r--r--apps/system/style/ttlview/ttlview.css16
-rw-r--r--apps/system/style/update_manager/images/grey-noise-bg.pngbin0 -> 943 bytes
-rw-r--r--apps/system/style/update_manager/images/iconindicator_download_24x24.pngbin0 -> 332 bytes
-rw-r--r--apps/system/style/update_manager/images/loader.pngbin0 -> 2011 bytes
-rw-r--r--apps/system/style/update_manager/update_manager.css253
-rw-r--r--apps/system/style/utility_tray/images/grippy.pngbin0 -> 246 bytes
-rw-r--r--apps/system/style/utility_tray/utility_tray.css29
-rw-r--r--apps/system/style/value_selector/date_picker.css152
-rw-r--r--apps/system/style/value_selector/images/icons/next.pngbin0 -> 296 bytes
-rw-r--r--apps/system/style/value_selector/images/icons/prev.pngbin0 -> 1425 bytes
-rw-r--r--apps/system/style/value_selector/images/ui/pattern-current.pngbin0 -> 116 bytes
-rw-r--r--apps/system/style/value_selector/images/ui/shadow-invert.pngbin0 -> 82 bytes
-rw-r--r--apps/system/style/value_selector/images/ui/shadow.pngbin0 -> 83 bytes
-rw-r--r--apps/system/style/value_selector/spin_date_picker.css249
-rw-r--r--apps/system/style/value_selector/time_picker.css212
-rw-r--r--apps/system/style/value_selector/value_selector.css170
-rw-r--r--apps/system/style/wrapper/images/back.pngbin0 -> 380 bytes
-rw-r--r--apps/system/style/wrapper/images/bookmark.pngbin0 -> 808 bytes
-rw-r--r--apps/system/style/wrapper/images/close.pngbin0 -> 241 bytes
-rw-r--r--apps/system/style/wrapper/images/forward.pngbin0 -> 289 bytes
-rw-r--r--apps/system/style/wrapper/images/open.pngbin0 -> 353 bytes
-rw-r--r--apps/system/style/wrapper/images/reload.pngbin0 -> 667 bytes
-rw-r--r--apps/system/style/wrapper/wrapper.css113
-rw-r--r--apps/system/style/zindex.css215
-rw-r--r--apps/system/test/integration/atoms/notification.js15
-rw-r--r--apps/system/test/integration/notification_test.js41
-rw-r--r--apps/system/test/integration/system_integration.js49
-rw-r--r--apps/system/test/unit/_proxy.html49
-rw-r--r--apps/system/test/unit/_sandbox.html28
-rw-r--r--apps/system/test/unit/app_install_manager_test.js1036
-rw-r--r--apps/system/test/unit/battery_manager_test.js204
-rw-r--r--apps/system/test/unit/date_picker_test.js483
-rw-r--r--apps/system/test/unit/identity_test.js92
-rw-r--r--apps/system/test/unit/lockscreen_test.js122
-rw-r--r--apps/system/test/unit/mobile_operator_test.js96
-rw-r--r--apps/system/test/unit/mock_app.js93
-rw-r--r--apps/system/test/unit/mock_applications.js26
-rw-r--r--apps/system/test/unit/mock_apps_mgmt.js64
-rw-r--r--apps/system/test/unit/mock_asyncStorage.js36
-rw-r--r--apps/system/test/unit/mock_chrome_event.js4
-rw-r--r--apps/system/test/unit/mock_custom_dialog.js26
-rw-r--r--apps/system/test/unit/mock_gesture_detector.js9
-rw-r--r--apps/system/test/unit/mock_l10n.js17
-rw-r--r--apps/system/test/unit/mock_manifest_helper.js5
-rw-r--r--apps/system/test/unit/mock_mobile_operator.js15
-rw-r--r--apps/system/test/unit/mock_modal_dialog.js34
-rw-r--r--apps/system/test/unit/mock_navigator_battery.js55
-rw-r--r--apps/system/test/unit/mock_navigator_moz_mobile_connection.js21
-rw-r--r--apps/system/test/unit/mock_navigator_moz_telephony.js55
-rw-r--r--apps/system/test/unit/mock_navigator_settings.js64
-rw-r--r--apps/system/test/unit/mock_navigator_wake_lock.js41
-rw-r--r--apps/system/test/unit/mock_notification_helper.js22
-rw-r--r--apps/system/test/unit/mock_notification_screen.js38
-rw-r--r--apps/system/test/unit/mock_settings_listener.js16
-rw-r--r--apps/system/test/unit/mock_sleep_menu.js5
-rw-r--r--apps/system/test/unit/mock_statusbar.js36
-rw-r--r--apps/system/test/unit/mock_system_banner.js13
-rw-r--r--apps/system/test/unit/mock_trusted_ui_manager.js25
-rw-r--r--apps/system/test/unit/mock_updatable.js72
-rw-r--r--apps/system/test/unit/mock_update_manager.js60
-rw-r--r--apps/system/test/unit/mock_utility_tray.js14
-rw-r--r--apps/system/test/unit/mock_window_manager.js16
-rw-r--r--apps/system/test/unit/mocks_helper.js40
-rw-r--r--apps/system/test/unit/notifications_test.js132
-rw-r--r--apps/system/test/unit/statusbar_test.js421
-rw-r--r--apps/system/test/unit/style/lockscreen/images/mask.pngbin0 -> 18182 bytes
-rw-r--r--apps/system/test/unit/updatable_test.js630
-rw-r--r--apps/system/test/unit/update_manager_test.js1449
-rw-r--r--build/BUSYBOX_LICENSE16
-rw-r--r--build/applications-data.js243
-rw-r--r--build/busybox-armv6lbin0 -> 1085140 bytes
-rw-r--r--build/install-gaia.py176
-rw-r--r--build/multilocale.py248
-rw-r--r--build/offline-cache.js159
-rw-r--r--build/optimize-clean.js28
-rwxr-xr-xbuild/otoro-install-busybox.sh17
-rw-r--r--build/payment-prefs.js5
-rw-r--r--build/preferences.js100
-rw-r--r--build/settings.py236
-rw-r--r--build/ua-override-prefs.js102
-rw-r--r--build/utils.js222
-rw-r--r--build/wallpaper.jpgbin0 -> 33533 bytes
-rw-r--r--build/webapp-manifests.js179
-rw-r--r--build/webapp-optimize.js386
-rw-r--r--build/webapp-zip.js293
341 files changed, 36016 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..de35c8e
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,680 @@
+###############################################################################
+# Global configurations. Protip: set your own overrides in a local.mk file. #
+# #
+# GAIA_DOMAIN : change that if you plan to use a different domain to update #
+# your applications or want to use a local domain #
+# #
+# HOMESCREEN : url of the homescreen to start on #
+# #
+# ADB : if you use a device and plan to send update it with your work #
+# you need to have adb in your path or you can edit this line to#
+# specify its location. #
+# #
+# DEBUG : debug mode enables mode output on the console and disable the #
+# the offline cache. This is mostly for desktop debugging. #
+# #
+# REPORTER : Mocha reporter to use for test output. #
+# #
+# GAIA_APP_SRCDIRS : list of directories to search for web apps #
+# #
+###############################################################################
+-include local.mk
+
+# Headless bot does not need the full output of wget
+# and it can cause crashes in bot.io option is here so
+# -nv can be passed and turn off verbose output.
+WGET_OPTS?=-c
+GAIA_DOMAIN?=gaiamobile.org
+
+DEBUG?=0
+PRODUCTION?=0
+DOGFOOD?=0
+
+LOCAL_DOMAINS?=1
+
+ADB?=adb
+
+ifeq ($(DEBUG),1)
+SCHEME=http://
+else
+SCHEME=app://
+endif
+
+HOMESCREEN?=$(SCHEME)system.$(GAIA_DOMAIN)
+
+BUILD_APP_NAME?=*
+ifneq ($(APP),)
+BUILD_APP_NAME=$(APP)
+endif
+
+REPORTER?=Spec
+
+GAIA_APP_SRCDIRS?=apps test_apps showcase_apps
+GAIA_INSTALL_PARENT?=/data/local
+ADB_REMOUNT?=0
+
+GAIA_ALL_APP_SRCDIRS=$(GAIA_APP_SRCDIRS)
+
+ifeq ($(MAKECMDGOALS), demo)
+GAIA_DOMAIN=thisdomaindoesnotexist.org
+GAIA_APP_SRCDIRS=apps showcase_apps
+else ifeq ($(MAKECMDGOALS), dogfood)
+DOGFOOD=1
+PRODUCTION=1
+B2G_SYSTEM_APPS=1
+else ifeq ($(MAKECMDGOALS), production)
+PRODUCTION=1
+B2G_SYSTEM_APPS=1
+endif
+
+# PRODUCTION is also set for user and userdebug B2G builds
+ifeq ($(PRODUCTION), 1)
+GAIA_APP_SRCDIRS=apps
+ADB_REMOUNT=1
+endif
+
+ifeq ($(MAKECMDGOALS), dogfood)
+GAIA_APP_SRCDIRS=apps dogfood_apps
+endif
+
+ifeq ($(B2G_SYSTEM_APPS), 1)
+GAIA_INSTALL_PARENT=/system/b2g
+endif
+
+ifneq ($(GAIA_OUTOFTREE_APP_SRCDIRS),)
+ $(shell mkdir -p outoftree_apps \
+ $(foreach dir,$(GAIA_OUTOFTREE_APP_SRCDIRS),\
+ $(foreach appdir,$(wildcard $(dir)/*),\
+ && ln -sf $(appdir) outoftree_apps/)))
+ GAIA_APP_SRCDIRS += outoftree_apps
+endif
+
+GAIA_LOCALES_PATH?=locales
+LOCALES_FILE?=shared/resources/languages.json
+GAIA_LOCALE_SRCDIRS=shared $(GAIA_APP_SRCDIRS)
+GAIA_DEFAULT_LOCALE?=en-US
+GAIA_INLINE_LOCALES?=1
+
+###############################################################################
+# The above rules generate the profile/ folder and all its content. #
+# The profile folder content depends on different rules: #
+# 1. webapp manifest #
+# A directory structure representing the applications installed using the #
+# Apps API. In Gaia all applications use this method. #
+# See https://developer.mozilla.org/en/Apps/Apps_JavaScript_API #
+# #
+# 2. offline #
+# An Application Cache database containing Gaia apps, so the phone can be #
+# used offline and application can be updated easily. For details about it#
+# see: https://developer.mozilla.org/en/Using_Application_Cache #
+# #
+# 3. preferences #
+# A preference file used by the platform to configure permissions #
+# #
+###############################################################################
+
+# In debug mode the offline cache is not used (even if it is generated) and
+# Gaia is loaded by a built-in web server via port GAIA_PORT.
+#
+# XXX For now the name of the domain should be mapped to localhost manually
+# by editing /etc/hosts on linux/mac. This steps would not be required
+# anymore once https://bugzilla.mozilla.org/show_bug.cgi?id=722197 will land.
+ifeq ($(DEBUG),1)
+GAIA_PORT?=:8080
+else
+GAIA_PORT?=
+endif
+
+# Force bash for all shell commands since we depend on bash-specific syntax
+SHELL := /bin/bash
+
+# what OS are we on?
+SYS=$(shell uname -s)
+ARCH?=$(shell uname -m)
+MSYS_FIX=
+ifeq (${SYS}/${ARCH},Darwin/i386)
+ARCH=x86_64
+endif
+SEP=/
+ifneq (,$(findstring MINGW32_,$(SYS)))
+CURDIR:=$(shell pwd -W | sed -e 's|/|\\\\|g')
+SEP=\\
+# Mingw mangle path and append c:\mozilla-build\msys\data in front of paths
+MSYS_FIX=/
+endif
+
+ifeq ($(SYS),Darwin)
+MD5SUM = md5 -r
+SED_INPLACE_NO_SUFFIX = /usr/bin/sed -i ''
+DOWNLOAD_CMD = /usr/bin/curl -O
+else
+MD5SUM = md5sum -b
+SED_INPLACE_NO_SUFFIX = sed -i
+DOWNLOAD_CMD = wget $(WGET_OPTS)
+endif
+
+# Test agent setup
+TEST_COMMON=test_apps/test-agent/common
+TEST_AGENT_DIR=tools/test-agent/
+ifeq ($(strip $(NODEJS)),)
+ NODEJS := `which node`
+endif
+
+ifeq ($(strip $(NPM)),)
+ NPM := `which npm`
+endif
+
+TEST_AGENT_CONFIG="./test_apps/test-agent/config.json"
+
+#Marionette testing variables
+#make sure we're python 2.7.x
+ifeq ($(strip $(PYTHON_27)),)
+PYTHON_27 := `which python`
+endif
+PYTHON_FULL := $(wordlist 2,4,$(subst ., ,$(shell $(PYTHON_27) --version 2>&1)))
+PYTHON_MAJOR := $(word 1,$(PYTHON_FULL))
+PYTHON_MINOR := $(word 2,$(PYTHON_FULL))
+MARIONETTE_HOST ?= localhost
+MARIONETTE_PORT ?= 2828
+TEST_DIRS ?= $(CURDIR)/tests
+
+# Generate profile/
+
+profile: multilocale applications-data preferences app-makefiles test-agent-config offline extensions install-xulrunner-sdk profile/settings.json
+ @echo "Profile Ready: please run [b2g|firefox] -profile $(CURDIR)$(SEP)profile"
+
+LANG=POSIX # Avoiding sort order differences between OSes
+
+.PHONY: multilocale
+multilocale:
+ifneq ($(LOCALE_BASEDIR),)
+ $(MAKE) multilocale-clean
+ @echo "Enable locales specified in $(LOCALES_FILE)..."
+ @targets=""; \
+ for appdir in $(GAIA_LOCALE_SRCDIRS); do \
+ targets="$$targets --target $$appdir"; \
+ done; \
+ python $(CURDIR)/build/multilocale.py \
+ --config $(LOCALES_FILE) \
+ --source $(LOCALE_BASEDIR) \
+ $$targets;
+ @echo "Done"
+ifneq ($(LOCALES_FILE),shared/resources/languages.json)
+ cp $(LOCALES_FILE) shared/resources/languages.json
+endif
+endif
+
+.PHONY: multilocale-clean
+multilocale-clean:
+ @echo "Cleaning l10n bits..."
+ifeq ($(wildcard .hg),.hg)
+ @hg update --clean
+ @hg status -n $(GAIA_LOCALE_SRCDIRS) | grep '\.properties' | xargs rm -rf
+else
+ @git ls-files --other --exclude-standard $(GAIA_LOCALE_SRCDIRS) | grep '\.properties' | xargs rm -f
+ @git ls-files --modified $(GAIA_LOCALE_SRCDIRS) | grep '\.properties' | xargs git checkout --
+ifneq ($(DEBUG),1)
+ @# Leave these files modified in DEBUG profiles
+ @git ls-files --modified $(GAIA_LOCALE_SRCDIRS) | grep 'manifest.webapp' | xargs git checkout --
+ @git ls-files --modified $(GAIA_LOCALE_SRCDIRS) | grep '\.ini' | xargs git checkout --
+ @git checkout -- shared/resources/languages.json
+ @echo "Done"
+endif
+endif
+
+app-makefiles:
+ @for d in ${GAIA_APP_SRCDIRS}; \
+ do \
+ for mfile in `find $$d -mindepth 2 -maxdepth 2 -name "Makefile"` ;\
+ do \
+ make -C `dirname $$mfile`; \
+ done; \
+ done;
+
+# Generate profile/webapps/
+# We duplicate manifest.webapp to manifest.webapp and manifest.json
+# to accommodate Gecko builds without bug 757613. Should be removed someday.
+webapp-manifests: install-xulrunner-sdk
+ @mkdir -p profile/webapps
+ @$(call run-js-command, webapp-manifests)
+ @#cat profile/webapps/webapps.json
+
+# Generate profile/webapps/APP/application.zip
+webapp-zip: stamp-commit-hash install-xulrunner-sdk
+ifneq ($(DEBUG),1)
+ @rm -rf apps/system/camera
+ @cp -r apps/camera apps/system/camera
+ @rm apps/system/camera/manifest.webapp
+ @mkdir -p profile/webapps
+ @$(call run-js-command, webapp-zip)
+endif
+
+# Web app optimization steps (like precompling l10n, concatenating js files, etc..).
+webapp-optimize: install-xulrunner-sdk
+ @$(call run-js-command, webapp-optimize)
+
+# Remove temporary l10n files
+optimize-clean: install-xulrunner-sdk
+ @$(call run-js-command, optimize-clean)
+
+# Populate appcache
+offline-cache: webapp-manifests install-xulrunner-sdk
+ @echo "Populate external apps appcache"
+ @$(call run-js-command, offline-cache)
+ @echo "Done"
+
+# Create webapps
+offline: webapp-manifests webapp-optimize webapp-zip optimize-clean
+
+
+# The install-xulrunner target arranges to get xulrunner downloaded and sets up
+# some commands for invoking it. But it is platform dependent
+XULRUNNER_SDK_URL=http://ftp.mozilla.org/pub/mozilla.org/xulrunner/nightly/2012/09/2012-09-20-03-05-43-mozilla-central/xulrunner-18.0a1.en-US.
+
+ifeq ($(SYS),Darwin)
+# For mac we have the xulrunner-sdk so check for this directory
+# We're on a mac
+XULRUNNER_MAC_SDK_URL=$(XULRUNNER_SDK_URL)mac-
+ifeq ($(ARCH),i386)
+# 32-bit
+XULRUNNER_SDK_DOWNLOAD=$(XULRUNNER_MAC_SDK_URL)i386.sdk.tar.bz2
+else
+# 64-bit
+XULRUNNER_SDK_DOWNLOAD=$(XULRUNNER_MAC_SDK_URL)x86_64.sdk.tar.bz2
+endif
+XULRUNNERSDK=./xulrunner-sdk/bin/run-mozilla.sh
+XPCSHELLSDK=./xulrunner-sdk/bin/xpcshell
+
+else ifeq ($(findstring MINGW32,$(SYS)), MINGW32)
+# For windows we only have one binary
+XULRUNNER_SDK_DOWNLOAD=$(XULRUNNER_SDK_URL)win32.sdk.zip
+XULRUNNERSDK=
+XPCSHELLSDK=./xulrunner-sdk/bin/xpcshell
+
+else
+# Otherwise, assume linux
+# downloads and installs locally xulrunner to run the xpchsell
+# script that creates the offline cache
+XULRUNNER_LINUX_SDK_URL=$(XULRUNNER_SDK_URL)linux-
+ifeq ($(ARCH),x86_64)
+XULRUNNER_SDK_DOWNLOAD=$(XULRUNNER_LINUX_SDK_URL)x86_64.sdk.tar.bz2
+else
+XULRUNNER_SDK_DOWNLOAD=$(XULRUNNER_LINUX_SDK_URL)i686.sdk.tar.bz2
+endif
+XULRUNNERSDK=./xulrunner-sdk/bin/run-mozilla.sh
+XPCSHELLSDK=./xulrunner-sdk/bin/xpcshell
+endif
+
+.PHONY: install-xulrunner-sdk
+install-xulrunner-sdk:
+ifndef USE_LOCAL_XULRUNNER_SDK
+ifneq ($(XULRUNNER_SDK_DOWNLOAD),$(shell cat .xulrunner-url 2> /dev/null))
+ rm -rf xulrunner-sdk
+ $(DOWNLOAD_CMD) $(XULRUNNER_SDK_DOWNLOAD)
+ifeq ($(findstring MINGW32,$(SYS)), MINGW32)
+ unzip xulrunner*.zip && rm xulrunner*.zip
+else
+ tar xjf xulrunner*.tar.bz2 && rm xulrunner*.tar.bz2
+endif
+ @echo $(XULRUNNER_SDK_DOWNLOAD) > .xulrunner-url
+endif
+endif # USE_LOCAL_XULRUNNER_SDK
+
+define run-js-command
+ echo "run-js-command $1"; \
+ JS_CONSTS=' \
+ const GAIA_DIR = "$(CURDIR)"; const PROFILE_DIR = "$(CURDIR)$(SEP)profile"; \
+ const GAIA_SCHEME = "$(SCHEME)"; const GAIA_DOMAIN = "$(GAIA_DOMAIN)"; \
+ const DEBUG = $(DEBUG); const LOCAL_DOMAINS = $(LOCAL_DOMAINS); \
+ const HOMESCREEN = "$(HOMESCREEN)"; const GAIA_PORT = "$(GAIA_PORT)"; \
+ const GAIA_APP_SRCDIRS = "$(GAIA_APP_SRCDIRS)"; \
+ const GAIA_LOCALES_PATH = "$(GAIA_LOCALES_PATH)"; \
+ const LOCALES_FILE = "$(LOCALES_FILE)"; \
+ const BUILD_APP_NAME = "$(BUILD_APP_NAME)"; \
+ const PRODUCTION = "$(PRODUCTION)"; \
+ const DOGFOOD = "$(DOGFOOD)"; \
+ const OFFICIAL = "$(MOZILLA_OFFICIAL)"; \
+ const GAIA_DEFAULT_LOCALE = "$(GAIA_DEFAULT_LOCALE)"; \
+ const GAIA_INLINE_LOCALES = "$(GAIA_INLINE_LOCALES)"; \
+ const GAIA_ENGINE = "xpcshell"; \
+ '; \
+ $(XULRUNNERSDK) $(XPCSHELLSDK) -e "$$JS_CONSTS" -f build/utils.js "build/$(strip $1).js"
+endef
+
+# Optional files that may be provided to extend the set of default
+# preferences installed for gaia. If the preferences in these files
+# conflict, the result is undefined.
+EXTENDED_PREF_FILES = \
+ custom-prefs.js \
+ payment-prefs.js \
+ ua-override-prefs.js \
+
+# Generate profile/prefs.js
+preferences: install-xulrunner-sdk
+ @test -d profile || mkdir -p profile
+ @$(call run-js-command, preferences)
+ @$(foreach prefs_file,$(addprefix build/,$(EXTENDED_PREF_FILES)),\
+ if [ -f $(prefs_file) ]; then \
+ cat $(prefs_file) >> profile/user.js; \
+ fi; \
+ )
+
+# Generate profile/
+applications-data: install-xulrunner-sdk
+ test -d profile || mkdir -p profile
+ @$(call run-js-command, applications-data)
+
+# Generate profile/extensions
+EXT_DIR=profile/extensions
+extensions:
+ @mkdir -p profile
+ @rm -rf $(EXT_DIR)
+ifeq ($(DEBUG),1)
+ cp -r tools/extensions $(EXT_DIR)
+endif
+ @echo "Finished: Generating extensions"
+
+
+
+###############################################################################
+# Tests #
+###############################################################################
+
+MOZ_TESTS = "$(MOZ_OBJDIR)/_tests/testing/mochitest"
+INJECTED_GAIA = "$(MOZ_TESTS)/browser/gaia"
+
+TEST_PATH=gaia/tests/${TEST_FILE}
+
+ifeq ($(TESTS),)
+ ifneq ($(APP),)
+ TESTS=$(shell find apps/$(APP)/test/integration/ -name "*_test.js" -type f )
+ else
+ TESTS=$(shell find apps -name "*_test.js" -type f | grep integration)
+ endif
+endif
+.PHONY: test-integration
+test-integration:
+ @./tests/js/bin/runner $(TESTS)
+
+.PHONY: tests
+tests: webapp-manifests offline
+ echo "Checking if the mozilla build has tests enabled..."
+ test -d $(MOZ_TESTS) || (echo "Please ensure you don't have |ac_add_options --disable-tests| in your mozconfig." && exit 1)
+ echo "Checking the injected Gaia..."
+ test -L $(INJECTED_GAIA) || ln -s $(CURDIR) $(INJECTED_GAIA)
+ TEST_PATH=$(TEST_PATH) make -C $(MOZ_OBJDIR) mochitest-browser-chrome EXTRA_TEST_ARGS="--browser-arg=\"\" --extra-profile-file=$(CURDIR)/profile/webapps --extra-profile-file=$(CURDIR)/profile/user.js"
+
+.PHONY: common-install
+common-install:
+ @test -x $(NODEJS) || (echo "Please Install NodeJS -- (use aptitude on linux or homebrew on osx)" && exit 1 )
+ @test -x $(NPM) || (echo "Please install NPM (node package manager) -- http://npmjs.org/" && exit 1 )
+
+ cd $(TEST_AGENT_DIR) && npm install .
+
+.PHONY: update-common
+update-common: common-install
+ # integration tests
+ rm -f tests/vendor/marionette.js
+ cp $(TEST_AGENT_DIR)/node_modules/marionette-client/marionette.js tests/js/vendor/
+
+ # common testing tools
+ mkdir -p $(TEST_COMMON)/vendor/test-agent/
+ mkdir -p $(TEST_COMMON)/vendor/chai/
+ rm -Rf tools/xpcwindow
+ rm -f $(TEST_COMMON)/vendor/test-agent/test-agent*.js
+ rm -f $(TEST_COMMON)/vendor/chai/*.js
+ cp -R $(TEST_AGENT_DIR)/node_modules/xpcwindow tools/xpcwindow
+ rm -R tools/xpcwindow/vendor/
+ cp $(TEST_AGENT_DIR)/node_modules/test-agent/test-agent.js $(TEST_COMMON)/vendor/test-agent/
+ cp $(TEST_AGENT_DIR)/node_modules/test-agent/test-agent.css $(TEST_COMMON)/vendor/test-agent/
+ cp $(TEST_AGENT_DIR)/node_modules/chai/chai.js $(TEST_COMMON)/vendor/chai/
+
+# Create the json config file
+# for use with the test agent GUI
+test-agent-config: test-agent-bootstrap-apps
+ @rm -f $(TEST_AGENT_CONFIG)
+ @touch $(TEST_AGENT_CONFIG)
+ @rm -f /tmp/test-agent-config;
+ @# Build json array of all test files
+ @for d in ${GAIA_APP_SRCDIRS}; \
+ do \
+ find $$d -name '*_test.js' | sed "s:$$d/::g" >> /tmp/test-agent-config; \
+ done;
+ @echo '{"tests": [' >> $(TEST_AGENT_CONFIG)
+ @cat /tmp/test-agent-config | \
+ sed 's:\(.*\):"\1":' | \
+ sed -e ':a' -e 'N' -e '$$!ba' -e 's/\n/,\
+ /g' >> $(TEST_AGENT_CONFIG);
+ @echo ' ]}' >> $(TEST_AGENT_CONFIG);
+ @echo "Finished: test ui config file: $(TEST_AGENT_CONFIG)"
+ @rm -f /tmp/test-agent-config
+
+.PHONY: test-agent-bootstrap-apps
+test-agent-bootstrap-apps:
+ @for d in `find -L ${GAIA_APP_SRCDIRS} -mindepth 1 -maxdepth 1 -type d` ;\
+ do \
+ mkdir -p $$d/test/unit ; \
+ mkdir -p $$d/test/integration ; \
+ cp -f $(TEST_COMMON)/test/boilerplate/_proxy.html $$d/test/unit/_proxy.html; \
+ cp -f $(TEST_COMMON)/test/boilerplate/_sandbox.html $$d/test/unit/_sandbox.html; \
+ done
+ @echo "Finished: bootstrapping test proxies/sandboxes";
+
+# Temp make file method until we can switch
+# over everything in test
+ifneq ($(strip $(APP)),)
+APP_TEST_LIST=$(shell find apps/$(APP)/test/unit -name '*_test.js')
+endif
+.PHONY: test-agent-test
+test-agent-test:
+ifneq ($(strip $(APP)),)
+ @echo 'Running tests for $(APP)';
+ @$(TEST_AGENT_DIR)/node_modules/test-agent/bin/js-test-agent test --reporter $(REPORTER) $(APP_TEST_LIST)
+else
+ @echo 'Running all tests';
+ @$(TEST_AGENT_DIR)/node_modules/test-agent/bin/js-test-agent test --reporter $(REPORTER)
+endif
+
+.PHONY: test-agent-server
+test-agent-server: common-install
+ $(TEST_AGENT_DIR)/node_modules/test-agent/bin/js-test-agent server -c ./$(TEST_AGENT_DIR)/test-agent-server.js --http-path . --growl
+
+.PHONY: marionette
+marionette:
+#need the profile
+ test -d $(GAIA)/profile || $(MAKE) profile
+ifneq ($(PYTHON_MAJOR), 2)
+ @echo "Python 2.7.x is needed for the marionette client. You can set the PYTHON_27 variable to your python2.7 path." && exit 1
+endif
+ifneq ($(PYTHON_MINOR), 7)
+ @echo "Python 2.7.x is needed for the marionette client. You can set the PYTHON_27 variable to your python2.7 path." && exit 1
+endif
+ifeq ($(strip $(MC_DIR)),)
+ @echo "Please have the MC_DIR environment variable point to the top of your mozilla-central tree." && exit 1
+endif
+#if B2G_BIN is defined, we will run the b2g binary, otherwise, we assume an instance is running
+ifneq ($(strip $(B2G_BIN)),)
+ cd $(MC_DIR)/testing/marionette/client/marionette && \
+ sh venv_test.sh $(PYTHON_27) --address=$(MARIONETTE_HOST):$(MARIONETTE_PORT) --b2gbin=$(B2G_BIN) $(TEST_DIRS)
+else
+ cd $(MC_DIR)/testing/marionette/client/marionette && \
+ sh venv_test.sh $(PYTHON_27) --address=$(MARIONETTE_HOST):$(MARIONETTE_PORT) $(TEST_DIRS)
+endif
+
+###############################################################################
+# Utils #
+###############################################################################
+
+# Lint apps
+lint:
+ @# ignore lint on:
+ @# cubevid
+ @# crystalskull
+ @# towerjelly
+ @gjslint --nojsdoc -r apps -e 'homescreen/everything.me,sms/js/ext,pdfjs/content,pdfjs/test,email/js/ext,music/js/ext,calendar/js/ext'
+ @gjslint --nojsdoc -r shared/js
+
+# Generate a text file containing the current changeset of Gaia
+# XXX I wonder if this should be a replace-in-file hack. This would let us
+# let us remove the update-offline-manifests target dependancy of the
+# default target.
+stamp-commit-hash:
+ @(if [ -e gaia_commit_override.txt ]; then \
+ cp gaia_commit_override.txt apps/settings/resources/gaia_commit.txt; \
+ elif [ -d ./.git ]; then \
+ git log -1 --format="%H%n%at" HEAD > apps/settings/resources/gaia_commit.txt; \
+ else \
+ echo 'Unknown Git commit; build date shown here.' > apps/settings/resources/gaia_commit.txt; \
+ date +%s >> apps/settings/resources/gaia_commit.txt; \
+ fi)
+
+# Erase all the indexedDB databases on the phone, so apps have to rebuild them.
+delete-databases:
+ @echo 'Stopping b2g'
+ $(ADB) shell stop b2g
+ $(ADB) shell rm -r $(MSYS_FIX)/data/local/indexedDB/*
+ @echo 'Starting b2g'
+ $(ADB) shell start b2g
+
+# Take a screenshot of the device and put it in screenshot.png
+screenshot:
+ mkdir -p screenshotdata
+ $(ADB) pull $(MSYS_FIX)/dev/graphics/fb0 screenshotdata/fb0
+ dd bs=1920 count=800 if=screenshotdata/fb0 of=screenshotdata/fb0b
+ ffmpeg -vframes 1 -vcodec rawvideo -f rawvideo -pix_fmt rgb32 -s 480x800 -i screenshotdata/fb0b -f image2 -vcodec png screenshot.png
+ rm -rf screenshotdata
+
+# Forward port to use the RIL daemon from the device
+forward:
+ $(ADB) shell touch $(MSYS_FIX)/data/local/rilproxyd
+ $(ADB) shell killall rilproxy
+ $(ADB) forward tcp:6200 localreserved:rilproxyd
+
+
+# update the manifest.appcache files to match what's actually there
+update-offline-manifests:
+ for d in `find -L ${GAIA_APP_SRCDIRS} -mindepth 1 -maxdepth 1 -type d` ;\
+ do \
+ rm -rf $$d/manifest.appcache ;\
+ if [ -f $$d/manifest.webapp ] ;\
+ then \
+ echo \\t$$d ; \
+ ( cd $$d ; \
+ echo "CACHE MANIFEST" > manifest.appcache ;\
+ cat `find * -type f | sort -nfs` | $(MD5SUM) | cut -f 1 -d ' ' | sed 's/^/\#\ Version\ /' >> manifest.appcache ;\
+ find * -type f | grep -v tools | sort >> manifest.appcache ;\
+ $(SED_INPLACE_NO_SUFFIX) -e 's|manifest.appcache||g' manifest.appcache ;\
+ echo "http://$(GAIA_DOMAIN)$(GAIA_PORT)/webapi.js" >> manifest.appcache ;\
+ echo "NETWORK:" >> manifest.appcache ;\
+ echo "http://*" >> manifest.appcache ;\
+ echo "https://*" >> manifest.appcache ;\
+ ) ;\
+ fi \
+ done
+
+
+# If your gaia/ directory is a sub-directory of the B2G directory, then
+# you should use the install-gaia target of the B2G Makefile. But if you're
+# working on just gaia itself, and you already have B2G firmware on your
+# phone, and you have adb in your path, then you can use the install-gaia
+# target to update the gaia files and reboot b2g
+TARGET_FOLDER = webapps/$(BUILD_APP_NAME).$(GAIA_DOMAIN)
+APP_NAME = $(shell cat apps/${BUILD_APP_NAME}/manifest.webapp | grep name | head -1 | cut -d '"' -f 4)
+APP_PID = $(shell adb shell b2g-ps | grep '${APP_NAME}' | tr -s '${APP_NAME}' ' ' | tr -s ' ' ' ' | cut -f 3 -d' ')
+install-gaia: profile
+ $(ADB) start-server
+ @echo 'Stopping b2g'
+ifeq ($(BUILD_APP_NAME),*)
+ $(ADB) shell stop b2g
+else ifeq ($(BUILD_APP_NAME), system)
+ $(ADB) shell stop b2g
+else ifneq (${APP_PID},)
+ $(ADB) shell kill ${APP_PID}
+endif
+ $(ADB) shell rm -r $(MSYS_FIX)/cache/*
+
+ifeq ($(ADB_REMOUNT),1)
+ $(ADB) remount
+endif
+
+ifeq ($(BUILD_APP_NAME),*)
+ python build/install-gaia.py "$(ADB)" "$(MSYS_FIX)$(GAIA_INSTALL_PARENT)"
+else
+ $(ADB) push profile/$(TARGET_FOLDER)/manifest.webapp $(MSYS_FIX)$(GAIA_INSTALL_PARENT)/$(TARGET_FOLDER)/manifest.webapp
+ $(ADB) push profile/$(TARGET_FOLDER)/application.zip $(MSYS_FIX)$(GAIA_INSTALL_PARENT)/$(TARGET_FOLDER)/application.zip
+endif
+ @echo "Installed gaia into profile/."
+ @echo 'Starting b2g'
+ $(ADB) shell start b2g
+
+# Copy demo media to the sdcard.
+# If we've got old style directories on the phone, rename them first.
+install-media-samples:
+ $(ADB) shell 'if test -d /sdcard/Pictures; then mv /sdcard/Pictures /sdcard/DCIM; fi'
+ $(ADB) shell 'if test -d /sdcard/music; then mv /sdcard/music /sdcard/music.temp; mv /sdcard/music.temp /sdcard/Music; fi'
+ $(ADB) shell 'if test -d /sdcard/videos; then mv /sdcard/videos /sdcard/Movies; fi'
+
+ $(ADB) push media-samples/DCIM $(MSYS_FIX)/sdcard/DCIM
+ $(ADB) push media-samples/Movies $(MSYS_FIX)/sdcard/Movies
+ $(ADB) push media-samples/Music $(MSYS_FIX)/sdcard/Music
+
+install-test-media:
+ $(ADB) push test_media/DCIM $(MSYS_FIX)/sdcard/DCIM
+ $(ADB) push test_media/Movies $(MSYS_FIX)/sdcard/Movies
+ $(ADB) push test_media/Music $(MSYS_FIX)/sdcard/Music
+
+dialer-demo:
+ @cp -R apps/contacts apps/dialer
+ @rm apps/dialer/contacts/manifest*
+ @mv apps/dialer/contacts/index.html apps/dialer/contacts/contacts.html
+ @sed -i.bak 's/manifest.appcache/..\/manifest.appcache/g' apps/dialer/contacts/contacts.html
+ @find apps/dialer/ -name '*.bak' -exec rm {} \;
+
+demo: install-media-samples install-gaia
+
+production: reset-gaia
+dogfood: reset-gaia
+
+# Remove everything and install a clean profile
+reset-gaia: purge install-gaia install-settings-defaults
+
+# remove the memories and apps on the phone
+purge:
+ $(ADB) shell stop b2g
+ @(for FILE in `$(ADB) shell ls $(MSYS_FIX)/data/local | tr -d '\r'`; \
+ do \
+ [ $$FILE != 'tmp' ] && $(ADB) shell rm -r $(MSYS_FIX)/data/local/$$FILE; \
+ done);
+ $(ADB) shell rm -r $(MSYS_FIX)/cache/*
+ $(ADB) shell rm -r $(MSYS_FIX)/data/b2g/*
+ $(ADB) shell rm -r $(MSYS_FIX)$(GAIA_INSTALL_PARENT)/webapps
+
+# Build the settings.json file from settings.py
+ifeq ($(NOFTU), 1)
+SETTINGS_ARG=--noftu
+endif
+
+# We want the console to be disabled for device builds using the user variant.
+ifneq ($(TARGET_BUILD_VARIANT),user)
+SETTINGS_ARG += --console
+endif
+
+profile/settings.json: build/settings.py build/wallpaper.jpg
+ python build/settings.py $(SETTINGS_ARG) --locale $(GAIA_DEFAULT_LOCALE) --homescreen $(SCHEME)homescreen.$(GAIA_DOMAIN)$(GAIA_PORT)/manifest.webapp --ftu $(SCHEME)communications.$(GAIA_DOMAIN)$(GAIA_PORT)/manifest.webapp --wallpaper build/wallpaper.jpg --override build/custom-settings.json --output $@
+
+# push profile/settings.json to the phone
+install-settings-defaults: profile/settings.json
+ $(ADB) shell stop b2g
+ $(ADB) remount
+ $(ADB) push profile/settings.json /system/b2g/defaults/settings.json
+ $(ADB) shell start b2g
+
+
+# clean out build products
+clean:
+ rm -rf profile
+
+# clean out build products
+really-clean: clean
+ rm -rf xulrunner-sdk .xulrunner-url
+
diff --git a/apps/system/blank.html b/apps/system/blank.html
new file mode 100644
index 0000000..946ffa7
--- /dev/null
+++ b/apps/system/blank.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body></body>
+</html>
diff --git a/apps/system/camera/index.html b/apps/system/camera/index.html
new file mode 100644
index 0000000..c06bf41
--- /dev/null
+++ b/apps/system/camera/index.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="pragma" content="no-cache">
+ <title>Camera</title>
+ <link rel="resource" type="application/l10n" href="locales/locales.ini" />
+ <link rel="resource" type="application/l10n" href="/shared/locales/date.ini" />
+ <link type="text/css" rel="stylesheet" href="style/camera.css"/>
+ <link type="text/css" rel="stylesheet" href="style/filmstrip.css"/>
+ <link type="text/css" rel="stylesheet" href="style/VideoPlayer.css"/>
+ </head>
+ <body>
+ <div id="focus-ring"></div>
+ <video id="viewfinder" autoplay></video>
+
+ <div id="hud">
+ <a id="toggle-camera" class="hidden"></an>
+ <a id="toggle-flash" class="hidden"></a>
+ </div>
+
+ <div id="controls">
+ <a id="switch-button" name="Switch source" class="hidden" disabled="disabled"><span></span></a>
+ <a id="capture-button" name="Capture" disabled="disabled"><span></span></a>
+ <div id="misc-button">
+ <a id="gallery-button" class="hidden" name="View Gallery"><span></span></a>
+ <a id="cancel-pick" class="hidden"><span></span></a>
+ <span id="video-timer">00:00</span>
+ </div>
+ </div>
+
+ <div id="overlay" class="hidden">
+ <div id="overlay-content">
+ <h1 id="overlay-title"></h1>
+ <p id="overlay-text"><p>
+ </div>
+ </div>
+
+ <!-- see filmstrip.js and filmstrip.css for these elements -->
+ <div id="filmstrip" class="hidden">
+ <a id="filmstrip-gallery-button" class="hidden button"></a>
+ </div>
+ <div id="preview" class="offscreen">
+ <div id="frame-container"> <!-- media frame rotates inside this -->
+ <div id="media-frame"></div> <!-- image or video here -->
+ </div>
+ <footer id="preview-controls"> <!-- camera, delete, share buttons -->
+ <a id="camera-button" class="button"></a>
+ <a id="delete-button" class="button"></a>
+ <a id="share-button" class="button"></a>
+ </footer>
+ </div>
+
+ <script type="text/javascript" src="/shared/js/l10n.js"></script>
+ <script type="text/javascript" src="/shared/js/l10n_date.js"></script>
+ <script type="text/javascript" src="/shared/js/async_storage.js"></script>
+ <script type="text/javascript" src="/shared/js/blobview.js"></script>
+ <script type="text/javascript" src="/shared/js/media/jpeg_metadata_parser.js"></script>
+ <script type="text/javascript" src="/shared/js/media/get_video_rotation.js"></script>
+ <script type="text/javascript" src="/shared/js/media/video_player.js"></script>
+ <script type="text/javascript" src="/shared/js/media/media_frame.js"></script>
+ <script type="text/javascript" src="/shared/js/gesture_detector.js"></script>
+ <script type="text/javascript" src="js/camera.js"></script>
+ <script type="text/javascript" src="js/filmstrip.js"></script>
+ </body>
+</html>
diff --git a/apps/system/camera/js/camera.js b/apps/system/camera/js/camera.js
new file mode 100644
index 0000000..25f12ae
--- /dev/null
+++ b/apps/system/camera/js/camera.js
@@ -0,0 +1,982 @@
+'use strict';
+
+// Utility functions
+function padLeft(num, length) {
+ var r = String(num);
+ while (r.length < length) {
+ r = '0' + r;
+ }
+ return r;
+}
+
+
+// This handles the logic pertaining to the naming of files according
+// to the Design rule for Camera File System
+// * http://en.wikipedia.org/wiki/Design_rule_for_Camera_File_system
+var DCFApi = (function() {
+
+ var api = {};
+
+ var dcfConfigLoaded = false;
+ var deferredArgs = null;
+ var defaultSeq = {file: 1, dir: 100};
+
+ var dcfConfig = {
+ key: 'dcf_key',
+ seq: null,
+ postFix: 'MZLLA',
+ prefix: {video: 'VID_', image: 'IMG_'},
+ ext: {video: '3gp', image: 'jpg'}
+ };
+
+ api.init = function() {
+
+ asyncStorage.getItem(dcfConfig.key, function(value) {
+
+ dcfConfigLoaded = true;
+ dcfConfig.seq = value ? value : defaultSeq;
+
+ // We have a previous call to createDCFFilename that is waiting for
+ // a response, fire it again
+ if (deferredArgs) {
+ var args = deferredArgs;
+ api.createDCFFilename(args.storage, args.type, args.callback);
+ deferredArgs = null;
+ }
+ });
+ };
+
+ api.createDCFFilename = function(storage, type, callback) {
+
+ // We havent loaded the current counters from indexedDB yet, defer
+ // the call
+ if (!dcfConfigLoaded) {
+ deferredArgs = {storage: storage, type: type, callback: callback};
+ return;
+ }
+
+ var filepath = 'DCIM/' + dcfConfig.seq.dir + dcfConfig.postFix + '/';
+ var filename = dcfConfig.prefix[type] +
+ padLeft(dcfConfig.seq.file, 4) + '.' +
+ dcfConfig.ext[type];
+
+ // A file with this name may have been written by the user or
+ // our indexeddb sequence tracker was cleared, check we wont overwrite
+ // anything
+ var req = storage.get(filepath + filename);
+
+ // A file existed, we bump the directory then try to generate a
+ // new filename
+ req.onsuccess = function() {
+ dcfConfig.seq.file = 1;
+ dcfConfig.seq.dir += 1;
+ asyncStorage.setItem(dcfConfig.key, dcfConfig.seq, function() {
+ api.createDCFFilename(storage, type, callback);
+ });
+ };
+
+ // No file existed, we are good to go
+ req.onerror = function() {
+ if (dcfConfig.seq.file < 9999) {
+ dcfConfig.seq.file += 1;
+ } else {
+ dcfConfig.seq.file = 1;
+ dcfConfig.seq.dir += 1;
+ }
+ asyncStorage.setItem(dcfConfig.key, dcfConfig.seq, function() {
+ callback(filepath, filename);
+ });
+ };
+ };
+
+ return api;
+
+})();
+
+var Camera = {
+ _cameras: null,
+ _camera: 0,
+ _captureMode: null,
+ _recording: false,
+
+ // In secure mode the user cannot browse to the gallery
+ _secureMode: window.parent !== window,
+ _currentOverlay: null,
+
+ CAMERA: 'camera',
+ VIDEO: 'video',
+
+ _videoTimer: null,
+ _videoStart: null,
+ _videoPath: null,
+
+ _autoFocusSupported: 0,
+ _manuallyFocused: false,
+
+ _timeoutId: 0,
+ _cameraObj: null,
+
+ _photosTaken: [],
+ _cameraProfile: null,
+
+ _resumeViewfinderTimer: null,
+ _waitingToGenerateThumb: false,
+
+ _styleSheet: document.styleSheets[0],
+ _orientationRule: null,
+ _phoneOrientation: 0,
+
+ _pictureStorage: null,
+ _videoStorage: null,
+ _storageState: null,
+
+ STORAGE_INIT: 0,
+ STORAGE_AVAILABLE: 1,
+ STORAGE_NOCARD: 2,
+ STORAGE_UNMOUNTED: 3,
+ STORAGE_CAPACITY: 4,
+
+ _pictureSize: null,
+ _previewPaused: false,
+ _previewActive: false,
+
+ PREVIEW_PAUSE: 500,
+ FILMSTRIP_DURATION: 5000, // show filmstrip for 5s before fading
+
+ _flashModes: [],
+ _currentFlashMode: 0,
+
+ _config: {
+ fileFormat: 'jpeg'
+ },
+
+ get _previewConfig() {
+ delete this._previewConfig;
+ return this._previewConfig = {
+ width: document.body.clientHeight,
+ height: document.body.clientWidth
+ };
+ },
+
+ _previewConfigVideo: {
+ profile: 'cif',
+ rotation: 0,
+ width: 352,
+ height: 288
+ },
+
+ _shutterKey: 'camera.shutter.enabled',
+ _shutterSound: null,
+ _shutterSoundEnabled: true,
+
+ PROMPT_DELAY: 2000,
+
+ _watchId: null,
+ _position: null,
+
+ _pendingPick: null,
+
+ // The minimum available disk space to start recording a video.
+ RECORD_SPACE_MIN: 1024 * 1024 * 2,
+
+ // Number of bytes left on disk to let us stop recording.
+ RECORD_SPACE_PADDING: 1024 * 1024 * 1,
+
+ // Maximum image resolution for still photos taken with camera
+ MAX_IMAGE_RES: 1600 * 1200, // Just under 2 megapixels
+
+ get overlayTitle() {
+ return document.getElementById('overlay-title');
+ },
+
+ get overlayText() {
+ return document.getElementById('overlay-text');
+ },
+
+ get overlay() {
+ return document.getElementById('overlay');
+ },
+
+ get viewfinder() {
+ return document.getElementById('viewfinder');
+ },
+
+ get switchButton() {
+ return document.getElementById('switch-button');
+ },
+
+ get captureButton() {
+ return document.getElementById('capture-button');
+ },
+
+ get galleryButton() {
+ return document.getElementById('gallery-button');
+ },
+
+ get videoTimer() {
+ return document.getElementById('video-timer');
+ },
+
+ get focusRing() {
+ return document.getElementById('focus-ring');
+ },
+
+ get toggleButton() {
+ return document.getElementById('toggle-camera');
+ },
+
+ get toggleFlashBtn() {
+ return document.getElementById('toggle-flash');
+ },
+
+ // We have seperated init and delayedInit as we want to make sure
+ // that on first launch we dont interfere and load the camera
+ // previewStream as fast as possible, once the previewStream is
+ // active we do the rest of the initialisation.
+ init: function() {
+ this.setCaptureMode(this.CAMERA);
+ this.loadCameraPreview(this._camera, this.delayedInit.bind(this));
+ },
+
+ delayedInit: function camera_delayedInit() {
+ // If we don't have any pending messages, show the usual UI
+ // Otherwise, determine which buttons to show once we get our
+ // activity message
+ if (!navigator.mozHasPendingMessage('activity')) {
+ this.galleryButton.classList.remove('hidden');
+ this.switchButton.classList.remove('hidden');
+ this.enableButtons();
+ }
+
+ // Dont let the phone go to sleep while the camera is
+ // active, user must manually close it
+ if (navigator.requestWakeLock) {
+ navigator.requestWakeLock('screen');
+ }
+
+ this.setToggleCameraStyle();
+
+ // We lock the screen orientation and deal with rotating
+ // the icons manually
+ var css = '#switch-button span, #capture-button span, ' +
+ '#gallery-button span { -moz-transform: rotate(0deg); }';
+ var insertId = this._styleSheet.cssRules.length - 1;
+ this._orientationRule = this._styleSheet.insertRule(css, insertId);
+ window.addEventListener('deviceorientation', this.orientChange.bind(this));
+
+ this.toggleButton.addEventListener('click', this.toggleCamera.bind(this));
+ this.toggleFlashBtn.addEventListener('click', this.toggleFlash.bind(this));
+ this.viewfinder.addEventListener('click', this.toggleFilmStrip.bind(this));
+
+ this.switchButton
+ .addEventListener('click', this.toggleModePressed.bind(this));
+ this.captureButton
+ .addEventListener('click', this.capturePressed.bind(this));
+ this.galleryButton
+ .addEventListener('click', this.galleryBtnPressed.bind(this));
+
+ if (!navigator.mozCameras) {
+ this.captureButton.setAttribute('disabled', 'disabled');
+ return;
+ }
+
+ if (this._secureMode) {
+ this.galleryButton.setAttribute('disabled', 'disabled');
+ }
+
+ this._shutterSound = new Audio('./resources/sounds/shutter.ogg');
+ this._shutterSound.mozAudioChannelType = 'publicnotification';
+
+ if ('mozSettings' in navigator) {
+ var req = navigator.mozSettings.createLock().get(this._shutterKey);
+ req.onsuccess = (function onsuccess() {
+ this._shutterSoundEnabled = req.result[this._shutterKey];
+ }).bind(this);
+
+ navigator.mozSettings.addObserver(this._shutterKey, (function(e) {
+ this._shutterSoundEnabled = e.settingValue;
+ }).bind(this));
+ }
+
+ this._storageState = this.STORAGE_INIT;
+
+ this._pictureStorage = navigator.getDeviceStorage('pictures');
+ this._videoStorage = navigator.getDeviceStorage('videos'),
+
+ this._pictureStorage
+ .addEventListener('change', this.deviceStorageChangeHandler.bind(this));
+
+ navigator.mozSetMessageHandler('activity', function(activity) {
+ var name = activity.source.name;
+ if (name === 'pick') {
+ Camera.initPick(activity);
+ }
+ else {
+ // We got another activity. Perhaps we were launched from gallery
+ // So show our usual buttons
+ Camera.galleryButton.classList.remove('hidden');
+ Camera.switchButton.classList.remove('hidden');
+ }
+ Camera.enableButtons();
+ });
+
+ DCFApi.init();
+ },
+
+ enableButtons: function camera_enableButtons() {
+ if (!this._pendingPick) {
+ this.switchButton.removeAttribute('disabled');
+ }
+ this.captureButton.removeAttribute('disabled');
+ },
+
+ disableButtons: function camera_disableButtons() {
+ this.switchButton.setAttribute('disabled', 'disabled');
+ this.captureButton.setAttribute('disabled', 'disabled');
+ },
+
+ // When inside an activity the user cannot switch between
+ // the gallery or video recording.
+ initPick: function camera_initPick(activity) {
+ this._pendingPick = activity;
+
+ // Hide the gallery and switch buttons, leaving only the shutter
+ this.galleryButton.classList.add('hidden');
+ this.switchButton.classList.add('hidden');
+
+ // Display the cancel button and add an event listener for it
+ var cancelButton = document.getElementById('cancel-pick');
+ cancelButton.classList.remove('hidden');
+ cancelButton.onclick = this.cancelPick.bind(this);
+ },
+
+ cancelPick: function camera_cancelPick() {
+ if (this._pendingPick) {
+ this._pendingPick.postError('pick cancelled');
+ }
+ this._pendingPick = null;
+ },
+
+ toggleModePressed: function camera_toggleCaptureMode(e) {
+ if (e.target.getAttribute('disabled')) {
+ return;
+ }
+
+ var newMode = (this.captureMode === this.CAMERA) ? this.VIDEO : this.CAMERA;
+ this.disableButtons();
+ this.setCaptureMode(newMode);
+
+ function gotPreviewStream(stream) {
+ this.viewfinder.mozSrcObject = stream;
+ this.viewfinder.play();
+ this.enableButtons();
+ }
+ if (this.captureMode === this.CAMERA) {
+ this._cameraObj.getPreviewStream(this._previewConfig,
+ gotPreviewStream.bind(this));
+ } else {
+ this._previewConfigVideo.rotation = this._phoneOrientation;
+ this._cameraObj.getPreviewStreamVideoMode(this._previewConfigVideo,
+ gotPreviewStream.bind(this));
+ }
+ },
+
+ toggleCamera: function camera_toggleCamera() {
+ this._camera = 1 - this._camera;
+ this.loadCameraPreview(this._camera, this.enableButtons.bind(this));
+ this.setToggleCameraStyle();
+ },
+
+ setToggleCameraStyle: function camera_setToggleCameraStyle() {
+ var modeName = this._camera === 0 ? 'back' : 'front';
+ this.toggleButton.setAttribute('data-mode', modeName);
+ },
+
+ toggleFlash: function camera_toggleFlash() {
+ if (this._currentFlashMode === this._flashModes.length - 1) {
+ this._currentFlashMode = 0;
+ } else {
+ this._currentFlashMode = this._currentFlashMode + 1;
+ }
+ this.setFlashMode();
+ },
+
+ setFlashMode: function camera_setFlashMode() {
+ var flashModeName = this._flashModes[this._currentFlashMode];
+ this.toggleFlashBtn.setAttribute('data-mode', flashModeName);
+ this._cameraObj.flashMode = flashModeName;
+ },
+
+ toggleRecording: function camera_toggleRecording() {
+ if (this._recording) {
+ this.stopRecording();
+ return;
+ }
+
+ this.startRecording();
+ },
+
+ startRecording: function camera_startRecording() {
+ var captureButton = this.captureButton;
+ var switchButton = this.switchButton;
+
+ var onerror = function() {
+ handleError('error-recording');
+ }
+ var onsuccess = (function onsuccess() {
+ document.body.classList.add('capturing');
+ captureButton.removeAttribute('disabled');
+ this._recording = true;
+ this.startRecordingTimer();
+
+ // Hide the filmstrip to prevent the users from
+ // entering the preview mode after Camera starts recording
+ if (Filmstrip.isShown())
+ Filmstrip.hide();
+
+ // User closed app while recording was trying to start
+ if (document.mozHidden) {
+ this.stopRecording();
+ }
+ }).bind(this);
+
+ var handleError = (function handleError(id) {
+ this.enableButtons();
+ alert(navigator.mozL10n.get(id + '-title') + '. ' +
+ navigator.mozL10n.get(id + '-text'));
+ }).bind(this);
+
+ this.disableButtons();
+
+ var startRecording = (function startRecording(freeBytes) {
+ if (freeBytes < this.RECORD_SPACE_MIN) {
+ handleError('nospace');
+ return;
+ }
+
+ var config = {
+ rotation: this._phoneOrientation,
+ maxFileSizeBytes: freeBytes - this.RECORD_SPACE_PADDING
+ };
+ this._cameraObj.startRecording(config,
+ this._videoStorage, this._videoPath,
+ onsuccess, onerror);
+ }).bind(this);
+
+ DCFApi.createDCFFilename(this._videoStorage, 'video', (function(path, name) {
+ this._videoPath = path + name;
+
+ // The CameraControl API will not automatically create directories
+ // for the new file if they do not exist, so write a dummy file
+ // to the same directory via DeviceStorage to ensure that the directory
+ // exists before recording starts.
+ var dummyblob = new Blob([''], {type: 'video/3gpp'});
+ var dummyfilename = path + '.' + name;
+ var req = this._videoStorage.addNamed(dummyblob, dummyfilename);
+ req.onerror = onerror;
+ req.onsuccess = (function fileCreated() {
+ this._videoStorage.delete(dummyfilename); // No need to wait for success
+ // Determine the number of bytes available on disk.
+ var spaceReq = this._videoStorage.freeSpace();
+ spaceReq.onerror = onerror;
+ spaceReq.onsuccess = function() {
+ startRecording(spaceReq.result);
+ }
+ }).bind(this);
+ }).bind(this));
+ },
+
+ startRecordingTimer: function camera_startRecordingTimer() {
+ this._videoStart = new Date().getTime();
+ this.videoTimer.textContent = this.formatTimer(0);
+ this._videoTimer =
+ window.setInterval(this.updateVideoTimer.bind(this), 1000);
+ },
+
+ updateVideoTimer: function camera_updateVideoTimer() {
+ var videoLength =
+ Math.round((new Date().getTime() - this._videoStart) / 1000);
+ this.videoTimer.textContent = this.formatTimer(videoLength);
+ },
+
+ stopRecording: function camera_stopRecording() {
+ this._cameraObj.stopRecording();
+ this._recording = false;
+ window.clearInterval(this._videoTimer);
+ this.enableButtons();
+ document.body.classList.remove('capturing');
+
+ // XXX
+ // I need some way to know when the camera is done writing this file
+ // currently I'm sending this to the filmstrip which is trying to
+ // determine its rotation and fails sometimes if the file is not
+ // yet complete. For now, I just defer for a second, but
+ // there ought to be a better way.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=817367
+ // Maybe I'll get a device storage callback... check this.
+ var videofile = this._videoPath;
+ setTimeout(function() {
+ Filmstrip.addVideo(videofile);
+ Filmstrip.show(Camera.FILMSTRIP_DURATION);
+ }, 1000);
+ },
+
+ formatTimer: function camera_formatTimer(time) {
+ var minutes = Math.floor(time / 60);
+ var seconds = Math.round(time % 60);
+ if (minutes < 60) {
+ return padLeft(minutes, 2) + ':' + padLeft(seconds, 2);
+ }
+ return '';
+ },
+
+ capturePressed: function camera_doCapture(e) {
+ if (e.target.getAttribute('disabled')) {
+ return;
+ }
+
+ if (this.captureMode === this.CAMERA) {
+ this.prepareTakePicture();
+ } else {
+ this.toggleRecording();
+ }
+ },
+
+ galleryBtnPressed: function camera_galleryBtnPressed() {
+ // Can't launch the gallery if the lockscreen is locked.
+ // The button shouldn't even be visible in this case, but
+ // let's be really sure here.
+ if (this._secureMode)
+ return;
+
+ // Launch the gallery with an activity
+ var a = new MozActivity({
+ name: 'browse',
+ data: {
+ type: 'photos'
+ }
+ });
+ },
+
+ orientChange: function camera_orientChange(e) {
+ // Orientation is 0 starting at 'natural portrait' increasing
+ // going clockwise
+ var orientation = (e.beta > 45) ? 180 :
+ (e.beta < -45) ? 0 :
+ (e.gamma < -45) ? 90 :
+ (e.gamma > 45) ? 270 : 0;
+
+ if (orientation !== this._phoneOrientation) {
+ var rule = this._styleSheet.cssRules[this._orientationRule];
+ // PLEASE DO SOMETHING KITTENS ARE DYING
+ // Setting MozRotate to 90 or 270 causes element to disappear
+ rule.style.MozTransform = 'rotate(' + -(orientation + 1) + 'deg)';
+ this._phoneOrientation = orientation;
+
+ Filmstrip.setOrientation(orientation);
+ }
+ },
+
+ setCaptureMode: function camera_setCaptureMode(mode) {
+ if (this.captureMode) {
+ document.body.classList.remove(this.captureMode);
+ }
+ this.captureMode = mode;
+ document.body.classList.add(mode);
+ },
+
+ toggleFilmStrip: function camera_toggleFilmStrip(ev) {
+ // We will just ignore
+ // because the filmstrip shouldn't be shown
+ // while Camera is recording
+ if (this._recording)
+ return;
+
+ if (Filmstrip.isShown())
+ Filmstrip.hide();
+ else
+ Filmstrip.show();
+ },
+
+ loadCameraPreview: function camera_loadCameraPreview(camera, callback) {
+
+ this.viewfinder.mozSrcObject = null;
+ this._timeoutId = 0;
+
+ var viewfinder = this.viewfinder;
+ var style = viewfinder.style;
+ var width = document.body.clientHeight;
+ var height = document.body.clientWidth;
+
+ style.top = ((width / 2) - (height / 2)) + 'px';
+ style.left = -((width / 2) - (height / 2)) + 'px';
+
+ var transform = 'rotate(90deg)';
+ var rotation;
+ if (camera == 1) {
+ /* backwards-facing camera */
+ transform += ' scale(-1, 1)';
+ rotation = 0;
+ } else {
+ /* forwards-facing camera */
+ rotation = 0;
+ }
+
+ style.MozTransform = transform;
+ style.width = width + 'px';
+ style.height = height + 'px';
+
+ this._cameras = navigator.mozCameras.getListOfCameras();
+ var options = {camera: this._cameras[this._camera]};
+
+ function gotPreviewScreen(stream) {
+ viewfinder.mozSrcObject = stream;
+ viewfinder.play();
+
+ if (callback) {
+ callback();
+ }
+
+ this._previewActive = true;
+ this.checkStorageSpace();
+ setTimeout(this.initPositionUpdate.bind(this), this.PROMPT_DELAY);
+ }
+
+ function gotCamera(camera) {
+ this._cameraObj = camera;
+ this._config.rotation = rotation;
+ this._autoFocusSupported =
+ camera.capabilities.focusModes.indexOf('auto') !== -1;
+ this._pictureSize =
+ this.pickPictureSize(camera.capabilities.pictureSizes);
+ this.enableCameraFeatures(camera.capabilities);
+ camera.onShutter = (function() {
+ if (this._shutterSoundEnabled) {
+ this._shutterSound.play();
+ }
+ }).bind(this);
+ camera.onRecorderStateChange = this.recordingStateChanged.bind(this);
+ if (this.captureMode === this.CAMERA) {
+ camera.getPreviewStream(this._previewConfig, gotPreviewScreen.bind(this));
+ } else {
+ this._previewConfigVideo.rotation = this._phoneOrientation;
+ this._cameraObj.getPreviewStreamVideoMode(this._previewConfigVideo,
+ gotPreviewScreen.bind(this));
+ }
+ }
+
+ // If there is already a camera, we would have to release it first.
+ if (this._cameraObj) {
+ this.release(function camera_release_callback() {
+ navigator.mozCameras.getCamera(options, gotCamera.bind(this));
+ });
+ } else {
+ navigator.mozCameras.getCamera(options, gotCamera.bind(this));
+ }
+ },
+
+ recordingStateChanged: function(msg) {
+ if (msg === 'FileSizeLimitReached') {
+ this.stopRecording();
+ alert(navigator.mozL10n.get('size-limit-reached'));
+ }
+ },
+
+ enableCameraFeatures: function camera_enableCameraFeatures(capabilities) {
+ if (this._cameras.length > 1) {
+ this.toggleButton.classList.remove('hidden');
+ } else {
+ this.toggleButton.classList.add('hidden');
+ }
+
+ this._flashModes = capabilities.flashModes;
+ if (this._flashModes) {
+ this.setFlashMode();
+ this.toggleFlashBtn.classList.remove('hidden');
+ } else {
+ this.toggleFlashBtn.classList.add('hidden');
+ }
+ },
+
+ startPreview: function camera_startPreview() {
+ this.viewfinder.play();
+ this.loadCameraPreview(this._camera, this.enableButtons.bind(this));
+ this._previewActive = true;
+ },
+
+ stopPreview: function camera_stopPreview() {
+ if (this._recording) {
+ this.stopRecording();
+ }
+ this.disableButtons();
+ this.viewfinder.pause();
+ this._previewActive = false;
+ this.viewfinder.mozSrcObject = null;
+ this.release();
+ },
+
+ resumePreview: function camera_resumePreview() {
+ this._cameraObj.resumePreview();
+ this._previewActive = true;
+ this.enableButtons();
+ },
+
+ restartPreview: function camera_restartPreview() {
+ this._resumeViewfinderTimer =
+ window.setTimeout(this.resumePreview.bind(this), this.PREVIEW_PAUSE);
+ },
+
+ takePictureSuccess: function camera_takePictureSuccess(blob) {
+ this._manuallyFocused = false;
+ this.hideFocusRing();
+ this.restartPreview();
+ DCFApi.createDCFFilename(this._pictureStorage, 'image', (function(path, name) {
+ var addreq = this._pictureStorage.addNamed(blob, path + name);
+ addreq.onsuccess = (function() {
+ if (this._pendingPick) {
+ // XXX: https://bugzilla.mozilla.org/show_bug.cgi?id=806503
+ // We ought to just be able to pass this blob to the activity.
+ // But there seems to be a bug with blob lifetimes and activities.
+ // So we'll get a new blob back out of device storage to ensure
+ // that we've got a file-backed blob instead of a memory-backed blob.
+ var getreq = this._pictureStorage.get(path + name);
+ getreq.onsuccess = (function() {
+ this._pendingPick.postResult({
+ type: 'image/jpeg',
+ blob: getreq.result
+ });
+ this.cancelPick();
+ }).bind(this);
+
+ return;
+ }
+
+ Filmstrip.addImage(path + name, blob);
+ Filmstrip.show(Camera.FILMSTRIP_DURATION);
+ this.checkStorageSpace();
+
+ }).bind(this);
+
+ addreq.onerror = function() {
+ alert(navigator.mozL10n.get('error-saving-title') + '. ' +
+ navigator.mozL10n.get('error-saving-text'));
+ };
+ }).bind(this));
+ },
+
+ hideFocusRing: function camera_hideFocusRing() {
+ this.focusRing.removeAttribute('data-state');
+ },
+
+ checkStorageSpace: function camera_checkStorageSpace() {
+ if (this.updateOverlay()) {
+ return;
+ }
+
+ // The first time we're called, we need to make sure that there
+ // is an sdcard and that it is mounted. (Subsequently the device
+ // storage change handler will track that.)
+ if (this._storageState === this.STORAGE_INIT) {
+ this._pictureStorage.available().onsuccess = (function(e) {
+ this.updateStorageState(e.target.result);
+ this.updateOverlay();
+ // Now call the parent method again, so that if the sdcard is
+ // available we will actually verify that there is enough space on it
+ this.checkStorageSpace();
+ }.bind(this));
+ return;
+ }
+
+ // Now verify that there is enough space to store a picture
+ // 4 bytes per pixel plus some room for a header should be more
+ // than enough for a JPEG image.
+ var MAX_IMAGE_SIZE =
+ (this._pictureSize.width * this._pictureSize.height * 4) + 4096;
+
+ this._pictureStorage.freeSpace().onsuccess = (function(e) {
+ // XXX
+ // If we ever enter this out-of-space condition, it looks like
+ // this code will never be able to exit. The user will have to
+ // quit the app and start it again. Just deleting files will
+ // not be enough to get back to the STORAGE_AVAILABLE state.
+ // To fix this, we need an else clause here, and also a change
+ // in the updateOverlay() method.
+ if (e.target.result < MAX_IMAGE_SIZE) {
+ this._storageState = this.STORAGE_CAPACITY;
+ }
+ this.updateOverlay();
+ }).bind(this);
+ },
+
+ deviceStorageChangeHandler: function camera_deviceStorageChangeHandler(e) {
+ switch (e.reason) {
+ case 'available':
+ case 'unavailable':
+ case 'shared':
+ this.updateStorageState(e.reason);
+ break;
+ }
+ this.checkStorageSpace();
+ },
+
+ updateStorageState: function camera_updateStorageState(state) {
+ switch (state) {
+ case 'available':
+ this._storageState = this.STORAGE_AVAILABLE;
+ break;
+ case 'unavailable':
+ this._storageState = this.STORAGE_NOCARD;
+ break;
+ case 'shared':
+ this._storageState = this.STORAGE_UNMOUNTED;
+ break;
+ }
+ },
+
+ updateOverlay: function camera_updateOverlay() {
+ if (this._storageState === this.STORAGE_INIT) {
+ return false;
+ }
+
+ if (this._storageState === this.STORAGE_AVAILABLE) {
+ // Preview may have previously been paused if storage
+ // was not available
+ if (!this._previewActive && !document.mozHidden) {
+ this.startPreview();
+ }
+ this.showOverlay(null);
+ return false;
+ }
+
+ switch (this._storageState) {
+ case this.STORAGE_NOCARD:
+ this.showOverlay('nocard');
+ break;
+ case this.STORAGE_UNMOUNTED:
+ this.showOverlay('pluggedin');
+ break;
+ case this.STORAGE_CAPACITY:
+ this.showOverlay('nospace2');
+ break;
+ }
+ if (this._previewActive) {
+ this.stopPreview();
+ }
+ return true;
+ },
+
+ prepareTakePicture: function camera_takePicture() {
+ this.disableButtons();
+ this.focusRing.setAttribute('data-state', 'focusing');
+ if (this._autoFocusSupported && !this._manuallyFocused) {
+ this._cameraObj.autoFocus(this.autoFocusDone.bind(this));
+ } else {
+ this.takePicture();
+ }
+ },
+
+ autoFocusDone: function camera_autoFocusDone(success) {
+ if (!success) {
+ this.enableButtons();
+ this.focusRing.setAttribute('data-state', 'fail');
+ window.setTimeout(this.hideFocusRing.bind(this), 1000);
+ return;
+ }
+ this.focusRing.setAttribute('data-state', 'focused');
+ this.takePicture();
+ },
+
+ takePicture: function camera_takePicture() {
+ this._config.rotation = this._phoneOrientation;
+ this._config.pictureSize = this._pictureSize;
+ if (this._position) {
+ this._config.position = this._position;
+ }
+ this._cameraObj
+ .takePicture(this._config, this.takePictureSuccess.bind(this));
+ },
+
+ showOverlay: function camera_showOverlay(id) {
+ this._currentOverlay = id;
+
+ if (id === null) {
+ this.overlay.classList.add('hidden');
+ return;
+ }
+
+ this.overlayTitle.textContent = navigator.mozL10n.get(id + '-title');
+ this.overlayText.textContent = navigator.mozL10n.get(id + '-text');
+ this.overlay.classList.remove('hidden');
+ },
+
+ pickPictureSize: function camera_pickPictureSize(pictureSizes) {
+ var maxRes = this.MAX_IMAGE_RES;
+ var size = pictureSizes.reduce(function(acc, size) {
+ var mp = size.width * size.height;
+ return (mp > acc.width * acc.height && mp <= maxRes) ? size : acc;
+ }, {width: 0, height: 0});
+
+ if (size.width === 0 && size.height === 0) {
+ return pictureSizes[0];
+ } else {
+ return size;
+ }
+ },
+
+ initPositionUpdate: function camera_initPositionUpdate() {
+ if (this._watchId || document.mozHidden) {
+ return;
+ }
+ this._watchId = navigator.geolocation
+ .watchPosition(this.updatePosition.bind(this));
+ },
+
+ updatePosition: function camera_updatePosition(position) {
+ this._position = {
+ timestamp: position.timestamp,
+ altitude: position.coords.altitude,
+ latitude: position.coords.latitude,
+ longitude: position.coords.longitude
+ };
+ },
+
+ cancelPositionUpdate: function camera_cancelPositionUpdate() {
+ navigator.geolocation.clearWatch(this._watchId);
+ this._watchId = null;
+ },
+
+ release: function camera_release(callback) {
+ if (!this._cameraObj)
+ return;
+
+ this._cameraObj.release(function cameraReleased() {
+ Camera._cameraObj = null;
+ if (callback)
+ callback.call(Camera);
+ }, function releaseError() {
+ console.warn('Camera: failed to release hardware?');
+ if (callback)
+ callback.call(Camera);
+ });
+ }
+};
+
+Camera.init();
+
+document.addEventListener('mozvisibilitychange', function() {
+ if (document.mozHidden) {
+ Camera.stopPreview();
+ Camera.cancelPick();
+ Camera.cancelPositionUpdate();
+ if (this._secureMode) // If the lockscreen is locked
+ Filmstrip.clear(); // then forget everything when closing camera
+ } else {
+ Camera.startPreview();
+ }
+});
+
+window.addEventListener('beforeunload', function() {
+ window.clearTimeout(Camera._timeoutId);
+ delete Camera._timeoutId;
+ Camera.viewfinder.mozSrcObject = null;
+});
diff --git a/apps/system/camera/js/filmstrip.js b/apps/system/camera/js/filmstrip.js
new file mode 100644
index 0000000..d428801
--- /dev/null
+++ b/apps/system/camera/js/filmstrip.js
@@ -0,0 +1,568 @@
+/*
+ * filmstrip.js: filmstrip, thumbnails and previews for the camera.
+ */
+
+'use strict';
+
+var Filmstrip = (function() {
+
+ // This array holds all the data we need for image and video previews
+ var items = [];
+ var currentItemIndex;
+
+ // Maximum number of thumbnails in the filmstrip
+ var MAX_THUMBNAILS = 5;
+ var THUMBNAIL_WIDTH = 46; // size of each thumbnail
+ var THUMBNAIL_HEIGHT = 46;
+
+ // Timer for auto-hiding the filmstrip
+ var hideTimer = null;
+
+ // Document elements we care about
+ var filmstrip = document.getElementById('filmstrip');
+ var preview = document.getElementById('preview');
+ var frameContainer = document.getElementById('frame-container');
+ var mediaFrame = document.getElementById('media-frame');
+ var cameraButton = document.getElementById('camera-button');
+ var shareButton = document.getElementById('share-button');
+ var deleteButton = document.getElementById('delete-button');
+ var filmstripGalleryButton =
+ document.getElementById('filmstrip-gallery-button');
+
+ // Offscreen elements for generating thumbnails with
+ var offscreenImage = new Image();
+ var offscreenVideo = document.createElement('video');
+
+ // Set up event handlers
+ cameraButton.onclick = returnToCameraMode;
+ deleteButton.onclick = deleteCurrentItem;
+ shareButton.onclick = shareCurrentItem;
+ filmstripGalleryButton.onclick = Camera.galleryBtnPressed;
+ mediaFrame.addEventListener('dbltap', handleDoubleTap);
+ mediaFrame.addEventListener('transform', handleTransform);
+ mediaFrame.addEventListener('pan', handlePan);
+ mediaFrame.addEventListener('swipe', handleSwipe);
+
+ // Generate gesture events
+ var gestureDetector = new GestureDetector(mediaFrame);
+ gestureDetector.startDetecting();
+
+ // Create the MediaFrame for previews
+ var frame = new MediaFrame(mediaFrame);
+
+ // Start off with it positioned correctly.
+ setOrientation(Camera._phoneOrientation);
+
+ // If we're running in secure mode, we never want the user to see the
+ // gallery button or the share button.
+ filmstrip.removeChild(filmstripGalleryButton);
+ shareButton.parentNode.removeChild(shareButton);
+
+ function isShown() {
+ return !filmstrip.classList.contains('hidden');
+ }
+
+ function hide() {
+ filmstrip.classList.add('hidden');
+ if (hideTimer) {
+ clearTimeout(hideTimer);
+ hideTimer = null;
+ }
+ }
+
+ /*
+ * With a time, show the filmstrip and then hide it after the time is up.
+ * Without time, show until hidden.
+ * Tapping in the camera toggles it. And if toggled on, it will be on
+ * without a timer.
+ * It is always on when a preview is shown.
+ * After recording a photo or video, it is shown for 5 seconds.
+ * And it is also shown for 5 seconds after leaving preview mode.
+ */
+ function show(time) {
+ filmstrip.classList.remove('hidden');
+ if (hideTimer) {
+ clearTimeout(hideTimer);
+ hideTimer = null;
+ }
+ if (time)
+ hideTimer = setTimeout(hide, time);
+ }
+
+ filmstrip.onclick = function(event) {
+ var target = event.target;
+ if (!target || !target.classList.contains('thumbnail'))
+ return;
+
+ var index = parseInt(target.dataset.index);
+ previewItem(index);
+ // If we're showing previews be sure we're showing the filmstrip
+ // with no timeout and be sure that the viewfinder video is paused.
+ show();
+ Camera.viewfinder.pause();
+ // If there is a preview shown, we want the gallery button in
+ // the filmstrip
+ filmstripGalleryButton.classList.remove('hidden');
+ };
+
+ function previewItem(index) {
+ // Don't redisplay the item if it is already displayed
+ if (currentItemIndex === index)
+ return;
+
+ var item = items[index];
+
+ if (item.isImage) {
+ frame.displayImage(item.blob, item.width, item.height, item.preview);
+ }
+ else if (item.isVideo) {
+ frame.displayVideo(item.blob, item.width, item.height, item.rotation);
+ }
+
+ preview.classList.remove('offscreen');
+ currentItemIndex = index;
+
+ // Highlight the border of the thumbnail we're previewing
+ // and clear the highlight on all others
+ items.forEach(function(item, itemindex) {
+ if (itemindex === index)
+ item.element.classList.add('previewed');
+ else
+ item.element.classList.remove('previewed');
+ });
+ }
+
+ function returnToCameraMode() {
+ Camera.viewfinder.play(); // Restart the viewfinder
+ show(Camera.FILMSTRIP_DURATION); // Fade the filmstrip after a delay
+ // hide the gallery button in the filmstrip
+ filmstripGalleryButton.classList.add('hidden');
+ preview.classList.add('offscreen');
+ frame.clear();
+ if (items.length > 0)
+ items[currentItemIndex].element.classList.remove('previewed');
+ currentItemIndex = null;
+ }
+
+ function deleteCurrentItem() {
+ var item = items[currentItemIndex];
+ var msg, storage, filename;
+
+ if (item.isImage) {
+ msg = navigator.mozL10n.get('delete-photo?');
+ storage = Camera._pictureStorage;
+ filename = item.filename;
+ }
+ else {
+ msg = navigator.mozL10n.get('delete-video?');
+ storage = Camera._videoStorage;
+ filename = item.filename;
+ }
+
+ // The system app is not allowed to use confirm, I think
+ // so if we're running in secure mode, just delete the file without
+ // confirmation
+ if (Camera._secureMode || confirm(msg)) {
+ // Remove the item from the array of items
+ items.splice(currentItemIndex, 1);
+
+ // Remove the thumbnail image from the filmstrip
+ filmstrip.removeChild(item.element);
+ URL.revokeObjectURL(item.element.src);
+
+ // Renumber the item elements
+ items.forEach(function(item, index) {
+ item.element.dataset.index = index;
+ });
+
+ // If there are no more items, go back to the camera
+ if (items.length === 0) {
+ returnToCameraMode();
+ }
+ else {
+ // Otherwise, switch the frame to display the next item. But if
+ // we just deleted the last item, then we'll need to display the
+ // previous item.
+ var newindex = currentItemIndex;
+ if (newindex >= items.length)
+ newindex = items.length - 1;
+ currentItemIndex = null;
+ previewItem(newindex);
+ }
+
+ // Actually delete the file
+ storage.delete(filename).onerror = function(e) {
+ console.warn('Failed to delete', filename,
+ 'from DeviceStorage:', e.target.error);
+ }
+ }
+ }
+
+ function shareCurrentItem() {
+ if (Camera._secureMode)
+ return;
+ var item = items[currentItemIndex];
+ var type = item.isImage ? 'image/*' : 'video/*';
+ var nameonly = item.filename.substring(item.filename.lastIndexOf('/') + 1);
+ var activity = new MozActivity({
+ name: 'share',
+ data: {
+ type: type,
+ number: 1,
+ blobs: [item.blob],
+ filenames: [nameonly],
+ filepaths: [item.filename] /* temporary hack for bluetooth app */
+ }
+ });
+ activity.onerror = function(e) {
+ console.warn('Share activity error:', activity.error.name);
+ }
+ }
+
+ function handleDoubleTap(e) {
+ if (!items[currentItemIndex].isImage)
+ return;
+
+ var scale;
+ if (frame.fit.scale > frame.fit.baseScale)
+ scale = frame.fit.baseScale / frame.fit.scale;
+ else
+ scale = 2;
+
+ // If the phone orientation is 0 (unrotated) then the gesture detector's
+ // event coordinates match what's on the screen, and we use them to
+ // specify a point to zoom in or out on. For other orientations we could
+ // calculate the correct point, but instead just use the midpoint.
+ var x, y;
+ if (Camera._phoneOrientation === 0) {
+ x = e.detail.clientX;
+ y = e.detail.clientY;
+ }
+ else {
+ x = mediaFrame.offsetWidth / 2;
+ y = mediaFrame.offsetHeight / 2;
+ }
+
+ frame.zoom(scale, x, y, 200);
+ }
+
+ function handleTransform(e) {
+ if (!items[currentItemIndex].isImage)
+ return;
+
+ // If the phone orientation is 0 (unrotated) then the gesture detector's
+ // event coordinates match what's on the screen, and we use them to
+ // specify a point to zoom in or out on. For other orientations we could
+ // calculate the correct point, but instead just use the midpoint.
+ var x, y;
+ if (Camera._phoneOrientation === 0) {
+ x = e.detail.midpoint.clientX;
+ y = e.detail.midpoint.clientY;
+ }
+ else {
+ x = mediaFrame.offsetWidth / 2;
+ y = mediaFrame.offsetHeight / 2;
+ }
+
+ frame.zoom(e.detail.relative.scale, x, y);
+ }
+
+ function handlePan(e) {
+ if (!items[currentItemIndex].isImage)
+ return;
+
+ // The gesture detector event does not take our CSS rotation into
+ // account, so we have to pan by a dx and dy that depend on how
+ // the MediaFrame is rotated
+ var dx, dy;
+ switch (Camera._phoneOrientation) {
+ case 0:
+ dx = e.detail.relative.dx;
+ dy = e.detail.relative.dy;
+ break;
+ case 90:
+ dx = -e.detail.relative.dy;
+ dy = e.detail.relative.dx;
+ break;
+ case 180:
+ dx = -e.detail.relative.dx;
+ dy = -e.detail.relative.dy;
+ break;
+ case 270:
+ dx = e.detail.relative.dy;
+ dy = -e.detail.relative.dx;
+ break;
+ }
+
+ frame.pan(dx, dy);
+ }
+
+ function handleSwipe(e) {
+ // Because the stuff around the media frame does not change position
+ // when the phone is rotated, we don't alter these directions based
+ // on orientation. To dismiss the preview, the user always swipes toward
+ // the filmstrip.
+
+ switch (e.detail.direction) {
+ case 'up': // close the preview if the swipe is fast enough
+ if (e.detail.vy < -1)
+ returnToCameraMode();
+ break;
+ case 'left': // go to next image if fast enough
+ if (e.detail.vx < -1 && currentItemIndex < items.length - 1)
+ previewItem(currentItemIndex + 1);
+ break;
+ case 'right': // go to previous image if fast enough
+ if (e.detail.vx > 1 && currentItemIndex > 0)
+ previewItem(currentItemIndex - 1);
+ break;
+ }
+ }
+
+ function addImage(filename, blob) {
+ parseJPEGMetadata(blob, function getPreviewBlob(metadata) {
+ if (metadata.preview) {
+ var previewBlob = blob.slice(metadata.preview.start,
+ metadata.preview.end,
+ 'image/jpeg');
+
+ offscreenImage.src = URL.createObjectURL(previewBlob);
+ offscreenImage.onload = function() {
+ createThumbnailFromElement(offscreenImage, false, 0,
+ function(thumbnail) {
+ addItem({
+ isImage: true,
+ filename: filename,
+ thumbnail: thumbnail,
+ blob: blob,
+ width: metadata.width,
+ height: metadata.height,
+ preview: metadata.preview
+ });
+ });
+ URL.revokeObjectURL(offscreenImage.src);
+ offscreenImage.onload = null;
+ offscreenImage.src = null;
+ };
+ }
+ }, function logerr(msg) { console.warn(msg); });
+ }
+
+ function addVideo(filename) {
+ var request = Camera._videoStorage.get(filename);
+ request.onerror = function() {
+ console.warn('addVideo:', filename, request.error.name);
+ };
+ request.onsuccess = function() {
+ var blob = request.result;
+ getVideoRotation(blob, function(rotation) {
+ if (typeof rotation !== 'number') {
+ console.warn('Unexpected rotation:', rotation);
+ rotation = 0;
+ }
+
+ var url = URL.createObjectURL(blob);
+
+ offscreenVideo.preload = 'metadata';
+ offscreenVideo.style.width = THUMBNAIL_WIDTH + 'px';
+ offscreenVideo.style.height = THUMBNAIL_HEIGHT + 'px';
+ offscreenVideo.src = url;
+
+ offscreenVideo.onerror = function() {
+ URL.revokeObjectURL(url);
+ offscreenVideo.onerror = null;
+ offscreenVideo.onloadedmetadata = null;
+ offscreenVideo.removeAttribute('src');
+ offscreenVideo.load();
+ console.warn('not a video file', filename);
+ }
+
+ offscreenVideo.onloadedmetadata = function() {
+ createThumbnailFromElement(offscreenVideo, true, rotation,
+ function(thumbnail) {
+ addItem({
+ isVideo: true,
+ filename: filename,
+ thumbnail: thumbnail,
+ blob: blob,
+ width: offscreenVideo.videoWidth,
+ height: offscreenVideo.videoHeight,
+ rotation: rotation
+ });
+ });
+ URL.revokeObjectURL(url);
+ offscreenVideo.onerror = null;
+ offscreenVideo.onloadedmetadata = null;
+ offscreenVideo.removeAttribute('src');
+ offscreenVideo.load();
+ };
+ });
+ };
+ }
+
+ // Add a thumbnail to the filmstrip.
+ // The details object contains everything we need to know
+ // to display the thumbnail and preview the image or video
+ function addItem(item) {
+ // Thumbnails go from most recent to least recent.
+ items.unshift(item);
+
+ // Create an image element for this new thumbnail and display it
+ item.element = new Image();
+ item.element.src = URL.createObjectURL(item.thumbnail);
+ item.element.classList.add('thumbnail');
+ filmstrip.insertBefore(item.element, filmstrip.firstElementChild);
+
+ // If we have too many thumbnails now, remove the oldest one from
+ // the array, and remove its element from the filmstrip and release
+ // its blob url
+ if (items.length > MAX_THUMBNAILS) {
+ var oldest = items.pop();
+ filmstrip.removeChild(oldest.element);
+ URL.revokeObjectURL(oldest.element.src);
+ }
+
+ // Now update the index associated with each of the remaining elements
+ // so that the click event handle knows which one it clicked on
+ items.forEach(function(item, index) {
+ item.element.dataset.index = index;
+ });
+ }
+
+ // Remove all items from the filmstrip. Don't delete the files, but
+ // forget all of our state. This also exits preview mode if we're in it.
+ function clear() {
+ if (!preview.classList.contains('offscreen'))
+ returnToCameraMode();
+ items.forEach(function(item) {
+ filmstrip.removeChild(item.element);
+ URL.revokeObjectURL(item.element.src);
+ });
+ items.length = 0;
+ }
+
+ // Create a thumbnail size canvas, copy the <img> or <video> into it
+ // cropping the edges as needed to make it fit, and then extract the
+ // thumbnail image as a blob and pass it to the callback.
+ function createThumbnailFromElement(elt, video, rotation, callback) {
+ // Create a thumbnail image
+ var canvas = document.createElement('canvas');
+ var context = canvas.getContext('2d');
+ canvas.width = THUMBNAIL_WIDTH;
+ canvas.height = THUMBNAIL_HEIGHT;
+ var eltwidth = video ? elt.videoWidth : elt.width;
+ var eltheight = video ? elt.videoHeight : elt.height;
+ var scalex = canvas.width / eltwidth;
+ var scaley = canvas.height / eltheight;
+
+ // Take the larger of the two scales: we crop the image to the thumbnail
+ var scale = Math.max(scalex, scaley);
+
+ // Calculate the region of the image that will be copied to the
+ // canvas to create the thumbnail
+ var w = Math.round(THUMBNAIL_WIDTH / scale);
+ var h = Math.round(THUMBNAIL_HEIGHT / scale);
+ var x = Math.round((eltwidth - w) / 2);
+ var y = Math.round((eltheight - h) / 2);
+
+ // If a rotation is specified, rotate the canvas context
+ if (rotation) {
+ context.save();
+ switch (rotation) {
+ case 90:
+ context.translate(THUMBNAIL_WIDTH, 0);
+ context.rotate(Math.PI / 2);
+ break;
+ case 180:
+ context.translate(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
+ context.rotate(Math.PI);
+ break;
+ case 270:
+ context.translate(0, THUMBNAIL_HEIGHT);
+ context.rotate(-Math.PI / 2);
+ break;
+ }
+ }
+
+ // Draw that region of the image into the canvas, scaling it down
+ context.drawImage(elt, x, y, w, h,
+ 0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
+
+ // Restore the default rotation so the play arrow comes out correctly
+ if (rotation) {
+ context.restore();
+ }
+
+ // If this is a video, superimpose a translucent play button over
+ // the captured video frame to distinguish it from a still photo thumbnail
+ if (video) {
+ // First draw a transparent gray circle
+ context.fillStyle = 'rgba(0, 0, 0, .3)';
+ context.beginPath();
+ context.arc(THUMBNAIL_WIDTH / 2, THUMBNAIL_HEIGHT / 2,
+ THUMBNAIL_HEIGHT / 3, 0, 2 * Math.PI, false);
+ context.fill();
+
+ // Now outline the circle in white
+ context.strokeStyle = 'rgba(255,255,255,.6)';
+ context.lineWidth = 2;
+ context.stroke();
+
+ // And add a white play arrow.
+ context.beginPath();
+ context.fillStyle = 'rgba(255,255,255,.6)';
+ // The height of an equilateral triangle is sqrt(3)/2 times the side
+ var side = THUMBNAIL_HEIGHT / 3;
+ var triangle_height = side * Math.sqrt(3) / 2;
+ context.moveTo(THUMBNAIL_WIDTH / 2 + triangle_height * 2 / 3,
+ THUMBNAIL_HEIGHT / 2);
+ context.lineTo(THUMBNAIL_WIDTH / 2 - triangle_height / 3,
+ THUMBNAIL_HEIGHT / 2 - side / 2);
+ context.lineTo(THUMBNAIL_WIDTH / 2 - triangle_height / 3,
+ THUMBNAIL_HEIGHT / 2 + side / 2);
+ context.closePath();
+ context.fill();
+ }
+
+ canvas.toBlob(callback, 'image/jpeg');
+ }
+
+ function setOrientation(orientation) {
+ preview.dataset.orientation = orientation;
+ filmstrip.dataset.orientation = orientation;
+ mediaFrame.dataset.orientation = orientation;
+
+ // When we rotate the media frame, we also have to change its size
+ var containerWidth = frameContainer.offsetWidth;
+ var containerHeight = frameContainer.offsetHeight;
+ if (orientation === 0 || orientation === 180) {
+ mediaFrame.style.width = containerWidth + 'px';
+ mediaFrame.style.height = containerHeight + 'px';
+ mediaFrame.style.top = 0 + 'px';
+ mediaFrame.style.left = 0 + 'px';
+ }
+ else {
+ mediaFrame.style.width = containerHeight + 'px';
+ mediaFrame.style.height = containerWidth + 'px';
+ mediaFrame.style.top = ((containerHeight - containerWidth) / 2) + 'px';
+ mediaFrame.style.left = ((containerWidth - containerHeight) / 2) + 'px';
+ }
+
+ // And rotate so this new size fills the screen
+ mediaFrame.style.transform = 'rotate(-' + orientation + 'deg)';
+
+ // And we have to resize the frame (and its video player)
+ frame.resize();
+ frame.video.setPlayerSize();
+ }
+
+ return {
+ isShown: isShown,
+ hide: hide,
+ show: show,
+ addImage: addImage,
+ addVideo: addVideo,
+ clear: clear,
+ setOrientation: setOrientation
+ };
+}());
diff --git a/apps/system/camera/locales/camera.ar.properties b/apps/system/camera/locales/camera.ar.properties
new file mode 100644
index 0000000..9ca7690
--- /dev/null
+++ b/apps/system/camera/locales/camera.ar.properties
@@ -0,0 +1,19 @@
+nospace2-title = لا توجد مساحة كافية على بطاقة الذاكرة
+nospace2-text = حرِّر مساحة بحذف ملفات.
+
+error-saving-title = لم يتم حفظ الصورة
+error-saving-text = حدث خطأ منع الكاميرا من حفظ الصورة.
+
+error-recording-title = لم يتم تصوير الفيديو
+error-recording-text = هنالك خطأ منع الكاميرا من تصوير الفيديو
+
+nocard-title = لم يتم العثور على بطاقة الذاكرة
+nocard-text = أدخل بطاقة الذاكرة لأخذ صور.
+
+pluggedin-title = لا يمكن استخدام الكاميرا والجوَّال موصول.
+pluggedin-text = إفصل الجوَّال لعرض الصور.
+
+size-limit-reached = لا توجد مساحة كافية على بطاقة الذاكرة
+
+delete-photo? = حذف الصورة ؟
+delete-video? = حذف الفيديو؟
diff --git a/apps/system/camera/locales/camera.en-US.properties b/apps/system/camera/locales/camera.en-US.properties
new file mode 100644
index 0000000..90b318d
--- /dev/null
+++ b/apps/system/camera/locales/camera.en-US.properties
@@ -0,0 +1,19 @@
+nospace2-title = Not enough space on memory card
+nospace2-text = Try freeing up space by deleting media.
+
+error-saving-title = Picture not saved
+error-saving-text = An error prevented Camera from saving the picture.
+
+error-recording-title = Video not recorded
+error-recording-text = An error prevented Camera from recording the video.
+
+nocard-title = No memory card found
+nocard-text = Insert a memory card to take pictures.
+
+pluggedin-title = Camera can not be used while plugged in
+pluggedin-text = Unplug the phone to view pictures.
+
+size-limit-reached = You have run out of space on your SD card.
+
+delete-photo? = Delete photo?
+delete-video? = Delete video?
diff --git a/apps/system/camera/locales/camera.fr.properties b/apps/system/camera/locales/camera.fr.properties
new file mode 100644
index 0000000..56d7d3b
--- /dev/null
+++ b/apps/system/camera/locales/camera.fr.properties
@@ -0,0 +1,19 @@
+nospace2-title = Espace libre sur la carte mémoire insuffisant
+nospace2-text = Essayez de libérer de l’espace en supprimant des médias.
+
+error-saving-title = Photo non sauvegardée
+error-saving-text = Une erreur a empêché l’application Photo de sauvegarder la photo.
+
+error-recording-title = Vidéo non enregistrée
+error-recording-text = Une erreur a empêché l’application Photo d’enregistrer la vidéo.
+
+nocard-title = Aucune carte mémoire trouvée
+nocard-text = Pour prendre des photos, insérez une carte mémoire.
+
+pluggedin-title = L’application Photo ne peut pas être utilisée tant que le téléphone est branché
+pluggedin-text = Pour afficher les photos, débranchez le téléphone.
+
+size-limit-reached = Vous n’avez pas assez d’espace sur votre carte SD.
+
+delete-photo? = Supprimer la photo ?
+delete-video? = Supprimer la vidéo ?
diff --git a/apps/system/camera/locales/camera.zh-TW.properties b/apps/system/camera/locales/camera.zh-TW.properties
new file mode 100644
index 0000000..9cf042a
--- /dev/null
+++ b/apps/system/camera/locales/camera.zh-TW.properties
@@ -0,0 +1,20 @@
+nospace2-title = 記憶卡的空間不足
+nospace2-text = 刪除媒體檔案以釋放可用空間。
+
+error-saving-title = 相片未儲存
+error-saving-text = 發生錯誤,相機無法儲存相片。
+
+error-recording-title = 影片未紀錄
+error-recording-text = 有錯誤發生使照相機無法錄製影片。
+
+nocard-title = 找不到記憶卡
+nocard-text = 插入記憶卡以拍照。
+
+pluggedin-title = 連接到電腦時無法使用相機
+pluggedin-text = 拔線以檢視照片。
+
+size-limit-reached = 您已用盡 SD 卡上的空間。
+
+delete-photo? = 刪除相片?
+delete-video? = 刪除影片?
+
diff --git a/apps/system/camera/locales/locales.ini b/apps/system/camera/locales/locales.ini
new file mode 100644
index 0000000..93f5cbc
--- /dev/null
+++ b/apps/system/camera/locales/locales.ini
@@ -0,0 +1,11 @@
+@import url(camera.en-US.properties)
+
+[ar]
+@import url(camera.ar.properties)
+
+[fr]
+@import url(camera.fr.properties)
+
+[zh-TW]
+@import url(camera.zh-TW.properties)
+
diff --git a/apps/system/camera/resources/sounds/shutter.ogg b/apps/system/camera/resources/sounds/shutter.ogg
new file mode 100644
index 0000000..f0c67d6
--- /dev/null
+++ b/apps/system/camera/resources/sounds/shutter.ogg
Binary files differ
diff --git a/apps/system/camera/style/VideoPlayer.css b/apps/system/camera/style/VideoPlayer.css
new file mode 100644
index 0000000..bc2b23c
--- /dev/null
+++ b/apps/system/camera/style/VideoPlayer.css
@@ -0,0 +1,152 @@
+/* styles for the video element itself */
+.videoPlayer {
+ position: absolute;
+ left: 0; /* we position it with a transform */
+ top:0;
+ transform-origin: 0 0;
+}
+
+/* video player controls */
+.videoPlayerControls {
+ position: absolute;
+ left: 0px;
+ right: 0px;
+ top: 0px;
+ bottom: 0px;
+ margin: 0;
+ padding: 0;
+}
+
+.videoPlayerPlayButton {
+ position: absolute;
+ width: 106px;
+ height: 106px;
+ left: calc(50% - 53px);
+ top: calc(50% - 53px);
+ background: url("images/video_play_button.png") center no-repeat,
+ url("images/video_play_normal.png") center no-repeat;
+ border-width: 0;
+}
+
+.videoPlayerPlayButton:active {
+ background: url("images/video_play_button.png") center no-repeat,
+ url("images/video_play_focus.png") center no-repeat
+}
+
+.videoPlayerPlayButton.hidden {
+ opacity: 0;
+}
+
+.videoPlayerFooter {
+ position: absolute;
+ left: 0px;
+ right: 0px;
+ bottom: 0px;
+ height: 50px;
+ margin: 0;
+ padding: 0;
+ background-color: rgba(0, 0, 0, 0.3);
+ overflow: hidden;
+ opacity: 1;
+ transition: opacity 0.5s;
+ font-family: "MozTT", sans-serif;
+ -moz-user-select: none;
+}
+
+.videoPlayerFooter.hidden {
+ opacity: 0;
+ pointer-events: none;
+}
+
+.videoPlayerPauseButton {
+ position: absolute;
+ width: 100px;
+ height: 100px;
+ padding: 0;
+ margin: 0;
+ background: url("images/video_pause_button.png") center no-repeat,
+ rgba(0,0,0,.5);
+ border-radius: 53px;
+ border: solid #ccc 3px;
+ top: -25px;
+ left: 10px;
+}
+
+.videoPlayerPauseButton:active {
+ background: url("images/video_pause_button.png") center no-repeat,
+ url("images/video_play_focus.png") center no-repeat
+}
+
+button::-moz-focus-inner {
+ padding: 0;
+ border: none;
+}
+
+/* time slider */
+.videoPlayerSlider {
+ position: absolute;
+ left: 110px;
+ top: 0px;
+ right: 0px;
+ height: 100%;
+}
+
+.videoPlayerSlider > span {
+ display: block;
+ width: 45px;
+ position: absolute;
+ color: white;
+ height: 100%;
+ line-height: 50px;
+ text-align: center;
+ font-size: 15px;
+}
+
+.videoPlayerElapsedText {
+ left: 10px;
+}
+
+.videoPlayerDurationText {
+ right: 10px;
+}
+
+.videoPlayerProgress {
+ position: absolute;
+ top: 0px;
+ left: 70px;
+ right: 70px;
+ height: 100%;
+}
+
+.videoPlayerProgress > div {
+ position: absolute;
+ pointer-events: none;
+}
+
+.videoPlayerElapsedBar, .videoPlayerBackgroundBar {
+ height: 4px;
+ width: 0%;
+ top: 50%;
+ margin-top: -2px;
+ border-radius: 6px;
+}
+
+.videoPlayerElapsedBar {
+ background-color: #0ac;
+}
+
+.videoPlayerBackgroundBar {
+ background-color: #333;
+ width: 100%;
+}
+
+.videoPlayerPlayHead {
+ display: block;
+ height: 20px;
+ width: 25px;
+ border-radius: 25px;
+ background-color: white;
+ top: 50%;
+ margin: -10px 0 0 -12px;
+}
+
diff --git a/apps/system/camera/style/camera.css b/apps/system/camera/style/camera.css
new file mode 100644
index 0000000..6a1ca02
--- /dev/null
+++ b/apps/system/camera/style/camera.css
@@ -0,0 +1,314 @@
+html, body {
+ font-family: "MozTT", sans-serif;
+ font-size: 10px;
+ height: 100%;
+ width: 100%;
+ padding: 0;
+ margin: 0;
+ overflow: hidden;
+ background-color: black;
+}
+
+#viewfinder {
+ position: absolute;
+ z-index: 25;
+}
+
+#controls {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ left: 0;
+ height: 45px;
+ z-index: 50;
+ background-color: rgba(0, 0, 0, 0.8);
+ overflow: hidden;
+}
+
+#switch-button, #capture-button, #misc-button {
+ position: absolute;
+}
+
+#switch-button, #misc-button {
+ height: 45px;
+ width: 33%;
+}
+
+#switch-button span,
+#capture-button span,
+#gallery-button span,
+#cancel-pick span
+{
+ -moz-transition: 0.2s ease-in-out;
+ pointer-events: none;
+ background-position: center center;
+ background-repeat: no-repeat;
+ display: block;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ margin-left: -15px;
+ margin-top: -15px;
+ width: 30px;
+ height: 30px;
+}
+
+#switch-button {
+ left: 66%;
+}
+
+#misc-button {
+ text-align: center;
+ left: 0;
+}
+
+#video-timer {
+ position:relative;
+ top:50%;
+ margin-top:-0.5em;
+}
+
+#gallery-button {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+
+#gallery-button.hidden {
+ display:none;
+}
+
+#gallery-button span {
+ background-image: url(images/grid.png);
+}
+
+#gallery-button[disabled=disabled] {
+ display: none;
+}
+
+#cancel-pick {
+ display:block;
+ width: 100%;
+ height: 100%;
+}
+
+#cancel-pick.hidden {
+ display:none
+}
+
+#cancel-pick span {
+ background-image: url(images/actionicon_cancel.png);
+}
+
+#capture-button[disabled=disabled] {
+ opacity: 0.5;
+}
+
+#switch-button[disabled=disabled] {
+ opacity: 0.5;
+}
+
+#capture-button {
+ background-color: #03a2b4;
+ border-radius: 100px;
+ left: 33%;
+ height: 100px;
+ width: 33%;
+ top: -28px;
+}
+
+#video-timer {
+ display: none;
+ color: white;
+}
+
+/* Specific to when we are capturing video */
+.capturing #video-timer {
+ display: block;
+}
+
+.capturing #gallery-button {
+ display: none;
+}
+
+.capturing #capture-button {
+ background-color: #d3361c;
+}
+
+.video.capturing #capture-button span {
+ background-image: url(images/stop.png);
+}
+
+/* Swap the camera and video icons depending on mode */
+.video #switch-button span {
+ background-image: url(images/camera.png);
+}
+
+.camera #switch-button span {
+ background-image: url(images/video.png);
+}
+
+.camera #capture-button span {
+ background-image: url(images/camera.png);
+}
+
+.video #capture-button span {
+ background-image: url(images/video.png);
+}
+
+#focus-ring {
+ position: absolute;
+ z-index: 100;
+ display: none;
+ width: 50px;
+ height: 50px;
+ border-radius: 50px;
+ top: 50%;
+ left: 50%;
+ margin-top: -30px;
+ margin-left: -30px;
+}
+
+#focus-ring[data-state=focused] {
+ border: 4px solid rgba(0, 255, 0, 0.3);
+ display: block;
+}
+
+#focus-ring[data-state=focusing] {
+ border: 4px solid rgba(0, 0, 0, 0.8);
+ display: block;
+}
+
+#focus-ring[data-state=fail] {
+ border: 4px solid rgba(255, 0, 0, 0.3);
+ display: block;
+}
+
+/*
+ * The overlay is where we display messages like Scanning, No Videos,
+ * No SD card and SD Card in Use along with instructions for resolving
+ * the issue. The user can't interact with the app while the overlay
+ * is displayed.
+ */
+#overlay {
+ /* it takes up the whole screen */
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+
+ /* almost transparent gray */
+ background-color: rgba(0, 0, 0, 0.4);
+ z-index: 100;
+}
+
+/*
+ * The overlay content area holds the text of the overlay.
+ * It has borders and a less transparent background so that
+ * the overlay text shows up more clearly
+ */
+#overlay-content {
+ background:
+ url(images/ui/pattern.png) repeat left top,
+ url(images/ui/gradient.png) no-repeat left top;
+ background-size: auto auto, 100% 100%;
+ /* We can't use shortand with background size because is not implemented yet:
+ https://bugzilla.mozilla.org/show_bug.cgi?id=570326; */
+ overflow: hidden;
+ position: absolute;
+ z-index: 100;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ font-family: "MozTT", Sans-serif;
+ font-size: 0;
+ /* Using font-size: 0; we avoid the unwanted visual space (about 3px)
+ created by white-spaces and break lines in the code betewen inline-block elements */
+ color: #fff;
+ padding: 110px 25px 0px 25px;
+}
+
+#overlay-title {
+ font-weight: normal;
+ font-size: 1.9rem;
+ color: #fff;
+ margin: 0 5px -10px 5px;
+}
+
+#overlay-text {
+ padding: 10px 5px 0 5px;
+ border-top: 1px solid #686868;
+ font-weight: 300;
+ font-size: 2.5rem;
+ color: #ebebeb;
+}
+
+.hidden {
+ display: none;
+}
+
+#hud {
+ position: absolute;
+ top: 20px;
+ height: 75px;
+ left: 0;
+ right: 0;
+ z-index: 50;
+}
+
+#hud a {
+ position: absolute;
+ z-index: 50;
+ height: 75px;
+ width: 75px;
+ border: 0;
+ background-position: center center;
+ background-repeat: no-repeat;
+ background-image: url(images/hud_button_underlay.png);
+}
+
+#hud a:after {
+ content: " ";
+ display: block;
+ position: relative;
+ z-index: 60;
+ height: 75px;
+ width: 75px;
+ background: transparent;
+ background-position: center center;
+ background-repeat: no-repeat;
+}
+
+#hud a:active {
+ background-image: url(images/hud_button_underlay_focus.png);
+}
+
+#toggle-camera {
+ right: 20px;
+}
+
+#toggle-flash {
+ left: 20px;
+}
+
+#toggle-camera[data-mode=back]:after {
+ background-image: url(images/toggle_front.png);
+}
+#toggle-camera[data-mode=front]:after {
+ background-image: url(images/toggle_back.png);
+}
+
+#toggle-flash[data-mode=on]:after {
+ background-image: url(images/flash_on.png);
+}
+#toggle-flash[data-mode=off]:after {
+ background-image: url(images/flash_off.png);
+}
+#toggle-flash[data-mode=auto]:after {
+ background-image: url(images/flash_auto.png);
+}
+#toggle-flash[data-mode=torch]:after {
+ background-image: url(images/flash_torch.png);
+}
diff --git a/apps/system/camera/style/filmstrip.css b/apps/system/camera/style/filmstrip.css
new file mode 100644
index 0000000..fd32dc6
--- /dev/null
+++ b/apps/system/camera/style/filmstrip.css
@@ -0,0 +1,187 @@
+#filmstrip {
+ transition: 0.2s ease-in-out;
+ position: absolute;
+ z-index: 100;
+ left: 0;
+ right: 0;
+ height: 50px;
+ /*
+ * the background must be solid for preview mode because otherwise some
+ * of the frozen viewfinder shows through. If it really need to be translucent
+ * in camera mode, we'll have to change it with javascript
+ */
+ background: black;
+}
+
+#filmstrip.hidden {
+ transform: translateY(-50px);
+}
+
+img.thumbnail {
+ position: relative;
+ width: 46px;
+ height: 46px;
+ border: 2px solid white;
+ margin-right: 4px;
+ float: left; /* XXX: do we need this? */
+ -moz-user-select: none;
+ transition: 0.2s ease-in-out;
+}
+
+img.thumbnail.previewed { /* if the thumbnail is being previewed */
+ border: 2px solid #0ac;
+}
+
+/*
+ * Make the thumbnails rotate with the phone
+ */
+#filmstrip[data-orientation="90"] img.thumbnail {
+ transform: rotate(-90deg);
+}
+#filmstrip[data-orientation="180"] img.thumbnail {
+ transform: rotate(-180deg);
+}
+#filmstrip[data-orientation="270"] img.thumbnail {
+ transform: rotate(-270deg);
+}
+
+/* this is where we display image and video previews */
+#preview {
+ position: absolute;
+ left: 0;
+ width: 100%;
+ top: 50px;
+ bottom: 0px;
+ padding: 0;
+ margin: 0;
+ border-width: 0;
+ background: #000; /* opaque */
+ z-index: 100; /* on top of all the camera stuff */
+ transition: transform 0.5s linear;
+ overflow: hidden;
+ transform-origin: 0 0;
+}
+
+#preview.offscreen {
+ transform: translateY(-100%) scale(.125) translateX(50%);
+}
+
+#frame-container {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ bottom: 40px;
+ padding: 0px;
+ margin: 0px;
+ overflow: hidden;
+ -moz-user-select: none;
+}
+
+#media-frame {
+ position: absolute;
+ /* size, position, and rotation are set based on the phone orientation */
+}
+
+#media-frame > img {
+ top: 0px; /* javascript modifies this position with a transform */
+ left: 0px;
+ position: absolute;
+ border-width: 0px;
+ padding: 0px;
+ margin: 0px;
+ transform-origin: 0px 0px;
+ pointer-events: none;
+ -moz-user-select: none;
+}
+
+/*
+ * these styes apply when we're swapping out a preview image to replace
+ * it with a full-resolution image, but the full image isn't ready yet.
+ * This happens when the user starts to zoom in on the image. We need
+ * some sort of simple visual effect to fill ~500ms of dead time so the
+ * user doesn't think the app has frozen up
+ */
+#media-frame > img.swapping {
+ opacity: 0.8;
+ outline: dashed #0ac 4px;
+ outline-offset: -4px;
+}
+
+#media-frame > video {
+ transform-origin: 0px 0px;
+}
+
+#preview-controls {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0px;
+ height: 40px;
+ background-color: rgba(0, 0, 0, 0.8);
+ z-index: 100; /* above the dynamically inserted frame elements */
+}
+
+a.button {
+ display: block;
+ padding: 0;
+ margin: 0;
+ border-width: 0;
+ background-position: center center;
+ background-repeat: no-repeat;
+ transition: 0.2s ease-in-out;
+}
+
+a.button:active, a.button:focus {
+ outline: none;
+}
+
+a.button.hidden {
+ display: none;
+}
+
+#camera-button {
+ position: absolute;
+ left: 0;
+ width: 33%;
+ height: 100%;
+ background-image: url(images/camera.png);
+}
+
+#share-button {
+ position: absolute;
+ left: 33%;
+ width: 33%;
+ height: 100%;
+ background-image: url(images/share.png);
+}
+
+#delete-button {
+ position: absolute;
+ left: 67%;
+ width: 33%;
+ height: 100%;
+ background-image: url(images/delete.png);
+}
+
+#filmstrip-gallery-button {
+ position: absolute;
+ right: 0;
+ top: 0;
+ width: 50px;
+ height: 50px;
+ background-image: url(images/grid.png);
+}
+
+/*
+ * Make the button icons rotate with the phone
+ */
+#preview[data-orientation="90"] a.button {
+ transform: rotate(-90deg);
+}
+#preview[data-orientation="180"] a.button {
+ transform: rotate(-180deg);
+}
+#preview[data-orientation="270"] a.button {
+ transform: rotate(-270deg);
+}
diff --git a/apps/system/camera/style/icons/60/Camera.png b/apps/system/camera/style/icons/60/Camera.png
new file mode 100644
index 0000000..f27f506
--- /dev/null
+++ b/apps/system/camera/style/icons/60/Camera.png
Binary files differ
diff --git a/apps/system/camera/style/icons/Camera.png b/apps/system/camera/style/icons/Camera.png
new file mode 100644
index 0000000..f27f506
--- /dev/null
+++ b/apps/system/camera/style/icons/Camera.png
Binary files differ
diff --git a/apps/system/camera/style/images/actionicon_cancel.png b/apps/system/camera/style/images/actionicon_cancel.png
new file mode 100644
index 0000000..5e73322
--- /dev/null
+++ b/apps/system/camera/style/images/actionicon_cancel.png
Binary files differ
diff --git a/apps/system/camera/style/images/camera.png b/apps/system/camera/style/images/camera.png
new file mode 100644
index 0000000..85a80f5
--- /dev/null
+++ b/apps/system/camera/style/images/camera.png
Binary files differ
diff --git a/apps/system/camera/style/images/delete.png b/apps/system/camera/style/images/delete.png
new file mode 100644
index 0000000..0f2450e
--- /dev/null
+++ b/apps/system/camera/style/images/delete.png
Binary files differ
diff --git a/apps/system/camera/style/images/flash_auto.png b/apps/system/camera/style/images/flash_auto.png
new file mode 100644
index 0000000..3018d8d
--- /dev/null
+++ b/apps/system/camera/style/images/flash_auto.png
Binary files differ
diff --git a/apps/system/camera/style/images/flash_off.png b/apps/system/camera/style/images/flash_off.png
new file mode 100644
index 0000000..0fc7112
--- /dev/null
+++ b/apps/system/camera/style/images/flash_off.png
Binary files differ
diff --git a/apps/system/camera/style/images/flash_on.png b/apps/system/camera/style/images/flash_on.png
new file mode 100644
index 0000000..c7983d1
--- /dev/null
+++ b/apps/system/camera/style/images/flash_on.png
Binary files differ
diff --git a/apps/system/camera/style/images/flash_torch.png b/apps/system/camera/style/images/flash_torch.png
new file mode 100644
index 0000000..3018d8d
--- /dev/null
+++ b/apps/system/camera/style/images/flash_torch.png
Binary files differ
diff --git a/apps/system/camera/style/images/grid.png b/apps/system/camera/style/images/grid.png
new file mode 100644
index 0000000..b53bcf2
--- /dev/null
+++ b/apps/system/camera/style/images/grid.png
Binary files differ
diff --git a/apps/system/camera/style/images/hud_button_underlay.png b/apps/system/camera/style/images/hud_button_underlay.png
new file mode 100644
index 0000000..5853adb
--- /dev/null
+++ b/apps/system/camera/style/images/hud_button_underlay.png
Binary files differ
diff --git a/apps/system/camera/style/images/hud_button_underlay_focus.png b/apps/system/camera/style/images/hud_button_underlay_focus.png
new file mode 100644
index 0000000..c3542bc
--- /dev/null
+++ b/apps/system/camera/style/images/hud_button_underlay_focus.png
Binary files differ
diff --git a/apps/system/camera/style/images/play_overlay.png b/apps/system/camera/style/images/play_overlay.png
new file mode 100644
index 0000000..2a56d04
--- /dev/null
+++ b/apps/system/camera/style/images/play_overlay.png
Binary files differ
diff --git a/apps/system/camera/style/images/share.png b/apps/system/camera/style/images/share.png
new file mode 100644
index 0000000..6a56f19
--- /dev/null
+++ b/apps/system/camera/style/images/share.png
Binary files differ
diff --git a/apps/system/camera/style/images/stop.png b/apps/system/camera/style/images/stop.png
new file mode 100644
index 0000000..b358cc5
--- /dev/null
+++ b/apps/system/camera/style/images/stop.png
Binary files differ
diff --git a/apps/system/camera/style/images/toggle_back.png b/apps/system/camera/style/images/toggle_back.png
new file mode 100644
index 0000000..5e767b4
--- /dev/null
+++ b/apps/system/camera/style/images/toggle_back.png
Binary files differ
diff --git a/apps/system/camera/style/images/toggle_front.png b/apps/system/camera/style/images/toggle_front.png
new file mode 100644
index 0000000..b67507f
--- /dev/null
+++ b/apps/system/camera/style/images/toggle_front.png
Binary files differ
diff --git a/apps/system/camera/style/images/ui/gradient.png b/apps/system/camera/style/images/ui/gradient.png
new file mode 100644
index 0000000..b288545
--- /dev/null
+++ b/apps/system/camera/style/images/ui/gradient.png
Binary files differ
diff --git a/apps/system/camera/style/images/ui/pattern.png b/apps/system/camera/style/images/ui/pattern.png
new file mode 100644
index 0000000..af03f56
--- /dev/null
+++ b/apps/system/camera/style/images/ui/pattern.png
Binary files differ
diff --git a/apps/system/camera/style/images/video.png b/apps/system/camera/style/images/video.png
new file mode 100644
index 0000000..5c41986
--- /dev/null
+++ b/apps/system/camera/style/images/video.png
Binary files differ
diff --git a/apps/system/camera/style/images/video_pause_button.png b/apps/system/camera/style/images/video_pause_button.png
new file mode 100644
index 0000000..b0224f8
--- /dev/null
+++ b/apps/system/camera/style/images/video_pause_button.png
Binary files differ
diff --git a/apps/system/camera/style/images/video_play_button.png b/apps/system/camera/style/images/video_play_button.png
new file mode 100644
index 0000000..56dba6b
--- /dev/null
+++ b/apps/system/camera/style/images/video_play_button.png
Binary files differ
diff --git a/apps/system/camera/style/images/video_play_focus.png b/apps/system/camera/style/images/video_play_focus.png
new file mode 100644
index 0000000..1bb0537
--- /dev/null
+++ b/apps/system/camera/style/images/video_play_focus.png
Binary files differ
diff --git a/apps/system/camera/style/images/video_play_normal.png b/apps/system/camera/style/images/video_play_normal.png
new file mode 100644
index 0000000..0cabf3d
--- /dev/null
+++ b/apps/system/camera/style/images/video_play_normal.png
Binary files differ
diff --git a/apps/system/camera/test/unit/_proxy.html b/apps/system/camera/test/unit/_proxy.html
new file mode 100644
index 0000000..2102451
--- /dev/null
+++ b/apps/system/camera/test/unit/_proxy.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <title>Serve the tests</title>
+
+ <script type="text/javascript" charset="utf-8">
+ (function(window){
+ var Loader = window.CommonResourceLoader = {},
+ host = document.location.host,
+ domain = host.replace(/(^[\w\d-]+\.)?([\w\d]+\.[a-z]+)/, 'test-agent.$2');
+
+ Loader.domain = document.location.protocol + '//' + domain;
+ Loader.url = function(url){
+ return this.domain + url;
+ }
+
+ Loader.script = function(url, doc){
+ doc = doc || document;
+ doc.write('<script type="application/javascript;version=1.8" src="' + this.url(url) + '"><\/script>');
+ return this;
+ };
+ }(this));
+ </script>
+
+ <style type="text/css" media="all">
+ html,body,iframe {
+ height: 100%;
+ width: 100%;
+ }
+ </style>
+
+</head>
+<body>
+
+<!-- Test Agent UI will be loaded in here -->
+<div id="test-agent-ui">
+</div>
+
+<script type="text/javascript" charset="utf-8">
+CommonResourceLoader.
+ script('/common/test/test_url_resolver.js').
+ script('/common/vendor/test-agent/test-agent.js').
+ script('/common/test/agent_proxy.js');
+</script>
+</body>
+</html>
+
+
diff --git a/apps/system/camera/test/unit/_sandbox.html b/apps/system/camera/test/unit/_sandbox.html
new file mode 100644
index 0000000..70d0efa
--- /dev/null
+++ b/apps/system/camera/test/unit/_sandbox.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <title>Tests</title>
+ <link rel="stylesheet" type="text/css" href="/vendor/mocha/mocha.css" />
+ <style type="text/css" media="all">
+ iframe {
+ border: none;
+ padding: 0px;
+ }
+ </style>
+ <script type="text/javascript" charset="utf-8">
+ </script>
+</head>
+
+<body>
+
+<div id="mocha">
+</div>
+
+<div id="test">
+</div>
+
+</body>
+</html>
+
+
diff --git a/apps/system/emergency-call/index.html b/apps/system/emergency-call/index.html
new file mode 100644
index 0000000..534c05e
--- /dev/null
+++ b/apps/system/emergency-call/index.html
@@ -0,0 +1,198 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="pragma" content="no-cache">
+ <title>Emergency Call Dialer</title>
+ <link rel="stylesheet" type="text/css" href="style/dialer.css">
+ <link rel="stylesheet" type="text/css" href="style/keypad.css">
+ <!-- Shared code -->
+ <script defer type="application/javascript" src="/shared/js/settings_listener.js"></script>
+ <script src="/shared/js/l10n.js"></script>
+ <link rel="resource" type="application/l10n" href="/locales/locales.ini">
+
+ <!-- Specific code -->
+ <script defer type="application/javascript" src="js/keypad.js"></script>
+ <script defer type="application/javascript" src="js/dialer.js"></script>
+ </head>
+ <body class="hidden">
+ <article id="views" role="tablist">
+ <section id="keyboard-view" role="tabpanel">
+ <div id="phone-number-view-container">
+ <div class="grid-cell grid-v-align">
+ <div class="grid-wrapper">
+ <input id="phone-number-view" type="text" class="phone-number-font" readonly="readonly">
+ <div id="fake-phone-number-view"></div>
+ </div>
+ </div>
+ <div id="keypad-delete" class="grid-cell grid-v-align" data-value="delete">
+ <div>
+ </div>
+ </div>
+ </div>
+ <article id="keyboard-container">
+ <section id="keypad">
+ <div class="keypad-cell">
+ <div class="keypad-key">
+ <div class="keypad-key-label-container">
+ <div class="keypad-key-label" data-value="1">
+ <span class="keypad-number font-light">1</span>
+ </div>
+ </div>
+ </div>
+ <div class="keypad-key">
+ <div class="keypad-key-label-container">
+ <div class="keypad-key-label" data-value="4">
+ <span class="keypad-number font-light">4</span>
+ <span class="keypad-text font-semibold">GHI</span>
+ </div>
+ </div>
+ </div>
+ <div class="keypad-key">
+ <div class="keypad-key-label-container">
+ <div class="keypad-key-label" data-value="7">
+ <span class="keypad-number font-light">7</span>
+ <span class="keypad-text font-semibold">PQRS</span>
+ </div>
+ </div>
+ </div>
+ <div class="keypad-key">
+ <div class="keypad-key-label-container">
+ <div class="keypad-key-label keypad-key-label-centered" data-value="*">
+ <span>
+ <div class="asterisk"></div>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="keypad-cell">
+ <div class="keypad-key">
+ <div class="keypad-key-label-container">
+ <div class="keypad-key-label" data-value="2">
+ <span class="keypad-number font-light">2</span>
+ <span class="keypad-text font-semibold font-semibold">ABC</span>
+ </div>
+ </div>
+ </div>
+ <div class="keypad-key">
+ <div class="keypad-key-label-container">
+ <div class="keypad-key-label" data-value="5">
+ <span class="keypad-number font-light">5</span>
+ <span class="keypad-text font-semibold">JKL</span>
+ </div>
+ </div>
+ </div>
+ <div class="keypad-key">
+ <div class="keypad-key-label-container">
+ <div class="keypad-key-label" data-value="8">
+ <span class="keypad-number font-light">8</span>
+ <span class="keypad-text font-semibold">TUV</span>
+ </div>
+ </div>
+ </div>
+ <div class="keypad-key">
+ <div class="keypad-key-label-container">
+ <div class="keypad-key-label" data-value="0">
+ <span class="keypad-number font-light">0</span>
+ <span class="keypad-text font-semibold font-size-plus">+</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="keypad-cell">
+ <div class="keypad-key">
+ <div class="keypad-key-label-container">
+ <div class="keypad-key-label" data-value="3">
+ <span class="keypad-number font-light">3</span>
+ <span class="keypad-text font-semibold">DEF</span>
+ </div>
+ </div>
+ </div>
+ <div class="keypad-key">
+ <div class="keypad-key-label-container">
+ <div class="keypad-key-label" data-value="6">
+ <span class="keypad-number font-light">6</span>
+ <span class="keypad-text font-semibold">MNO</span>
+ </div>
+ </div>
+ </div>
+ <div class="keypad-key">
+ <div class="keypad-key-label-container">
+ <div class="keypad-key-label" data-value="9">
+ <span class="keypad-number font-light">9</span>
+ <span class="keypad-text font-semibold">WXYZ</span>
+ </div>
+ </div>
+ </div>
+ <div class="keypad-key">
+ <div class="keypad-key-label-container">
+ <div class="keypad-key-label keypad-key-label-centered" data-type="dial" data-value="#">
+ <span>
+ <div class="sharp"></div>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <section id="keypad-callbar">
+ <span role="button" id="keypad-callbar-cancel" data-type="action" data-value="cancel" >
+ <div>Cancel</div>
+ </span>
+ <span role="button" id="keypad-callbar-call-action" data-type="action" data-value="make-call">
+ <div>
+ </div>
+ </span>
+ </section>
+ </article>
+ </section>
+ </article>
+ <article id="call-screen" class="grid">
+ <article id="calls">
+ <section>
+ <div id="emergency-number" class="number">
+ </div>
+ <div class="fake-number">
+ </div>
+ <div class="additionalContactInfo">
+ <span >Emergency call</span>
+ </div>
+ <div class="duration">
+ <span></span>
+ <div class="direction">
+ <div>
+ </div>
+ </div>
+ </div>
+ </section>
+ </article>
+ <article id="main-container" class="grid-row">
+ <div id="actions-container" class="grid-wrapper grid">
+ <footer id="call-options" class="grid-row">
+ <div class="grid-wrapper grid">
+ <section id="co-advanced" class="grid-row">
+ <div class="grid-wrapper grid">
+ <span class="grid-cell options-column">
+ <button id="speaker" class="co-advanced-option grid-wraper grid">
+ <span>
+ </span>
+ </button>
+ </span>
+ </div>
+ </section>
+ <footer id="callbar">
+ <div id="callbar-hang-up">
+ <span role="button" id="callbar-hang-up-action" class="callbar-button">
+ <div>
+ </div>
+ </span>
+ </div>
+ </footer>
+ </div>
+ </footer>
+ </div>
+ </article>
+ </article>
+ </body>
+</html>
diff --git a/apps/system/emergency-call/js/dialer.js b/apps/system/emergency-call/js/dialer.js
new file mode 100644
index 0000000..ff93009
--- /dev/null
+++ b/apps/system/emergency-call/js/dialer.js
@@ -0,0 +1,80 @@
+'use strict';
+
+var CallHandler = {
+ _call: null,
+ _telephony: window.navigator.mozTelephony,
+
+ call: function ch_call(number) {
+ var sanitizedNumber = number.replace(/-/g, '');
+ var telephony = this._telephony;
+ if (telephony) {
+ this._call = telephony.dialEmergency(sanitizedNumber);
+ var call = this._call;
+ if (call) {
+ var cb = function clearPhoneView() {
+ KeypadManager.updatePhoneNumber('');
+ };
+ call.onconnected = cb;
+
+ call.ondisconnected = function callEnded() {
+ cb();
+ CallScreen.hide();
+ };
+
+ CallScreen.number = call.number;
+ CallScreen.show();
+ }
+ }
+ },
+
+ end: function ch_end() {
+ if (!this._call) {
+ CallScreen.hide();
+ return;
+ }
+
+ this._call.hangUp();
+ this._call = null;
+ },
+
+ toggleSpeaker: function ch_toggleSpeaker() {
+ this._telephony.speakerEnabled = !this._telephony.speakerEnabled;
+ }
+};
+
+var CallScreen = {
+ screen: document.getElementById('call-screen'),
+ numberView: document.getElementById('emergency-number'),
+
+ hangUpButton: document.getElementById('callbar-hang-up'),
+ speakerButton: document.getElementById('speaker'),
+
+ set number(value) {
+ this.numberView.textContent = value;
+ },
+
+ init: function cs_init() {
+ this.hangUpButton.addEventListener('mouseup',
+ CallHandler.end.bind(CallHandler));
+ this.speakerButton.addEventListener('click', this.toggleSpeaker.bind(this));
+ },
+
+ show: function cs_show() {
+ this.screen.classList.add('displayed');
+ },
+
+ hide: function cs_hide() {
+ this.screen.classList.remove('displayed');
+ },
+
+ toggleSpeaker: function cs_toggleSpeaker() {
+ this.speakerButton.classList.toggle('speak');
+ CallHandler.toggleSpeaker();
+ }
+};
+
+window.addEventListener('load', function onload() {
+ window.removeEventListener('load', onload);
+ KeypadManager.init();
+ CallScreen.init();
+});
diff --git a/apps/system/emergency-call/js/keypad.js b/apps/system/emergency-call/js/keypad.js
new file mode 100644
index 0000000..6163ecf
--- /dev/null
+++ b/apps/system/emergency-call/js/keypad.js
@@ -0,0 +1,461 @@
+/*
+ * The code is being shared between system/emergency-call/js/keypad.js
+ * and dialer/js/keypad.js. Be sure to update both file when you commit!
+ *
+ */
+
+'use strict';
+
+var kFontStep = 4;
+var minFontSize = 12;
+
+// Frequencies comming from http://en.wikipedia.org/wiki/Telephone_keypad
+var gTonesFrequencies = {
+ '1': [697, 1209], '2': [697, 1336], '3': [697, 1477],
+ '4': [770, 1209], '5': [770, 1336], '6': [770, 1477],
+ '7': [852, 1209], '8': [852, 1336], '9': [852, 1477],
+ '*': [941, 1209], '0': [941, 1336], '#': [941, 1477]
+};
+
+var keypadSoundIsEnabled = true;
+SettingsListener.observe('phone.ring.keypad', true, function(value) {
+ keypadSoundIsEnabled = !!value;
+});
+
+var TonePlayer = {
+ _sampleRate: 4000,
+
+ init: function tp_init() {
+ document.addEventListener('mozvisibilitychange',
+ this.visibilityChange.bind(this));
+ this.ensureAudio();
+ },
+
+ ensureAudio: function tp_ensureAudio() {
+ if (this._audio)
+ return;
+
+ this._audio = new Audio();
+ this._audio.volume = 0.5;
+ this._audio.mozSetup(2, this._sampleRate);
+ },
+
+ generateFrames: function tp_generateFrames(soundData, freqRow, freqCol) {
+ var currentSoundSample = 0;
+ var kr = 2 * Math.PI * freqRow / this._sampleRate;
+ var kc = 2 * Math.PI * freqCol / this._sampleRate;
+ for (var i = 0; i < soundData.length; i += 2) {
+ var smoother = 0.5 + (Math.sin((i * Math.PI) / soundData.length)) / 2;
+
+ soundData[i] = Math.sin(kr * currentSoundSample) * smoother;
+ soundData[i + 1] = Math.sin(kc * currentSoundSample) * smoother;
+
+ currentSoundSample++;
+ }
+ },
+
+ play: function tp_play(frequencies) {
+ var soundDataSize = this._sampleRate / 4;
+ var soundData = new Float32Array(soundDataSize);
+ this.generateFrames(soundData, frequencies[0], frequencies[1]);
+ this._audio.mozWriteAudio(soundData);
+ },
+
+ // If the app loses focus, close the audio stream. This works around an
+ // issue in Gecko where the Audio Data API causes gfx performance problems,
+ // in particular when scrolling the homescreen.
+ // See: https://bugzilla.mozilla.org/show_bug.cgi?id=779914
+ visibilityChange: function tp_visibilityChange(e) {
+ if (!document.mozHidden) {
+ this.ensureAudio();
+ } else {
+ // Reset the audio stream. This ensures that the stream is shutdown
+ // *immediately*.
+ this._audio.src = '';
+ delete this._audio;
+ }
+ }
+
+};
+
+var KeypadManager = {
+ _phoneNumber: '',
+ _onCall: false,
+
+ get phoneNumberView() {
+ delete this.phoneNumberView;
+ return this.phoneNumberView = document.getElementById('phone-number-view');
+ },
+
+ get fakePhoneNumberView() {
+ delete this.fakePhoneNumberView;
+ return this.fakePhoneNumberView =
+ document.getElementById('fake-phone-number-view');
+ },
+
+ get phoneNumberViewContainer() {
+ delete this.phoneNumberViewContainer;
+ return this.phoneNumberViewContainer =
+ document.getElementById('phone-number-view-container');
+ },
+
+ get keypad() {
+ delete this.keypad;
+ return this.keypad = document.getElementById('keypad');
+ },
+
+ get callBar() {
+ delete this.callBar;
+ return this.callBar =
+ document.getElementById('keypad-callbar');
+ },
+
+ get hideBar() {
+ delete this.hideBar;
+ return this.hideBar = document.getElementById('keypad-hidebar');
+ },
+
+ get callBarAddContact() {
+ delete this.callBarAddContact;
+ return this.callBarAddContact =
+ document.getElementById('keypad-callbar-add-contact');
+ },
+
+ get callBarCallAction() {
+ delete this.callBarCallAction;
+ return this.callBarCallAction =
+ document.getElementById('keypad-callbar-call-action');
+ },
+
+ get callBarCancelAction() {
+ delete this.callBarCancelAction;
+ return this.callBarCancelAction =
+ document.getElementById('keypad-callbar-cancel');
+ },
+
+ get deleteButton() {
+ delete this.deleteButton;
+ return this.deleteButton = document.getElementById('keypad-delete');
+ },
+
+ get hideBarHangUpAction() {
+ delete this.hideBarHangUpAction;
+ return this.hideBarHangUpAction =
+ document.getElementById('keypad-hidebar-hang-up-action-wrapper');
+ },
+
+ get hideBarHideAction() {
+ delete this.hideBarHideAction;
+ return this.hideBarHideAction =
+ document.getElementById('keypad-hidebar-hide-keypad-action');
+ },
+
+ init: function kh_init() {
+ // Update the minimum phone number phone size.
+ // The UX team states that the minimum font size should be
+ // 10pt. First off, we convert it to px multiplying it 0.226 times,
+ // then we convert it to rem multiplying it a number of times equal
+ // to the font-size property of the body element.
+ var defaultFontSize = window.getComputedStyle(document.body, null)
+ .getPropertyValue('font-size');
+ minFontSize = parseInt(parseInt(defaultFontSize) * 10 * 0.226);
+
+ this.phoneNumberView.value = '';
+ this._phoneNumber = '';
+
+ var keyHandler = this.keyHandler.bind(this);
+ this.keypad.addEventListener('mousedown', keyHandler, true);
+ this.keypad.addEventListener('mouseup', keyHandler, true);
+ this.deleteButton.addEventListener('mousedown', keyHandler);
+ this.deleteButton.addEventListener('mouseup', keyHandler);
+
+ // The keypad add contact bar is only included in the normal version of
+ // the keypad.
+ if (this.callBarAddContact) {
+ this.callBarAddContact.addEventListener('mouseup',
+ this.addContact.bind(this));
+ }
+
+ // The keypad add contact bar is only included in the normal version and
+ // the emergency call version of the keypad.
+ if (this.callBarCallAction) {
+ this.callBarCallAction.addEventListener('mouseup',
+ this.makeCall.bind(this));
+ }
+
+ // The keypad cancel bar is only the emergency call version of the keypad.
+ if (this.callBarCancelAction) {
+ this.callBarCancelAction.addEventListener('mouseup', function() {
+ window.parent.LockScreen.switchPanel();
+ });
+ }
+
+ // The keypad hide bar is only included in the on call version of the
+ // keypad.
+ if (this.hideBarHideAction) {
+ this.hideBarHideAction.addEventListener('mouseup',
+ this.callbarBackAction);
+ }
+
+ if (this.hideBarHangUpAction) {
+ this.hideBarHangUpAction.addEventListener('mouseup',
+ this.hangUpCallFromKeypad);
+ }
+
+ TonePlayer.init();
+
+ this.render();
+ },
+
+ moveCaretToEnd: function hk_util_moveCaretToEnd(el) {
+ if (typeof el.selectionStart == 'number') {
+ el.selectionStart = el.selectionEnd = el.value.length;
+ } else if (typeof el.createTextRange != 'undefined') {
+ el.focus();
+ var range = el.createTextRange();
+ range.collapse(false);
+ range.select();
+ }
+ },
+
+ render: function hk_render(layoutType) {
+ if (layoutType == 'oncall') {
+ this._onCall = true;
+ var numberNode = CallScreen.activeCall.querySelector('.number');
+ this._phoneNumber = numberNode.textContent;
+ this.phoneNumberViewContainer.classList.add('keypad-visible');
+ if (this.callBar) {
+ this.callBar.classList.add('hide');
+ }
+
+ if (this.hideBar) {
+ this.hideBar.classList.remove('hide');
+ }
+
+ this.deleteButton.classList.add('hide');
+ } else {
+ this.phoneNumberViewContainer.classList.remove('keypad-visible');
+ if (this.callBar) {
+ this.callBar.classList.remove('hide');
+ }
+
+ if (this.hideBar) {
+ this.hideBar.classList.add('hide');
+ }
+
+ this.deleteButton.classList.remove('hide');
+ }
+ },
+
+ makeCall: function hk_makeCall(event) {
+ event.stopPropagation();
+
+ if (this._phoneNumber != '') {
+ CallHandler.call(KeypadManager._phoneNumber);
+ }
+ },
+
+ addContact: function hk_addContact(event) {
+ var number = this._phoneNumber;
+ if (!number)
+ return;
+
+ try {
+ var activity = new MozActivity({
+ name: 'new',
+ data: {
+ type: 'webcontacts/contact',
+ params: {
+ 'tel': number
+ }
+ }
+ });
+ } catch (e) {
+ console.log('WebActivities unavailable? : ' + e);
+ }
+ },
+
+ callbarBackAction: function hk_callbarBackAction(event) {
+ CallScreen.hideKeypad();
+ },
+
+ hangUpCallFromKeypad: function hk_hangUpCallFromKeypad(event) {
+ CallScreen.views.classList.remove('show');
+ OnCallHandler.end();
+ },
+
+ formatPhoneNumber: function kh_formatPhoneNumber(mode) {
+ switch (mode) {
+ case 'dialpad':
+ var fakeView = this.fakePhoneNumberView;
+ var view = this.phoneNumberView;
+
+ // We consider the case where the delete button may have
+ // been used to delete the whole phone number.
+ if (view.value == '') {
+ view.style.fontSize = view.dataset.size;
+ return;
+ }
+ break;
+
+ case 'on-call':
+ var fakeView = CallScreen.activeCall.querySelector('.fake-number');
+ var view = CallScreen.activeCall.querySelector('.number');
+ break;
+ }
+
+ var computedStyle = window.getComputedStyle(view, null);
+ var currentFontSize = computedStyle.getPropertyValue('font-size');
+ if (!('size' in view.dataset)) {
+ view.dataset.size = currentFontSize;
+ }
+
+ var newFontSize = this.getNextFontSize(view, fakeView,
+ parseInt(view.dataset.size),
+ parseInt(currentFontSize));
+ view.style.fontSize = newFontSize + 'px';
+ this.addEllipsis(view, fakeView, newFontSize);
+ },
+
+ addEllipsis: function kh_addEllipsis(view, fakeView, currentFontSize) {
+ var viewWidth = view.getBoundingClientRect().width;
+ fakeView.style.fontSize = currentFontSize + 'px';
+ fakeView.innerHTML = view.value;
+
+ var counter = 1;
+ var value = view.value;
+
+ var newPhoneNumber;
+ while (fakeView.getBoundingClientRect().width > viewWidth) {
+ newPhoneNumber = '\u2026' + value.substr(-value.length + counter);
+ fakeView.innerHTML = newPhoneNumber;
+ counter++;
+ }
+
+ if (newPhoneNumber) {
+ view.value = newPhoneNumber;
+ }
+ },
+
+ getNextFontSize: function kh_getNextFontSize(view, fakeView,
+ fontSize, initialFontSize) {
+ var viewWidth = view.getBoundingClientRect().width;
+ fakeView.style.fontSize = fontSize + 'px';
+ fakeView.innerHTML = view.value;
+
+ var rect = fakeView.getBoundingClientRect();
+ while ((rect.width > viewWidth) && (fontSize > minFontSize)) {
+ fontSize = Math.max(fontSize - kFontStep, minFontSize);
+ fakeView.style.fontSize = fontSize + 'px';
+ rect = fakeView.getBoundingClientRect();
+ }
+
+ if ((rect.width < viewWidth) && (fontSize < initialFontSize)) {
+ fakeView.style.fontSize = (fontSize + kFontStep) + 'px';
+ rect = fakeView.getBoundingClientRect();
+ if (rect.width <= viewWidth) {
+ fontSize += kFontStep;
+ }
+ }
+ return fontSize;
+ },
+
+ keyHandler: function kh_keyHandler(event) {
+ var key = event.target.dataset.value;
+
+ if (!key)
+ return;
+
+ event.stopPropagation();
+ if (event.type == 'mousedown') {
+ this._longPress = false;
+
+ if (key != 'delete') {
+ if (keypadSoundIsEnabled) {
+ TonePlayer.play(gTonesFrequencies[key]);
+ }
+
+ // Sending the DTMF tone if on a call
+ var telephony = navigator.mozTelephony;
+ if (telephony && telephony.active &&
+ telephony.active.state == 'connected') {
+
+ telephony.startTone(key);
+ window.setTimeout(function ch_stopTone() {
+ telephony.stopTone();
+ }, 100);
+
+ }
+ }
+
+ // Manage long press
+ if (key == '0' || key == 'delete') {
+ this._holdTimer = setTimeout(function(self) {
+ if (key == 'delete') {
+ self._phoneNumber = '';
+ } else {
+ self._phoneNumber += '+';
+ }
+
+ self._longPress = true;
+ self._updatePhoneNumberView();
+ }, 400, this);
+ }
+
+ // Voicemail long press (needs to be longer since it actually dials)
+ if (event.target.dataset.voicemail) {
+ this._holdTimer = setTimeout(function vm_call(self) {
+ self._longPress = true;
+ self._callVoicemail();
+ }, 3000, this);
+ }
+ } else if (event.type == 'mouseup') {
+ // If it was a long press our work is already done
+ if (this._longPress) {
+ this._longPress = false;
+ this._holdTimer = null;
+ return;
+ }
+ if (key == 'delete') {
+ this._phoneNumber = this._phoneNumber.slice(0, -1);
+ } else {
+ this._phoneNumber += key;
+ }
+
+ if (this._holdTimer)
+ clearTimeout(this._holdTimer);
+
+ this._updatePhoneNumberView();
+ }
+ },
+
+ updatePhoneNumber: function kh_updatePhoneNumber(number) {
+ this._phoneNumber = number;
+ this._updatePhoneNumberView();
+ },
+
+ _updatePhoneNumberView: function kh_updatePhoneNumberview() {
+ var phoneNumber = this._phoneNumber;
+
+ // If there are digits in the phone number, show the delete button.
+ var visibility = (phoneNumber.length > 0) ? 'visible' : 'hidden';
+ this.deleteButton.style.visibility = visibility;
+
+ if (this._onCall) {
+ var view = CallScreen.activeCall.querySelector('.number');
+ view.textContent = phoneNumber;
+ this.formatPhoneNumber('on-call');
+ } else {
+ this.phoneNumberView.value = phoneNumber;
+ this.moveCaretToEnd(this.phoneNumberView);
+ this.formatPhoneNumber('dialpad');
+ }
+ },
+
+ _callVoicemail: function kh_callVoicemail() {
+ var voicemail = navigator.mozVoicemail;
+ if (voicemail && voicemail.number) {
+ CallHandler.call(voicemail.number);
+ }
+ }
+};
diff --git a/apps/system/emergency-call/style/dialer.css b/apps/system/emergency-call/style/dialer.css
new file mode 100644
index 0000000..7a3519a
--- /dev/null
+++ b/apps/system/emergency-call/style/dialer.css
@@ -0,0 +1,250 @@
+html, body {
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ margin: 0;
+ color: white;
+ font-family: 'MozTT', sans-serif;
+ font-size: 10px;
+ background: black;
+}
+
+a {
+ outline: 0;
+}
+
+a:hover, a:active, a:focus {
+ outline: 0;
+}
+
+html * {
+ overflow: hidden;
+}
+
+.font-regular {
+ font-family: 'MozTT';
+}
+
+.font-semibold {
+ font-family: 'MozTT';
+ font-weight: 600;
+}
+
+.font-light {
+ font-family: 'MozTT';
+}
+
+.contact-primary-info {
+ font-size: -moz-calc(15*0.226rem);
+ color: black;
+}
+
+.contact-secondary-info {
+ font-size: -moz-calc(6*0.226rem);
+ color: white;
+}
+
+.grid-wrapper {
+ width: 100%;
+ height: 100%;
+}
+
+.grid-v-align {
+ vertical-align: middle;
+}
+
+.grid-row {
+ display: table-row;
+}
+
+.grid-cell {
+ display: table-cell;
+}
+
+.grid {
+ display: table;
+ table-layout: fixed;
+}
+
+.center {
+ text-align: center;
+}
+
+#views {
+ top: 0;
+ height: 100%;
+ width: 100%;
+}
+
+#views > *[role=tabpanel] {
+ height: 100%;
+ width: 100%;
+}
+
+body.hidden *[data-l10n-id] {
+ visibility: hidden;
+}
+
+/* Call screen */
+#call-screen {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ border: 0;
+ border-radius: 10px;
+ background: black;
+ -moz-transform: translateY(-100%);
+ z-index: 100;
+
+ -moz-transition: -moz-transform 0.5s ease;
+}
+
+#call-screen.displayed {
+ -moz-transform: translateY(0);
+}
+
+#main-container {
+ position: relative;
+ height: 100%;
+ background: transparent;
+}
+
+#actions-container {
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ height: 15.5rem;
+}
+
+#call-options {
+ height:9.5rem;
+}
+
+#co-advanced {
+ opacity: 1.0;
+ height: 9.5rem;
+}
+
+.co-advanced-option {
+ background: rgba(0,0,0,.8);
+ height: 9.5rem;
+ width: 100%;
+ border: 0;
+ border-top: 1px solid #3A3A3A;
+ border-bottom: 1px solid #3A3A3A;
+ border-right: 1px solid #3A3A3A;
+}
+
+#co-advanced span.grid-cell:last-child .co-advanced-option {
+ border-right: 0px;
+}
+
+#speaker span {
+ display:inline-block;
+ background-color: #DDD;
+ background:url('images/ActionIcon_40x40_bluetooth.png') ;
+ background-size: 4rem 4rem;
+ width:4rem;
+ height:4rem;
+ opacity: 1.0;
+}
+
+#speaker.speak > span {
+ background:url('images/ActionIcon_40x40_bluetooth_active.png');
+}
+
+#callbar {
+ background:rgba(0,0,0,.8);
+ opacity: 1.0;
+}
+
+#callbar-hang-up {
+ float: left;
+ height: 6.5rem;
+ width: 100%;
+}
+
+.callbar-button {
+ height: 4rem;
+ border:0;
+ border-radius:.3rem;
+ display: block;
+}
+
+#callbar-hang-up-action {
+ background: -moz-linear-gradient(top, #ff0000 1%, #ce0000 100%);
+ opacity: 1.0;
+ margin: 1rem .5rem 1.5rem 1.5rem;
+}
+
+#callbar-hang-up.full-space > #callbar-hang-up-action {
+ margin: 1rem 1.5rem 1.5rem 1.5rem;
+}
+
+#callbar-hang-up-action > div {
+ margin: 0 auto;
+ background-image: url('images/ActionIcon_40x40_hangup.png');
+ background-repeat: no-repeat;
+ background-size: 4rem 4rem;
+ background-position: center;
+ width: 4rem;
+ height: 4rem;
+}
+
+#calls {
+ position: absolute;
+ top: 0;
+ width: 100%;
+ height: 22rem;
+
+ z-index: 500;
+}
+
+#calls > section {
+ position: relative;
+ height: 9rem;
+
+ font-size: 1.8em;
+ line-height: 7rem;
+ background-color: #01c5ed;
+
+ transition: opacity 0.3s linear;
+ opacity: 1;
+}
+
+#calls > section div {
+ padding-left: 2rem;
+ padding-right: 1.5rem;
+}
+
+#calls > section .number {
+ height: 4rem;
+ padding: 2rem 2rem 0 2rem;
+ background: #01c5ed;
+ font-size: 1.6em;
+ line-height: 4rem;
+ color: black;
+}
+
+#calls > section .additionalContactInfo {
+ height: 2rem;
+ padding: 0 2rem 2rem 2rem;
+ background: #01c5ed;
+ font-size: 1.4rem;
+ line-height: 2rem;
+ color: white;
+}
+
+#calls > section .duration {
+ position: absolute;
+ top: 12rem;
+ left: 0;
+ height: 8rem;
+ padding: 2rem;
+ font-size: 2.6em;
+ font-weight: 300;
+ line-height: 8rem;
+}
diff --git a/apps/system/emergency-call/style/images/ActionIcon_40x40_bluetooth.png b/apps/system/emergency-call/style/images/ActionIcon_40x40_bluetooth.png
new file mode 100644
index 0000000..a946ff2
--- /dev/null
+++ b/apps/system/emergency-call/style/images/ActionIcon_40x40_bluetooth.png
Binary files differ
diff --git a/apps/system/emergency-call/style/images/ActionIcon_40x40_bluetooth_active.png b/apps/system/emergency-call/style/images/ActionIcon_40x40_bluetooth_active.png
new file mode 100644
index 0000000..86abf23
--- /dev/null
+++ b/apps/system/emergency-call/style/images/ActionIcon_40x40_bluetooth_active.png
Binary files differ
diff --git a/apps/system/emergency-call/style/images/ActionIcon_40x40_hangup.png b/apps/system/emergency-call/style/images/ActionIcon_40x40_hangup.png
new file mode 100644
index 0000000..f955956
--- /dev/null
+++ b/apps/system/emergency-call/style/images/ActionIcon_40x40_hangup.png
Binary files differ
diff --git a/apps/system/emergency-call/style/images/ActionIcon_40x40_pickup.png b/apps/system/emergency-call/style/images/ActionIcon_40x40_pickup.png
new file mode 100644
index 0000000..ad8423e
--- /dev/null
+++ b/apps/system/emergency-call/style/images/ActionIcon_40x40_pickup.png
Binary files differ
diff --git a/apps/system/emergency-call/style/images/asterisk.png b/apps/system/emergency-call/style/images/asterisk.png
new file mode 100644
index 0000000..4199485
--- /dev/null
+++ b/apps/system/emergency-call/style/images/asterisk.png
Binary files differ
diff --git a/apps/system/emergency-call/style/images/dialer_icon_delete.png b/apps/system/emergency-call/style/images/dialer_icon_delete.png
new file mode 100644
index 0000000..8e842cd
--- /dev/null
+++ b/apps/system/emergency-call/style/images/dialer_icon_delete.png
Binary files differ
diff --git a/apps/system/emergency-call/style/images/sharp.png b/apps/system/emergency-call/style/images/sharp.png
new file mode 100644
index 0000000..36033bf
--- /dev/null
+++ b/apps/system/emergency-call/style/images/sharp.png
Binary files differ
diff --git a/apps/system/emergency-call/style/keypad.css b/apps/system/emergency-call/style/keypad.css
new file mode 100644
index 0000000..ea9fb90
--- /dev/null
+++ b/apps/system/emergency-call/style/keypad.css
@@ -0,0 +1,301 @@
+/*
+ * The code is being shared between system/emergency-call/js/keypad.js
+ * and dialer/js/keypad.js. Be sure to update both file when you commit!
+ *
+ */
+
+.keypad-text {
+ font-size: -moz-calc(6*0.226rem);
+ color: #96AAB4;
+}
+
+#keyboard-view {
+ width: 100%;
+ height: 100%;
+}
+
+#fake-phone-number-view {
+ position: absolute;
+ line-height: 0;
+ visibility: hidden;
+}
+
+#phone-number-view-container {
+ width: 100%;
+ height: -moz-calc(100% - 34.5rem);
+ background: #242B36;
+ text-align: center;
+ display: table;
+ table-layout: fixed;
+ border-spacing: 1.5rem 0rem;
+ font-family: 'MozTT';
+ font-weight: 300;
+}
+
+#phone-number-view-container.keypad-visible {
+ height: -moz-calc(100% - 35rem);
+ visibility: hidden;
+}
+
+#phone-number-view {
+ width: 100%;
+ border: 0;
+ background: transparent;
+ text-align: left;
+ cursor: none;
+ -moz-user-select: none;
+}
+
+#keypad-delete {
+ text-align: center;
+ width: 4rem;
+ visibility: hidden;
+}
+
+#keypad-delete:active {
+ opacity: 0.7;
+}
+
+#keypad-delete > div {
+ width: 3.8rem;
+ height: 2.8rem;
+ background: url('images/dialer_icon_delete.png') ;
+ background-size: 3.8rem 2.8rem;
+ background-repeat: no-repeat;
+}
+
+#keyboard-container {
+ width: 100%;
+ height: 34.5rem;
+}
+
+#keypad {
+ background: -moz-linear-gradient(top, #191E25 0%, #080B0D 100%);
+ height: -moz-calc(100% - 10rem);
+ width: 100%;
+ display: table;
+ table-layout: fixed;
+}
+
+.keypad-cell {
+ display: table-cell;
+ border-right: 1px solid #6F6F6F;
+ border-top: 1px solid #6F6F6F;;
+}
+
+.keypad-cell:last-child {
+ border-right: 0;
+}
+
+.keypad-key {
+ border: 0;
+ border-bottom: 1px solid #6F6F6F;
+ height: 7rem;
+ vertical-align: middle;
+ position: relative;
+}
+
+.keypad-key:active {
+ background: black;
+ opacity: 0.7;
+}
+
+.keypad-key-label-container {
+ width: 100%;
+ height: 100%;
+ display: table;
+}
+
+.keypad-key-label {
+ display: table-cell;
+ padding-left: 1.5rem;
+ vertical-align: middle;
+}
+
+.keypad-key-label *,
+#keypad-delete * {
+ pointer-events: none;
+}
+
+.keypad-key-label-centered {
+ padding-left: 0;
+ text-align: center;
+}
+
+.font-size-plus {
+ font-size: 2rem;
+}
+
+.keypad-subicon {
+ background-repeat: no-repeat;
+ background-position: center bottom;
+
+ -moz-user-select: none;
+ pointer-events: none;
+
+ position: absolute;
+ left: 3.75rem;
+ bottom: 0.75rem;
+
+ width: 3rem;
+ height: 3rem;
+}
+
+.voicemail {
+ background-image: url('images/voicemail.png');
+}
+
+.asterisk {
+ background-image: url('images/asterisk.png');
+ background-repeat: no-repeat;
+ height: 3rem;
+ width: 3rem;
+ background-size: 3rem 3rem;
+ background-position: center;
+ margin: auto;
+ pointer-events: none;
+}
+
+.sharp {
+ background-image: url('images/sharp.png');
+ background-repeat: no-repeat;
+ height: 3rem;
+ width: 3rem;
+ background-size: 3rem 3rem;
+ background-position: center;
+ margin: auto;
+ pointer-events: none;
+}
+
+#keypad-callbar {
+ background: black;
+ height: 6.5rem;
+ width: 100%;
+ display: table;
+ table-layout: fixed;
+ border-spacing: 1rem 1rem;
+}
+
+#keypad-callbar-add-contact,
+#keypad-callbar-cancel {
+ display: table-cell;
+ width: 9rem;
+ background: -moz-linear-gradient(top, #242b36 0%, #19191a 100%);
+ border: .1rem solid #242B36;
+ border-radius: .2rem;
+ vertical-align: middle;
+}
+
+#keypad-callbar-cancel {
+ width: 10rem;
+}
+
+#keypad-callbar-cancel:active {
+ opacity: 0.7;
+}
+
+#keypad-callbar-cancel > div {
+ text-align: center;
+ font: 2rem 'MozTT';
+}
+
+#keypad-callbar-call-action {
+ display: table-cell;
+ width: 100%;
+ background: #84c82c;
+ border: 0;
+ border-radius: .2rem;
+ background: -moz-linear-gradient(top, #84c82c 0%, #5f9b0a 100%);
+ vertical-align: middle;
+}
+
+#keypad-callbar-call-action:active {
+ opacity: 0.7;
+}
+
+.icon-add-contact {
+ margin: auto;
+ width: 4rem;
+ height: 4rem;
+ background-image: url("images/ActionIcon_40x40_addcontact.png");
+ background-repeat: no-repeat;
+ background-size: 4rem 4rem;
+ background-position: center;
+}
+
+#keypad-callbar-call-action > div {
+ margin: auto;
+ width: 4rem;
+ height: 4rem;
+ background-image: url("images/ActionIcon_40x40_pickup.png");
+ background-repeat: no-repeat;
+ background-size: 4rem 4rem;
+ background-position: center;
+}
+
+#keypad-hidebar {
+ background: rgba(0,0,0,.8);
+ opacity: 1.0;
+}
+
+#keypad-hidebar-hang-up-action-wrapper {
+ float: left;
+ height: 6.5rem;
+ width: 50%;
+}
+
+#keypad-hidebar-hide-keypad-action-wrapper {
+ height: 6.5rem;
+ width: 50%;
+}
+
+.keypad-hidebar-button {
+ height: 4rem;
+ border: 0;
+ border-radius:.3rem;
+ display: block;
+}
+
+#keypad-hidebar-hang-up-action {
+ background: -moz-linear-gradient(top, #ff0000 0%, #ce0000 100%);
+ opacity: 1.0;
+ margin: 1rem .5rem 1.5rem 1.5rem;
+}
+
+#keypad-hidebar-hang-up-action > div {
+ margin: 0 auto;
+ background-image: url('images/ActionIcon_40x40_hangup.png');
+ background-repeat: no-repeat;
+ background-size: 4rem 4rem;
+ background-position: center;
+ width: 4rem;
+ height: 4rem;
+}
+
+#keypad-hidebar-hide-keypad-action {
+ background: -moz-linear-gradient(top, #6A6A6A 0%, #3E3E3E 100%);
+ opacity: 1.0;
+ margin: 1rem 1.5rem 1.5rem .5rem;
+}
+
+#keypad-hidebar-hide-keypad-action > div {
+ margin: -moz-calc((4rem - 3rem)/2) auto;
+ background-image: url('images/ActionIcons_30x30_dismiss_keyboard.png');
+ background-repeat: no-repeat;
+ background-size: 3rem 3rem;
+ background-position: center;
+ width: 3rem;
+ height: 3rem;
+}
+
+.phone-number-font {
+ font-size:-moz-calc(18*0.226rem);
+ color: white;
+ font-family:'MozTT';
+}
+
+.keypad-number {
+ font-size: -moz-calc(25*0.226rem);
+ color: white;
+}
+
diff --git a/apps/system/index.html b/apps/system/index.html
new file mode 100644
index 0000000..731a3f3
--- /dev/null
+++ b/apps/system/index.html
@@ -0,0 +1,896 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="pragma" content="no-cache">
+
+ <!-- Shared code -->
+ <script src="shared/js/l10n.js"></script>
+ <script src="shared/js/l10n_date.js"></script>
+ <script src="shared/js/idletimer.js"></script>
+ <script defer src="shared/js/gesture_detector.js"></script>
+ <script defer src="shared/js/settings_listener.js"></script>
+ <script defer src="shared/js/custom_dialog.js"></script>
+ <script defer src="shared/js/notification_helper.js"></script>
+ <script defer src="shared/js/async_storage.js"></script>
+ <script defer src="shared/js/mobile_operator.js"></script>
+ <script defer src="shared/js/manifest_helper.js"></script>
+
+ <!-- Wrapper -->
+ <link rel="stylesheet" type="text/css" href="style/wrapper/wrapper.css">
+ <script type="text/javascript" defer src="js/wrapper.js"></script>
+
+ <!-- System -->
+ <link rel="stylesheet" type="text/css" href="style/system/system.css">
+ <link rel="stylesheet" type="text/css" href="style/system/keyboard.css">
+ <link rel="stylesheet" type="text/css" href="style/sound_manager/sound_manager.css">
+
+ <!-- Include shared building blocks -->
+ <link rel="stylesheet" type="text/css" href="shared/style/confirm.css"/>
+ <link rel="stylesheet" type="text/css" href="shared/style/headers.css"/>
+ <link rel="stylesheet" type="text/css" href="shared/style/switches.css"/>
+
+ <link rel="stylesheet" type="text/css" href="style/fake-notification.css"/>
+
+ <!-- Include shared resources
+ <link rel="resource" type="application/json" href="shared/resources/apn.json"/>
+ <link rel="resource" type="image/png" href="shared/resources/branding/initlogo.png"/>
+ <link rel="resource" type="image/png" href="shared/resources/branding/splash_screen_generic.png"/>
+ -->
+
+ <!-- applications.js must not be deferred
+ in order to catch webapps-registry-ready mozChromeEvent -->
+ <script src="js/applications.js"></script>
+ <script defer src="js/screen_manager.js"></script>
+ <script defer src="js/background_service.js"></script>
+ <script defer src="js/mouse2touch.js"></script>
+ <script defer src="js/activities.js"></script>
+ <script defer src="js/bootstrap.js"></script>
+ <script defer src="js/screenshot.js"></script>
+ <script defer src="js/sound_manager.js"></script>
+ <script defer src="js/source_view.js"></script>
+ <script defer src="js/storage.js"></script>
+ <script defer src="js/hardware_buttons.js"></script>
+ <script defer src="js/system_banner.js"></script>
+ <script defer src="js/system_dialog.js"></script>
+ <script defer src="js/icc_cache.js"></script>
+
+ <!-- Grid View -->
+ <link rel="stylesheet" type="text/css" href="style/gridview/gridview.css">
+ <script defer src="js/gridview.js"></script>
+
+ <!-- TTL View -->
+ <link rel="stylesheet" type="text/css" href="style/ttlview/ttlview.css">
+ <script defer src="js/ttlview.js"></script>
+
+ <!-- Accessibility -->
+ <link rel="stylesheet" type="text/css" href="style/accessibility/accessibility.css">
+ <script defer src="js/accessibility.js"></script>
+
+ <!-- Sleep Menu -->
+ <link rel="stylesheet" type="text/css" href="style/sleep_menu/sleep_menu.css">
+ <script defer src="js/sleep_menu.js"></script>
+
+ <!-- Battery Manager -->
+ <link rel="stylesheet" type="text/css" href="style/battery_manager/battery_manager.css">
+ <script defer src="js/battery_manager.js"></script>
+
+ <!-- Keyboard -->
+ <script defer src="js/keyboard_manager.js"></script>
+
+ <!-- Utility Tray -->
+ <link rel="stylesheet" type="text/css" href="style/utility_tray/utility_tray.css">
+ <script defer src="js/utility_tray.js"></script>
+
+ <!-- Attention Screen -->
+ <link rel="stylesheet" type="text/css" href="style/attention_screen.css">
+ <script defer src="js/attention_screen.js"></script>
+
+ <!-- List Menu -->
+ <link rel="stylesheet" type="text/css" href="style/list_menu/list_menu.css">
+ <script defer src="js/list_menu.js"></script>
+
+ <!-- Context Menu -->
+ <script defer src="js/context_menu.js"></script>
+
+ <!-- Statusbar -->
+ <link rel="stylesheet" type="text/css" href="style/statusbar/statusbar.css">
+ <script defer src="js/statusbar.js"></script>
+
+ <!-- LockScreen -->
+ <link rel="stylesheet" type="text/css" href="style/lockscreen/lockscreen.css">
+ <script defer src="js/lockscreen.js"></script>
+
+ <!-- PIN Unlocking -->
+ <script defer src="js/sim_lock.js"></script>
+ <link rel="stylesheet" type="text/css" href="style/simcard.css">
+ <script defer src="js/simcard_dialog.js"></script>
+
+ <!-- Airplane Mode -->
+ <script defer src="js/airplane_mode.js"></script>
+
+ <!-- Modal Dialog -->
+ <link rel="stylesheet" type="text/css" href="style/modal_dialog/prompt.css">
+ <link rel="stylesheet" type="text/css" href="style/modal_dialog/modal_dialog.css">
+ <script defer src="js/modal_dialog.js"></script>
+
+ <!-- Authentication Dialog-->
+ <script defer src="js/authentication_dialog.js"></script>
+
+ <!-- Value selector -->
+ <link rel="stylesheet" type="text/css" href="style/value_selector/value_selector.css">
+ <link rel="stylesheet" type="text/css" href="style/value_selector/time_picker.css">
+ <link rel="stylesheet" type="text/css" href="style/value_selector/spin_date_picker.css">
+ <script defer src="js/value_selector/value_picker.js"></script>
+ <script defer src="js/value_selector/spin_date_picker.js"></script>
+ <script defer src="js/value_selector/value_selector.js"></script>
+ <script defer src="js/value_selector/input_parser.js"></script>
+
+ <!-- Popup Manager -->
+ <link rel="stylesheet" type="text/css" href="style/popup_manager/popup_manager.css">
+ <script defer src="js/popup_manager.js"></script>
+
+ <!-- Trusted UI -->
+ <link rel="stylesheet" type="text/css" href="style/trusted_ui/trusted_ui.css">
+ <script defer src="js/trusted_ui.js"></script>
+
+ <!-- Permission Manager -->
+ <link rel="stylesheet" type="text/css" href="style/permission_manager/permission_manager.css">
+ <script defer src="js/permission_manager.js"></script>
+
+ <!-- App Install -->
+ <link rel="stylesheet" type="text/css" href="style/app_install_manager/app_install_manager.css">
+ <script defer src="js/app_install_manager.js"></script>
+
+ <!-- Cost Control -->
+ <link rel="stylesheet" type="text/css" href="style/cost_control/cost_control.css">
+ <script defer src="js/cost_control.js"></script>
+
+ <!-- Quick Settings -->
+ <link rel="stylesheet" type="text/css" href="style/quick_settings/quick_settings.css">
+ <script defer src="js/quick_settings.js"></script>
+
+ <!-- Notifications -->
+ <link rel="stylesheet" type="text/css" href="style/notifications/notifications.css">
+ <script defer src="js/notifications.js"></script>
+
+ <!-- Cards View -->
+ <link rel="stylesheet" type="text/css" href="style/cards_view/cards_view.css">
+ <script defer src="js/cards_view.js"></script>
+
+ <!-- Bluetooth -->
+ <script defer src="js/bluetooth.js"></script>
+ <script defer src="js/bluetooth_transfer.js"></script>
+ <link rel="stylesheet" type="text/css" href="style/bluetooth_transfer/bluetooth_transfer.css">
+
+ <!-- Wifi -->
+ <script defer src="js/wifi.js"></script>
+
+ <!-- Voicemail -->
+ <script defer src="js/voicemail.js"></script>
+
+ <!-- Update Manager -->
+ <script defer src="js/updatable.js"></script>
+ <script defer src="js/update_manager.js"></script>
+ <link rel="stylesheet" type="text/css" href="style/update_manager/update_manager.css">
+
+ <!-- Theme and localization -->
+ <link rel="stylesheet" type="text/css" href="style/themes/default/system.css">
+ <link rel="stylesheet" type="text/css" href="style/themes/default/core.css">
+ <link rel="stylesheet" type="text/css" href="style/themes/default/menus.css">
+ <link rel="stylesheet" type="text/css" href="style/themes/default/buttons.css">
+ <link rel="stylesheet" type="text/css" href="style/themes/default/banner.css">
+ <link rel="resource" type="application/l10n" href="shared/locales/branding.ini">
+ <link rel="resource" type="application/l10n" href="locales/locales.ini">
+ <link rel="resource" type="application/l10n" href="shared/locales/date.ini">
+ <link rel="resource" type="application/l10n" href="shared/locales/permissions.ini">
+
+ <!-- include building blocks -->
+ <link rel="stylesheet" type="text/css" href="style/bb/value_selector.css">
+
+ <!-- Payment -->
+ <script defer src="js/payment.js"></script>
+
+ <!-- Identity -->
+ <script defer src="js/identity.js"></script>
+
+ <!-- Operator Variant Mechanism -->
+ <script defer src="js/operator_variant/operator_variant.js"></script>
+
+ <!-- Crash Reporter -->
+ <link rel="stylesheet" type="text/css" href="style/crash_reporter/crash_reporter.css">
+ <script defer src="js/crash_reporter.js"></script>
+
+ <!-- Captive Portal Login -->
+ <script defer src="js/captive_portal.js"></script>
+
+ <!-- Windows -->
+ <!-- Any module that wants to intercept the Home button and prevent the -->
+ <!-- homescreen from being displayed must come before this script. -->
+ <script defer src="js/window.js"></script>
+ <script defer src="js/window_manager.js"></script>
+
+ <!-- Remote Debugger -->
+ <script defer src="js/remote_debugger.js"></script>
+
+ <!-- Call Forwarding -->
+ <script defer src="js/call_forwarding.js"></script>
+
+ <!-- z-indexes of all the overlays in the system -->
+ <link rel="stylesheet" type="text/css" href="style/zindex.css">
+
+ <style>
+ /* initlogo style is here to prevent flashing */
+ #initlogo {
+ position: absolute;
+ overflow: hidden;
+ width: 100%;
+ height: 100%;
+
+ background: #000 url('shared/resources/branding/initlogo.png') bottom right no-repeat;
+
+ z-index: 65536;
+
+ opacity: 1;
+ transition: opacity 1s ease 3s;
+ pointer-events: none;
+ }
+
+ #initlogo.hide {
+ opacity: 0;
+ }
+ </style>
+
+ </head>
+
+ <body role="application">
+ <div id="screen" class="locked">
+ <div id="initlogo" data-z-index-level="initlogo"></div>
+
+ <div id="statusbar" data-z-index-level="statusbar">
+ <!-- Carrier & date label -->
+ <div id="statusbar-label" class="sb-start-upper"></div>
+ <!-- Notification -->
+ <div id="statusbar-notification" class="sb-start sb-icon sb-icon-notification"
+ data-num="" hidden></div>
+
+ <!-- Time -->
+ <div id="statusbar-time"></div>
+
+ <!-- Status -->
+ <div id="statusbar-battery" class="sb-icon sb-icon-battery"
+ data-level="100" charging hidden></div>
+ <div id="statusbar-wifi" class="sb-icon sb-icon-wifi"
+ data-level="4" hidden></div>
+ <div id="statusbar-data" class="sb-icon sb-icon-data"
+ data-type="" hidden></div>
+ <div id="statusbar-flight-mode" class="sb-icon sb-icon-flight-mode" hidden></div>
+ <div id="statusbar-signal" class="sb-icon sb-icon-signal"
+ data-level="5" hidden></div>
+ <div id="statusbar-headphones" class="sb-icon sb-icon-headphones" hidden></div>
+
+ <!-- Permissions -->
+ <div id="statusbar-tethering" class="sb-icon sb-icon-tethering" hidden></div>
+ <div id="statusbar-alarm" class="sb-icon sb-icon-alarm" hidden></div>
+ <div id="statusbar-bluetooth" class="sb-icon sb-icon-bluetooth" hidden></div>
+ <div id="statusbar-mute" class="sb-icon sb-icon-mute" hidden></div>
+ <div id="statusbar-recording" class="sb-icon sb-icon-recording" hidden></div>
+ <div id="statusbar-sms" class="sb-icon sb-icon-sms" hidden></div>
+ <div id="statusbar-geolocation" class="sb-icon sb-icon-geolocation" hidden></div>
+ <div id="statusbar-usb" class="sb-icon sb-icon-usb" hidden></div>
+ <div id="statusbar-call-forwarding" class="sb-icon sb-icon-call-forwarding" hidden></div>
+
+ <!-- Activity -->
+ <div id="statusbar-system-downloads"
+ class="sb-icon sb-icon-system-downloads"
+ hidden></div>
+ <div id="statusbar-network-activity" class="sb-icon sb-icon-network-activity" hidden></div>
+ </div>
+
+ <div id="utility-tray" data-z-index-level="utility-tray">
+ <!-- notifications -->
+ <div id="notification-bar">
+ <span data-l10n-id="notifications">Notifications</span>
+ <button id="notification-clear" data-l10n-id="clear-all">Clear all</button>
+ </div>
+
+ <div id="notifications-container">
+ <!-- Update Manager -->
+ <div id="update-manager-container" class='fake-notification'>
+ <div class="icon">
+ </div>
+ <div class="activity">
+ </div>
+ <div class="message">
+ </div>
+ </div>
+ <!-- App Install Manager -->
+ <div id="install-manager-notification-container">
+ </div>
+ <!-- bluetooth transfer -->
+ <div id="bluetooth-transfer-status-list">
+ </div>
+
+ <div id="desktop-notifications-container">
+ </div>
+ </div>
+
+ <!-- credit module -->
+ <div id="cost-control-widget"></div>
+
+ <!-- quick settings -->
+ <div id="quick-settings">
+ <a href="#" id="quick-settings-wifi" data-enabled="false"></a>
+ <div class="separator"></div>
+ <a href="#" id="quick-settings-data" data-enabled="false"></a>
+ <div class="separator"></div>
+ <a href="#" id="quick-settings-bluetooth" data-enabled="false"></a>
+ <div class="separator"></div>
+ <a href="#" id="quick-settings-airplane-mode" data-enabled="false"></a>
+ <div class="separator"></div>
+ <a href="#" id="quick-settings-full-app" data-enabled="false"></a>
+ </div>
+
+ <div id="utility-tray-grippy">
+ </div>
+ </div>
+
+ <div id="windows" data-z-index-level="app">
+ <!-- application windows are added here -->
+ <header id="wrapper-activity-indicator"></header>
+ <footer id="wrapper-footer" class="closed">
+ <div id="handler"></div>
+ <menu type="buttonbar">
+ <button type="button" id="back-button" alt="Back" data-disabled="disabled"></button>
+ <button type="button" id="forward-button" alt="Forward" data-disabled="disabled"></button>
+ <button type="button" id="reload-button" alt="Reload"></button>
+ <button type="button" id="bookmark-button" alt="Bookmark" data-disabled="disabled"></button>
+ <button type="button" id="close-button" alt="Close"></button>
+ </menu>
+ </footer>
+ </div>
+
+ <div id="dialog-overlay" data-z-index-level="dialog-overlay">
+ <!-- Unlock SIM Pin dialog -->
+ <div id="simpin-dialog" role="dialog" data-z-index-level="simpin-dialog" hidden>
+ <section role="region">
+ <header>
+ <button type="reset">
+ <span data-l10n-id="close" class="icon icon-close">Close</span>
+ </button>
+ <menu type="toolbar">
+ <button data-l10n-id="ok" type="submit">Done</button>
+ </menu>
+ <h1></h1>
+ </header>
+
+ <div class="container">
+ <div id="errorMsg" class="error" hidden>
+ <div id="messageHeader">The PIN was incorrect.</div>
+ <span id="messageBody">3 tries left.</span>
+ </div>
+
+ <!-- sim pin input field -->
+ <div id="pinArea" hidden>
+ <div data-l10n-id="simPin">SIM PIN</div>
+ <div class="input-wrapper">
+ <input name="simpin" type="number" size="8" maxlength="8" />
+ <input name="simpinVis" type="text" size="8" maxlength="8" />
+ </div>
+ </div>
+ <!-- sim puk input field -->
+ <div id="pukArea" hidden>
+ <div data-l10n-id="pukCode">PUK Code</div>
+ <div class="input-wrapper">
+ <input name="simpuk" type="number" size="8" maxlength="8" />
+ <input name="simpukVis" type="text" size="8" maxlength="8" />
+ </div>
+ </div>
+ <!-- sim nck input field -->
+ <div id="nckArea" hidden>
+ <div data-l10n-id="nckCode">NCK Code</div>
+ <div class="input-wrapper">
+ <input name="nckpin" type="number" size="16" maxlength="16" />
+ <input name="nckpinVis" type="text" size="16" maxlength="16" />
+ </div>
+ </div>
+ <!-- new sim pin input field -->
+ <div id="newPinArea" hidden>
+ <div data-l10n-id="newSimPinMsg">
+ Create PIN (must contain 4 to 8 digits)
+ </div>
+ <div class="input-wrapper">
+ <input name="newSimpin" type="number" size="8" maxlength="8" />
+ <input name="newSimpinVis" type="text" size="8" maxlength="8" />
+ </div>
+ </div>
+ <!-- confirm new sim pin input field -->
+ <div id="confirmPinArea" hidden>
+ <div>Confirm New PIN</div>
+ <div class="input-wrapper">
+ <input name="confirmNewSimpin" type="number" size="8" maxlength="8" />
+ <input name="confirmNewSimpinVis" type="text" size="8" maxlength="8" />
+ </div>
+ </div>
+ </div>
+ </section>
+ </div>
+
+ <!-- Crash reporter -->
+ <div id="crash-dialog">
+ <form role="dialog" data-type="confirm">
+ <section id="crash-dialog-contents">
+ <h1 id="crash-dialog-title"></h1>
+ <p data-l10n-id="crash-dialog-message">Would you like to send Mozilla a report about the crash to help us fix the problem? (Reports are sent over Wi-Fi only)</p>
+ <p><a id="crash-info-link" data-l10n-id="crash-info-link">What's in a crash report?</a></p>
+ <p>
+ <input id="always-send" type="checkbox" checked="true"/>
+ <label for="always-send" data-l10n-id="crash-always-report">Always send Mozilla a report when a crash occurs.</label>
+ <p>
+ </section>
+ <menu>
+ <button id="dont-send-report" disabled="true" data-l10n-id="crash-dont-send">Don't Send</button>
+ <button id="send-report" class="recommend" data-l10n-id="crash-end">Send Report</button>
+ </menu>
+ </form>
+
+ <!-- "Crash Reports" information page -->
+ <section role="region" class="skin-dark">
+ <header>
+ <menu type="toolbar">
+ <button id="crash-reports-done" data-l10n-id="done">Done</button>
+ </menu>
+ <h1 data-l10n-id="crashReports">Crash Reports</h1>
+ </header>
+ <p data-l10n-id="crash-reports-description-1"></p>
+ <p data-l10n-id="crash-reports-description-2"></p>
+ <p>
+ <span data-l10n-id="crash-reports-description-3-start"></span><span data-l10n-id="crash-reports-description-3-privacy"></span><span data-l10n-id="crash-reports-description-3-end"></span>
+ </p>
+ </section>
+ </div>
+
+ <div id="popup-container">
+ <section role="region" class="title-container skin-organic">
+ <header>
+ <button id="popup-close"><span class="icon icon-close">close</span></button>
+ <h1 id="popup-title"></h1>
+ <div id="popup-throbber"></div>
+ </header>
+ </section>
+ <div id="frame-container">
+ <div id="popup-error-dialog" role="dialog">
+ <div class="inner">
+ <h3 data-l10n-id="error-title" id="popup-error-title">Hmm, the app is having problems.</h3>
+ <p>
+ <span data-l10n-id="error-message" id="popup-error-message">The app has encountered an error and is not loading properly.</span>
+ </p>
+ </div>
+ <menu data-items="2">
+ <button id="popup-error-back" data-l10n-id="close">Close</button>
+ <button id="popup-error-reload" data-l10n-id="try-again">Try again</button>
+ </menu>
+ </div>
+ </div>
+ </div>
+
+ <div id="trustedui-container" class="up">
+ <div id="trustedui-inner">
+ <section role="region" class="title-container skin-organic">
+ <header>
+ <button id="trustedui-close"><span class="icon icon-close">close</span></button>
+ <h1 id="trustedui-title"></h1>
+ <div id="trustedui-throbber"></div>
+ </header>
+ </section>
+ <div id="trustedui-frame-container"></div>
+ </div>
+ </div>
+
+ <div id="authentication-dialog">
+ <!--div id="authentication-dialog-ftp" role="dialog">
+ </div-->
+ <div id="authentication-dialog-http-authentication" role="dialog">
+ <div class="authentication-dialog-message-container inner">
+ <h3 id="authentication-dialog-title"></h3>
+ <p>
+ <span id="authentication-dialog-http-authentication-message"></span>
+ </p>
+ <p>
+ <span data-l10n-id="username">Username</span>
+ <input id="authentication-dialog-http-username-input" />
+ <span data-l10n-id="password">Password</span>
+ <input id="authentication-dialog-http-password-input" type="password" />
+ </p>
+ </div>
+ <menu data-items="2">
+ <button id="authentication-dialog-http-authentication-cancel" data-l10n-id="cancel">Cancel</button>
+ <button id="authentication-dialog-http-authentication-ok" data-l10n-id="login" class="affirmative">Login</button>
+ </menu>
+ </div>
+ </div>
+
+ <div id="modal-dialog">
+ <div id="modal-dialog-alert" role="dialog">
+ <div class="modal-dialog-message-container inner">
+ <h3 id="modal-dialog-alert-title"></h3>
+ <p>
+ <span id="modal-dialog-alert-message"></span>
+ </p>
+ </div>
+ <menu>
+ <button id="modal-dialog-alert-ok" data-l10n-id="ok" class="affirmative">OK</button>
+ </menu>
+ </div>
+
+ <div id="modal-dialog-confirm" role="dialog">
+ <div class="modal-dialog-message-container inner">
+ <h3 id="modal-dialog-confirm-title"></h3>
+ <p>
+ <span id="modal-dialog-confirm-message"></span>
+ </p>
+ </div>
+ <menu data-items="2">
+ <button id="modal-dialog-confirm-cancel" data-l10n-id="cancel">Cancel</button>
+ <button id="modal-dialog-confirm-ok" data-l10n-id="ok" class="affirmative">OK</button>
+ </menu>
+ </div>
+
+ <div id="modal-dialog-prompt" role="dialog">
+ <div class="modal-dialog-message-container inner">
+ <h3 id="modal-dialog-prompt-title"></h3>
+ <p>
+ <span id="modal-dialog-prompt-message"></span>
+ <input id="modal-dialog-prompt-input" />
+ </p>
+ </div>
+ <menu data-items="2">
+ <button id="modal-dialog-prompt-cancel" data-l10n-id="cancel">Cancel</button>
+ <button id="modal-dialog-prompt-ok" data-l10n-id="ok" class="affirmative">OK</button>
+ </menu>
+ </div>
+
+ <div id="modal-dialog-select-one" role="dialog">
+ <div class="modal-dialog-message-container inner">
+ <h3 id="modal-dialog-select-one-title"></h3>
+ <ul id="modal-dialog-select-one-menu"></ul>
+ </div>
+ <menu>
+ <button id="modal-dialog-select-one-cancel" data-l10n-id="cancel">Cancel</button>
+ </menu>
+ </div>
+ </div> <!-- end of #modal-dialog -->
+
+ </div> <!-- end of #overlay-dialog -->
+
+ <!-- value selector -->
+ <div id="value-selector" hidden data-z-index-level="value-selector">
+ <!-- popup of options for select element -->
+ <form id="select-option-popup" role="dialog">
+ <section id="value-selector-container">
+ <h1 data-l10n-id="choose-option">Choose your option</h1>
+ <ol role="listbox">
+ </ol>
+ </section>
+ <menu id="select-options-buttons">
+ <button class="value-option-confirm affirmative full" data-type="ok" data-l10n-id="ok">Ok</button>
+ </menu>
+ </form>
+
+ <!-- Time Picker -->
+ <div id="time-picker-popup" hidden>
+ <div class="table-wrapper">
+ <div class="table-cell">
+ <div id="picker-bar">
+ <h3 data-l10n-id="select-time">Select time</h3>
+ <div id="picker-container">
+ <div id="value-indicator-bottom"></div>
+ <div id="picker-bar-background"></div>
+ <div id="left-picker-separator"></div>
+ <div id="value-indicator-wrapper">
+ <div id="value-indicator-hover-time">:</div>
+ <div id="value-indicator-hover"></div>
+ </div>
+ <div id="value-picker-hours" class="animation-on"></div>
+ <div id="value-picker-minutes" class="animation-on"></div>
+ <div id="right-picker-separator"></div>
+ <div id="value-picker-hour24-state" class="animation-on"></div>
+ <div id="picker-bar-gradient"></div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div id="time-picker-buttons">
+ <button class="value-selector-cancel" data-type="cancel" data-l10n-id="cancel">Cancel</button>
+ <button class="value-selector-confirm affirmative" data-type="ok" data-l10n-id="ok">Ok</button>
+ </div>
+ </div>
+
+ <!-- Spin Date Picker -->
+ <div id="spin-date-picker-popup" hidden>
+ <div id="spin-date-picker" class="table-wrapper">
+ <div class="table-cell">
+ <div class="picker-bar">
+ <h3 data-l10n-id="select-day">Select day</h3>
+ <div class="picker-container">
+ <div class="value-indicator-bottom"></div>
+ <div class="picker-bar-background"></div>
+ <div class="left-picker-separator"></div>
+ <div class="right-picker-separator"></div>
+ <div class="value-indicator-wrapper">
+ <div class="value-indicator-hover"></div>
+ </div>
+ <div class="value-picker-date-wrapper">
+ <div class="value-picker-date animation-on"></div>
+ <div class="value-picker-date animation-on"></div>
+ <div class="value-picker-date animation-on"></div>
+ <div class="value-picker-date animation-on"></div>
+ </div>
+ <div class="value-picker-month-wrapper">
+ <div class="value-picker-month" class="animation-on"></div>
+ </div>
+ <div class="value-picker-year-wrapper">
+ <div class="value-picker-year" class="animation-on"></div>
+ </div>
+ <div class="picker-bar-gradient"></div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <menu id="spin-date-picker-buttons" data-items="2">
+ <button class="value-selector-cancel" data-type="cancel" data-l10n-id="cancel">Cancel</button>
+ <button class="value-option-confirm affirmative" data-type="ok" data-l10n-id="ok">Ok</button>
+ </menu>
+ </div>
+ </div>
+
+ <div id="permission-screen" data-z-index-level="permission-screen">
+ <div id="permission-dialog" role="dialog">
+ <div class="inner">
+ <h2 id="permission-message"></h2>
+ <div id="permission-remember-section">
+ <a data-l10n-id="remember-my-choice" id="permission-remember-label">Remember my choice</a>
+ <label>
+ <input data-type="switch" type="checkbox" id="permission-remember-checkbox" />
+ <span></span>
+ </label>
+ </div>
+ <menu data-items="2">
+ <button id="permission-no" data-l10n-id="deny">Don't allow</button>
+ <button id="permission-yes" class="affirmative" data-l10n-id="allow">Allow</button>
+ </menu>
+ </div>
+ </div>
+ </div>
+
+ <form id="app-install-dialog" class='app-install-dialog'
+ data-type="confirm" role="dialog"
+ data-z-index-level="app-install-dialog">
+ <section>
+ <h1 id="app-install-message"></h1>
+ <table>
+ <tr>
+ <th data-l10n-id="size">Size</th>
+ <td id="app-install-size"></td>
+ </tr>
+ <tr>
+ <th data-l10n-id="author">Author</th>
+ <td>
+ <span id="app-install-author-name"></span>
+ <br /><span id="app-install-author-url"></span>
+ </td>
+ </tr>
+ </table>
+ <menu>
+ <button id="app-install-cancel-button" data-l10n-id="cancel">Cancel</button>
+ <button id="app-install-install-button" class="recommend" data-l10n-id="install">Install</button>
+ </menu>
+ </section>
+ </form>
+
+ <form id="app-install-cancel-dialog" class='app-install-dialog'
+ data-type="confirm" role="dialog"
+ data-z-index-level="app-install-dialog">
+ <section>
+ <h1 data-l10n-id="cancel-install">Cancel Install</h1>
+ <p>
+ <small data-l10n-id="cancelling-will-not-refund">Cancelling will not refund a purchase. Refunds for paid content are provided by the original seller.</small>
+ <small data-l10n-id="apps-can-be-installed-later">Apps can be installed later from the original installation source.</small>
+ </p>
+ <p data-l10n-id="are-you-sure-you-want-to-cancel">Are you sure you want to cancel this install?</p>
+ <menu>
+ <button id="app-install-confirm-cancel-button" type="reset" data-l10n-id="cancel-install">Cancel Install</button>
+ <button id="app-install-resume-button" type="submit" data-l10n-id="resume">Resume</button>
+ </menu>
+ </section>
+ </form>
+
+ <form id="app-download-cancel-dialog" class='app-install-dialog'
+ data-type="confirm" role="dialog"
+ data-z-index-level="app-install-dialog">
+ <section>
+ <h1></h1>
+ <p data-l10n-id="app-download-can-be-restarted">The download can
+ be restarted later.</p>
+ <menu>
+ <button id="app-download-stop-button" class="danger confirm"
+ data-l10n-id="app-download-stop-button">Stop Download</button>
+ <button id="app-download-continue-button" class="cancel"
+ type="reset" data-l10n-id="continue">Continue</button>
+ </menu>
+ </section>
+ </form>
+
+ <form id="updates-download-dialog" data-type="confirm" role="dialog"
+ data-z-index-level="updates-download-dialog" data-nowifi="false"
+ data-data-connection-inline-warning="false">
+ <section>
+ <h1>
+ Updates
+ </h1>
+ <ul>
+ </ul>
+ </section>
+ <p id="updates-data-connection-warning" data-l10n-id="downloadDataConnectionWarning">
+ Updates are downloaded via data connection when Wi-Fi is not available. Additional data charges may apply.
+ </p>
+ <p id="updates-offline-warning" data-l10n-id="downloadOfflineWarning">
+ Connection unavailable. Connect to a network to download updates.
+ </p>
+ <menu>
+ <button id="updates-later-button" type="reset"
+ data-l10n-id="later">Later</button>
+ <button id="updates-download-button" type="submit"
+ data-l10n-id="download">Download</button>
+ </menu>
+ </form>
+
+ <form id="updates-viaDataConnection-dialog" data-type="confirm" role="dialog" data-z-index-level="updates-viaDataConnection-dialog">
+ <section>
+ <h1 data-l10n-id="downloadUpdatesViaDataConnection">
+ Download updates via data connection?
+ </h1>
+ <p data-l10n-id="downloadUpdatesViaDataConnectionMessage">
+ Updates are downloaded via data connection when Wi-Fi is not available. When using data connection, phone calls may be blocked and additional charges may also apply.
+ </p>
+ </section>
+ <menu>
+ <button id="updates-viaDataConnection-notnow-button" type="reset" data-l10n-id="notNow">Not Now</button>
+ <button id="updates-viaDataConnection-download-button" type="submit" data-l10n-id="download">Download</button>
+ </menu>
+ </form>
+
+ <div id="lockscreen-camera" data-z-index-level="lockscreen-camera"></div>
+
+ <div id="lockscreen" class="uninit" data-panel="main" data-z-index-level="lockscreen">
+ <div id="lockscreen-container">
+ <div id="lockscreen-panel-main" class="lockscreen-panel" data-wallpaper>
+ <div id="lockscreen-header">
+ <div id="lockscreen-connstate" hidden><span></span><span></span></div>
+ <div id="lockscreen-mute" hidden></div>
+ <div class="lockscreen-clock">
+ <span id="lockscreen-clock-numbers"></span>
+ <span id="lockscreen-clock-meridiem"></span>
+ </div>
+ <div id="lockscreen-date"></div>
+ </div>
+ <div id="notifications-lockscreen-container">
+ </div>
+ <div id="lockscreen-area"></div>
+ <div id="lockscreen-icon-container">
+ <div id="lockscreen-area-handle"></div>
+ <div id="lockscreen-area-camera" class="lockscreen-icon-area lockscreen-icon-left">
+ <div class="lockscreen-icon">
+ <div id="lockscreen-accessibility-camera" role="button" class="lockscreen-icon-a11y-button" data-l10n-id="camera-a11y-button" aria-label="Camera"></div>
+ </div>
+ </div>
+ <div id="lockscreen-area-unlock" class="lockscreen-icon-area lockscreen-icon-right">
+ <div class="lockscreen-icon">
+ <div id="lockscreen-accessibility-unlock" role="button" class="lockscreen-icon-a11y-button" data-l10n-id="unlock-a11y-button" aria-label="Unlock"></div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div id="lockscreen-panel-passcode" class="lockscreen-panel" data-wallpaper>
+ <h2 id="lockscreen-passcode-status" data-l10n-id="enter-security-code">Enter Security&nbsp;Code</h2>
+ <p id="lockscreen-passcode-code"><span></span><span></span><span></span><span></span></p>
+ <div id="lockscreen-passcode-pad">
+ <a role="button" href="#" data-key="1">1<span>.<span></a>
+ <a role="button" href="#" data-key="2">2<span>ABC<span></a>
+ <a role="button" href="#" data-key="3">3<span>DEF<span></a>
+ <a role="button" href="#" data-key="4">4<span>GHI<span></a>
+ <a role="button" href="#" data-key="5">5<span>JKL<span></a>
+ <a role="button" href="#" data-key="6">6<span>MNO<span></a>
+ <a role="button" href="#" data-key="7">7<span>PQRS<span></a>
+ <a role="button" href="#" data-key="8">8<span>TUV<span></a>
+ <a role="button" href="#" data-key="9">9<span>WXYZ<span></a>
+ <a role="button" href="#" data-key="e" data-l10n-id="emergency-call-button" class="lockscreen-passcode-pad-func last-row">Emergency Call</a>
+ <a role="button" href="#" data-key="0" class="last-row">0</a>
+ <a role="button" href="#" data-key="c" data-l10n-id="cancel" class="lockscreen-passcode-pad-func last-row">Cancel</a>
+ <a role="button" href="#" data-key="b" class="last-row">⌫</a>
+ </div>
+ </div>
+
+ <div id="lockscreen-panel-emergency-call" class="lockscreen-panel"></div>
+ </div>
+ </div>
+
+ <div id="attention-screen" data-z-index-level="attention-screen">
+ <div id="attention-bar"></div>
+ </div>
+
+ <div id="update-manager-toaster" data-z-index-level="notification-toaster">
+ <div class="icon">
+ </div>
+ <div class="message">
+ </div>
+ </div>
+
+ <div id="notification-toaster" data-z-index-level="notification-toaster">
+ <img id="toaster-icon" />
+ <div id="toaster-title"></div>
+ <div id="toaster-detail"></div>
+ </div>
+
+ <div id="listmenu" data-z-index-level="list-menu" role="dialog">
+ <menu class="actions">
+ </menu>
+ </div>
+
+ <!-- keyboard -->
+ <div id="keyboard-frame" class="hide" data-z-index-level="keyboard-frame"></div>
+
+ <div id="cards-view" data-z-index-level="cards-view">
+ <ul>
+ </ul>
+ </div>
+
+ <div id="sleep-menu" data-z-index-level="sleep-menu">
+ <div id="sleep-menu-container">
+ <h3 data-l10n-id="deviceMenu">Device menu</h3>
+ <ul>
+ </ul>
+ </div>
+ <menu>
+ <button data-l10n-id="cancel">Cancel</button>
+ </menu>
+ </div>
+
+ <section id="system-banner" role="dialog" class="banner"
+ data-z-index-level="system-notification-banner" data-button="false">
+ <p></p>
+ <button></button>
+ </section>
+
+ <div id="system-overlay" data-z-index-level="system-overlay">
+ <div id="battery">
+ <span class="icon-battery"></span>
+ <span class="battery-notification" data-l10n-id="battery-almost-empty">Battery almost empty</span>
+ </div>
+
+ <div id="volume" class="vibration">
+ <span class="vibration"></span>
+ <span class="mute-state"></span>
+ <div class="active"></div>
+ <div class="active"></div>
+ <div class="active"></div>
+ <div class="active"></div>
+ <div class="active"></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <span class="volume"></span>
+ </div>
+ </div>
+
+ </div> <!-- end of #screen -->
+ </body>
+</html>
diff --git a/apps/system/js/accessibility.js b/apps/system/js/accessibility.js
new file mode 100644
index 0000000..c2fa111
--- /dev/null
+++ b/apps/system/js/accessibility.js
@@ -0,0 +1,19 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+SettingsListener.observe('accessibility.invert', false, function(value) {
+ var screen = document.getElementById('screen');
+ if (value)
+ screen.classList.add('accessibility-invert');
+ else
+ screen.classList.remove('accessibility-invert');
+});
+
+SettingsListener.observe('accessibility.screenreader', false, function(value) {
+ var event = document.createEvent('CustomEvent');
+ event.initCustomEvent('mozContentEvent', true, true,
+ {type: 'accessibility-screenreader', enabled: value});
+ window.dispatchEvent(event);
+});
diff --git a/apps/system/js/activities.js b/apps/system/js/activities.js
new file mode 100644
index 0000000..f4db861
--- /dev/null
+++ b/apps/system/js/activities.js
@@ -0,0 +1,86 @@
+/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var Activities = {
+ init: function act_init() {
+ window.addEventListener('mozChromeEvent', this);
+ },
+
+ handleEvent: function act_handleEvent(evt) {
+ switch (evt.type) {
+ case 'mozChromeEvent':
+ var detail = evt.detail;
+ switch (detail.type) {
+ case 'activity-choice':
+ this.chooseActivity(detail);
+ break;
+ }
+ break;
+ }
+ },
+
+ chooseActivity: function chooseActivity(detail) {
+ this._id = detail.id;
+
+ var choices = detail.choices;
+ if (choices.length === 1) {
+ this.choose('0');
+ } else {
+ // Since the mozChromeEvent could be triggered by a 'click', and gecko
+ // event are synchronous make sure to exit the event loop before
+ // showing the list.
+ setTimeout((function nextTick() {
+ var activityName = navigator.mozL10n.get('activity-' + detail.name);
+ ListMenu.request(this._listItems(choices), activityName,
+ this.choose.bind(this), this.cancel.bind(this));
+ }).bind(this));
+ }
+ },
+
+ choose: function act_choose(choice) {
+ var returnedChoice = {
+ id: this._id,
+ type: 'activity-choice',
+ value: choice
+ };
+
+ this._sendEvent(returnedChoice);
+ delete this._id;
+ },
+
+ cancel: function act_cancel(value) {
+ var returnedChoice = {
+ id: this._id,
+ type: 'activity-choice',
+ value: -1
+ };
+
+ this._sendEvent(returnedChoice);
+ delete this._id;
+ },
+
+ _sendEvent: function act_sendEvent(value) {
+ var event = document.createEvent('CustomEvent');
+ event.initCustomEvent('mozContentEvent', true, true, value);
+ window.dispatchEvent(event);
+ },
+
+ _listItems: function act_listItems(choices) {
+ var items = [];
+
+ choices.forEach(function(choice, index) {
+ var app = Applications.getByManifestURL(choice.manifest);
+ items.push({
+ label: new ManifestHelper(app.manifest).name,
+ icon: choice.icon,
+ value: index
+ });
+ });
+
+ return items;
+ }
+};
+
+Activities.init();
diff --git a/apps/system/js/airplane_mode.js b/apps/system/js/airplane_mode.js
new file mode 100644
index 0000000..781a53f
--- /dev/null
+++ b/apps/system/js/airplane_mode.js
@@ -0,0 +1,138 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var AirplaneMode = {
+ enabled: false,
+
+ init: function apm_init() {
+ if (!window.navigator.mozSettings)
+ return;
+
+ var mobileDataEnabled = false;
+ SettingsListener.observe('ril.data.enabled', false, function(value) {
+ mobileDataEnabled = value;
+ });
+
+ var bluetoothEnabled = false;
+ SettingsListener.observe('bluetooth.enabled', false, function(value) {
+ bluetoothEnabled = value;
+ });
+
+ var wifiEnabled = false;
+ SettingsListener.observe('wifi.enabled', false, function(value) {
+ wifiEnabled = value;
+ });
+
+ var geolocationEnabled = false;
+ SettingsListener.observe('geolocation.enabled', false, function(value) {
+ geolocationEnabled = value;
+ });
+
+ var bluetooth = window.navigator.mozBluetooth;
+ var wifiManager = window.navigator.mozWifiManager;
+ var mobileData = window.navigator.mozMobileConnection &&
+ window.navigator.mozMobileConnection.data;
+ var fmRadio = window.navigator.mozFMRadio;
+
+ var restoreMobileData = false;
+ var restoreBluetooth = false;
+ var restoreWifi = false;
+ var restoreGeolocation = false;
+ // Note that we don't restore Wifi tethering when leaving airplane mode
+ // because Wifi tethering can't be switched on before data connection is
+ // established.
+
+ var self = this;
+ SettingsListener.observe('ril.radio.disabled', false, function(value) {
+ if (value) {
+ // Entering airplane mode.
+ self.enabled = true;
+
+ // Turn off mobile data
+ // We toggle the mozSettings value here just for the sake of UI,
+ // platform ril disconnects mobile data when
+ // 'ril.radio.disabled' is true.
+ if (mobileData) {
+ restoreMobileData = mobileDataEnabled;
+ if (mobileDataEnabled) {
+ SettingsListener.getSettingsLock().set({
+ 'ril.data.enabled': false
+ });
+ }
+ }
+
+ // Turn off Bluetooth.
+ if (bluetooth) {
+ restoreBluetooth = bluetoothEnabled;
+ if (bluetoothEnabled) {
+ SettingsListener.getSettingsLock().set({
+ 'bluetooth.enabled': false
+ });
+ }
+ }
+
+ // Turn off Wifi.
+ if (wifiManager) {
+ restoreWifi = wifiEnabled;
+ if (wifiEnabled) {
+ SettingsListener.getSettingsLock().set({
+ 'wifi.enabled': false
+ });
+ }
+
+ // Turn off Wifi tethering.
+ SettingsListener.getSettingsLock().set({
+ 'tethering.wifi.enabled': false
+ });
+ }
+
+ // Turn off Geolocation.
+ restoreGeolocation = geolocationEnabled;
+ if (geolocationEnabled) {
+ SettingsListener.getSettingsLock().set({
+ 'geolocation.enabled': false
+ });
+ }
+
+ // Turn off FM Radio.
+ if (fmRadio && fmRadio.enabled)
+ fmRadio.disable();
+
+ } else {
+ self.enabled = false;
+ // Don't attempt to turn on mobile data if it's already on
+ if (mobileData && !mobileDataEnabled && restoreMobileData) {
+ SettingsListener.getSettingsLock().set({
+ 'ril.data.enabled': true
+ });
+ }
+
+ // Don't attempt to turn on Bluetooth if it's already on
+ if (bluetooth && !bluetooth.enabled && restoreBluetooth) {
+ SettingsListener.getSettingsLock().set({
+ 'bluetooth.enabled': true
+ });
+ }
+
+ // Don't attempt to turn on Wifi if it's already on
+ if (wifiManager && !wifiManager.enabled && restoreWifi) {
+ SettingsListener.getSettingsLock().set({
+ 'wifi.enabled': true
+ });
+ }
+
+ // Don't attempt to turn on Geolocation if it's already on
+ if (!geolocationEnabled && restoreGeolocation) {
+ SettingsListener.getSettingsLock().set({
+ 'geolocation.enabled': true
+ });
+ }
+ }
+ });
+ }
+};
+
+AirplaneMode.init();
+
diff --git a/apps/system/js/app_install_manager.js b/apps/system/js/app_install_manager.js
new file mode 100644
index 0000000..146a01b
--- /dev/null
+++ b/apps/system/js/app_install_manager.js
@@ -0,0 +1,443 @@
+'use strict';
+
+var AppInstallManager = {
+ mapDownloadErrorsToMessage: {
+ 'NETWORK_ERROR': 'download-failed',
+ 'DOWNLOAD_ERROR': 'download-failed',
+ 'MISSING_MANIFEST': 'install-failed',
+ 'INVALID_MANIFEST': 'install-failed',
+ 'INSTALL_FROM_DENIED': 'install-failed',
+ 'INVALID_SECURITY_LEVEL': 'install-failed',
+ 'INVALID_PACKAGE': 'install-failed',
+ 'APP_CACHE_DOWNLOAD_ERROR': 'download-failed'
+ },
+
+ init: function ai_init() {
+ this.dialog = document.getElementById('app-install-dialog');
+ this.msg = document.getElementById('app-install-message');
+ this.size = document.getElementById('app-install-size');
+ this.authorName = document.getElementById('app-install-author-name');
+ this.authorUrl = document.getElementById('app-install-author-url');
+ this.installButton = document.getElementById('app-install-install-button');
+ this.cancelButton = document.getElementById('app-install-cancel-button');
+ this.installCancelDialog =
+ document.getElementById('app-install-cancel-dialog');
+ this.downloadCancelDialog =
+ document.getElementById('app-download-cancel-dialog');
+ this.confirmCancelButton =
+ document.getElementById('app-install-confirm-cancel-button');
+ this.resumeButton = document.getElementById('app-install-resume-button');
+
+ this.notifContainer =
+ document.getElementById('install-manager-notification-container');
+ this.appInfos = {};
+
+ window.addEventListener('mozChromeEvent',
+ (function ai_handleChromeEvent(e) {
+ if (e.detail.type == 'webapps-ask-install') {
+ this.handleAppInstallPrompt(e.detail);
+ }
+ }).bind(this));
+
+ window.addEventListener('applicationinstall',
+ this.handleApplicationInstall.bind(this));
+
+
+ this.installButton.onclick = this.handleInstall.bind(this);
+ this.cancelButton.onclick = this.showInstallCancelDialog.bind(this);
+ this.confirmCancelButton.onclick = this.handleInstallCancel.bind(this);
+ this.resumeButton.onclick = this.hideInstallCancelDialog.bind(this);
+ this.notifContainer.onclick = this.showDownloadCancelDialog.bind(this);
+
+ this.downloadCancelDialog.querySelector('.confirm').onclick =
+ this.handleConfirmDownloadCancel.bind(this);
+ this.downloadCancelDialog.querySelector('.cancel').onclick =
+ this.handleCancelDownloadCancel.bind(this);
+
+ // bind these handlers so that we can have only one instance and check
+ // them later on
+ ['handleDownloadSuccess',
+ 'handleDownloadError',
+ 'handleProgress',
+ 'handleApplicationReady'
+ ].forEach(function(name) {
+ this[name] = this[name].bind(this);
+ }, this);
+
+ window.addEventListener('applicationready',
+ this.handleApplicationReady);
+ },
+
+ handleApplicationReady: function ai_handleApplicationReady(e) {
+ window.removeEventListener('applicationready',
+ this.handleApplicationReady);
+
+ var apps = e.detail.applications;
+
+ Object.keys(apps)
+ .filter(function(key) { return apps[key].installState === 'pending'; })
+ .map(function(key) { return apps[key]; })
+ .forEach(this.prepareForDownload, this);
+ },
+
+ handleApplicationInstall: function ai_handleApplicationInstallEvent(e) {
+ var app = e.detail.application;
+
+ if (app.installState === 'installed') {
+ this.showInstallSuccess(app);
+ return;
+ }
+
+ this.prepareForDownload(app);
+ },
+
+ handleAppInstallPrompt: function ai_handleInstallPrompt(detail) {
+ var _ = navigator.mozL10n.get;
+ var app = detail.app;
+ // updateManifest is used by packaged apps until they are installed
+ var manifest = app.manifest ? app.manifest : app.updateManifest;
+
+ if (!manifest)
+ return;
+
+ this.dialog.classList.add('visible');
+
+ var id = detail.id;
+
+ if (manifest.size) {
+ this.size.textContent = this.humanizeSize(manifest.size);
+ } else {
+ this.size.textContent = _('unknown');
+ }
+
+ // Wrap manifest to get localized properties
+ manifest = new ManifestHelper(manifest);
+ var msg = _('install-app', {'name': manifest.name});
+ this.msg.textContent = msg;
+
+ if (manifest.developer) {
+ this.authorName.textContent = manifest.developer.name || _('unknown');
+ this.authorUrl.textContent = manifest.developer.url || '';
+ } else {
+ this.authorName.textContent = _('unknown');
+ this.authorUrl.textContent = '';
+ }
+
+ this.installCallback = (function ai_installCallback() {
+ this.dispatchResponse(id, 'webapps-install-granted');
+ }).bind(this);
+
+ this.installCancelCallback = (function ai_cancelCallback() {
+ this.dispatchResponse(id, 'webapps-install-denied');
+ }).bind(this);
+
+ },
+
+ handleInstall: function ai_handleInstall(evt) {
+ if (evt)
+ evt.preventDefault();
+ if (this.installCallback)
+ this.installCallback();
+ this.installCallback = null;
+ this.dialog.classList.remove('visible');
+ },
+
+ prepareForDownload: function ai_prepareForDownload(app) {
+ var manifestURL = app.manifestURL;
+ this.appInfos[manifestURL] = {};
+
+ // these methods are already bound to |this|
+ app.ondownloadsuccess = this.handleDownloadSuccess;
+ app.ondownloaderror = this.handleDownloadError;
+ app.onprogress = this.handleProgress;
+ },
+
+ showInstallSuccess: function ai_showInstallSuccess(app) {
+ var manifest = app.manifest || app.updateManifest;
+ var name = new ManifestHelper(manifest).name;
+ var _ = navigator.mozL10n.get;
+ var msg = _('app-install-success', { appName: name });
+ SystemBanner.show(msg);
+ },
+
+ handleDownloadSuccess: function ai_handleDownloadSuccess(evt) {
+ var app = evt.application;
+ this.showInstallSuccess(app);
+ this.onDownloadStop(app);
+ this.onDownloadFinish(app);
+ },
+
+ handleDownloadError: function ai_handleDownloadError(evt) {
+ var app = evt.application;
+ var _ = navigator.mozL10n.get;
+ var manifest = app.manifest || app.updateManifest;
+ var name = new ManifestHelper(manifest).name;
+
+ var errorName = app.downloadError.name;
+
+ switch (errorName) {
+ case 'INSUFFICIENT_STORAGE':
+ var title = _('not-enough-space'),
+ buttonText = _('ok'),
+ message = _('not-enough-space-message');
+
+ ModalDialog.alert(title, message, {title: buttonText});
+ break;
+ default:
+ // showing the real error to a potential developer
+ console.info('downloadError event, error code is', errorName);
+
+ var key = this.mapDownloadErrorsToMessage[errorName] || 'generic-error';
+ var msg = _('app-install-' + key, { appName: name });
+ SystemBanner.show(msg);
+ }
+
+ this.onDownloadStop(app);
+ },
+
+ onDownloadStart: function ai_onDownloadStart(app) {
+ if (! this.hasNotification(app)) {
+ StatusBar.incSystemDownloads();
+ this.addNotification(app);
+ this.requestWifiLock(app);
+ }
+ },
+
+ onDownloadStop: function ai_onDownloadStop(app) {
+ if (this.hasNotification(app)) {
+ StatusBar.decSystemDownloads();
+ this.removeNotification(app);
+ this.releaseWifiLock(app);
+ }
+ },
+
+ hasNotification: function ai_hasNotification(app) {
+ var appInfo = this.appInfos[app.manifestURL];
+ return appInfo && !!appInfo.installNotification;
+ },
+
+ onDownloadFinish: function ai_onDownloadFinish(app) {
+ delete this.appInfos[app.manifestURL];
+
+ // check if these are our handlers before removing them
+ if (app.ondownloadsuccess === this.handleDownloadSuccess) {
+ app.ondownloadsuccess = null;
+ }
+
+ if (app.ondownloaderror === this.handleDownloadError) {
+ app.ondownloaderror = null;
+ }
+
+ if (app.onprogress === this.handleProgress) {
+ app.onprogress = null;
+ }
+ },
+
+ addNotification: function ai_addNotification(app) {
+ // should be unique (this is used already in applications.js)
+ var manifestURL = app.manifestURL,
+ manifest = app.manifest || app.updateManifest,
+ appInfo = this.appInfos[manifestURL];
+
+ if (appInfo.installNotification) {
+ return;
+ }
+
+ var newNotif =
+ '<div class="fake-notification">' +
+ '<div class="message"></div>' +
+ '<progress></progress>' +
+ '</div>';
+
+ this.notifContainer.insertAdjacentHTML('afterbegin', newNotif);
+
+ var newNode = this.notifContainer.firstElementChild;
+ newNode.dataset.manifest = manifestURL;
+
+ var _ = navigator.mozL10n.get;
+
+ var message = _('downloadingAppMessage', {
+ appName: new ManifestHelper(manifest).name
+ });
+
+ newNode.querySelector('.message').textContent = message;
+
+ var size = manifest.size,
+ progressNode = newNode.querySelector('progress');
+ if (size) {
+ progressNode.max = size;
+ appInfo.hasMax = true;
+ }
+
+ appInfo.installNotification = newNode;
+ NotificationScreen.incExternalNotifications();
+ },
+
+ getNotificationProgressNode: function ai_getNotificationProgressNode(app) {
+ var appInfo = this.appInfos[app.manifestURL];
+ var progressNode = appInfo &&
+ appInfo.installNotification &&
+ appInfo.installNotification.querySelector('progress');
+ return progressNode || null;
+ },
+
+ handleProgress: function ai_handleProgress(evt) {
+ var app = evt.application,
+ appInfo = this.appInfos[app.manifestURL];
+
+ this.onDownloadStart(app);
+
+
+ var progressNode = this.getNotificationProgressNode(app);
+ var message;
+ var _ = navigator.mozL10n.get;
+
+ if (isNaN(app.progress) || app.progress == null) {
+ // now we get NaN if there is no progress information but let's
+ // handle the null and undefined cases as well
+ message = _('downloadingAppProgressIndeterminate');
+ progressNode.value = undefined; // switch to indeterminate state
+ } else if (appInfo.hasMax) {
+ message = _('downloadingAppProgress',
+ {
+ progress: this.humanizeSize(app.progress),
+ max: this.humanizeSize(progressNode.max)
+ });
+ progressNode.value = app.progress;
+ } else {
+ message = _('downloadingAppProgressNoMax',
+ { progress: this.humanizeSize(app.progress) });
+ }
+ progressNode.textContent = message;
+ },
+
+ removeNotification: function ai_removeNotification(app) {
+ var manifestURL = app.manifestURL,
+ appInfo = this.appInfos[manifestURL],
+ node = appInfo.installNotification;
+
+ if (!node) {
+ return;
+ }
+
+ node.parentNode.removeChild(node);
+ delete appInfo.installNotification;
+ NotificationScreen.decExternalNotifications();
+ },
+
+ requestWifiLock: function ai_requestWifiLock(app) {
+ var appInfo = this.appInfos[app.manifestURL];
+ if (! appInfo.wifiLock) {
+ // we don't want 2 locks for the same app
+ appInfo.wifiLock = navigator.requestWakeLock('wifi');
+ }
+ },
+
+ releaseWifiLock: function ai_releaseWifiLock(app) {
+ var appInfo = this.appInfos[app.manifestURL];
+
+ if (appInfo.wifiLock) {
+ try {
+ appInfo.wifiLock.unlock();
+ } catch (e) {
+ // this can happen if the lock is already unlocked
+ console.error('error during unlock', e);
+ }
+
+ delete appInfo.wifiLock;
+ }
+ },
+
+ dispatchResponse: function ai_dispatchResponse(id, type) {
+ var event = document.createEvent('CustomEvent');
+
+ event.initCustomEvent('mozContentEvent', true, true, {
+ id: id,
+ type: type
+ });
+
+ window.dispatchEvent(event);
+ },
+
+ humanizeSize: function ai_humanizeSize(bytes) {
+ var _ = navigator.mozL10n.get;
+ var units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'];
+
+ if (!bytes)
+ return '0.00 ' + _(units[0]);
+
+ var e = Math.floor(Math.log(bytes) / Math.log(1024));
+ return (bytes / Math.pow(1024, Math.floor(e))).toFixed(2) + ' ' +
+ _(units[e]);
+ },
+
+ showInstallCancelDialog: function ai_showInstallCancelDialog(evt) {
+ if (evt)
+ evt.preventDefault();
+ this.installCancelDialog.classList.add('visible');
+ this.dialog.classList.remove('visible');
+ },
+
+ hideInstallCancelDialog: function ai_hideInstallCancelDialog(evt) {
+ if (evt)
+ evt.preventDefault();
+ this.dialog.classList.add('visible');
+ this.installCancelDialog.classList.remove('visible');
+ },
+
+ showDownloadCancelDialog: function ai_showDownloadCancelDialog(e) {
+ var currentNode = e.target;
+
+ if (!currentNode.classList.contains('fake-notification')) {
+ // tapped outside of a notification
+ return;
+ }
+
+ var manifestURL = currentNode.dataset.manifest,
+ app = Applications.getByManifestURL(manifestURL),
+ manifest = app.manifest || app.updateManifest,
+ dialog = this.downloadCancelDialog;
+
+ var title = dialog.querySelector('h1');
+
+ title.textContent = navigator.mozL10n.get('stopDownloading', {
+ app: new ManifestHelper(manifest).name
+ });
+
+ dialog.classList.add('visible');
+ dialog.dataset.manifest = manifestURL;
+ UtilityTray.hide();
+ },
+
+ handleInstallCancel: function ai_handleInstallCancel() {
+ if (this.installCancelCallback)
+ this.installCancelCallback();
+ this.installCancelCallback = null;
+ this.installCancelDialog.classList.remove('visible');
+ },
+
+ handleConfirmDownloadCancel: function ai_handleConfirmDownloadCancel(e) {
+ e && e.preventDefault();
+ var dialog = this.downloadCancelDialog,
+ manifestURL = dialog.dataset.manifest;
+ if (manifestURL) {
+ var app = Applications.getByManifestURL(manifestURL);
+ app && app.cancelDownload();
+ }
+
+ this.hideDownloadCancelDialog();
+ },
+
+ handleCancelDownloadCancel: function ai_handleCancelDownloadCancel(e) {
+ e && e.preventDefault();
+ this.hideDownloadCancelDialog();
+ },
+
+ hideDownloadCancelDialog: function() {
+ var dialog = this.downloadCancelDialog;
+ dialog.classList.remove('visible');
+ delete dialog.dataset.manifest;
+ }
+};
+
+AppInstallManager.init();
diff --git a/apps/system/js/applications.js b/apps/system/js/applications.js
new file mode 100644
index 0000000..f1e286f
--- /dev/null
+++ b/apps/system/js/applications.js
@@ -0,0 +1,99 @@
+/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+// Application module handles the information of apps on behalf of other
+// modules.
+
+var Applications = {
+ installedApps: {},
+ ready: false,
+ init: function a_init() {
+ var self = this;
+ var apps = navigator.mozApps;
+
+ var getAllApps = function getAllApps() {
+ navigator.mozApps.mgmt.getAll().onsuccess = function mozAppGotAll(evt) {
+ var apps = evt.target.result;
+ apps.forEach(function(app) {
+ self.installedApps[app.manifestURL] = app;
+ // TODO Followup for retrieving homescreen & comms app
+ });
+
+ self.ready = true;
+ self.fireApplicationReadyEvent();
+ };
+ };
+
+ // We need to wait for the chrome shell to let us know when it's ok to
+ // launch activities. This prevents race conditions.
+ // The event does not fire again when we reload System app in on
+ // B2G Desktop, so we save the information into sessionStorage.
+ if (window.sessionStorage.getItem('webapps-registry-ready')) {
+ getAllApps();
+ } else {
+ window.addEventListener('mozChromeEvent', function mozAppReady(event) {
+ if (event.detail.type != 'webapps-registry-ready')
+ return;
+
+ window.sessionStorage.setItem('webapps-registry-ready', 'yes');
+ window.removeEventListener('mozChromeEvent', mozAppReady);
+
+ getAllApps();
+ });
+ }
+
+ apps.mgmt.oninstall = function a_install(evt) {
+ var newapp = evt.application;
+ self.installedApps[newapp.manifestURL] = newapp;
+
+ self.fireApplicationInstallEvent(newapp);
+ };
+
+ apps.mgmt.onuninstall = function a_uninstall(evt) {
+ var deletedapp = evt.application;
+ delete self.installedApps[deletedapp.manifestURL];
+
+ self.fireApplicationUninstallEvent(deletedapp);
+ };
+ },
+
+ getByManifestURL: function a_getByManifestURL(manifestURL) {
+ if (manifestURL in this.installedApps) {
+ return this.installedApps[manifestURL];
+ }
+
+ return null;
+ },
+
+ fireApplicationReadyEvent: function a_fireAppReadyEvent() {
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('applicationready',
+ /* canBubble */ true, /* cancelable */ false,
+ { applications: this.installedApps });
+ window.dispatchEvent(evt);
+ },
+
+ // We need to dispatch the following events because
+ // mozApps is not doing so right now.
+ // ref: https://bugzilla.mozilla.org/show_bug.cgi?id=731746
+
+ fireApplicationInstallEvent: function a_fireApplicationInstallEvent(app) {
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('applicationinstall',
+ /* canBubble */ true, /* cancelable */ false,
+ { application: app });
+ window.dispatchEvent(evt);
+ },
+
+ fireApplicationUninstallEvent: function a_fireApplicationUninstallEvent(app) {
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('applicationuninstall',
+ /* canBubble */ true, /* cancelable */ false,
+ { application: app });
+ window.dispatchEvent(evt);
+ }
+};
+
+Applications.init();
diff --git a/apps/system/js/attention_screen.js b/apps/system/js/attention_screen.js
new file mode 100644
index 0000000..c14dbe8
--- /dev/null
+++ b/apps/system/js/attention_screen.js
@@ -0,0 +1,289 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+'use strict';
+
+var AttentionScreen = {
+ get mainScreen() {
+ delete this.mainScreen;
+ return this.mainScreen = document.getElementById('screen');
+ },
+
+ get attentionScreen() {
+ delete this.attentionScreen;
+ return this.attentionScreen = document.getElementById('attention-screen');
+ },
+
+ get bar() {
+ delete this.bar;
+ return this.bar = document.getElementById('attention-bar');
+ },
+
+ isVisible: function as_isVisible() {
+ return this.attentionScreen.classList.contains('displayed');
+ },
+
+ isFullyVisible: function as_isFullyVisible() {
+ return (this.isVisible() &&
+ !this.mainScreen.classList.contains('active-statusbar'));
+ },
+
+ init: function as_init() {
+ window.addEventListener('mozbrowseropenwindow', this.open.bind(this), true);
+ window.addEventListener('mozbrowserclose', this.close.bind(this), true);
+ window.addEventListener('mozbrowsererror', this.close.bind(this), true);
+ window.addEventListener('keyboardchange', this.resize.bind(this), true);
+ window.addEventListener('keyboardhide', this.resize.bind(this), true);
+
+ this.bar.addEventListener('click', this.show.bind(this));
+ window.addEventListener('home', this.hide.bind(this));
+ window.addEventListener('holdhome', this.hide.bind(this));
+ window.addEventListener('appwillopen', this.hide.bind(this));
+ },
+
+ resize: function as_resize(evt) {
+ if (evt.type == 'keyboardchange') {
+ if (!this.isFullyVisible())
+ return;
+
+ this.attentionScreen.style.height =
+ window.innerHeight - evt.detail.height + 'px';
+ } else if (evt.type == 'keyboardhide') {
+ // We still need to reset the height property even when the attention
+ // screen is not fully visible, or it will overrides the height
+ // we defined with #attention-screen.status-mode
+ this.attentionScreen.style.height = '';
+ }
+ },
+
+ // show the attention screen overlay with newly created frame
+ open: function as_open(evt) {
+ if (evt.detail.features != 'attention')
+ return;
+
+ // stopPropagation means we are not allowing
+ // Popup Manager to handle this event
+ evt.stopPropagation();
+
+ // Canceling any full screen web content
+ if (document.mozFullScreen)
+ document.mozCancelFullScreen();
+
+ // Check if the app has the permission to open attention screens
+ var manifestURL = evt.target.getAttribute('mozapp');
+ var app = Applications.getByManifestURL(manifestURL);
+
+ if (!app || !this._hasAttentionPermission(app))
+ return;
+
+ // Hide sleep menu/list menu if it is shown now
+ ListMenu.hide();
+ SleepMenu.hide();
+
+ // We want the user attention, so we need to turn the screen on
+ // if it's off. The lockscreen will grab the focus when we do that
+ // so we need to do it before adding the new iframe to the dom
+ if (!ScreenManager.screenEnabled)
+ ScreenManager.turnScreenOn();
+
+ var attentionFrame = evt.detail.frameElement;
+ attentionFrame.dataset.frameType = 'attention';
+ attentionFrame.dataset.frameName = evt.detail.name;
+ attentionFrame.dataset.frameOrigin = evt.target.dataset.frameOrigin;
+
+ // We would like to put the dialer call screen on top of all other
+ // attention screens by ensure it is the last iframe in the DOM tree
+ if (this._hasTelephonyPermission(app)) {
+ this.attentionScreen.appendChild(attentionFrame);
+ } else {
+ this.attentionScreen.insertBefore(attentionFrame,
+ this.bar.nextElementSibling);
+ }
+
+ this._updateAttentionFrameVisibility();
+
+ // Make the overlay visible if we are not displayed yet.
+ // alternatively, if the newly appended frame is the visible frame
+ // and we are in the status bar mode, expend to full screen mode.
+ if (!this.isVisible()) {
+ this.attentionScreen.classList.add('displayed');
+ this.mainScreen.classList.add('attention');
+ this.dispatchEvent('attentionscreenshow', {
+ origin: attentionFrame.dataset.frameOrigin
+ });
+ } else if (!this.isFullyVisible() &&
+ this.attentionScreen.lastElementChild === attentionFrame) {
+ this.show();
+ }
+ },
+
+ // Make sure visibililty state of all attention screens are set correctly
+ _updateAttentionFrameVisibility: function as_updateAtteFrameVisibility() {
+ var frames = this.attentionScreen.querySelectorAll('iframe');
+ var i = frames.length - 1;
+
+ // In case someone call this function w/o checking for frame first
+ if (i < 0)
+ return;
+
+ // set the last one in the DOM to visible
+ // The setTimeout() and the closure is used to workaround
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=810431
+ setTimeout(function(frame) {
+ frame.setVisible(true);
+ frame.focus();
+ }, 0, frames[i]);
+
+ while (i--) {
+ // The setTimeout() and the closure is used to workaround
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=810431
+ setTimeout(function(frame) {
+ frame.setVisible(false);
+ frame.blur();
+ }, 0, frames[i]);
+ }
+ },
+
+ // close the attention screen overlay
+ close: function as_close(evt) {
+ if (!'frameType' in evt.target.dataset ||
+ evt.target.dataset.frameType !== 'attention' ||
+ (evt.type === 'mozbrowsererror' && evt.detail.type !== 'fatal'))
+ return;
+
+ // Remove the frame
+ var origin = evt.target.dataset.frameOrigin;
+ this.attentionScreen.removeChild(evt.target);
+
+ // We've just removed the focused window leaving the system
+ // without any focused window, let's fix this.
+ window.focus();
+
+ // if there are other attention frames,
+ // we need to update the visibility and show() the overlay.
+ if (this.attentionScreen.querySelectorAll('iframe').length) {
+ this._updateAttentionFrameVisibility();
+
+ this.dispatchEvent('attentionscreenclose', { origin: origin });
+
+ if (!this.isFullyVisible())
+ this.show();
+
+ return;
+ }
+
+ // There is no iframes left;
+ // we should close the attention screen overlay.
+
+ // If the the attention screen is closed during active-statusbar
+ // mode, we would need to leave that mode.
+ if (!this.isFullyVisible()) {
+ this.mainScreen.classList.remove('active-statusbar');
+ this.attentionScreen.classList.remove('status-mode');
+ this.dispatchEvent('status-inactive',
+ { origin: this.attentionScreen.lastElementChild.dataset.frameOrigin });
+ }
+
+ this.attentionScreen.classList.remove('displayed');
+ this.mainScreen.classList.remove('attention');
+ this.dispatchEvent('attentionscreenhide', { origin: origin });
+ },
+
+ // expend the attention screen overlay to full screen
+ show: function as_show() {
+ // leaving "status-mode".
+ this.attentionScreen.classList.remove('status-mode');
+ // there shouldn't be a transition from "status-mode" to "active-statusbar"
+ this.attentionScreen.style.transition = 'none';
+
+ var self = this;
+ setTimeout(function nextTick() {
+ self.attentionScreen.style.transition = '';
+
+ // leaving "active-statusbar" mode,
+ // with a transform: translateY() slide down transition.
+ self.mainScreen.classList.remove('active-statusbar');
+ self.dispatchEvent('status-inactive', {
+ origin: self.attentionScreen.lastElementChild.dataset.frameOrigin
+ });
+ });
+ },
+
+ // shrink the attention screen overlay to status bar
+ // invoked when we get a "home" event
+ hide: function as_hide() {
+ if (!this.isFullyVisible())
+ return;
+
+ // entering "active-statusbar" mode,
+ // with a transform: translateY() slide up transition.
+ this.mainScreen.classList.add('active-statusbar');
+
+ // The only way to hide attention screen is the home/holdhome event.
+ // So we don't fire any origin information here.
+ // The expected behavior is restore homescreen visibility to 'true'
+ // in the Window Manager.
+ this.dispatchEvent('status-active');
+
+ var attentionScreen = this.attentionScreen;
+ attentionScreen.addEventListener('transitionend', function trWait() {
+ attentionScreen.removeEventListener('transitionend', trWait);
+
+ // transition completed, entering "status-mode" (40px height iframe)
+ attentionScreen.classList.add('status-mode');
+ });
+ },
+
+ dispatchEvent: function as_dispatchEvent(name, detail) {
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent(name, true, true, detail);
+ window.dispatchEvent(evt);
+ },
+
+ // If an app with an active attention screen is switched to,
+ // we would need to cover it with it's attention screen.
+ // Invoked when displayedApp in Window Manager changes
+ // XXX should be replaced with a call that listens to appwillopen
+ // TBD: display the attention screen underneath other attention screens.
+ showForOrigin: function as_showForOrigin(origin) {
+ if (!this.isVisible() || this.isFullyVisible())
+ return;
+
+ var attentionFrame = this.attentionScreen.lastElementChild;
+ var frameOrigin = attentionFrame.dataset.frameOrigin;
+ if (origin === frameOrigin) {
+ this.show();
+ }
+ },
+
+ getAttentionScreenOrigins: function as_getAttentionScreenOrigins() {
+ var attentionScreen = this.attentionScreen;
+ var frames = this.attentionScreen.querySelectorAll('iframe');
+ var attentiveApps = [];
+ Array.prototype.forEach.call(frames, function pushFrame(frame) {
+ attentiveApps.push(frame.dataset.frameOrigin);
+ });
+ return attentiveApps;
+ },
+
+ _hasAttentionPermission: function as_hasAttentionPermission(app) {
+ var mozPerms = navigator.mozPermissionSettings;
+ if (!mozPerms)
+ return false;
+
+ var value = mozPerms.get('attention', app.manifestURL, app.origin, false);
+
+ return (value === 'allow');
+ },
+
+ _hasTelephonyPermission: function as_hasAttentionPermission(app) {
+ var mozPerms = navigator.mozPermissionSettings;
+ if (!mozPerms)
+ return false;
+
+ var value = mozPerms.get('telephony', app.manifestURL, app.origin, false);
+
+ return (value === 'allow');
+ }
+};
+
+AttentionScreen.init();
diff --git a/apps/system/js/authentication_dialog.js b/apps/system/js/authentication_dialog.js
new file mode 100644
index 0000000..3a893ee
--- /dev/null
+++ b/apps/system/js/authentication_dialog.js
@@ -0,0 +1,178 @@
+/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+// This module listens to mozbrowserusernameandpasswordrequired event.
+// It's for http authentication only.
+// XXX: ftp authentication will be implemented here but not supported yet.
+
+var AuthenticationDialog = {
+ // Used for element id access.
+ // e.g., 'authentication-dialog-alert-ok'
+ prefix: 'authentication-dialog-',
+
+ // DOM
+ elements: {},
+
+ // Get all elements when inited.
+ getAllElements: function ad_getAllElements() {
+ var elementsID = [
+ 'http-authentication', 'http-username-input', 'http-password-input',
+ 'http-authentication-message', 'http-authentication-ok',
+ 'http-authentication-cancel', 'title'
+ ];
+
+ var toCamelCase = function toCamelCase(str) {
+ return str.replace(/\-(.)/g, function replacer(str, p1) {
+ return p1.toUpperCase();
+ });
+ };
+
+ elementsID.forEach(function createElementRef(name) {
+ this.elements[toCamelCase(name)] =
+ document.getElementById(this.prefix + name);
+ }, this);
+
+ this.screen = document.getElementById('screen');
+ this.overlay = document.getElementById('dialog-overlay');
+ },
+
+ // Save the events returned by
+ // mozbrowserusernameandpasswordrequired for later use.
+ // The events are stored according to webapp origin
+ // e.g., 'http://uitest.gaiamobile.org': evt
+ currentEvents: {},
+
+ init: function ad_init() {
+ // Get all elements initially.
+ this.getAllElements();
+ var elements = this.elements;
+
+ // Bind events
+ window.addEventListener('mozbrowserusernameandpasswordrequired', this);
+ window.addEventListener('appopen', this);
+ window.addEventListener('appwillclose', this);
+ window.addEventListener('appterminated', this);
+ window.addEventListener('resize', this);
+ window.addEventListener('keyboardchange', this);
+ window.addEventListener('keyboardhide', this);
+
+ for (var id in elements) {
+ if (elements[id].tagName.toLowerCase() == 'button') {
+ elements[id].addEventListener('click', this);
+ }
+ }
+ },
+
+ // Default event handler
+ handleEvent: function ad_handleEvent(evt) {
+ var elements = this.elements;
+ switch (evt.type) {
+ case 'mozbrowserusernameandpasswordrequired':
+ if (evt.target.dataset.frameType != 'window')
+ return;
+
+ evt.preventDefault();
+ var origin = evt.target.dataset.frameOrigin;
+ this.currentEvents[origin] = evt;
+
+ if (origin == WindowManager.getDisplayedApp())
+ this.show(origin);
+ break;
+
+ case 'click':
+ if (evt.currentTarget === elements.httpAuthenticationCancel) {
+ this.cancelHandler();
+ } else {
+ this.confirmHandler();
+ }
+ break;
+
+ case 'appopen':
+ if (this.currentEvents[evt.detail.origin])
+ this.show(evt.detail.origin);
+ break;
+
+ case 'appwillclose':
+ // Do nothing if the app is closed at background.
+ if (evt.detail.origin !== this.currentOrigin)
+ return;
+
+ // Reset currentOrigin
+ this.hide();
+ break;
+
+ case 'appterminated':
+ if (this.currentEvents[evt.detail.origin])
+ delete this.currentEvents[evt.detail.origin];
+
+ break;
+
+ case 'resize':
+ case 'keyboardhide':
+ if (!this.currentOrigin)
+ return;
+
+ this.setHeight(window.innerHeight - StatusBar.height);
+ break;
+
+ case 'keyboardchange':
+ this.setHeight(window.innerHeight -
+ evt.detail.height - StatusBar.height);
+ break;
+ }
+ },
+
+ setHeight: function ad_setHeight(height) {
+ if (this.isVisible())
+ this.overlay.style.height = height + 'px';
+ },
+
+ show: function ad_show(origin) {
+ this.currentOrigin = origin;
+ var evt = this.currentEvents[origin];
+ var elements = this.elements;
+ this.screen.classList.add('authentication-dialog');
+ elements.httpAuthentication.classList.add('visible');
+ elements.title.textContent = evt.detail.host;
+ elements.httpAuthenticationMessage.textContent = evt.detail.realm;
+ elements.httpUsernameInput.value = '';
+ elements.httpPasswordInput.value = '';
+
+ this.setHeight(window.innerHeight - StatusBar.height);
+ },
+
+ hide: function ad_hide() {
+ this.elements.httpUsernameInput.blur();
+ this.elements.httpPasswordInput.blur();
+ this.currentOrigin = null;
+ this.elements.httpAuthentication.classList.remove('visible');
+ this.screen.classList.remove('authentication-dialog');
+ },
+
+ confirmHandler: function ad_confirmHandler() {
+ var elements = this.elements;
+ var evt = this.currentEvents[this.currentOrigin];
+ evt.detail.authenticate(elements.httpUsernameInput.value,
+ elements.httpPasswordInput.value);
+ elements.httpAuthentication.classList.remove('visible');
+ delete this.currentEvents[this.currentOrigin];
+ this.screen.classList.remove('authentication-dialog');
+ },
+
+ cancelHandler: function ad_cancelHandler() {
+ var evt = this.currentEvents[this.currentOrigin];
+ var elements = this.elements;
+ evt.detail.cancel();
+ elements.httpAuthentication.classList.remove('visible');
+ delete this.currentEvents[this.currentOrigin];
+ this.screen.classList.remove('authentication-dialog');
+ },
+
+ isVisible: function ad_isVisible() {
+ return this.screen.classList.contains('authentication-dialog');
+ }
+};
+
+AuthenticationDialog.init();
diff --git a/apps/system/js/background_service.js b/apps/system/js/background_service.js
new file mode 100644
index 0000000..117b556
--- /dev/null
+++ b/apps/system/js/background_service.js
@@ -0,0 +1,196 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+/*
+ Allow web apps to inject a tiny persistent background iframe
+ as the phone starts.
+*/
+var BackgroundServiceManager = (function bsm() {
+ /* We keep the references to background page iframes here.
+ The iframes will be append to body */
+ var frames = {};
+
+ /* The name of the background window open by background_page in
+ manifest. */
+ var AUTO_OPEN_BG_PAGE_NAME = 'background';
+
+ /* Init */
+ var init = function bsm_init() {
+ var applications = Applications.installedApps;
+ Object.keys(applications).forEach(function bsm_each(manifestURL) {
+ var app = applications[manifestURL];
+ if (!app.manifest.background_page)
+ return;
+
+ // XXX: this work as if background_page is always a path not a full URL.
+ var url = app.origin + app.manifest.background_page;
+ open(manifestURL, AUTO_OPEN_BG_PAGE_NAME, url);
+ });
+ };
+
+ /* mozbrowseropenwindow */
+ window.addEventListener('mozbrowseropenwindow', function bsm_winopen(evt) {
+ if (evt.detail.features !== 'background')
+ return;
+
+ // stopPropagation means we are not allowing
+ // Popup Manager to handle this event
+ evt.stopPropagation();
+
+ var manifestURL = evt.target.getAttribute('mozapp');
+ var detail = evt.detail;
+
+ open(manifestURL, detail.name, detail.url, detail.frameElement);
+ }, true);
+
+ /* mozbrowserclose */
+ window.addEventListener('mozbrowserclose', function bsm_winclose(evt) {
+ if (!'frameType' in evt.target.dataset ||
+ evt.target.dataset.frameType !== 'background')
+ return;
+
+ var manifestURL = evt.target.getAttribute('mozapp');
+
+ close(manifestURL, evt.target.dataset.frameName);
+ }, true);
+
+ /* mozbrowsererror */
+ window.addEventListener('mozbrowsererror', function bsm_winclose(evt) {
+ if (!'frameType' in evt.target.dataset ||
+ evt.target.dataset.frameType !== 'background' ||
+ evt.detail.type !== 'fatal')
+ return;
+
+ var target = evt.target;
+ var manifestURL = target.getAttribute('mozapp');
+
+ // This bg service has just crashed, clean up the frame
+ var name = target.dataset.frameName;
+ close(manifestURL, name);
+ }, true);
+
+ /* OnInstall */
+ window.addEventListener('applicationinstall', function bsm_oninstall(evt) {
+ var app = evt.detail.application;
+ var origin = app.origin;
+ if (!app.manifest.background_page)
+ return;
+
+ // XXX: this work as if background_page is always a path not a full URL.
+ var url = origin + app.manifest.background_page;
+ open(manifestURL, AUTO_OPEN_BG_PAGE_NAME, url);
+ });
+
+ /* OnUninstall */
+ window.addEventListener('applicationuninstall', function bsm_oninstall(evt) {
+ var app = evt.detail.application;
+ close(app.manifestURL);
+ });
+
+ /* Check if the app has the permission to open a background page */
+ var hasBackgroundPermission = function bsm_checkPermssion(app) {
+ var mozPerms = navigator.mozPermissionSettings;
+ if (!mozPerms)
+ return false;
+
+ var value = mozPerms.get('backgroundservice', app.manifestURL,
+ app.origin, false);
+
+ return (value === 'allow');
+ };
+
+ /* The open function is responsible of containing the iframe */
+ var open = function bsm_open(manifestURL, name, url, frame) {
+ var app = Applications.getByManifestURL(manifestURL);
+ if (!app || !hasBackgroundPermission(app))
+ return false;
+
+ if (frames[manifestURL] && frames[manifestURL][name]) {
+ console.error('Window with the same name is there but Gecko ' +
+ ' failed to use it. See bug 766873. origin: "' + origin +
+ '", name: "' + name + '".');
+ return false;
+ }
+
+ if (!frame) {
+ frame = document.createElement('iframe');
+
+ // If we have a frame element, it's provided by mozbrowseropenwindow, and
+ // it has the mozbrowser, mozapp, and src attributes set already.
+ frame.setAttribute('mozbrowser', 'mozbrowser');
+ frame.setAttribute('mozapp', manifestURL);
+ frame.setAttribute('name', name);
+
+ var appName = app.manifest.name;
+ frame.setAttribute('remote', 'true');
+ console.info('%%%%% Launching', appName, 'bg service as remote (OOP)');
+ frame.src = url;
+ }
+ frame.className = 'backgroundWindow';
+ frame.dataset.frameType = 'background';
+ frame.dataset.frameName = name;
+
+ if (!frames[manifestURL])
+ frames[manifestURL] = {};
+ frames[manifestURL][name] = frame;
+
+ document.body.appendChild(frame);
+
+ // Background services should load in the background.
+ //
+ // (The funky setTimeout(0) is to work around
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=810431 .)
+ setTimeout(function() { frame.setVisible(false) }, 0);
+
+ return true;
+ };
+
+ /* The close function will remove the iframe from DOM and
+ delete the reference */
+ var close = function bsm_close(manifestURL, name) {
+ if (!frames[manifestURL])
+ return false;
+
+ if (typeof name == 'undefined') {
+ // Close all windows
+ Object.keys(frames[manifestURL]).forEach(function closeEach(name) {
+ document.body.removeChild(frames[manifestURL][name]);
+ frames[manifestURL][name] = null;
+ });
+ delete frames[manifestURL];
+ return true;
+ }
+
+ // Close one window
+ var frame = frames[manifestURL][name];
+ if (!frame)
+ return false;
+
+ document.body.removeChild(frame);
+ delete frames[manifestURL][name];
+
+ if (!Object.keys(frames[manifestURL]).length)
+ delete frames[manifestURL];
+ return true;
+ };
+
+ /* start initialization */
+ if (Applications.ready) {
+ init();
+ } else {
+ window.addEventListener('applicationready',
+ function bsm_appListReady(event) {
+ window.removeEventListener('applicationready', bsm_appListReady);
+ init();
+ });
+ }
+
+ /* Return the public APIs */
+ return {
+ 'open': open,
+ 'close': close
+ };
+}());
+
diff --git a/apps/system/js/battery_manager.js b/apps/system/js/battery_manager.js
new file mode 100644
index 0000000..d2eacf8
--- /dev/null
+++ b/apps/system/js/battery_manager.js
@@ -0,0 +1,277 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var BatteryManager = {
+ TOASTER_TIMEOUT: 5000,
+ TRANSITION_SPEED: 1.8,
+ TRANSITION_FRACTION: 0.30,
+
+ AUTO_SHUTDOWN_LEVEL: 0.02,
+ EMPTY_BATTERY_LEVEL: 0.1,
+
+ _battery: window.navigator.battery,
+ _notification: null,
+
+ getAllElements: function bm_getAllElements() {
+ this.screen = document.getElementById('screen');
+ this.overlay = document.getElementById('system-overlay');
+ this.notification = document.getElementById('battery');
+ },
+
+ checkBatteryDrainage: function bm_checkBatteryDrainage() {
+ var battery = this._battery;
+ if (!battery)
+ return;
+
+ if (battery.level <= this.AUTO_SHUTDOWN_LEVEL)
+ SleepMenu.startPowerOff(false);
+ },
+
+ init: function bm_init() {
+ this.getAllElements();
+ var battery = this._battery;
+ if (battery) {
+ // When the device is booted, check if the battery is drained.
+ // If so, SleepMenu.startPowerOff() would be called.
+ this.checkBatteryDrainage();
+
+ battery.addEventListener('levelchange', this);
+ battery.addEventListener('chargingchange', this);
+ }
+ window.addEventListener('screenchange', this);
+ this._toasterGD = new GestureDetector(this.notification);
+ ['mousedown', 'swipe'].forEach(function(evt) {
+ this.notification.addEventListener(evt, this);
+ }, this);
+
+ this._screenOn = true;
+ this._wasEmptyBatteryNotificationDisplayed = false;
+
+ this.displayIfNecessary();
+ },
+
+ handleEvent: function bm_handleEvent(evt) {
+ switch (evt.type) {
+ case 'screenchange':
+ this._screenOn = evt.detail.screenEnabled;
+ this.displayIfNecessary();
+ break;
+
+ case 'levelchange':
+ var battery = this._battery;
+ if (!battery)
+ return;
+
+ this.checkBatteryDrainage();
+ this.displayIfNecessary();
+
+ PowerSaveHandler.onBatteryChange();
+ break;
+ case 'chargingchange':
+ PowerSaveHandler.onBatteryChange();
+
+ var battery = this._battery;
+ // We turn the screen on if needed in order to let
+ // the user knows the device is charging
+
+ if (battery && battery.charging) {
+ this.hide();
+ this._wasEmptyBatteryNotificationDisplayed = false;
+
+ if (!this._screenOn) {
+ ScreenManager.turnScreenOn();
+ }
+ } else {
+ this.displayIfNecessary();
+ }
+ break;
+
+ case 'mousedown':
+ this.mousedown(evt);
+ break;
+ case 'swipe':
+ this.swipe(evt);
+ break;
+ }
+ },
+
+ _shouldWeDisplay: function bm_shouldWeDisplay() {
+ var battery = this._battery;
+ if (!battery) {
+ return false;
+ }
+
+ return (!this._wasEmptyBatteryNotificationDisplayed &&
+ !battery.charging &&
+ battery.level <= this.EMPTY_BATTERY_LEVEL &&
+ this._screenOn);
+ },
+
+ displayIfNecessary: function bm_display() {
+ if (! this._shouldWeDisplay()) {
+ return;
+ }
+
+ // we know it's here, it's checked in shouldWeDisplay()
+ var level = this._battery.level;
+
+ this.overlay.classList.add('battery');
+
+ this._toasterGD.startDetecting();
+ this._wasEmptyBatteryNotificationDisplayed = true;
+
+ if (this._toasterTimeout) {
+ clearTimeout(this._toasterTimeout);
+ }
+
+ this._toasterTimeout = setTimeout(this.hide.bind(this),
+ this.TOASTER_TIMEOUT);
+ },
+
+ hide: function bm_hide() {
+ var overlayCss = this.overlay.classList;
+ if (overlayCss.contains('battery')) {
+ this.overlay.classList.remove('battery');
+ this._toasterTimeout = null;
+ this._toasterGD.stopDetecting();
+ }
+ },
+
+ // Swipe handling
+ mousedown: function bm_mousedown(evt) {
+ evt.preventDefault();
+ this._containerWidth = this.overlay.clientWidth;
+ },
+
+ swipe: function bm_swipe(evt) {
+ var detail = evt.detail;
+ var distance = detail.start.screenX - detail.end.screenX;
+ var fastEnough = Math.abs(detail.vx) > this.TRANSITION_SPEED;
+ var farEnough = Math.abs(distance) >
+ this._containerWidth * this.TRANSITION_FRACTION;
+
+ // If the swipe distance is too short or swipe speed is too slow,
+ // do nothing.
+ if (!(farEnough || fastEnough))
+ return;
+
+ var self = this;
+ this.notification.addEventListener('animationend', function animationend() {
+ self.notification.removeEventListener('animationend', animationend);
+ self.notification.classList.remove('disappearing');
+ self.hide();
+ });
+ this.notification.classList.add('disappearing');
+ }
+};
+
+var PowerSaveHandler = (function PowerSaveHandler() {
+
+ var _powerSaveResume = {};
+ var _powerSaveEnabled = false;
+ var _states = {
+ 'wifi.enabled' : false,
+ 'ril.data.enabled' : false,
+ 'bluetooth.enabled' : false,
+ 'geolocation.enabled' : false
+ };
+
+ function init() {
+ SettingsListener.observe('powersave.enabled', false,
+ function sl_getPowerSave(value) {
+ var enabled = value;
+ if (enabled) {
+ enablePowerSave();
+ } else {
+ disablePowerSave();
+ }
+ _powerSaveEnabled = enabled;
+ });
+
+ // Monitor the states of various modules
+ for (var j in _states) {
+ SettingsListener.observe(j, true, function getState(state, value) {
+ _states[state] = value;
+ }.bind(null, j));
+ }
+ }
+
+ // XXX Break down obj keys in a for each loop because mozSettings
+ // does not currently supports multiple keys in one set()
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=779381
+ function setMozSettings(keypairs) {
+ var setlock = SettingsListener.getSettingsLock();
+ for (var key in keypairs) {
+ var obj = {};
+ obj[key] = keypairs[key];
+ setlock.set(obj);
+ }
+ }
+
+ function enablePowerSave() {
+ // Keep the original states of various modules
+ for (var j in _states) {
+ _powerSaveResume[j] = _states[j];
+ }
+
+ var settingsToSet = {
+ // Turn off Wifi
+ 'wifi.enabled' : false,
+ // Turn off Data
+ 'ril.data.enabled' : false,
+ // Turn off Bluetooth
+ 'bluetooth.enabled' : false,
+ // Turn off Geolocation
+ 'geolocation.enabled' : false
+ };
+
+ setMozSettings(settingsToSet);
+ }
+
+ function disablePowerSave() {
+
+ var settingsToSet = {};
+
+ for (var state in _powerSaveResume) {
+ if (_powerSaveResume[state] == true)
+ settingsToSet[state] = true;
+ }
+
+ setMozSettings(settingsToSet);
+ }
+
+ function onBatteryChange() {
+ var battery = BatteryManager._battery;
+
+ if (battery.charging) {
+ if (_powerSaveEnabled)
+ setMozSettings({'powersave.enabled' : false});
+
+ return;
+ }
+
+ SettingsListener.observe('powersave.threshold', 0,
+ function getThreshold(value) {
+ if (battery.level <= value && !_powerSaveEnabled) {
+ setMozSettings({'powersave.enabled' : true});
+ return;
+ }
+
+ if (value != 0 && battery.level > value && _powerSaveEnabled) {
+ setMozSettings({'powersave.enabled' : false});
+ return;
+ }
+ });
+ }
+
+ return {
+ init: init,
+ onBatteryChange: onBatteryChange
+ };
+})();
+
+// init PowerSaveHandler first, since it will be used by BatteryManager
+PowerSaveHandler.init();
+BatteryManager.init();
diff --git a/apps/system/js/bluetooth.js b/apps/system/js/bluetooth.js
new file mode 100644
index 0000000..3a77cc7
--- /dev/null
+++ b/apps/system/js/bluetooth.js
@@ -0,0 +1,136 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var Bluetooth = {
+
+ /* this property store a reference of the default adapter */
+ defaultAdapter: null,
+
+ /* keep a global connected property here */
+ connected: false,
+
+ init: function bt_init() {
+ if (!window.navigator.mozSettings)
+ return;
+
+ var bluetooth = window.navigator.mozBluetooth;
+
+ SettingsListener.observe('bluetooth.enabled', true, function(value) {
+ if (!bluetooth) {
+ // roll back the setting value to notify the UIs
+ // that Bluetooth interface is not available
+ if (value) {
+ SettingsListener.getSettingsLock().set({
+ 'bluetooth.enabled': false
+ });
+ }
+ return;
+ }
+ });
+
+ var self = this;
+ // when bluetooth adapter is ready, emit event to notify QuickSettings
+ // and try to get defaultAdapter at this moment
+ bluetooth.onadapteradded = function bt_onAdapterAdded() {
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('bluetooth-adapter-added',
+ /* canBubble */ true, /* cancelable */ false, null);
+ window.dispatchEvent(evt);
+ self.initDefaultAdapter();
+ };
+ // if bluetooth is enabled in booting time, try to get adapter now
+ this.initDefaultAdapter();
+
+ // when bluetooth is really disabled, emit event to notify QuickSettings
+ bluetooth.ondisabled = function bt_onDisabled() {
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('bluetooth-disabled',
+ /* canBubble */ true, /* cancelable */ false, null);
+ window.dispatchEvent(evt);
+ };
+
+ /* for v1, we only support two use cases for bluetooth connection:
+ * 1. connecting with a headset
+ * 2. transfering a file to/from another device
+ * So we need to monitor their event messages to know we are (aren't)
+ * connected, then summarize to an event and dispatch to StatusBar
+ */
+
+ // In headset connected case:
+ navigator.mozSetMessageHandler('bluetooth-hfp-status-changed',
+ this.updateConnected.bind(this)
+ );
+
+ /* In file transfering case:
+ * since System Message can't be listened in two js files within a app,
+ * so we listen here but dispatch events to bluetooth_transfer.js
+ * when getting bluetooth file transfer start/complete system messages
+ */
+ var self = this;
+ navigator.mozSetMessageHandler('bluetooth-opp-transfer-start',
+ function bt_fileTransferUpdate(transferInfo) {
+ self.updateConnected();
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('bluetooth-opp-transfer-start',
+ /* canBubble */ true, /* cancelable */ false,
+ {transferInfo: transferInfo});
+ window.dispatchEvent(evt);
+ }
+ );
+
+ navigator.mozSetMessageHandler('bluetooth-opp-transfer-complete',
+ function bt_fileTransferUpdate(transferInfo) {
+ self.updateConnected();
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('bluetooth-opp-transfer-complete',
+ /* canBubble */ true, /* cancelable */ false,
+ {transferInfo: transferInfo});
+ window.dispatchEvent(evt);
+ }
+ );
+
+ },
+
+ // Get adapter for BluetoothTransfer when everytime bluetooth is enabled
+ initDefaultAdapter: function bt_initDefaultAdapter() {
+ var bluetooth = window.navigator.mozBluetooth;
+ var self = this;
+
+ if (!bluetooth || !bluetooth.enabled ||
+ !('getDefaultAdapter' in bluetooth))
+ return;
+
+ var req = bluetooth.getDefaultAdapter();
+ req.onsuccess = function bt_gotDefaultAdapter(evt) {
+ self.defaultAdapter = req.result;
+ };
+ },
+
+ updateConnected: function bt_updateConnected() {
+ var bluetooth = window.navigator.mozBluetooth;
+
+ if (!bluetooth || !('isConnected' in bluetooth))
+ return;
+
+ var wasConnected = this.connected;
+ this.connected =
+ bluetooth.isConnected(0x111E) || bluetooth.isConnected(0x1105);
+
+ if (wasConnected !== this.connected) {
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('bluetoothconnectionchange',
+ /* canBubble */ true, /* cancelable */ false,
+ {deviceConnected: this.connected});
+ window.dispatchEvent(evt);
+ }
+ },
+
+ // This function is called by external (BluetoothTransfer) for re-use adapter
+ getAdapter: function bt_getAdapter() {
+ return this.defaultAdapter;
+ }
+};
+
+Bluetooth.init();
diff --git a/apps/system/js/bluetooth_transfer.js b/apps/system/js/bluetooth_transfer.js
new file mode 100644
index 0000000..47483f7
--- /dev/null
+++ b/apps/system/js/bluetooth_transfer.js
@@ -0,0 +1,511 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+'use strict';
+
+var BluetoothTransfer = {
+ bannerContainer: null,
+ pairList: {
+ index: []
+ },
+ _deviceStorage: navigator.getDeviceStorage('sdcard'),
+ _debug: false,
+
+ get transferStatusList() {
+ delete this.transferStatusList;
+ return this.transferStatusList =
+ document.getElementById('bluetooth-transfer-status-list');
+ },
+
+ get banner() {
+ delete this.banner;
+ return this.banner = document.getElementById('system-banner');
+ },
+
+ init: function bt_init() {
+ // Bind message handler for transferring file callback
+ navigator.mozSetMessageHandler('bluetooth-opp-receiving-file-confirmation',
+ this.onReceivingFileConfirmation.bind(this)
+ );
+
+ // Listen to 'bluetooth-opp-transfer-start' from bluetooth.js
+ window.addEventListener('bluetooth-opp-transfer-start',
+ this.onUpdateProgress.bind(this, 'start')
+ );
+
+ navigator.mozSetMessageHandler('bluetooth-opp-update-progress',
+ this.onUpdateProgress.bind(this, 'progress')
+ );
+
+ // Listen to 'bluetooth-opp-transfer-complete' from bluetooth.js
+ window.addEventListener('bluetooth-opp-transfer-complete',
+ this.onTransferComplete.bind(this)
+ );
+ this.bannerContainer = this.banner.firstElementChild;
+ },
+
+ getDeviceName: function bt_getDeviceName(address) {
+ var _ = navigator.mozL10n.get;
+ var length = this.pairList.index.length;
+ for (var i = 0; i < length; i++) {
+ if (this.pairList.index[i].address == address)
+ return this.pairList.index[i].name;
+ }
+ return _('unknown-device');
+ },
+
+ getPairedDevice: function bt_getPairedDevice(callback) {
+ var adapter = Bluetooth.getAdapter();
+ if (adapter == null) {
+ var msg = 'Cannot get Bluetooth adapter.';
+ this.debug(msg);
+ return;
+ }
+ var self = this;
+ var req = adapter.getPairedDevices();
+ req.onsuccess = function bt_getPairedSuccess() {
+ self.pairList.index = req.result;
+ var length = self.pairList.index.length;
+ if (length == 0) {
+ var msg =
+ 'There is no paired device! Please pair your bluetooth device first.';
+ self.debug(msg);
+ return;
+ }
+ if (callback) {
+ callback();
+ }
+ };
+ req.onerror = function() {
+ var msg = 'Can not get paired devices from adapter.';
+ self.debug(msg);
+ };
+ },
+
+ debug: function bt_debug(msg) {
+ if (!this._debug)
+ return;
+
+ console.log('[System Bluetooth Transfer]: ' + msg);
+ },
+
+ humanizeSize: function bt_humanizeSize(bytes) {
+ var _ = navigator.mozL10n.get;
+ var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
+ var size, e;
+ if (bytes) {
+ e = Math.floor(Math.log(bytes) / Math.log(1024));
+ size = (bytes / Math.pow(1024, e)).toFixed(2);
+ } else {
+ e = 0;
+ size = '0.00';
+ }
+ return _('fileSize', {
+ size: size,
+ unit: _('byteUnit-' + units[e])
+ });
+ },
+
+ onReceivingFileConfirmation: function bt_onReceivingFileConfirmation(evt) {
+ // Prompt appears when a transfer request from a paired device is received.
+ var _ = navigator.mozL10n.get;
+
+ var fileSize = evt.fileLength;
+ var self = this;
+ var icon = 'style/bluetooth_transfer/images/icon_bluetooth.png';
+
+ // Check storage is available or not before the prompt.
+ this.checkStorageSpace(fileSize,
+ function checkStorageSpaceComplete(isStorageAvailable, errorMessage) {
+ if (isStorageAvailable) {
+ NotificationHelper.send(_('notification-fileTransfer-title'),
+ _('notification-fileTransfer-description'),
+ icon,
+ function() {
+ UtilityTray.hide();
+ self.showReceivePrompt(evt);
+ });
+ } else {
+ self.showStorageUnavaliablePrompt(errorMessage);
+ }
+ });
+ },
+
+ showReceivePrompt: function bt_showReceivePrompt(evt) {
+ var _ = navigator.mozL10n.get;
+
+ var address = evt.address;
+ var fileName = evt.fileName;
+ var fileSize = this.humanizeSize(evt.fileLength);
+ var cancel = {
+ title: _('deny'),
+ callback: this.declineReceive.bind(this, address)
+ };
+
+ var confirm = {
+ title: _('transfer'),
+ callback: this.acceptReceive.bind(this, address)
+ };
+
+ var deviceName = '';
+ var self = this;
+ this.getPairedDevice(function getPairedDeviceComplete() {
+ deviceName = self.getDeviceName(address);
+ CustomDialog.show(_('acceptFileTransfer'),
+ _('wantToReceiveFile',
+ { deviceName: deviceName,
+ fileName: fileName,
+ fileSize: fileSize }),
+ cancel, confirm);
+ });
+ },
+
+ declineReceive: function bt_declineReceive(address) {
+ CustomDialog.hide();
+ var adapter = Bluetooth.getAdapter();
+ if (adapter != null) {
+ adapter.confirmReceivingFile(address, false);
+ } else {
+ var msg = 'Cannot get adapter from system Bluetooth monitor.';
+ this.debug(msg);
+ }
+ },
+
+ acceptReceive: function bt_acceptReceive(address, fileSize) {
+ CustomDialog.hide();
+ var adapter = Bluetooth.getAdapter();
+ if (adapter != null) {
+ adapter.confirmReceivingFile(address, true);
+ } else {
+ var msg = 'Cannot get adapter from system Bluetooth monitor.';
+ this.debug(msg);
+ }
+ },
+
+ showStorageUnavaliablePrompt: function bt_showStorageUnavaliablePrompt(msg) {
+ var _ = navigator.mozL10n.get;
+ var confirm = {
+ title: _('confirm'),
+ callback: function() {
+ CustomDialog.hide();
+ }
+ };
+
+ var body = msg;
+ CustomDialog.show(_('cannotReceiveFile'), body, confirm);
+ },
+
+ checkStorageSpace: function bt_checkStorageSpace(fileSize, callback) {
+ if (!callback)
+ return;
+
+ var _ = navigator.mozL10n.get;
+ var storage = this._deviceStorage;
+
+ var availreq = storage.available();
+ availreq.onsuccess = function(e) {
+ switch (availreq.result) {
+ case 'available':
+ // skip down to the code below
+ break;
+ case 'unavailable':
+ callback(false, _('sdcard-not-exist'));
+ return;
+ case 'shared':
+ callback(false, _('sdcard-in-use'));
+ return;
+ default:
+ callback(false, _('unknown-error'));
+ return;
+ }
+
+ // If we get here, then the sdcard is available, so we need to find out
+ // if there is enough free space on it
+ var freereq = storage.freeSpace();
+ freereq.onsuccess = function() {
+ if (freereq.result >= fileSize)
+ callback(true, '');
+ else
+ callback(false, _('sdcard-no-space2'));
+ };
+ freereq.onerror = function() {
+ callback(false, _('cannotGetStorageState'));
+ };
+ };
+
+ availreq.onerror = function(e) {
+ callback(false, _('cannotGetStorageState'));
+ };
+ },
+
+ onUpdateProgress: function bt_onUpdateProgress(mode, evt) {
+ switch (mode) {
+ case 'start':
+ var transferInfo = evt.detail.transferInfo;
+ this.initProgress(transferInfo);
+ break;
+
+ case 'progress':
+ var address = evt.address;
+ var processedLength = evt.processedLength;
+ var fileLength = evt.fileLength;
+ var progress = 0;
+ if (fileLength == 0) {
+ //XXX: May need to handle unknow progress
+ } else if (processedLength > fileLength) {
+ // According Bluetooth spec.,
+ // the processed length is a referenced value only.
+ // XXX: If processed length is bigger than file length,
+ // show an unknown progress
+ } else {
+ progress = processedLength / fileLength;
+ }
+ this.updateProgress(progress, evt);
+ break;
+ }
+ },
+
+ initProgress: function bt_initProgress(evt) {
+ var _ = navigator.mozL10n.get;
+ // Create progress dynamically in notification center
+ var address = evt.address;
+ var transferMode =
+ (evt.received == true) ?
+ _('bluetooth-receiving-progress') : _('bluetooth-sending-progress');
+ var content =
+ '<img src="style/bluetooth_transfer/images/transfer.png" />' +
+ '<div class="bluetooth-transfer-progress">' + transferMode + '</div>' +
+ // XXX: Bug 804533 - [Bluetooth]
+ // Need sending/receiving icon for Bluetooth file transfer
+ '<progress value="0" max="1"></progress>';
+
+ var transferTask = document.createElement('div');
+ transferTask.id = 'bluetooth-transfer-status';
+ transferTask.className = 'notification';
+ transferTask.setAttribute('data-id', address);
+ transferTask.innerHTML = content;
+ transferTask.addEventListener('click',
+ this.onCancelTransferTask.bind(this));
+ this.transferStatusList.appendChild(transferTask);
+ },
+
+ updateProgress: function bt_updateProgress(value, evt) {
+ var address = evt.address;
+ var id = 'div[data-id="' + address + '"] progress';
+ var progressEl = this.transferStatusList.querySelector(id);
+ progressEl.value = value;
+ },
+
+ removeProgress: function bt_removeProgress(evt) {
+ var address = evt.address;
+ var id = 'div[data-id="' + address + '"]';
+ var finishedTask = this.transferStatusList.querySelector(id);
+ finishedTask.removeEventListener('click',
+ this.onCancelTransferTask.bind(this));
+ this.transferStatusList.removeChild(finishedTask);
+ },
+
+ showBanner: function bt_showBanner(isComplete) {
+ var _ = navigator.mozL10n.get;
+ var status = (isComplete) ? 'complete' : 'failed';
+ this.banner.addEventListener('animationend', function animationend() {
+ this.banner.removeEventListener('animationend', animationend);
+ this.banner.classList.remove('visible');
+ }.bind(this));
+ this.bannerContainer.textContent = _('bluetooth-file-transfer-result',
+ { status: status });
+ this.banner.classList.add('visible');
+ },
+
+ onCancelTransferTask: function bt_onCancelTransferTask(evt) {
+ var id = evt.target.dataset.id;
+ // Show confirm dialog for user to cancel transferring task
+ UtilityTray.hide();
+ this.showCancelTransferPrompt(id);
+ },
+
+ showCancelTransferPrompt: function bt_showCancelTransferPrompt(address) {
+ var _ = navigator.mozL10n.get;
+
+ var cancel = {
+ title: _('continue'),
+ callback: this.continueTransfer.bind(this)
+ };
+
+ var confirm = {
+ title: _('cancel'),
+ callback: this.cancelTransfer.bind(this, address)
+ };
+
+ CustomDialog.show(_('cancelFileTransfer'), _('cancelFileTransfer'),
+ cancel, confirm);
+ },
+
+ continueTransfer: function bt_continueTransfer() {
+ CustomDialog.hide();
+ },
+
+ cancelTransfer: function bt_cancelTransfer(address) {
+ CustomDialog.hide();
+ var adapter = Bluetooth.getAdapter();
+ if (adapter != null) {
+ adapter.stopSendingFile(address);
+ } else {
+ var msg = 'Cannot get adapter from system Bluetooth monitor.';
+ this.debug(msg);
+ }
+ },
+
+ onTransferComplete: function bt_onTransferComplete(evt) {
+ var transferInfo = evt.detail.transferInfo;
+ var _ = navigator.mozL10n.get;
+ // Remove transferring progress
+ this.removeProgress(transferInfo);
+ var fileName =
+ (transferInfo.fileName) ? transferInfo.fileName : _('unknown-file');
+ var icon = 'style/bluetooth_transfer/images/icon_bluetooth.png';
+ // Show banner and notification
+ if (transferInfo.success == true) {
+ // Show completed message of transferred result on the banner
+ this.showBanner(true);
+ if (transferInfo.received) {
+ // Received file can be opened only
+ // TODO: Need to modify the icon after visual provide
+ NotificationHelper.send(_('transferFinished-receivedSuccessful-title'),
+ fileName,
+ icon,
+ this.openReceivedFile.bind(this, transferInfo));
+ } else {
+ NotificationHelper.send(_('transferFinished-sentSuccessful-title'),
+ fileName,
+ icon);
+ }
+ } else {
+ // Show failed message of transferred result on the banner
+ this.showBanner(false);
+ if (transferInfo.received) {
+ NotificationHelper.send(_('transferFinished-receivedFailed-title'),
+ fileName,
+ icon);
+ } else {
+ NotificationHelper.send(_('transferFinished-sentFailed-title'),
+ fileName,
+ icon);
+ }
+ }
+ },
+
+ openReceivedFile: function bt_openReceivedFile(evt) {
+ // Launch the gallery with an open activity to view this specific photo
+ // XXX: The prefix file path should be refined when API is ready to provide
+ var filePath = 'downloads/bluetooth/' + evt.fileName;
+ var contentType = evt.contentType;
+ var storageType = 'sdcard';
+ var self = this;
+ var storage = navigator.getDeviceStorage(storageType);
+ var getreq = storage.get(filePath);
+
+ getreq.onerror = function() {
+ var msg = 'failed to get file:' +
+ filePath + getreq.error.name +
+ a.error.name;
+ self.debug(msg);
+ };
+
+ getreq.onsuccess = function() {
+ var file = getreq.result;
+ // When we got the file by storage type of "sdcard"
+ // use the file.type to replace the empty fileType which is given by API
+ var fileType = '';
+ var fileName = file.name;
+ if (contentType != '' && contentType != 'image/*') {
+ fileType = contentType;
+ } else {
+ var fileNameExtension =
+ fileName.substring(fileName.lastIndexOf('.') + 1);
+ if (file.type != '') {
+ fileType = file.type;
+ // Refine the file type to "audio/ogg" when the file format is *.ogg
+ if (fileType == 'video/ogg' &&
+ (fileNameExtension.indexOf('ogg') != -1)) {
+ fileType == 'audio/ogg';
+ }
+ } else {
+ // Parse Filename Extension to find out MIMETYPE
+ // Following formats are supported by Gallery and Music APPs
+ var imageFormatList = ['jpg', 'jpeg', 'png'];
+ var audioFormatList = ['mp3', 'ogg', 'aac', 'mp4', 'm4a'];
+ var imageFormatIndex = imageFormatList.indexOf(fileNameExtension);
+ switch (imageFormatIndex) {
+ case 0:
+ case 1:
+ // The file type of format *.jpg, *.jpeg should be "image/jpeg"
+ fileType = 'image/jpeg';
+ break;
+ case 2:
+ // The file type of format *.png should be "image/png"
+ fileType = 'image/png';
+ break;
+ }
+
+ var audioFormatIndex = audioFormatList.indexOf(fileNameExtension);
+ switch (audioFormatIndex) {
+ case 0:
+ // The file type of format *.mp3 should be "audio/mpeg"
+ fileType = 'audio/mpeg';
+ break;
+ case 1:
+ // The file type of format *.ogg should be "audio/ogg"
+ fileType = 'audio/ogg';
+ break;
+ case 2:
+ case 3:
+ case 4:
+ // The file type of format *.acc, *.mp4, *.m4a
+ // should be "audio/mp4"
+ fileType = 'audio/mp4';
+ break;
+ }
+ }
+ }
+
+ var a = new MozActivity({
+ name: 'open',
+ data: {
+ type: fileType,
+ blob: file,
+ // XXX: https://bugzilla.mozilla.org/show_bug.cgi?id=812098
+ // Pass the file name for Music APP since it can not open blob
+ filename: file.name
+ }
+ });
+
+ a.onerror = function(e) {
+ var msg = 'open activity error:' + a.error.name;
+ self.debug(msg);
+ // Cannot identify MIMETYPE
+ // So, show cannot open file dialog with unknow media type
+ UtilityTray.hide();
+ self.showUnknownMediaPrompt(fileName);
+ };
+ a.onsuccess = function(e) {
+ var msg = 'open activity onsuccess';
+ self.debug(msg);
+ };
+ };
+ },
+
+ showUnknownMediaPrompt: function bt_showUnknownMediaPrompt(fileName) {
+ var _ = navigator.mozL10n.get;
+ var confirm = {
+ title: _('confirm'),
+ callback: function() {
+ CustomDialog.hide();
+ }
+ };
+
+ var body = _('unknownMediaTypeToOpen') + ' ' + fileName;
+ CustomDialog.show(_('cannotOpenFile'), body, confirm);
+ }
+};
+
+BluetoothTransfer.init();
diff --git a/apps/system/js/bootstrap.js b/apps/system/js/bootstrap.js
new file mode 100644
index 0000000..21e2238
--- /dev/null
+++ b/apps/system/js/bootstrap.js
@@ -0,0 +1,82 @@
+/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+window.addEventListener('load', function startup() {
+ function safelyLaunchFTU() {
+ WindowManager.retrieveHomescreen(WindowManager.retrieveFTU);
+ }
+
+ if (Applications.ready) {
+ safelyLaunchFTU();
+ } else {
+ window.addEventListener('applicationready', function appListReady(event) {
+ window.removeEventListener('applicationready', appListReady);
+ safelyLaunchFTU();
+ });
+ }
+
+ window.addEventListener('ftudone', function doneWithFTU() {
+ window.removeEventListener('ftudone', doneWithFTU);
+
+ var lock = window.navigator.mozSettings.createLock();
+ lock.set({
+ 'gaia.system.checkForUpdates': true
+ });
+ });
+
+ SourceView.init();
+ Shortcuts.init();
+ ScreenManager.turnScreenOn();
+
+ // We need to be sure to get the focus in order to wake up the screen
+ // if the phone goes to sleep before any user interaction.
+ // Apparently it works because no other window has the focus at this point.
+ window.focus();
+
+ // This is code copied from
+ // http://dl.dropbox.com/u/8727858/physical-events/index.html
+ // It appears to workaround the Nexus S bug where we're not
+ // getting orientation data. See:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=753245
+ // It seems it needs to be in both window_manager.js and bootstrap.js.
+ function dumbListener2(event) {}
+ window.addEventListener('devicemotion', dumbListener2);
+
+ window.setTimeout(function() {
+ window.removeEventListener('devicemotion', dumbListener2);
+ }, 2000);
+});
+
+/* === Shortcuts === */
+/* For hardware key handling that doesn't belong to anywhere */
+var Shortcuts = {
+ init: function rm_init() {
+ window.addEventListener('keyup', this);
+ },
+
+ handleEvent: function rm_handleEvent(evt) {
+ if (!ScreenManager.screenEnabled || evt.keyCode !== evt.DOM_VK_F6)
+ return;
+
+ document.location.reload();
+ }
+};
+
+/* === Localization === */
+/* set the 'lang' and 'dir' attributes to <html> when the page is translated */
+window.addEventListener('localized', function onlocalized() {
+ document.documentElement.lang = navigator.mozL10n.language.code;
+ document.documentElement.dir = navigator.mozL10n.language.direction;
+});
+
+// Define the default background to use for all homescreens
+SettingsListener.observe(
+ 'wallpaper.image',
+ 'resources/images/backgrounds/default.png',
+ function setWallpaper(value) {
+ document.getElementById('screen').style.backgroundImage =
+ 'url(' + value + ')';
+ }
+);
diff --git a/apps/system/js/call_forwarding.js b/apps/system/js/call_forwarding.js
new file mode 100644
index 0000000..ee46def
--- /dev/null
+++ b/apps/system/js/call_forwarding.js
@@ -0,0 +1,45 @@
+/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+(function() {
+
+ // Must be in sync with nsIDOMMozMobileCFInfo interface.
+ var _cfReason = {
+ CALL_FORWARD_REASON_UNCONDITIONAL: 0,
+ CALL_FORWARD_REASON_MOBILE_BUSY: 1,
+ CALL_FORWARD_REASON_NO_REPLY: 2,
+ CALL_FORWARD_REASON_NOT_REACHABLE: 3
+ };
+ var _cfAction = {
+ CALL_FORWARD_ACTION_DISABLE: 0,
+ CALL_FORWARD_ACTION_ENABLE: 1,
+ CALL_FORWARD_ACTION_QUERY_STATUS: 2,
+ CALL_FORWARD_ACTION_REGISTRATION: 3,
+ CALL_FORWARD_ACTION_ERASURE: 4
+ };
+
+ var settings = window.navigator.mozSettings;
+ if (!settings) {
+ return;
+ }
+ var mobileconnection = window.navigator.mozMobileConnection;
+ if (!mobileconnection) {
+ return;
+ }
+
+ mobileconnection.addEventListener('cfstatechange', function(event) {
+ if (event &&
+ event.reason == _cfReason.CALL_FORWARD_REASON_UNCONDITIONAL) {
+ var enabled = false;
+ if (event.success &&
+ (event.action == _cfAction.CALL_FORWARD_ACTION_REGISTRATION ||
+ event.action == _cfAction.CALL_FORWARD_ACTION_ENABLE)) {
+ enabled = true;
+ }
+ settings.createLock().set({'ril.cf.enabled': enabled});
+ }
+ });
+
+})();
diff --git a/apps/system/js/captive_portal.js b/apps/system/js/captive_portal.js
new file mode 100644
index 0000000..b23cb0d
--- /dev/null
+++ b/apps/system/js/captive_portal.js
@@ -0,0 +1,73 @@
+/* -*Mode: js; js-indent-level: 2; indent-tabs-mode: nil -**/
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+'use strict';
+
+var CaptivePortalLogin = (function() {
+ var eventId;
+ var isManualConnect = false;
+ var settings = window.navigator.mozSettings;
+ var notification = null;
+ var wifiManager = window.navigator.mozWifiManager;
+ var _ = window.navigator.mozL10n.get;
+ var captiveNotification_onTap = null;
+
+ function handleLogin(id, url) {
+ //captive portal login needed
+ eventId = id;
+ var currentNetwork = wifiManager.connection.network;
+ var networkName = (currentNetwork && currentNetwork.ssid) ?
+ currentNetwork.ssid : '';
+ var message = _('captive-wifi-available', { networkName: networkName});
+ if (!isManualConnect) {
+ notification = NotificationScreen.addNotification({
+ id: id, title: '', text: message, icon: null
+ });
+ captiveNotification_onTap = function() {
+ notification.removeEventListener('tap', captiveNotification_onTap);
+ captiveNotification_onTap = null;
+ NotificationScreen.removeNotification(id);
+ new MozActivity({
+ name: 'view',
+ data: { type: 'url', url: url }
+ });
+ };
+ notification.addEventListener('tap', captiveNotification_onTap);
+ } else {
+ settings.createLock().set({'wifi.connect_via_settings': false});
+ new MozActivity({
+ name: 'view',
+ data: { type: 'url', url: url }
+ });
+ }
+ }
+
+ function handleLoginAbort(id) {
+ if (id === eventId && notification) {
+ if (notification.parentNode) {
+ if (captiveNotification_onTap) {
+ notification.removeEventListener('tap', captiveNotification_onTap);
+ captiveNotification_onTap = null;
+ }
+ NotificationScreen.removeNotification(id);
+ notification = null;
+ }
+ }
+ }
+
+ window.addEventListener('mozChromeEvent', function handleChromeEvent(e) {
+ switch (e.detail.type) {
+ case 'captive-portal-login':
+ handleLogin(e.detail.id, e.detail.url);
+ break;
+ case 'captive-portal-login-abort':
+ handleLoginAbort(e.detail.id);
+ break;
+ }
+ });
+
+ // Using settings API to know whether user is manually selecting
+ // wifi AP from settings app.
+ SettingsListener.observe('wifi.connect_via_settings', true, function(value) {
+ isManualConnect = value;
+ });
+})();
diff --git a/apps/system/js/cards_view.js b/apps/system/js/cards_view.js
new file mode 100644
index 0000000..8bce02b
--- /dev/null
+++ b/apps/system/js/cards_view.js
@@ -0,0 +1,676 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+//
+// CardsView is responsible for managing opened apps
+//
+
+'use strict';
+
+var CardsView = (function() {
+
+ //display icon of an app on top of app's card
+ var DISPLAY_APP_ICON = false;
+ var USER_DEFINED_ORDERING = false;
+ // If 'true', scrolling moves the list one card
+ // at time, and snaps the list so the current card
+ // is centered in the view
+ // If 'false', use free, physics-based scrolling
+ // (Gaia default)
+ var SNAPPING_SCROLLING = true;
+ // if 'true' user can close the app
+ // by dragging it upwards
+ var MANUAL_CLOSING = true;
+
+ var cardsView = document.getElementById('cards-view');
+ var screenElement = document.getElementById('screen');
+ var cardsList = cardsView.firstElementChild;
+ var displayedApp;
+ var runningApps;
+ // Unkillable apps which have attention screen now
+ var attentionScreenApps = [];
+ // Card which we are re-ordering now
+ var reorderedCard = null;
+ var currentDisplayed = 0;
+ // Timer between scrolling CardList further,
+ // when reordering Cards
+ var scrollWhileSortingTimer;
+ // We don't allow user to scroll CardList
+ // before the timer ticks while in reordering
+ // mode
+ var allowScrollingWhileSorting = false;
+ // Initial margin of the reordered card
+ var dragMargin = 0;
+ // Are we reordering or removing the card now?
+ var draggingCardUp = false;
+ // Are we moving card left or right?
+ var sortingDirection;
+ // List of sorted apps
+ var userSortedApps = [];
+ var HVGA = document.documentElement.clientWidth < 480;
+ var cardsViewShown = false;
+
+ // init events
+ var gd = new GestureDetector(cardsView);
+ gd.startDetecting();
+
+ // A list of all the URLs we've created via URL.createObjectURL which we
+ // haven't yet revoked.
+ var screenshotObjectURLs = [];
+
+ /*
+ * Returns an icon URI
+ *
+ * @param{String} the app's origin
+ */
+ function getIconURI(origin) {
+ var icons = runningApps[origin].manifest.icons;
+ if (!icons) {
+ return null;
+ }
+
+ var sizes = Object.keys(icons).map(function parse(str) {
+ return parseInt(str, 10);
+ });
+
+ sizes.sort(function(x, y) { return y - x; });
+
+ var index = sizes[(HVGA) ? sizes.length - 1 : 0];
+ var iconPath = icons[index];
+
+ if (iconPath.indexOf('data:') !== 0) {
+ iconPath = origin + iconPath;
+ }
+
+ return iconPath;
+ }
+
+ // Build and display the card switcher overlay
+ // Note that we rebuild the switcher each time we need it rather
+ // than trying to keep it in sync with app launches. Performance is
+ // not an issue here given that the user has to hold the HOME button down
+ // for one second before the switcher will appear.
+ function showCardSwitcher() {
+ if (cardSwitcherIsShown())
+ return;
+
+ // events to handle
+ window.addEventListener('lock', CardsView);
+
+ // Close utility tray if it is opened.
+ UtilityTray.hide(true);
+
+ // Apps info from WindowManager
+ displayedApp = WindowManager.getDisplayedApp();
+ currentDisplayed = 0;
+ runningApps = WindowManager.getRunningApps();
+
+ // Switch to homescreen
+ WindowManager.launch(null);
+ cardsViewShown = true;
+
+ // If user is not able to sort apps manualy,
+ // display most recetly active apps on the far left
+ if (!USER_DEFINED_ORDERING) {
+ var sortable = [];
+ for (var origin in runningApps)
+ sortable.push({origin: origin, app: runningApps[origin]});
+
+ sortable.sort(function(a, b) {
+ return b.app.launchTime - a.app.launchTime;
+ });
+ runningApps = {};
+
+ // I assume that object properties are enumerated in
+ // the same order they were defined.
+ // There is nothing about that in spec, but I've never
+ // seen any unexpected behavior.
+ sortable.forEach(function(element) {
+ runningApps[element.origin] = element.app;
+ });
+
+ // First add an item to the cardsList for each running app
+ for (var origin in runningApps) {
+ addCard(origin, runningApps[origin], function showCards() {
+ screenElement.classList.add('cards-view');
+ cardsView.classList.add('active');
+ });
+ }
+
+ } else { // user ordering
+
+ // first run
+ if (userSortedApps.length === 0) {
+ for (var origin in runningApps) {
+ userSortedApps.push(origin);
+ }
+ } else {
+ for (var origin in runningApps) {
+ // if we have some new app opened
+ if (userSortedApps.indexOf(origin) === -1) {
+ userSortedApps.push(origin);
+ }
+ }
+ }
+
+ userSortedApps.forEach(function(origin) {
+ addCard(origin, runningApps[origin], function showCards() {
+ screenElement.classList.add('cards-view');
+ cardsView.classList.add('active');
+ });
+ });
+
+ cardsView.addEventListener('contextmenu', CardsView);
+
+ }
+
+ if (SNAPPING_SCROLLING) {
+ cardsView.style.overflow = 'hidden'; //disabling native scrolling
+ }
+
+ if (SNAPPING_SCROLLING || MANUAL_CLOSING) {
+ cardsView.addEventListener('mousedown', CardsView);
+ }
+
+ // Make sure we're in portrait mode
+ screen.mozLockOrientation('portrait-primary');
+
+ // If there is a displayed app, take keyboard focus away
+ if (displayedApp)
+ runningApps[displayedApp].frame.blur();
+
+ function addCard(origin, app, displayedAppCallback) {
+ // Display card switcher background first to make user focus on the
+ // frame closing animation without disturbing by homescreen display.
+ if (displayedApp == origin && displayedAppCallback) {
+ setTimeout(displayedAppCallback);
+ }
+ // Not showing homescreen
+ if (app.frame.classList.contains('homescreen')) {
+ return;
+ }
+
+ // Build a card representation of each window.
+ // And add it to the card switcher
+ var card = document.createElement('li');
+ card.classList.add('card');
+ card.dataset.origin = origin;
+
+ //display app icon on the tab
+ if (DISPLAY_APP_ICON) {
+ var iconURI = getIconURI(origin);
+ if (iconURI) {
+ var appIcon = document.createElement('img');
+ appIcon.classList.add('appIcon');
+ appIcon.src = iconURI;
+ card.appendChild(appIcon);
+ }
+ }
+
+ var title = document.createElement('h1');
+ title.textContent = app.name;
+ card.appendChild(title);
+
+ var frameForScreenshot = app.iframe;
+
+ if (PopupManager.getPopupFromOrigin(origin)) {
+ var popupFrame = PopupManager.getPopupFromOrigin(origin);
+ frameForScreenshot = popupFrame;
+
+ var subtitle = document.createElement('p');
+ subtitle.textContent =
+ PopupManager.getOpenedOriginFromOpener(origin);
+ card.appendChild(subtitle);
+ card.classList.add('popup');
+ } else if (getOffOrigin(app.frame.dataset.url ?
+ app.frame.dataset.url : app.frame.src, origin)) {
+ var subtitle = document.createElement('p');
+ subtitle.textContent = getOffOrigin(app.frame.dataset.url ?
+ app.frame.dataset.url : app.frame.src, origin);
+ card.appendChild(subtitle);
+ }
+
+ if (TrustedUIManager.hasTrustedUI(origin)) {
+ var popupFrame = TrustedUIManager.getDialogFromOrigin(origin);
+ frameForScreenshot = popupFrame.frame;
+ var header = document.createElement('section');
+ header.setAttribute('role', 'region');
+ header.classList.add('skin-organic');
+ header.innerHTML = '<header><button><span class="icon icon-close">';
+ header.innerHTML += '</span></button><h1>' + popupFrame.name;
+ header.innerHTML += '</h1></header>';
+ card.appendChild(header);
+ card.classList.add('trustedui');
+ } else if (attentionScreenApps.indexOf(origin) == -1) {
+ var closeButton = document.createElement('div');
+ closeButton.classList.add('close-card');
+ card.appendChild(closeButton);
+ }
+
+ cardsList.appendChild(card);
+ // rect is the final size (considering CSS transform) of the card.
+ var rect = card.getBoundingClientRect();
+
+ // And then switch it with screenshots when one will be ready
+ // (instead of -moz-element backgrounds)
+ frameForScreenshot.getScreenshot(rect.width, rect.height).onsuccess =
+ function gotScreenshot(screenshot) {
+ if (screenshot.target.result) {
+ var objectURL = URL.createObjectURL(screenshot.target.result);
+ screenshotObjectURLs.push(objectURL);
+ card.style.backgroundImage = 'url(' + objectURL + ')';
+ }
+ };
+
+ // Set up event handling
+ // A click elsewhere in the card switches to that task
+ card.addEventListener('tap', runApp);
+ }
+ }
+
+ function runApp(e) {
+ // Handle close events
+ if (e.target.classList.contains('close-card')) {
+ var element = e.target.parentNode;
+ cardsList.removeChild(element);
+ closeApp(element, true);
+ return;
+ }
+
+ var origin = this.dataset.origin;
+ alignCard(currentDisplayed, function cardAligned() {
+ WindowManager.launch(origin);
+ });
+ }
+
+ function closeApp(element, removeImmediately) {
+ // Stop the app itself
+ WindowManager.kill(element.dataset.origin);
+
+ // Fix for non selectable cards when we remove the last card
+ // Described in https://bugzilla.mozilla.org/show_bug.cgi?id=825293
+ if (cardsList.children.length === currentDisplayed) {
+ currentDisplayed--;
+ }
+
+ // If there are no cards left, then dismiss the task switcher.
+ if (!cardsList.children.length)
+ hideCardSwitcher(removeImmediately);
+ }
+
+ function getOriginObject(url) {
+ var parser = document.createElement('a');
+ parser.href = url;
+
+ return {
+ protocol: parser.protocol,
+ hostname: parser.hostname,
+ port: parser.port
+ };
+ }
+
+ function getOffOrigin(src, origin) {
+ // Use src and origin as cache key
+ var cacheKey = JSON.stringify(Array.prototype.slice.call(arguments));
+ if (!getOffOrigin.cache[cacheKey]) {
+ var native = getOriginObject(origin);
+ var current = getOriginObject(src);
+ if (current.protocol == 'http:') {
+ // Display http:// protocol anyway
+ getOffOrigin.cache[cacheKey] = current.protocol + '//' +
+ current.hostname;
+ } else if (native.protocol == current.protocol &&
+ native.hostname == current.hostname &&
+ native.port == current.port) {
+ // Same origin policy
+ getOffOrigin.cache[cacheKey] = '';
+ } else if (current.protocol == 'app:') {
+ // Avoid displaying app:// protocol
+ getOffOrigin.cache[cacheKey] = '';
+ } else {
+ getOffOrigin.cache[cacheKey] = current.protocol + '//' +
+ current.hostname;
+ }
+ }
+
+ return getOffOrigin.cache[cacheKey];
+ }
+
+ getOffOrigin.cache = {};
+
+ function hideCardSwitcher(removeImmediately) {
+ if (!cardSwitcherIsShown())
+ return;
+
+ // events to handle
+ window.removeEventListener('lock', CardsView);
+
+ // Make the cardsView overlay inactive
+ cardsView.classList.remove('active');
+ cardsViewShown = false;
+
+ // Release our screenshot blobs.
+ screenshotObjectURLs.forEach(function(url) {
+ URL.revokeObjectURL(url);
+ });
+ screenshotObjectURLs = [];
+
+ // And remove all the cards from the document after the transition
+ function removeCards() {
+ cardsView.removeEventListener('transitionend', removeCards);
+ screenElement.classList.remove('cards-view');
+
+ while (cardsList.firstElementChild) {
+ cardsList.removeChild(cardsList.firstElementChild);
+ }
+ }
+ if (removeImmediately) {
+ removeCards();
+ } else {
+ cardsView.addEventListener('transitionend', removeCards);
+ }
+ }
+
+ function cardSwitcherIsShown() {
+ return cardsViewShown;
+ }
+
+ //scrolling cards
+ var initialCardViewPosition;
+ var initialTouchPosition = {};
+ var threshold = window.innerWidth / 4;
+ // Distance after which dragged card starts moving
+ var moveCardThreshold = window.innerHeight / 6;
+ var removeCardThreshold = window.innerHeight / 4;
+
+ function alignCard(number, callback) {
+ if (!cardsList.children[number])
+ return;
+
+ var scrollLeft = cardsView.scrollLeft;
+ var targetScrollLeft = cardsList.children[number].offsetLeft;
+
+ if (Math.abs(scrollLeft - targetScrollLeft) < 4) {
+ cardsView.scrollLeft = cardsList.children[number].offsetLeft;
+ if (callback)
+ callback();
+ return;
+ }
+
+ cardsView.scrollLeft = scrollLeft + (targetScrollLeft - scrollLeft) / 2;
+
+ window.mozRequestAnimationFrame(function newFrameCallback() {
+ alignCard(number, callback);
+ });
+ }
+
+ function onStartEvent(evt) {
+ evt.stopPropagation();
+ evt.target.setCapture(true);
+ cardsView.addEventListener('mousemove', CardsView);
+ cardsView.addEventListener('swipe', CardsView);
+
+ initialCardViewPosition = cardsView.scrollLeft;
+ initialTouchPosition = {
+ x: evt.touches ? evt.touches[0].pageX : evt.pageX,
+ y: evt.touches ? evt.touches[0].pageY : evt.pageY
+ };
+ }
+
+ function onMoveEvent(evt) {
+ evt.stopPropagation();
+ var touchPosition = {
+ x: evt.touches ? evt.touches[0].pageX : evt.pageX,
+ y: evt.touches ? evt.touches[0].pageY : evt.pageY
+ };
+
+ if (evt.target.classList.contains('card') && MANUAL_CLOSING) {
+ var differenceY = initialTouchPosition.y - touchPosition.y;
+ if (differenceY > moveCardThreshold) {
+ // We don't want user to scroll the CardsView when one of the card is
+ // already dragger upwards
+ draggingCardUp = true;
+ evt.target.style.MozTransform = 'scale(0.6) translate(0, -' +
+ differenceY + 'px)';
+ }
+ }
+
+ // If we are not reordering or removing Cards now
+ // and Snapping Scrolling is enabled, we want to scroll
+ // the CardList
+ if (SNAPPING_SCROLLING && reorderedCard === null && !draggingCardUp) {
+ var differenceX = initialTouchPosition.x - touchPosition.x;
+ cardsView.scrollLeft = initialCardViewPosition + differenceX;
+ }
+
+ // If re are in reordering mode (there is a DOM element in)
+ // reorderedCard variable) we are able to put this element somewere
+ // among the others
+ if (USER_DEFINED_ORDERING && reorderedCard !== null) {
+ var differenceX = touchPosition.x - initialTouchPosition.x;
+ // Probably there is more clever solution for calculating
+ // position of transformed DOM element, but this was my
+ // first thought and it seems to work
+ var moveOffset = (cardsList.children[currentDisplayed].offsetLeft / 0.6) +
+ differenceX - (dragMargin / 0.6);
+
+ reorderedCard.style.MozTransform =
+ 'scale(0.6) translate(' + moveOffset + 'px, 0)';
+
+ if (Math.abs(differenceX) > threshold) {
+ // We don't want to jump to the next page immediately,
+ // We are waiting half a second for user to decide if
+ // he wants to leave the Card here or scroll further
+ if (allowScrollingWhileSorting) {
+ allowScrollingWhileSorting = false;
+
+ scrollWhileSortingTimer = setTimeout(function() {
+ allowScrollingWhileSorting = true;
+ }, 500);
+
+ if (differenceX > 0 &&
+ currentDisplayed <= cardsList.children.length) {
+ currentDisplayed++;
+ sortingDirection = 'right';
+ alignCard(currentDisplayed);
+ } else if (differenceX < 0 && currentDisplayed > 0) {
+ currentDisplayed--;
+ sortingDirection = 'left';
+ alignCard(currentDisplayed);
+ }
+ }
+ }
+ }
+ }
+
+ function onEndEvent(evt) {
+ evt.stopPropagation();
+ var element = evt.target;
+ var eventDetail = evt.detail;
+ var direction = eventDetail.direction;
+
+ document.releaseCapture();
+ cardsView.removeEventListener('mousemove', CardsView);
+ cardsView.removeEventListener('swipe', CardsView);
+
+ var touchPosition = {
+ x: eventDetail.end.pageX,
+ y: eventDetail.end.pageY
+ };
+
+ if (SNAPPING_SCROLLING && !draggingCardUp && reorderedCard === null) {
+ if (Math.abs(eventDetail.dx) > threshold) {
+ if (
+ direction === 'left' &&
+ currentDisplayed < cardsList.children.length - 1
+ ) {
+ currentDisplayed++;
+ alignCard(currentDisplayed);
+ } else if (direction === 'right' && currentDisplayed > 0) {
+ currentDisplayed--;
+ alignCard(currentDisplayed);
+ }
+ } else {
+ alignCard(currentDisplayed);
+ }
+ }
+
+ // if the element we start dragging on
+ // is a card and we are not in reordering mode
+ if (
+ element.classList.contains('card') &&
+ MANUAL_CLOSING &&
+ reorderedCard === null
+ ) {
+
+ draggingCardUp = false;
+ // Prevent user from closing the app with a attention screen
+ if (-eventDetail.dy > removeCardThreshold &&
+ attentionScreenApps.indexOf(element.dataset.origin) == -1
+ ) {
+
+ // remove the app also from the ordering list
+ if (
+ userSortedApps.indexOf(element.dataset.origin) !== -1 &&
+ USER_DEFINED_ORDERING
+ ) {
+ userSortedApps.splice(
+ userSortedApps.indexOf(element.dataset.origin),
+ 1
+ );
+ }
+
+ // Without removing the listener before closing card
+ // sometimes the 'click' event fires, even if 'mouseup'
+ // uses stopPropagation()
+ element.removeEventListener('tap', runApp);
+
+ // Remove the icon from the task list
+ cardsList.removeChild(element);
+
+ closeApp(element);
+
+ return;
+ } else {
+ element.style.MozTransform = '';
+ }
+ }
+
+ if (USER_DEFINED_ORDERING && reorderedCard !== null) {
+ // Position of the card depends on direction of scrolling
+ if (sortingDirection === 'right') {
+ if (currentDisplayed <= cardsList.children.length) {
+ cardsList.insertBefore(
+ reorderedCard,
+ cardsList.children[currentDisplayed + 1]
+ );
+ } else {
+ cardsList.appendChild(reorderedCard);
+ }
+ } else if (sortingDirection === 'left') {
+ cardsList.insertBefore(
+ reorderedCard,
+ cardsList.children[currentDisplayed]
+ );
+ }
+ reorderedCard.style.MozTransform = '';
+ reorderedCard.dataset['edit'] = 'false';
+ reorderedCard = null;
+
+ alignCard(currentDisplayed);
+
+ // remove the app origin from ordering array
+ userSortedApps.splice(
+ userSortedApps.indexOf(element.dataset.origin),
+ 1
+ );
+ // and put in on the new position
+ userSortedApps.splice(currentDisplayed, 0, element.dataset.origin);
+ }
+ }
+
+ function manualOrderStart(evt) {
+ evt.preventDefault();
+ reorderedCard = evt.target;
+ allowScrollingWhileSorting = true;
+ if (reorderedCard.classList.contains('card')) {
+ dragMargin = reorderedCard.offsetLeft;
+ reorderedCard.dataset['edit'] = true;
+ sortingDirection = 'left';
+ }
+ }
+
+ window.addEventListener('applicationuninstall',
+ function removeUninstaledApp(evt) {
+ var origin = evt.detail.application.origin;
+ if (userSortedApps.indexOf(origin) !== -1) {
+ userSortedApps.splice(userSortedApps.indexOf(origin), 1);
+ }
+ },
+ false);
+
+ function cv_handleEvent(evt) {
+ switch (evt.type) {
+ case 'mousedown':
+ onStartEvent(evt);
+ break;
+
+ case 'mousemove':
+ onMoveEvent(evt);
+ break;
+
+ case 'swipe':
+ onEndEvent(evt);
+ break;
+
+ case 'contextmenu':
+ manualOrderStart(evt);
+ break;
+
+ case 'home':
+ if (!cardSwitcherIsShown())
+ return;
+
+ evt.stopImmediatePropagation();
+ hideCardSwitcher();
+ break;
+
+ case 'lock':
+ case 'attentionscreenshow':
+ attentionScreenApps = AttentionScreen.getAttentionScreenOrigins();
+ hideCardSwitcher();
+ break;
+
+ case 'attentionscreenhide':
+ attentionScreenApps = AttentionScreen.getAttentionScreenOrigins();
+ break;
+
+ case 'holdhome':
+ if (LockScreen.locked)
+ return;
+
+ SleepMenu.hide();
+ showCardSwitcher();
+ break;
+
+ case 'appwillopen':
+ hideCardSwitcher();
+ break;
+ }
+ }
+
+ // Public API of CardsView
+ return {
+ showCardSwitcher: showCardSwitcher,
+ hideCardSwitcher: hideCardSwitcher,
+ cardSwitcherIsShown: cardSwitcherIsShown,
+ handleEvent: cv_handleEvent
+ };
+})();
+
+window.addEventListener('attentionscreenshow', CardsView);
+window.addEventListener('attentionscreenhide', CardsView);
+window.addEventListener('holdhome', CardsView);
+window.addEventListener('home', CardsView);
+window.addEventListener('appwillopen', CardsView);
+
diff --git a/apps/system/js/context_menu.js b/apps/system/js/context_menu.js
new file mode 100644
index 0000000..816ef71
--- /dev/null
+++ b/apps/system/js/context_menu.js
@@ -0,0 +1,24 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var ContextMenu = {
+ init: function cm_init() {
+ window.addEventListener('mozbrowsercontextmenu', this, true);
+ },
+
+ handleEvent: function cm_handleEvent(evt) {
+ var detail = evt.detail;
+ if (detail.contextmenu.items.length == 0)
+ return;
+
+ var onsuccess = function(action) {
+ detail.contextMenuItemSelected(action);
+ };
+
+ ListMenu.request(detail.contextmenu.items, '', onsuccess);
+ }
+};
+
+ContextMenu.init();
diff --git a/apps/system/js/cost_control.js b/apps/system/js/cost_control.js
new file mode 100644
index 0000000..0f07962
--- /dev/null
+++ b/apps/system/js/cost_control.js
@@ -0,0 +1,89 @@
+/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+(function() {
+
+ 'use strict';
+
+ var host = document.location.host;
+ var domain = host.replace(/(^[\w\d]+\.)?([\w\d]+\.[a-z]+)/, '$2');
+ var protocol = document.location.protocol + '//';
+ var origin = protocol + 'costcontrol.' + domain;
+
+ var widgetContainer = document.getElementById('cost-control-widget');
+
+ var widgetFrame;
+ function _ensureWidget() {
+ if (!Applications.ready)
+ return;
+
+ // Check widget is there
+ widgetFrame = widgetContainer.querySelector('iframe');
+ if (widgetFrame && !widgetFrame.dataset.killed)
+ return;
+
+ // Create the widget
+ if (!widgetFrame) {
+ widgetFrame = document.createElement('iframe');
+ widgetFrame.addEventListener('mozbrowsererror',
+ function ccdriver_onError(e) {
+ e.target.dataset.killed = true;
+ }
+ );
+ }
+
+ widgetFrame.dataset.frameType = 'widget';
+ widgetFrame.dataset.frameOrigin = origin;
+ delete widgetFrame.dataset.killed;
+
+ widgetFrame.setAttribute('mozbrowser', true);
+ widgetFrame.setAttribute('remote', 'true');
+ widgetFrame.setAttribute('mozapp', origin + '/manifest.webapp');
+
+ widgetFrame.src = origin + '/widget.html';
+ widgetContainer.appendChild(widgetFrame);
+
+ _adjustWidgetPosition();
+ }
+
+ function _showWidget() {
+ _ensureWidget();
+ widgetFrame.setVisible(true);
+ }
+
+ function _hideWidget() {
+ if (widgetFrame) {
+ widgetFrame.setVisible(false);
+ }
+ }
+
+ function _adjustWidgetPosition() {
+ // TODO: Remove this when weird bug #809031 (Bugzilla) is solved
+ // See cost_control.css as well to remove the last rule
+ var offsetY = document.getElementById('notification-bar').clientHeight;
+ offsetY +=
+ document.getElementById('notifications-container').clientHeight;
+ widgetFrame.style.transform = 'translate(0, ' + offsetY + 'px)';
+ }
+
+ // Listen to utilitytray show
+ window.addEventListener('utilitytrayshow', _showWidget);
+ window.addEventListener('utilitytrayhide', _hideWidget);
+
+ window.addEventListener('applicationready', function _onReady() {
+ asyncStorage.getItem('ftu.enabled', function _onValue(enabled) {
+ if (enabled !== false) {
+ window.addEventListener('ftudone', function ftudone(e) {
+ window.removeEventListener('ftudone', ftudone);
+ _ensureWidget();
+ widgetFrame.setVisible(false);
+ });
+ } else {
+ _ensureWidget();
+ widgetFrame.setVisible(false);
+ }
+ });
+ });
+
+ window.addEventListener('resize', _adjustWidgetPosition);
+}());
diff --git a/apps/system/js/crash_reporter.js b/apps/system/js/crash_reporter.js
new file mode 100644
index 0000000..4068291
--- /dev/null
+++ b/apps/system/js/crash_reporter.js
@@ -0,0 +1,140 @@
+/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+// This file calls getElementById without waiting for an onload event, so it
+// must have a defer attribute or be included at the end of the <body>.
+
+var CrashReporter = (function() {
+ var _ = navigator.mozL10n.get;
+ var settings = navigator.mozSettings;
+ var screen = document.getElementById('screen');
+
+ // The name of the app that just crashed.
+ var crashedAppName = '';
+
+ // Whether or not to show a "Report" button in the banner.
+ var showReportButton = false;
+
+ // Only show the "Report" button if the user hasn't set a preference to
+ // always/never report crashes.
+ SettingsListener.observe('app.reportCrashes', 'ask',
+ function handleCrashSetting(value) {
+ showReportButton = (value != 'always' && value != 'never');
+ });
+
+ // This function should only ever be called once.
+ function showDialog(crashID, isChrome) {
+ var title = isChrome ? _('crash-dialog-os2') :
+ _('crash-dialog-app', { name: crashedAppName });
+ document.getElementById('crash-dialog-title').textContent = title;
+
+ // "Don't Send Report" button in dialog
+ var noButton = document.getElementById('dont-send-report');
+ noButton.addEventListener('click', function onNoButtonClick() {
+ settings.createLock().set({'app.reportCrashes': 'never'});
+ removeDialog();
+ });
+
+ // "Send Report" button in dialog
+ var yesButton = document.getElementById('send-report');
+ yesButton.addEventListener('click', function onYesButtonClick() {
+ submitCrash(crashID);
+ if (checkbox.checked) {
+ settings.createLock().set({'app.reportCrashes': 'always'});
+ }
+ removeDialog();
+ });
+
+ var checkbox = document.getElementById('always-send');
+ checkbox.addEventListener('click', function onCheckboxClick() {
+ // Disable the "Don't Send Report" button if the "Always send..."
+ // checkbox is checked
+ noButton.disabled = this.checked;
+ });
+
+ // "What's in a crash report?" link
+ var crashInfoLink = document.getElementById('crash-info-link');
+ crashInfoLink.addEventListener('click', function onLearnMoreClick() {
+ var dialog = document.getElementById('crash-dialog');
+ document.getElementById('crash-reports-done').
+ addEventListener('click', function onDoneClick() {
+ this.removeEventListener('click', onDoneClick);
+ dialog.classList.remove('learn-more');
+ });
+ dialog.classList.add('learn-more');
+ });
+
+ screen.classList.add('crash-dialog');
+ }
+
+ // We can get rid of the dialog after it is shown once.
+ function removeDialog() {
+ screen.classList.remove('crash-dialog');
+ var dialog = document.getElementById('crash-dialog');
+ dialog.parentNode.removeChild(dialog);
+ }
+
+ function showBanner(crashID, isChrome) {
+ var message = isChrome ? _('crash-banner-os2') :
+ _('crash-banner-app', { name: crashedAppName });
+
+ var button = null;
+ if (showReportButton) {
+ button = {
+ label: _('crash-banner-report'),
+ callback: function reportCrash() {
+ submitCrash(crashID);
+ }
+ };
+ }
+
+ SystemBanner.show(message, button);
+ }
+
+ function submitCrash(crashID) {
+ var event = document.createEvent('CustomEvent');
+ event.initCustomEvent('mozContentEvent', true, true, {
+ type: 'submit-crash',
+ crashID: crashID
+ });
+ window.dispatchEvent(event);
+ }
+
+ // - Show a dialog only the first time there's a crash to report.
+ // - On subsequent crashes, show a banner letting the user know there was a
+ // crash.
+ // - If the user hasn't set a pref, add a "Report" button to the banner.
+ function handleCrash(crashID, isChrome) {
+ // Check to see if we should show a dialog.
+ var dialogReq = settings.createLock().get('crashReporter.dialogShown');
+ dialogReq.onsuccess = function dialogShownSuccess() {
+ var dialogShown = dialogReq.result['crashReporter.dialogShown'];
+ if (!dialogShown) {
+ settings.createLock().set({'crashReporter.dialogShown': true});
+ showDialog(crashID, isChrome);
+ } else {
+ showBanner(crashID, isChrome);
+ }
+ };
+ }
+
+ // We depend on window_manager.js calling this function before
+ // we get a 'handle-crash' event from shell.js
+ function setAppName(name) {
+ crashedAppName = name;
+ }
+
+ // We will be notified of system crashes from shell.js
+ window.addEventListener('mozChromeEvent', function handleChromeEvent(e) {
+ if (e.detail.type == 'handle-crash') {
+ handleCrash(e.detail.crashID, e.detail.chrome);
+ }
+ });
+
+ return {
+ setAppName: setAppName
+ };
+})();
+
diff --git a/apps/system/js/gridview.js b/apps/system/js/gridview.js
new file mode 100644
index 0000000..399bf3e
--- /dev/null
+++ b/apps/system/js/gridview.js
@@ -0,0 +1,40 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var GridView = {
+ grid: null,
+
+ get visible() {
+ return this.grid && this.grid.style.display === 'block';
+ },
+
+ hide: function gv_hide() {
+ if (this.grid)
+ this.grid.style.visibility = 'hidden';
+ },
+
+ show: function gv_show() {
+ var grid = this.grid;
+ if (!grid) {
+ grid = document.createElement('div');
+ grid.id = 'debug-grid';
+ grid.dataset.zIndexLevel = 'debug-grid';
+
+ this.grid = grid;
+ document.getElementById('screen').appendChild(grid);
+ }
+
+ grid.style.visibility = 'visible';
+ },
+
+ toggle: function gv_toggle() {
+ this.visible ? this.hide() : this.show();
+ }
+};
+
+SettingsListener.observe('debug.grid.enabled', false, function(value) {
+ !!value ? GridView.show() : GridView.hide();
+});
+
diff --git a/apps/system/js/hardware_buttons.js b/apps/system/js/hardware_buttons.js
new file mode 100644
index 0000000..3363463
--- /dev/null
+++ b/apps/system/js/hardware_buttons.js
@@ -0,0 +1,318 @@
+// hardware_buttons.js:
+//
+// Gecko code in b2g/chrome/content/shell.js sends mozChromeEvents
+// when the user presses or releases a hardware button such as Home, Sleep,
+// and Volume Up and Down.
+//
+// This module listens for those low-level mozChromeEvents, processes them
+// and generates higher-level events to handle autorepeat on the volume keys
+// long presses on Home and Sleep, and the Home+Sleep key combination.
+//
+// Other system app modules should listen for the high-level button events
+// generated by this module.
+//
+// The low-level input events processed by this module have type set
+// to "mozChromeEvent" and detail.type set to one of:
+//
+// home-button-press
+// home-button-release
+// sleep-button-press
+// sleep-button-release
+// volume-up-button-press
+// volume-up-button-release
+// volume-down-button-press
+// volume-down-button-release
+//
+// The high-level events generated by this module are simple Event objects
+// that are not cancelable and do not bubble. The are dispatched at the
+// window object. The type property is set to one of these:
+//
+// Event Type Meaning
+// --------------------------------------------------------------
+// home short press and release of home button
+// holdhome long press and hold of home button
+// sleep short press and release of sleep button
+// wake sleep or home pressed while sleeping
+// holdsleep long press and hold of sleep button
+// volumeup volume up pressed and released or autorepeated
+// volumedown volume down pressed and released or autorepeated
+// home+sleep home and sleep pressed at same time (used for screenshots)
+// home+volume home and either volume key at the same time (view source)
+//
+// Because these events are fired at the window object, they cannot be
+// captured. Many modules listen for the home event. Those that want
+// to respond to it and prevent others from responding should call
+// stopImmediatePropagation(). Overlays that want to prevent the window
+// manager from showing the homescreen on the home event should call that
+// method. Note, however, that this only works for scripts that run and
+// register their event handlers before window_manager.js does.
+//
+'use strict';
+
+(function() {
+ var HOLD_INTERVAL = 750; // How long for press and hold Home or Sleep
+ var REPEAT_DELAY = 700; // How long before volume autorepeat begins
+ var REPEAT_INTERVAL = 100; // How fast the autorepeat is.
+
+ // Dispatch a high-level event of the specified type
+ function fire(type) {
+ window.dispatchEvent(new Event(type));
+ }
+
+ // We process events with a finite state machine.
+ // Each state object has a process() method for handling events.
+ // And optionally has enter() and exit() methods called when the FSM
+ // enters and exits that state
+ var state;
+
+ // This function transitions to a new state
+ function setState(s, type) {
+ // Exit the current state()
+ if (state && state.exit)
+ state.exit(type);
+ state = s;
+ // Enter the new state
+ if (state && state.enter)
+ state.enter(type);
+ }
+
+ // This event handler listens for hardware button events and passes the
+ // event type to the process() method of the current state for processing
+ window.addEventListener('mozChromeEvent', function(e) {
+ var type = e.detail.type;
+ switch (type) {
+ case 'home-button-press':
+ case 'home-button-release':
+ case 'sleep-button-press':
+ case 'sleep-button-release':
+ case 'volume-up-button-press':
+ case 'volume-up-button-release':
+ case 'volume-down-button-press':
+ case 'volume-down-button-release':
+ state.process(type);
+ break;
+ }
+ });
+
+ // The base state is the default, when no hardware buttons are pressed
+ var baseState = {
+ process: function(type) {
+ switch (type) {
+ case 'home-button-press':
+ // If the phone is sleeping, then pressing Home wakes it
+ // (on press, not release)
+ if (!ScreenManager.screenEnabled) {
+ fire('wake');
+ setState(wakeState, type);
+ } else {
+ setState(homeState, type);
+ }
+ return;
+ case 'sleep-button-press':
+ // If the phone is sleeping, then pressing Sleep wakes it
+ // (on press, not release)
+ if (!ScreenManager.screenEnabled) {
+ fire('wake');
+ setState(wakeState, type);
+ } else {
+ setState(sleepState, type);
+ }
+ return;
+ case 'volume-up-button-press':
+ case 'volume-down-button-press':
+ setState(volumeState, type);
+ return;
+ case 'home-button-release':
+ case 'sleep-button-release':
+ case 'volume-up-button-release':
+ case 'volume-down-button-release':
+ // Ignore button releases that occur in this state.
+ // These can happen after home+sleep and home+volume.
+ return;
+ }
+ console.error('Unexpected hardware key: ', type);
+ }
+ };
+
+ // We enter the home state when the user presses the Home button
+ // We can fire home, holdhome, or homesleep events from this state
+ var homeState = {
+ timer: null,
+ enter: function() {
+ this.timer = setTimeout(function() {
+ fire('holdhome');
+ navigator.vibrate(50);
+ setState(baseState);
+ }, HOLD_INTERVAL);
+ },
+ exit: function() {
+ if (this.timer) {
+ clearTimeout(this.timer);
+ this.timer = null;
+ }
+ },
+ process: function(type) {
+ switch (type) {
+ case 'home-button-release':
+ fire('home');
+ navigator.vibrate(50);
+ setState(baseState, type);
+ return;
+ case 'sleep-button-press':
+ fire('home+sleep');
+ setState(baseState, type);
+ return;
+ case 'volume-up-button-press':
+ case 'volume-down-button-press':
+ fire('home+volume');
+ setState(baseState, type);
+ return;
+ }
+ console.error('Unexpected hardware key: ', type);
+ setState(baseState, type);
+ }
+ };
+
+ // We enter the sleep state when the user presses the Sleep button
+ // We can fire sleep, holdsleep, or homesleep events from this state
+ var sleepState = {
+ timer: null,
+ enter: function() {
+ this.timer = setTimeout(function() {
+ fire('holdsleep');
+ setState(baseState);
+ }, HOLD_INTERVAL);
+ },
+ exit: function() {
+ if (this.timer) {
+ clearTimeout(this.timer);
+ this.timer = null;
+ }
+ },
+ process: function(type) {
+ switch (type) {
+ case 'sleep-button-release':
+ fire('sleep');
+ setState(baseState, type);
+ return;
+ case 'home-button-press':
+ fire('home+sleep');
+ setState(baseState, type);
+ return;
+ case 'volume-up-button-press':
+ case 'volume-down-button-press':
+ setState(volumeState, type);
+ return;
+ }
+ console.error('Unexpected hardware key: ', type);
+ setState(baseState, type);
+ }
+ };
+
+ // We enter the volume state when the user presses the volume up or
+ // volume down buttons.
+ // We can fire volumeup and volumedown events from this state
+ var volumeState = {
+ direction: null,
+ timer: null,
+ repeating: false,
+ repeat: function() {
+ this.repeating = true;
+ if (this.direction === 'volume-up-button-press')
+ fire('volumeup');
+ else
+ fire('volumedown');
+ this.timer = setTimeout(this.repeat.bind(this), REPEAT_INTERVAL);
+ },
+ enter: function(type) {
+ var self = this;
+ this.direction = type; // Is volume going up or down?
+ this.repeating = false;
+ this.timer = setTimeout(this.repeat.bind(this), REPEAT_DELAY);
+ },
+ exit: function() {
+ if (this.timer) {
+ clearTimeout(this.timer);
+ this.timer = null;
+ }
+ },
+ process: function(type) {
+ switch (type) {
+ case 'home-button-press':
+ fire('home+volume');
+ setState(baseState, type);
+ return;
+ case 'sleep-button-press':
+ setState(sleepState, type);
+ return;
+ case 'volume-up-button-release':
+ if (this.direction === 'volume-up-button-press') {
+ if (!this.repeating)
+ fire('volumeup');
+ setState(baseState, type);
+ return;
+ }
+ break;
+ case 'volume-down-button-release':
+ if (this.direction === 'volume-down-button-press') {
+ if (!this.repeating)
+ fire('volumedown');
+ setState(baseState, type);
+ return;
+ }
+ break;
+ default:
+ // Ignore anything else (such as sleep button release)
+ return;
+ }
+ console.error('Unexpected hardware key: ', type);
+ setState(baseState, type);
+ }
+ };
+
+ // We enter this state when the user presses Home or Sleep on a sleeping
+ // phone. We give immediate feedback by waking the phone up on the press
+ // rather than waiting for the release, but this means we need a special
+ // state so that we don't actually send a home or sleep event on the
+ // key release. Note, however, that this state does set a timer so that
+ // it can send holdhome or holdsleep events. (This means that pressing and
+ // holding sleep will bring up the power menu, even on a sleeping phone.)
+ var wakeState = {
+ timer: null,
+ delegateState: null,
+ enter: function(type) {
+ if (type === 'home-button-press')
+ this.delegateState = homeState;
+ else
+ this.delegateState = sleepState;
+ this.timer = setTimeout(function() {
+ if (type === 'home-button-press') {
+ fire('holdhome');
+ } else if (type === 'sleep-button-press') {
+ fire('holdsleep');
+ }
+ setState(baseState, type);
+ }, HOLD_INTERVAL);
+ },
+ exit: function() {
+ if (this.timer) {
+ clearTimeout(this.timer);
+ this.timer = null;
+ }
+ },
+ process: function(type) {
+ switch (type) {
+ case 'home-button-release':
+ case 'sleep-button-release':
+ setState(baseState, type);
+ return;
+ default:
+ this.delegateState.process(type);
+ return;
+ }
+ }
+ };
+
+ // Kick off the FSM in the base state
+ setState(baseState);
+}());
diff --git a/apps/system/js/icc_cache.js b/apps/system/js/icc_cache.js
new file mode 100644
index 0000000..1f1d0df
--- /dev/null
+++ b/apps/system/js/icc_cache.js
@@ -0,0 +1,86 @@
+/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+(function() {
+ /**
+ * Constants
+ */
+ var DEBUG = false;
+
+ /**
+ * Debug method
+ */
+ function debug(msg, optObject) {
+ if (DEBUG) {
+ var output = '[DEBUG] STKCACHE: ' + msg;
+ if (optObject) {
+ output += JSON.stringify(optObject);
+ }
+ console.log(output);
+ }
+ }
+
+ if (!window.navigator.mozMobileConnection) {
+ return;
+ }
+
+ var icc = window.navigator.mozMobileConnection.icc;
+ // Remove previous menu
+ var resetApplications = window.navigator.mozSettings.createLock().set({
+ 'icc.applications': '{}'
+ });
+ resetApplications.onsuccess = function icc_resetApplications() {
+ debug('STK Cache Reseted');
+ // Register to receive STK commands
+ window.navigator.mozSetMessageHandler('icc-stkcommand',
+ function handleSTKCommand(command) {
+ debug('STK Proactive Command:', command);
+ if (command.typeOfCommand == icc.STK_CMD_SET_UP_MENU) {
+ debug('STK_CMD_SET_UP_MENU:', command.options);
+ var reqApplications = window.navigator.mozSettings.createLock().set({
+ 'icc.applications': JSON.stringify(command.options)
+ });
+ reqApplications.onsuccess = function icc_getApplications() {
+ debug('Cached');
+ icc.sendStkResponse(command, {
+ resultCode: icc.STK_RESULT_OK
+ });
+ }
+ } else {
+ // Unsolicited command? -> Open settings
+ debug('CMD: ', command);
+ var application = document.location.protocol + '//' +
+ document.location.host.replace('system', 'settings');
+ debug('application: ', application);
+ var reqIccData = window.navigator.mozSettings.createLock().set({
+ 'icc.data': JSON.stringify(command)
+ });
+ reqIccData.onsuccess = function icc_getIccData() {
+ if (WindowManager.getRunningApps()[application]) {
+ debug('Settings is running. Ignoring');
+ return; // If settings is opened, we don't manage it
+ }
+
+ debug('Locating settings . . .');
+ navigator.mozApps.mgmt.getAll().onsuccess = function gotApps(evt) {
+ var apps = evt.target.result;
+ apps.forEach(function appIterator(app) {
+ if (app.origin != application)
+ return;
+
+ var reqIccData = window.navigator.mozSettings.createLock().set({
+ 'icc.data': JSON.stringify(command)
+ });
+ reqIccData.onsuccess = function icc_getIccData() {
+ debug('Launching ', app.origin);
+ app.launch();
+ }
+ }, this);
+ }
+ }
+ }
+ });
+ }
+})();
diff --git a/apps/system/js/identity.js b/apps/system/js/identity.js
new file mode 100644
index 0000000..8030377
--- /dev/null
+++ b/apps/system/js/identity.js
@@ -0,0 +1,98 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+// When bug 794999 is resolved, switch to use the abstract Trusted UI Component
+
+'use strict';
+
+const kIdentityScreen = 'https://login.native-persona.org/sign_in#NATIVE';
+const kIdentityFrame =
+ 'https://login.native-persona.org/communication_iframe';
+
+var Identity = (function() {
+ var iframe;
+
+ return {
+ trustedUILayers: {},
+
+ init: function() {
+ window.addEventListener('mozChromeEvent', this);
+ },
+
+ handleEvent: function onMozChromeEvent(e) {
+ var chromeEventId = e.detail.id;
+ var requestId = e.detail.requestId;
+ switch (e.detail.type) {
+ // Chrome asks Gaia to show the identity dialog.
+ case 'open-id-dialog':
+ if (!chromeEventId)
+ return;
+
+ // When opening the dialog, we record the chrome event id, which
+ // we will need to send back to the TrustedUIManager when asking
+ // to close.
+ this.trustedUILayers[requestId] = chromeEventId;
+
+ if (!e.detail.showUI && iframe) {
+ this._dispatchEvent({
+ id: chromeEventId,
+ frame: iframe
+ });
+ return;
+ }
+ var frame = document.createElement('iframe');
+ frame.setAttribute('mozbrowser', 'true');
+ frame.setAttribute('remote', true);
+ frame.classList.add('screen');
+ frame.src = e.detail.showUI ? kIdentityScreen : kIdentityFrame;
+ frame.addEventListener('mozbrowserloadstart',
+ function loadStart(evt) {
+ // After creating the new frame containing the identity flow, we
+ // send it back to chrome so the identity callbacks can be injected.
+ this._dispatchEvent({
+ id: chromeEventId,
+ frame: evt.target
+ });
+ }.bind(this));
+
+
+ if (e.detail.showUI) {
+ // The identity flow is shown within the trusted UI.
+ TrustedUIManager.open(navigator.mozL10n.get('persona-signin'),
+ frame,
+ this.trustedUILayers[requestId]);
+ } else {
+ var container = document.getElementById('screen');
+ container.appendChild(frame);
+ frame.classList.add('communication-frame');
+ iframe = frame;
+ }
+ break;
+
+ case 'received-id-assertion':
+ if (e.detail.showUI) {
+ TrustedUIManager.close(this.trustedUILayers[requestId],
+ (function dialogClosed() {
+ delete this.trustedUILayers[requestId];
+ }).bind(this));
+ }
+ this._dispatchEvent({ id: chromeEventId });
+ break;
+ }
+ },
+ _dispatchEvent: function su_dispatchEvent(obj) {
+ var event = document.createEvent('CustomEvent');
+ event.initCustomEvent('mozContentEvent', true, true, obj);
+ window.dispatchEvent(event);
+ }
+ };
+})();
+
+// Make sure L10n is ready before init
+if (navigator.mozL10n.readyState == 'complete' ||
+ navigator.mozL10n.readyState == 'interactive') {
+ Identity.init();
+} else {
+ window.addEventListener('localized', Identity.init.bind(Identity));
+}
+
diff --git a/apps/system/js/keyboard_manager.js b/apps/system/js/keyboard_manager.js
new file mode 100644
index 0000000..62aeca7
--- /dev/null
+++ b/apps/system/js/keyboard_manager.js
@@ -0,0 +1,86 @@
+'use strict';
+
+var KeyboardManager = (function() {
+ function getKeyboardURL() {
+ // TODO: Retrieve it from Settings, allowing 3rd party keyboards
+ var host = document.location.host;
+ var domain = host.replace(/(^[\w\d]+\.)?([\w\d]+\.[a-z]+)/, '$2');
+ var protocol = document.location.protocol;
+
+ return protocol + '//keyboard.' + domain + '/';
+ }
+
+ function generateKeyboard(container, keyboardURL, manifestURL) {
+ var keyboard = document.createElement('iframe');
+ keyboard.src = keyboardURL;
+ keyboard.setAttribute('mozbrowser', 'true');
+ keyboard.setAttribute('mozpasspointerevents', 'true');
+ keyboard.setAttribute('mozapp', manifestURL);
+ //keyboard.setAttribute('remote', 'true');
+
+ container.appendChild(keyboard);
+ return keyboard;
+ }
+
+ // Generate a <iframe mozbrowser> containing the keyboard.
+ var container = document.getElementById('keyboard-frame');
+ var keyboardURL = getKeyboardURL() + 'index.html';
+ var manifestURL = getKeyboardURL() + 'manifest.webapp';
+ var keyboard = generateKeyboard(container, keyboardURL, manifestURL);
+
+ // Listen for mozbrowserlocationchange of keyboard iframe.
+ var previousHash = '';
+
+ var urlparser = document.createElement('a');
+ keyboard.addEventListener('mozbrowserlocationchange', function(e) {
+ urlparser.href = e.detail;
+ if (previousHash == urlparser.hash)
+ return;
+ previousHash = urlparser.hash;
+
+ var type = urlparser.hash.split('=');
+ switch (type[0]) {
+ case '#show':
+ var updateHeight = function updateHeight() {
+ container.removeEventListener('transitionend', updateHeight);
+ if (container.classList.contains('hide')) {
+ // The keyboard has been closed already, let's not resize the
+ // application and ends up with half apps.
+ return;
+ }
+
+ var detail = {
+ 'detail': {
+ 'height': parseInt(type[1])
+ }
+ };
+
+ dispatchEvent(new CustomEvent('keyboardchange', detail));
+ }
+
+ if (container.classList.contains('hide')) {
+ container.classList.remove('hide');
+ container.addEventListener('transitionend', updateHeight);
+ return;
+ }
+
+ updateHeight();
+ break;
+
+ case '#hide':
+ // inform window manager to resize app first or
+ // it may show the underlying homescreen
+ dispatchEvent(new CustomEvent('keyboardhide'));
+ container.classList.add('hide');
+ break;
+ }
+ });
+
+ // For Bug 812115: hide the keyboard when the app is closed here,
+ // since it would take a longer round-trip to receive focuschange
+ window.addEventListener('appwillclose', function closeKeyboard() {
+ dispatchEvent(new CustomEvent('keyboardhide'));
+ container.classList.add('hide');
+ });
+})();
+
diff --git a/apps/system/js/list_menu.js b/apps/system/js/list_menu.js
new file mode 100644
index 0000000..303fafe
--- /dev/null
+++ b/apps/system/js/list_menu.js
@@ -0,0 +1,180 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var ListMenu = {
+ get element() {
+ delete this.element;
+ return this.element = document.getElementById('listmenu');
+ },
+
+ get container() {
+ delete this.container;
+ return this.container = document.querySelector('#listmenu menu');
+ },
+
+ get visible() {
+ return this.element.classList.contains('visible');
+ },
+
+ // Listen to click event only
+ init: function lm_init() {
+ window.addEventListener('click', this, true);
+ window.addEventListener('screenchange', this, true);
+ window.addEventListener('home', this);
+ window.addEventListener('holdhome', this);
+ },
+
+ // Pass an array of list items and handler for clicking on the items
+ // Modified to fit contextmenu use case, loop into the menu items
+ request: function lm_request(listItems, title, successCb, errorCb) {
+ this.container.innerHTML = '';
+ this.currentLevel = 0;
+ this.internalList = [];
+ this.setTitle(title);
+ this.buildMenu(listItems);
+ this.internalList.forEach(function render_item(item) {
+ this.container.appendChild(item);
+ }, this);
+
+ this.onreturn = successCb || function() {};
+ this.oncancel = errorCb || function() {};
+
+ this.show();
+ },
+
+ buildMenu: function lm_buildMenu(items) {
+ var containerDiv = document.createElement('ul');
+ var _ = navigator.mozL10n.get;
+
+ if (this.currentLevel === 0) {
+ containerDiv.classList.add('list-menu-root');
+ containerDiv.id = 'list-menu-root';
+ } else {
+ containerDiv.id = 'list-menu-' + this.internalList.length;
+ }
+ this.internalList.push(containerDiv);
+
+ items.forEach(function traveseItems(item) {
+ var item_div = document.createElement('li');
+ var button = document.createElement('a');
+ button.setAttribute('role', 'button');
+ if (item.type && item.type == 'menu') {
+ // XXX: We disallow multi-level menu at this moment
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=824928
+ // for UX design and dev implementation tracking
+ return;
+ } else if (item.type && item.type == 'menuitem') {
+ button.dataset.value = item.id;
+ button.textContent = item.label;
+ } else {
+ button.dataset.value = item.value;
+ button.textContent = item.label;
+ }
+
+ item_div.appendChild(button);
+ if (item.icon) {
+ button.style.backgroundImage = 'url(' + item.icon + ')';
+ button.classList.add('icon');
+ }
+ containerDiv.appendChild(item_div);
+ }, this);
+
+ if (this.currentLevel > 0) {
+ var back = document.createElement('li');
+ var button = document.createElement('a');
+ button.setAttribute('role', 'button');
+ button.textContent = _('back');
+ button.href = '#' + this.currentParent;
+ back.classList.add('back');
+ back.appendChild(button);
+ containerDiv.appendChild(back);
+ } else {
+ var cancel = document.createElement('li');
+ var button = document.createElement('button');
+ button.textContent = _('cancel');
+ button.dataset.action = 'cancel';
+ cancel.appendChild(button);
+ containerDiv.appendChild(cancel);
+ }
+
+ containerDiv.dataset.level = this.currentLevel;
+ this.currentChild = containerDiv.id;
+ },
+
+ setTitle: function lm_setTitle(title) {
+ if (!title)
+ return;
+
+ var titleElement = document.createElement('h3');
+ titleElement.textContent = title;
+ this.container.appendChild(titleElement);
+ },
+
+ show: function lm_show() {
+ if (this.visible)
+ return;
+
+ this.container.classList.remove('slidedown');
+ this.element.classList.add('visible');
+ },
+
+ hide: function lm_hide() {
+ if (!this.visible)
+ return;
+
+ var self = this;
+ var container = this.container;
+ container.addEventListener('transitionend', function list_hide() {
+ container.removeEventListener('transitionend', list_hide);
+ self.element.classList.remove('visible');
+ });
+
+ setTimeout(function() {
+ container.classList.add('slidedown');
+ });
+ },
+
+ handleEvent: function lm_handleEvent(evt) {
+ switch (evt.type) {
+ case 'screenchange':
+ if (!evt.detail.screenEnabled) {
+ this.hide();
+ this.oncancel();
+ }
+ break;
+
+ case 'click':
+ if (!this.visible)
+ return;
+
+ var cancel = evt.target.dataset.action;
+ if (cancel && cancel == 'cancel') {
+ this.hide();
+ this.oncancel();
+ return;
+ }
+
+ var value = evt.target.dataset.value;
+ if (!value) {
+ return;
+ }
+
+ this.hide();
+ this.onreturn(value);
+ break;
+
+ case 'home':
+ case 'holdhome':
+ if (!this.visible)
+ return;
+
+ this.hide();
+ this.oncancel();
+ break;
+ }
+ }
+};
+
+ListMenu.init();
diff --git a/apps/system/js/lockscreen.js b/apps/system/js/lockscreen.js
new file mode 100644
index 0000000..f2275c9
--- /dev/null
+++ b/apps/system/js/lockscreen.js
@@ -0,0 +1,1019 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var LockScreen = {
+ /*
+ * Boolean return true when initialized.
+ */
+ ready: false,
+
+ /*
+ * Boolean return the status of the lock screen.
+ * Must not multate directly - use unlock()/lockIfEnabled()
+ * Listen to 'lock' and 'unlock' event to properly handle status changes
+ */
+ locked: true,
+
+ /*
+ * Boolean return whether if the lock screen is enabled or not.
+ * Must not multate directly - use setEnabled(val)
+ * Only Settings Listener should change this value to sync with data
+ * in Settings API.
+ */
+ enabled: true,
+
+ /*
+ * Boolean returns wether we want a sound effect when unlocking.
+ */
+ unlockSoundEnabled: true,
+
+ /*
+ * Boolean return whether if the lock screen is enabled or not.
+ * Must not multate directly - use setPassCodeEnabled(val)
+ * Only Settings Listener should change this value to sync with data
+ * in Settings API.
+ * Will be ignored if 'enabled' is set to false.
+ */
+ passCodeEnabled: false,
+
+ /*
+ * Four digit Passcode
+ * XXX: should come for Settings
+ */
+ passCode: '0000',
+
+ /*
+ * The time to request for passcode input since device is off.
+ */
+ passCodeRequestTimeout: 0,
+
+ /*
+ * Store the first time the screen went off since unlocking.
+ */
+ _screenOffTime: 0,
+
+ /*
+ * Check the timeout of passcode lock
+ */
+ _passCodeTimeoutCheck: false,
+
+ /*
+ * Current passcode entered by the user
+ */
+ passCodeEntered: '',
+
+ /**
+ * Are we currently switching panels ?
+ */
+ _switchingPanel: false,
+
+ /*
+ * Timeout after incorrect attempt
+ */
+ kPassCodeErrorTimeout: 500,
+
+ /*
+ * Airplane mode
+ */
+ airplaneMode: false,
+
+ /*
+ * Timeout ID for backing from triggered state to normal state
+ */
+ triggeredTimeoutId: 0,
+
+ /*
+ * Interval ID for elastic of curve and arrow
+ */
+ elasticIntervalId: 0,
+
+ /*
+ * elastic animation interval
+ */
+ ELASTIC_INTERVAL: 5000,
+
+ /*
+ * timeout for triggered state after swipe up
+ */
+ TRIGGERED_TIMEOUT: 7000,
+
+ /*
+ * Max value for handle swiper up
+ */
+ HANDLE_MAX: 70,
+
+ /* init */
+ init: function ls_init() {
+ if (this.ready) { // already initialized: just trigger a translation
+ this.updateTime();
+ this.updateConnState();
+ return;
+ }
+ this.ready = true;
+
+ this.getAllElements();
+
+ this.lockIfEnabled(true);
+ this.writeSetting(this.enabled);
+
+ /* Status changes */
+ window.addEventListener('volumechange', this);
+ window.addEventListener('screenchange', this);
+
+ /* Gesture */
+ this.area.addEventListener('mousedown', this);
+ this.areaCamera.addEventListener('click', this);
+ this.areaUnlock.addEventListener('click', this);
+ this.iconContainer.addEventListener('mousedown', this);
+
+ /* Unlock & camera panel clean up */
+ this.overlay.addEventListener('transitionend', this);
+
+ /* Passcode input pad*/
+ this.passcodePad.addEventListener('click', this);
+
+ /* switching panels */
+ window.addEventListener('home', this);
+
+ /* blocking holdhome and prevent Cards View from show up */
+ window.addEventListener('holdhome', this, true);
+
+ /* mobile connection state on lock screen */
+ var conn = window.navigator.mozMobileConnection;
+ if (conn && conn.voice) {
+ conn.addEventListener('voicechange', this);
+ conn.addEventListener('cardstatechange', this);
+ conn.addEventListener('iccinfochange', this);
+ this.updateConnState();
+ this.connstate.hidden = false;
+ }
+
+ var self = this;
+ if (navigator && navigator.mozCellBroadcast) {
+ navigator.mozCellBroadcast.onreceived = function onReceived(event) {
+ var msg = event.message;
+ if (conn &&
+ conn.voice.network.mcc === MobileOperator.BRAZIL_MCC &&
+ msg.messageId === MobileOperator.BRAZIL_CELLBROADCAST_CHANNEL) {
+ self.cellbroadcastLabel = msg.body;
+ self.updateConnState();
+ }
+ };
+ }
+
+ SettingsListener.observe('lockscreen.enabled', true, function(value) {
+ self.setEnabled(value);
+ });
+
+ SettingsListener.observe('ring.enabled', true, function(value) {
+ self.mute.hidden = value;
+ });
+
+ SettingsListener.observe('vibration.enabled', true, function(value) {
+ if (value) {
+ self.mute.classList.add('vibration');
+ } else {
+ self.mute.classList.remove('vibration');
+ }
+ });
+
+ SettingsListener.observe('ril.radio.disabled', false, function(value) {
+ self.airplaneMode = value;
+ self.updateConnState();
+ });
+
+ SettingsListener.observe('wallpaper.image',
+ 'resources/images/backgrounds/default.png',
+ function(value) {
+ self.updateBackground(value);
+ self.overlay.classList.remove('uninit');
+ });
+
+ SettingsListener.observe(
+ 'lockscreen.passcode-lock.code', '0000', function(value) {
+ self.passCode = value;
+ });
+
+ SettingsListener.observe(
+ 'lockscreen.passcode-lock.enabled', false, function(value) {
+ self.setPassCodeEnabled(value);
+ });
+
+ SettingsListener.observe('lockscreen.unlock-sound.enabled',
+ true, function(value) {
+ self.setUnlockSoundEnabled(value);
+ });
+
+ SettingsListener.observe('lockscreen.passcode-lock.timeout',
+ 0, function(value) {
+ self.passCodeRequestTimeout = value;
+ });
+ },
+
+ /*
+ * Set enabled state.
+ * If enabled state is somehow updated when the lock screen is enabled
+ * This function will unlock it.
+ */
+ setEnabled: function ls_setEnabled(val) {
+ if (typeof val === 'string') {
+ this.enabled = val == 'false' ? false : true;
+ } else {
+ this.enabled = val;
+ }
+
+ if (!this.enabled && this.locked) {
+ this.unlock();
+ }
+ },
+
+ setPassCodeEnabled: function ls_setPassCodeEnabled(val) {
+ if (typeof val === 'string') {
+ this.passCodeEnabled = val == 'false' ? false : true;
+ } else {
+ this.passCodeEnabled = val;
+ }
+ },
+
+ setUnlockSoundEnabled: function ls_setUnlockSoundEnabled(val) {
+ if (typeof val === 'string') {
+ this.unlockSoundEnabled = val == 'false' ? false : true;
+ } else {
+ this.unlockSoundEnabled = val;
+ }
+ },
+
+ handleEvent: function ls_handleEvent(evt) {
+ switch (evt.type) {
+ case 'screenchange':
+ // XXX: If the screen is not turned off by ScreenManager
+ // we would need to lock the screen again
+ // when it's being turned back on
+ if (!evt.detail.screenEnabled) {
+ // Don't update the time after we're already locked otherwise turning
+ // the screen off again will bypass the passcode before the timeout.
+ if (!this.locked) {
+ this._screenOffTime = new Date().getTime();
+ }
+
+ // Remove camera once screen turns off
+ if (this.camera.firstElementChild)
+ this.camera.removeChild(this.camera.firstElementChild);
+
+ } else {
+ var _screenOffInterval = new Date().getTime() - this._screenOffTime;
+ if (_screenOffInterval > this.passCodeRequestTimeout * 1000) {
+ this._passCodeTimeoutCheck = true;
+ } else {
+ this._passCodeTimeoutCheck = false;
+ }
+ }
+
+ this.lockIfEnabled(true);
+ break;
+ case 'voicechange':
+ case 'cardstatechange':
+ case 'iccinfochange':
+ this.updateConnState();
+
+ case 'click':
+ if (evt.target === this.areaUnlock || evt.target === this.areaCamera) {
+ this.handleIconClick(evt.target);
+ break;
+ }
+
+ if (!evt.target.dataset.key)
+ break;
+
+ // Cancel the default action of <a>
+ evt.preventDefault();
+ this.handlePassCodeInput(evt.target.dataset.key);
+ break;
+
+ case 'mousedown':
+ var leftTarget = this.areaCamera;
+ var rightTarget = this.areaUnlock;
+ var handle = this.areaHandle;
+ var overlay = this.overlay;
+ var target = evt.target;
+
+ // Reset timer when touch while overlay triggered
+ if (overlay.classList.contains('triggered')) {
+ clearTimeout(this.triggeredTimeoutId);
+ this.triggeredTimeoutId = setTimeout(this.unloadPanel.bind(this),
+ this.TRIGGERED_TIMEOUT);
+ break;
+ }
+
+ overlay.classList.remove('elastic');
+ this.setElasticEnabled(false);
+
+ this._touch = {
+ touched: false,
+ leftTarget: leftTarget,
+ rightTarget: rightTarget,
+ overlayWidth: this.overlay.offsetWidth,
+ handleWidth: this.areaHandle.offsetWidth,
+ maxHandleOffset: rightTarget.offsetLeft - handle.offsetLeft -
+ (handle.offsetWidth - rightTarget.offsetWidth) / 2
+ };
+ window.addEventListener('mouseup', this);
+ window.addEventListener('mousemove', this);
+
+ this._touch.touched = true;
+ this._touch.initX = evt.pageX;
+ this._touch.initY = evt.pageY;
+ overlay.classList.add('touched');
+ break;
+
+ case 'mousemove':
+ this.handleMove(evt.pageX, evt.pageY);
+ break;
+
+ case 'mouseup':
+ window.removeEventListener('mousemove', this);
+ window.removeEventListener('mouseup', this);
+
+ this.handleMove(evt.pageX, evt.pageY);
+ this.handleGesture();
+ delete this._touch;
+ this.overlay.classList.remove('touched');
+
+ break;
+
+ case 'transitionend':
+ if (evt.target !== this.overlay)
+ return;
+
+ if (this.overlay.dataset.panel !== 'camera' &&
+ this.camera.firstElementChild) {
+ this.camera.removeChild(this.camera.firstElementChild);
+ }
+
+ if (!this.locked)
+ this.switchPanel();
+ break;
+
+ case 'home':
+ if (this.locked) {
+ this.switchPanel();
+ evt.stopImmediatePropagation();
+ }
+ break;
+
+ case 'holdhome':
+ if (!this.locked)
+ return;
+
+ evt.stopImmediatePropagation();
+ evt.stopPropagation();
+ break;
+ }
+ },
+
+ handleMove: function ls_handleMove(pageX, pageY) {
+ var touch = this._touch;
+
+ if (!touch.touched) {
+ // Do nothing if the user have not move the finger to the handle yet
+ if (document.elementFromPoint(pageX, pageY) !== this.areaHandle)
+ return;
+
+ touch.touched = true;
+ touch.initX = pageX;
+ touch.initY = pageY;
+
+ var overlay = this.overlay;
+ overlay.classList.add('touched');
+ }
+
+ var dy = pageY - touch.initY;
+ var ty = Math.max(- this.HANDLE_MAX, dy);
+ var base = - ty / this.HANDLE_MAX;
+ // mapping position 20-100 to opacity 0.1-0.5
+ var opacity = base <= 0.2 ? 0.1 : base * 0.5;
+ touch.ty = ty;
+
+ this.iconContainer.style.transform = 'translateY(' + ty + 'px)';
+ this.areaCamera.style.opacity =
+ this.areaUnlock.style.opacity = opacity;
+ },
+
+ handleGesture: function ls_handleGesture() {
+ var touch = this._touch;
+ if (touch.ty < -50) {
+ this.areaHandle.style.transform =
+ this.areaHandle.style.opacity =
+ this.iconContainer.style.transform =
+ this.iconContainer.style.opacity =
+ this.areaCamera.style.transform =
+ this.areaCamera.style.opacity =
+ this.areaUnlock.style.transform =
+ this.areaUnlock.style.opacity = '';
+ this.overlay.classList.add('triggered');
+
+ this.triggeredTimeoutId =
+ setTimeout(this.unloadPanel.bind(this), this.TRIGGERED_TIMEOUT);
+ } else if (touch.ty > -10) {
+ touch.touched = false;
+ this.unloadPanel();
+ this.playElastic();
+
+ var self = this;
+ var container = this.iconContainer;
+ container.addEventListener('animationend', function prompt() {
+ container.removeEventListener('animationend', prompt);
+ self.overlay.classList.remove('elastic');
+ self.setElasticEnabled(true);
+ });
+ } else {
+ this.unloadPanel();
+ this.setElasticEnabled(true);
+ }
+ },
+
+ handleIconClick: function ls_handleIconClick(target) {
+ var self = this;
+ switch (target) {
+ case this.areaCamera:
+ var panelOrFullApp = function panelOrFullApp() {
+ if (self.passCodeEnabled) {
+ // Go to secure camera panel
+ self.switchPanel('camera');
+ return;
+ }
+
+ self.unlock();
+
+ var a = new MozActivity({
+ name: 'record',
+ data: {
+ type: 'photos'
+ }
+ });
+ a.onerror = function ls_activityError() {
+ console.log('MozActivity: camera launch error.');
+ };
+ };
+
+ panelOrFullApp();
+ break;
+
+ case this.areaUnlock:
+ var passcodeOrUnlock = function passcodeOrUnlock() {
+ if (!self.passCodeEnabled || !self._passCodeTimeoutCheck) {
+ self.unlock();
+ } else {
+ self.switchPanel('passcode');
+ }
+ };
+ passcodeOrUnlock();
+ break;
+ }
+ },
+
+ handlePassCodeInput: function ls_handlePassCodeInput(key) {
+ switch (key) {
+ case 'e': // Emergency Call
+ this.switchPanel('emergency-call');
+ break;
+
+ case 'c':
+ this.switchPanel();
+ break;
+
+ case 'b':
+ if (this.overlay.dataset.passcodeStatus)
+ return;
+
+ this.passCodeEntered =
+ this.passCodeEntered.substr(0, this.passCodeEntered.length - 1);
+ this.updatePassCodeUI();
+
+ break;
+ default:
+ if (this.overlay.dataset.passcodeStatus)
+ return;
+
+ this.passCodeEntered += key;
+ this.updatePassCodeUI();
+
+ if (this.passCodeEntered.length === 4)
+ this.checkPassCode();
+ break;
+ }
+ },
+
+ lockIfEnabled: function ls_lockIfEnabled(instant) {
+ if (this.enabled) {
+ this.lock(instant);
+ } else {
+ this.unlock(instant);
+ }
+ },
+
+ unlock: function ls_unlock(instant) {
+ var currentApp = WindowManager.getDisplayedApp();
+ WindowManager.setOrientationForApp(currentApp);
+
+ var currentFrame = WindowManager.getAppFrame(currentApp).firstChild;
+ var wasAlreadyUnlocked = !this.locked;
+ this.locked = false;
+ this.setElasticEnabled(false);
+ this.mainScreen.focus();
+
+ var repaintTimeout = 0;
+ var nextPaint = (function() {
+ clearTimeout(repaintTimeout);
+ currentFrame.removeNextPaintListener(nextPaint);
+
+ if (instant) {
+ this.overlay.classList.add('no-transition');
+ this.switchPanel();
+ } else {
+ this.overlay.classList.remove('no-transition');
+ }
+
+ this.mainScreen.classList.remove('locked');
+
+ if (!wasAlreadyUnlocked) {
+ // Any changes made to this,
+ // also need to be reflected in apps/system/js/storage.js
+ this.dispatchEvent('unlock');
+ this.writeSetting(false);
+
+ if (instant)
+ return;
+
+ if (this.unlockSoundEnabled) {
+ var unlockAudio = new Audio('./resources/sounds/unlock.ogg');
+ unlockAudio.play();
+ }
+ }
+ }).bind(this);
+
+ this.dispatchEvent('will-unlock');
+ currentFrame.addNextPaintListener(nextPaint);
+ repaintTimeout = setTimeout(function ensureUnlock() {
+ nextPaint();
+ }, 400);
+ },
+
+ lock: function ls_lock(instant) {
+ var wasAlreadyLocked = this.locked;
+ this.locked = true;
+
+ this.updateTime();
+
+ this.switchPanel();
+
+ this.setElasticEnabled(ScreenManager.screenEnabled);
+
+ this.overlay.focus();
+ if (instant)
+ this.overlay.classList.add('no-transition');
+ else
+ this.overlay.classList.remove('no-transition');
+
+ this.mainScreen.classList.add('locked');
+
+ screen.mozLockOrientation('portrait-primary');
+
+ if (!wasAlreadyLocked) {
+ if (document.mozFullScreen)
+ document.mozCancelFullScreen();
+
+ // Any changes made to this,
+ // also need to be reflected in apps/system/js/storage.js
+ this.dispatchEvent('lock');
+ this.writeSetting(true);
+ }
+ },
+
+ loadPanel: function ls_loadPanel(panel, callback) {
+ this._loadingPanel = true;
+ switch (panel) {
+ case 'passcode':
+ case 'main':
+ if (callback)
+ setTimeout(callback);
+ break;
+
+ case 'emergency-call':
+ // create the <iframe> and load the emergency call
+ var frame = document.createElement('iframe');
+
+ frame.src = './emergency-call/index.html';
+ frame.onload = function emergencyCallLoaded() {
+ if (callback)
+ callback();
+ };
+ this.panelEmergencyCall.appendChild(frame);
+
+ break;
+
+ case 'camera':
+ // create the <iframe> and load the camera
+ var frame = document.createElement('iframe');
+
+ frame.src = './camera/index.html';
+ var mainScreen = this.mainScreen;
+ frame.onload = function cameraLoaded() {
+ mainScreen.classList.add('lockscreen-camera');
+ if (callback)
+ callback();
+ };
+ this.overlay.classList.remove('no-transition');
+ this.camera.appendChild(frame);
+
+ break;
+ }
+ },
+
+ unloadPanel: function ls_unloadPanel(panel, toPanel, callback) {
+ switch (panel) {
+ case 'passcode':
+ // Reset passcode panel only if the status is not error
+ if (this.overlay.dataset.passcodeStatus == 'error')
+ break;
+
+ delete this.overlay.dataset.passcodeStatus;
+ this.passCodeEntered = '';
+ this.updatePassCodeUI();
+ break;
+
+ case 'camera':
+ this.mainScreen.classList.remove('lockscreen-camera');
+ break;
+
+ case 'emergency-call':
+ var ecPanel = this.panelEmergencyCall;
+ ecPanel.addEventListener('transitionend', function unloadPanel() {
+ ecPanel.removeEventListener('transitionend', unloadPanel);
+ ecPanel.removeChild(ecPanel.firstElementChild);
+ });
+ break;
+
+ case 'main':
+ default:
+ var self = this;
+ var unload = function unload() {
+ self.areaHandle.style.transform =
+ self.areaUnlock.style.transform =
+ self.areaCamera.style.transform =
+ self.iconContainer.style.transform =
+ self.iconContainer.style.opacity =
+ self.areaHandle.style.opacity =
+ self.areaUnlock.style.opacity =
+ self.areaCamera.style.opacity = '';
+ self.overlay.classList.remove('triggered');
+ self.areaHandle.classList.remove('triggered');
+ self.areaCamera.classList.remove('triggered');
+ self.areaUnlock.classList.remove('triggered');
+
+ clearTimeout(self.triggeredTimeoutId);
+ self.setElasticEnabled(false);
+ };
+
+ if (toPanel !== 'camera') {
+ unload();
+ break;
+ }
+
+ this.overlay.addEventListener('transitionend',
+ function ls_unloadDefaultPanel(evt) {
+ if (evt.target !== this)
+ return;
+
+ self.overlay.removeEventListener('transitionend',
+ ls_unloadDefaultPanel);
+ unload();
+ }
+ );
+
+ break;
+ }
+
+ if (callback)
+ setTimeout(callback);
+ },
+
+ switchPanel: function ls_switchPanel(panel) {
+ if (this._switchingPanel) {
+ return;
+ }
+
+ var overlay = this.overlay;
+ var self = this;
+ panel = panel || 'main';
+
+ this._switchingPanel = true;
+ this.loadPanel(panel, function panelLoaded() {
+ self.unloadPanel(overlay.dataset.panel, panel,
+ function panelUnloaded() {
+ if (overlay.dataset.panel !== panel)
+ self.dispatchEvent('lockpanelchange');
+
+ overlay.dataset.panel = panel;
+ self._switchingPanel = false;
+ });
+ });
+ },
+
+ updateTime: function ls_updateTime() {
+ if (!this.locked)
+ return;
+
+ var d = new Date();
+ var f = new navigator.mozL10n.DateTimeFormat();
+ var _ = navigator.mozL10n.get;
+
+ var timeFormat = _('shortTimeFormat') || '%H:%M';
+ var dateFormat = _('longDateFormat') || '%A %e %B';
+ var time = f.localeFormat(d, timeFormat);
+ this.clockNumbers.textContent = time.match(/([012]?\d).[0-5]\d/g);
+ this.clockMeridiem.textContent = (time.match(/AM|PM/i) || []).join('');
+ this.date.textContent = f.localeFormat(d, dateFormat);
+
+ var self = this;
+ window.setTimeout(function ls_clockTimeout() {
+ self.updateTime();
+ }, (59 - d.getSeconds()) * 1000);
+ },
+
+ updateConnState: function ls_updateConnState() {
+ var conn = window.navigator.mozMobileConnection;
+ if (!conn)
+ return;
+
+ var voice = conn.voice;
+ var iccInfo = conn.iccInfo;
+ var connstateLine1 = this.connstate.firstElementChild;
+ var connstateLine2 = this.connstate.lastElementChild;
+ var _ = navigator.mozL10n.get;
+
+ var updateConnstateLine1 = function updateConnstateLine1(l10nId) {
+ connstateLine1.dataset.l10nId = l10nId;
+ connstateLine1.textContent = _(l10nId) || '';
+ };
+
+ var self = this;
+ var updateConnstateLine2 = function updateConnstateLine2(l10nId) {
+ if (l10nId) {
+ self.connstate.classList.add('twolines');
+ connstateLine2.dataset.l10nId = l10nId;
+ connstateLine2.textContent = _(l10nId) || '';
+ } else {
+ self.connstate.classList.remove('twolines');
+ delete(connstateLine2.dataset.l10nId);
+ connstateLine2.textContent = '';
+ }
+ };
+
+ // Reset line 2
+ updateConnstateLine2();
+
+ if (this.airplaneMode) {
+ updateConnstateLine1('airplaneMode');
+ return;
+ }
+
+ // Possible value of voice.state are:
+ // 'notSearching', 'searching', 'denied', 'registered',
+ // where the latter three mean the phone is trying to grab the network.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=777057
+ if (voice.state == 'notSearching') {
+ updateConnstateLine1('noNetwork');
+ return;
+ }
+
+ if (!voice.connected && !voice.emergencyCallsOnly) {
+ // "Searching"
+ // voice.state can be any of the latter three values.
+ // (it's possible that the phone is briefly 'registered'
+ // but not yet connected.)
+ updateConnstateLine1('searching');
+ return;
+ }
+
+ if (voice.emergencyCallsOnly) {
+ updateConnstateLine1('emergencyCallsOnly');
+
+ switch (conn.cardState) {
+ case 'absent':
+ updateConnstateLine2('emergencyCallsOnly-noSIM');
+ break;
+
+ case 'pinRequired':
+ updateConnstateLine2('emergencyCallsOnly-pinRequired');
+ break;
+
+ case 'pukRequired':
+ updateConnstateLine2('emergencyCallsOnly-pukRequired');
+ break;
+
+ case 'networkLocked':
+ updateConnstateLine2('emergencyCallsOnly-networkLocked');
+ break;
+
+ default:
+ updateConnstateLine2();
+ break;
+ }
+ return;
+ }
+
+ var operatorInfos = MobileOperator.userFacingInfo(conn);
+ if (this.cellbroadcastLabel) {
+ connstateLine2.textContent = this.cellbroadcastLabel;
+ } else if (operatorInfos.carrier) {
+ connstateLine2.textContent = operatorInfos.carrier + ' ' +
+ operatorInfos.region;
+ }
+
+ var operator = operatorInfos.operator;
+
+ if (voice.roaming) {
+ var l10nArgs = { operator: operator };
+ connstateLine1.dataset.l10nId = 'roaming';
+ connstateLine1.dataset.l10nArgs = JSON.stringify(l10nArgs);
+ connstateLine1.textContent = _('roaming', l10nArgs);
+
+ return;
+ }
+
+ delete connstateLine1.dataset.l10nId;
+ connstateLine1.textContent = operator;
+ },
+
+ updatePassCodeUI: function lockscreen_updatePassCodeUI() {
+ var overlay = this.overlay;
+ if (overlay.dataset.passcodeStatus)
+ return;
+ if (this.passCodeEntered) {
+ overlay.classList.add('passcode-entered');
+ } else {
+ overlay.classList.remove('passcode-entered');
+ }
+ var i = 4;
+ while (i--) {
+ var span = this.passcodeCode.childNodes[i];
+ if (this.passCodeEntered.length > i) {
+ span.dataset.dot = true;
+ } else {
+ delete span.dataset.dot;
+ }
+ }
+ },
+
+ checkPassCode: function lockscreen_checkPassCode() {
+ if (this.passCodeEntered === this.passCode) {
+ var self = this;
+ this.overlay.dataset.passcodeStatus = 'success';
+ this.passCodeError = 0;
+
+ var transitionend = function() {
+ self.passcodeCode.removeEventListener('transitionend', transitionend);
+ self.unlock();
+ };
+ this.passcodeCode.addEventListener('transitionend', transitionend);
+ } else {
+ this.overlay.dataset.passcodeStatus = 'error';
+ if ('vibrate' in navigator)
+ navigator.vibrate([50, 50, 50]);
+
+ var self = this;
+ setTimeout(function error() {
+ delete self.overlay.dataset.passcodeStatus;
+ self.passCodeEntered = '';
+ self.updatePassCodeUI();
+ }, this.kPassCodeErrorTimeout);
+ }
+ },
+
+ updateBackground: function ls_updateBackground(background_datauri) {
+ this._imgPreload([background_datauri, 'style/lockscreen/images/mask.png'],
+ function(images) {
+
+ // Bug 829075 : We need a <canvas> in the DOM to prevent banding on
+ // Otoro-like devices
+ var canvas = document.createElement('canvas');
+ canvas.classList.add('lockscreen-wallpaper');
+ canvas.width = images[0].width;
+ canvas.height = images[0].height;
+
+ var ctx = canvas.getContext('2d');
+ ctx.drawImage(images[0], 0, 0);
+ ctx.drawImage(images[1], 0, 0);
+
+ var panels_selector = '.lockscreen-panel[data-wallpaper]';
+ var panels = document.querySelectorAll(panels_selector);
+ for (var i = 0, il = panels.length; i < il; i++) {
+ var copied_canvas;
+ var panel = panels[i];
+
+ // Remove previous <canvas> if they exist
+ var old_canvas = panel.querySelector('canvas');
+ if (old_canvas) {
+ old_canvas.parentNode.removeChild(old_canvas);
+ }
+
+ // For the first panel, we can use the existing <canvas>
+ if (!copied_canvas) {
+ copied_canvas = canvas;
+ } else {
+ // Otherwise, copy the node and content
+ copied_canvas = canvas.cloneNode();
+ copied_canvas.getContext('2d').drawImage(canvas, 0, 0);
+ }
+
+ panel.insertBefore(copied_canvas, panel.firstChild);
+ }
+ });
+ },
+
+ _imgPreload: function ls_imgPreload(img_paths, callback) {
+ var loaded = 0;
+ var images = [];
+ var il = img_paths.length;
+ var inc = function() {
+ loaded += 1;
+ if (loaded === il && callback) {
+ callback(images);
+ }
+ };
+ for (var i = 0; i < il; i++) {
+ images[i] = new Image();
+ images[i].onload = inc;
+ images[i].src = img_paths[i];
+ }
+ },
+
+ getAllElements: function ls_getAllElements() {
+ // ID of elements to create references
+ var elements = ['connstate', 'mute', 'clock-numbers', 'clock-meridiem',
+ 'date', 'area', 'area-unlock', 'area-camera', 'icon-container',
+ 'area-handle', 'passcode-code',
+ 'passcode-pad', 'camera', 'accessibility-camera',
+ 'accessibility-unlock', 'panel-emergency-call'];
+
+ var toCamelCase = function toCamelCase(str) {
+ return str.replace(/\-(.)/g, function replacer(str, p1) {
+ return p1.toUpperCase();
+ });
+ };
+
+ elements.forEach((function createElementRef(name) {
+ this[toCamelCase(name)] = document.getElementById('lockscreen-' + name);
+ }).bind(this));
+
+ this.overlay = document.getElementById('lockscreen');
+ this.mainScreen = document.getElementById('screen');
+ },
+
+ dispatchEvent: function ls_dispatchEvent(name) {
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent(name, true, true, null);
+ window.dispatchEvent(evt);
+ },
+
+ writeSetting: function ls_writeSetting(value) {
+ if (!window.navigator.mozSettings)
+ return;
+
+ SettingsListener.getSettingsLock().set({
+ 'lockscreen.locked': value
+ });
+ },
+
+ setElasticEnabled: function ls_setElasticEnabled(value) {
+ clearInterval(this.elasticIntervalId);
+ if (value) {
+ this.elasticIntervalId =
+ setInterval(this.playElastic.bind(this), this.ELASTIC_INTERVAL);
+ }
+ },
+
+ playElastic: function ls_playElastic() {
+ if (this._touch && this._touch.touched)
+ return;
+
+ var overlay = this.overlay;
+ var container = this.iconContainer;
+
+ overlay.classList.add('elastic');
+ container.addEventListener('animationend', function animationend(e) {
+ container.removeEventListener(e.type, animationend);
+ overlay.classList.remove('elastic');
+ });
+ }
+};
+
+// Bug 836195 - [Homescreen] Dock icons drop down in the UI
+// consistently when using a lockcode and visiting camera
+LockScreen.init();
+
+navigator.mozL10n.ready(LockScreen.init.bind(LockScreen));
+
diff --git a/apps/system/js/modal_dialog.js b/apps/system/js/modal_dialog.js
new file mode 100644
index 0000000..33f3201
--- /dev/null
+++ b/apps/system/js/modal_dialog.js
@@ -0,0 +1,443 @@
+/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+// The modal dialog listen to mozbrowsershowmodalprompt event.
+// Blocking the current app and then show cutom modal dialog
+// (alert/confirm/prompt)
+
+var ModalDialog = {
+ // Used for element id access.
+ // e.g., 'modal-dialog-alert-ok'
+ prefix: 'modal-dialog-',
+
+ // DOM
+ elements: {},
+
+ // Get all elements when inited.
+ getAllElements: function md_getAllElements() {
+ var elementsID = ['alert', 'alert-ok', 'alert-message',
+ 'prompt', 'prompt-ok', 'prompt-cancel', 'prompt-input', 'prompt-message',
+ 'confirm', 'confirm-ok', 'confirm-cancel', 'confirm-message',
+ 'select-one', 'select-one-cancel', 'select-one-menu', 'select-one-title',
+ 'alert-title', 'confirm-title', 'prompt-title'];
+
+ var toCamelCase = function toCamelCase(str) {
+ return str.replace(/\-(.)/g, function replacer(str, p1) {
+ return p1.toUpperCase();
+ });
+ };
+
+ // Loop and add element with camel style name to Modal Dialog attribute.
+ elementsID.forEach(function createElementRef(name) {
+ this.elements[toCamelCase(name)] =
+ document.getElementById(this.prefix + name);
+ }, this);
+
+ this.screen = document.getElementById('screen');
+ this.overlay = document.getElementById('dialog-overlay');
+ },
+
+ // Save the events returned by mozbrowsershowmodalprompt for later use.
+ // The events are stored according to webapp origin
+ // e.g., 'http://uitest.gaiamobile.org': evt
+ currentEvents: {},
+
+ init: function md_init() {
+ // Get all elements initially.
+ this.getAllElements();
+ var elements = this.elements;
+
+ // Bind events
+ window.addEventListener('mozbrowsershowmodalprompt', this);
+ window.addEventListener('appopen', this);
+ window.addEventListener('appwillclose', this);
+ window.addEventListener('appterminated', this);
+ window.addEventListener('resize', this);
+ window.addEventListener('keyboardchange', this);
+ window.addEventListener('keyboardhide', this);
+ window.addEventListener('home', this);
+ window.addEventListener('holdhome', this);
+
+ for (var id in elements) {
+ var tagName = elements[id].tagName.toLowerCase();
+ if (tagName == 'button' || tagName == 'ul') {
+ elements[id].addEventListener('click', this);
+ }
+ }
+ },
+
+ // Default event handler
+ handleEvent: function md_handleEvent(evt) {
+ var elements = this.elements;
+ switch (evt.type) {
+ case 'mozbrowsershowmodalprompt':
+ var frameType = evt.target.dataset.frameType;
+ if (frameType != 'window' && frameType != 'inline-activity')
+ return;
+
+ evt.preventDefault();
+ var origin = evt.target.dataset.frameOrigin;
+ this.currentEvents[origin] = evt;
+
+ // Show modal dialog only if
+ // the frame is currently displayed.
+ if (origin == WindowManager.getDisplayedApp() ||
+ frameType == 'inline-activity')
+ this.show(evt.target, origin);
+ break;
+
+ case 'click':
+ if (evt.currentTarget === elements.confirmCancel ||
+ evt.currentTarget === elements.promptCancel ||
+ evt.currentTarget === elements.selectOneCancel) {
+ this.cancelHandler();
+ } else if (evt.currentTarget === elements.selectOneMenu) {
+ this.selectOneHandler(evt.target);
+ } else {
+ this.confirmHandler();
+ }
+ break;
+
+ case 'appopen':
+ this.show(evt.target, evt.detail.origin);
+ break;
+
+ case 'home':
+ case 'holdhome':
+ // Inline activity, which origin is different from foreground app
+ if (this.isVisible() &&
+ this.currentOrigin != WindowManager.getDisplayedApp())
+ this.cancelHandler();
+ break;
+
+ case 'appwillclose':
+ // Do nothing if the app is closed at background.
+ if (evt.detail.origin !== this.currentOrigin)
+ return;
+
+ // Reset currentOrigin
+ this.hide();
+ break;
+
+ case 'appterminated':
+ if (this.currentEvents[evt.detail.origin])
+ delete this.currentEvents[evt.detail.origin];
+
+ break;
+
+ case 'resize':
+ case 'keyboardhide':
+ if (!this.currentOrigin)
+ return;
+
+ this.setHeight(window.innerHeight - StatusBar.height);
+ break;
+
+ case 'keyboardchange':
+ this.setHeight(window.innerHeight -
+ evt.detail.height - StatusBar.height);
+ break;
+ }
+ },
+
+ setHeight: function md_setHeight(height) {
+ if (this.isVisible())
+ this.overlay.style.height = height + 'px';
+ },
+
+ // Show relative dialog and set message/input value well
+ show: function md_show(target, origin) {
+ if (!(origin in this.currentEvents))
+ return;
+
+ var _ = navigator.mozL10n.get;
+ var evt = this.currentEvents[origin];
+ this.currentOrigin = origin;
+
+ var message = evt.detail.message || '';
+ var elements = this.elements;
+ this.screen.classList.add('modal-dialog');
+
+ function escapeHTML(str) {
+ var span = document.createElement('span');
+ span.textContent = str;
+ // Escape space for displaying multiple space in message.
+ span.innerHTML = span.innerHTML.replace(/\n/g, '<br/>');
+ return span.innerHTML;
+ }
+
+ var type = evt.detail.promptType || evt.detail.type;
+ if (type !== 'selectone') {
+ message = escapeHTML(message);
+ }
+
+ switch (type) {
+ case 'alert':
+ elements.alertMessage.innerHTML = message;
+ elements.alert.classList.add('visible');
+ this.setTitle('alert', '');
+ elements.alertOk.textContent = evt.yesText ? evt.yesText : _('ok');
+ break;
+
+ case 'prompt':
+ elements.prompt.classList.add('visible');
+ elements.promptInput.value = evt.detail.initialValue;
+ elements.promptMessage.innerHTML = message;
+ this.setTitle('prompt', '');
+ elements.promptOk.textContent = evt.yesText ? evt.yesText : _('ok');
+ elements.promptCancel.textContent = evt.noText ?
+ evt.noText : _('cancel');
+ break;
+
+ case 'confirm':
+ elements.confirm.classList.add('visible');
+ elements.confirmMessage.innerHTML = message;
+ this.setTitle('confirm', '');
+ elements.confirmOk.textContent = evt.yesText ? evt.yesText : _('ok');
+ elements.confirmCancel.textContent = evt.noText ?
+ evt.noText : _('cancel');
+ break;
+
+ case 'selectone':
+ this.buildSelectOneDialog(message);
+ elements.selectOne.classList.add('visible');
+ break;
+ }
+
+ this.setHeight(window.innerHeight - StatusBar.height);
+ },
+
+ hide: function md_hide() {
+ var evt = this.currentEvents[this.currentOrigin];
+ var type = evt.detail.promptType;
+ if (type == 'prompt') {
+ this.elements.promptInput.blur();
+ }
+ this.currentOrigin = null;
+ this.screen.classList.remove('modal-dialog');
+ this.elements[type].classList.remove('visible');
+ },
+
+ setTitle: function md_setTitle(type, title) {
+ this.elements[type + 'Title'].textContent = title;
+ },
+
+ // When user clicks OK button on alert/confirm/prompt
+ confirmHandler: function md_confirmHandler() {
+ this.screen.classList.remove('modal-dialog');
+ var elements = this.elements;
+
+ var evt = this.currentEvents[this.currentOrigin];
+
+ var type = evt.detail.promptType || evt.detail.type;
+ switch (type) {
+ case 'alert':
+ elements.alert.classList.remove('visible');
+ break;
+
+ case 'prompt':
+ evt.detail.returnValue = elements.promptInput.value;
+ elements.prompt.classList.remove('visible');
+ break;
+
+ case 'confirm':
+ evt.detail.returnValue = true;
+ elements.confirm.classList.remove('visible');
+ break;
+ }
+
+ if (evt.isPseudo && evt.callback) {
+ evt.callback(evt.detail.returnValue);
+ }
+
+ if (evt.detail.unblock)
+ evt.detail.unblock();
+
+ delete this.currentEvents[this.currentOrigin];
+ },
+
+ // When user clicks cancel button on confirm/prompt or
+ // when the user try to escape the dialog with the escape key
+ cancelHandler: function md_cancelHandler() {
+ var evt = this.currentEvents[this.currentOrigin];
+ this.screen.classList.remove('modal-dialog');
+ var elements = this.elements;
+
+ var type = evt.detail.promptType || evt.detail.type;
+ switch (type) {
+ case 'alert':
+ elements.alert.classList.remove('visible');
+ break;
+
+ case 'prompt':
+ /* return null when click cancel */
+ evt.detail.returnValue = null;
+ elements.prompt.classList.remove('visible');
+ break;
+
+ case 'confirm':
+ /* return false when click cancel */
+ evt.detail.returnValue = false;
+ elements.confirm.classList.remove('visible');
+ break;
+
+ case 'selectone':
+ /* return null when click cancel */
+ evt.detail.returnValue = null;
+ elements.selectOne.classList.remove('visible');
+ break;
+ }
+
+ if (evt.isPseudo && evt.cancelCallback) {
+ evt.cancelCallback(evt.detail.returnValue);
+ }
+
+ if (evt.detail.unblock)
+ evt.detail.unblock();
+
+ delete this.currentEvents[this.currentOrigin];
+ },
+
+ // When user selects an option on selectone dialog
+ selectOneHandler: function md_confirmHandler(target) {
+ this.screen.classList.remove('modal-dialog');
+ var elements = this.elements;
+
+ var evt = this.currentEvents[this.currentOrigin];
+
+ evt.detail.returnValue = target.id;
+ elements.selectOne.classList.remove('visible');
+
+ if (evt.isPseudo && evt.callback) {
+ evt.callback(evt.detail.returnValue);
+ }
+
+ if (evt.detail.unblock)
+ evt.detail.unblock();
+
+ delete this.currentEvents[this.currentOrigin];
+ },
+
+ buildSelectOneDialog: function md_buildSelectOneDialog(data) {
+ var elements = this.elements;
+ elements.selectOneTitle.textContent = data.title;
+ elements.selectOneMenu.innerHTML = '';
+
+ if (!data.options) {
+ return;
+ }
+
+ var itemsHTML = [];
+ for (var i = 0; i < data.options.length; i++) {
+ itemsHTML.push('<li><button id="');
+ itemsHTML.push(data.options[i].id);
+ itemsHTML.push('">');
+ itemsHTML.push(data.options[i].text);
+ itemsHTML.push('</button></li>');
+ }
+
+ elements.selectOneMenu.innerHTML = itemsHTML.join('');
+ },
+
+ /**
+ * Method about customized alert
+ * @param {String} title the title of the dialog. null or empty for
+ * no title.
+ * @param {String} text message for the dialog.
+ * @param {Object} confirm {title, callback} object when confirm.
+ */
+ alert: function md_alert(title, text, confirm) {
+ this.showWithPseudoEvent({
+ type: 'alert',
+ text: text,
+ callback: confirm.callback,
+ title: title,
+ yesText: confirm.title
+ });
+ },
+
+ /**
+ * Method about customized confirm
+ * @param {String} title the title of the dialog. null or empty for
+ * no title.
+ * @param {String} text message for the dialog.
+ * @param {Object} confirm {title, callback} object when confirm.
+ * @param {Object} cancel {title, callback} object when cancel.
+ */
+ confirm: function md_confirm(title, text, confirm, cancel) {
+ this.showWithPseudoEvent({
+ type: 'confirm',
+ text: text,
+ callback: confirm.callback,
+ cancel: cancel.callback,
+ title: title,
+ yesText: confirm.title,
+ noText: cancel.title
+ });
+ },
+
+ /**
+ * Method about customized prompt
+ * @param {String} title the title of the dialog. null or empty for
+ * no title.
+ * @param {String} text message for the dialog.
+ * @param {String} default_value message in the text field.
+ * @param {Object} confirm {title, callback} object when confirm.
+ * @param {Object} cancel {title, callback} object when cancel.
+ */
+ prompt: function md_prompt(title, text, default_value, confirm, cancel) {
+ this.showWithPseudoEvent({
+ type: 'prompt',
+ text: text,
+ initialValue: default_value,
+ callback: confirm.callback,
+ cancel: cancel.callback,
+ title: title,
+ yesText: confirm.title,
+ noText: cancel.title
+ });
+ },
+
+ selectOne: function md_selectOne(data, callback) {
+ this.showWithPseudoEvent({
+ type: 'selectone',
+ text: data,
+ callback: callback
+ });
+ },
+
+ showWithPseudoEvent: function md_showWithPseudoEvent(config) {
+ var pseudoEvt = {
+ isPseudo: true,
+ detail: {
+ unblock: null
+ }
+ };
+
+ pseudoEvt.detail.message = config.text;
+ pseudoEvt.callback = config.callback;
+ pseudoEvt.detail.promptType = config.type;
+ pseudoEvt.cancelCallback = config.cancel;
+ pseudoEvt.yesText = config.yesText;
+ pseudoEvt.noText = config.noText;
+ if (config.type == 'prompt') {
+ pseudoEvt.detail.initialValue = config.initialValue;
+ }
+
+ // Create a virtual mapping in this.currentEvents,
+ // since system-app uses the different way to call ModalDialog.
+ this.currentEvents['system'] = pseudoEvt;
+ this.show(null, 'system');
+ if (config.title)
+ this.setTitle(config.type, config.title);
+ },
+
+ isVisible: function md_isVisible() {
+ return this.screen.classList.contains('modal-dialog');
+ }
+};
+
+ModalDialog.init();
+
diff --git a/apps/system/js/mouse2touch.js b/apps/system/js/mouse2touch.js
new file mode 100644
index 0000000..b9d6a89
--- /dev/null
+++ b/apps/system/js/mouse2touch.js
@@ -0,0 +1,70 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+/*
+* Mouse2Touch is a shim that listens to MouseEvent but passes
+* fake TouchEvent object to touch event handler
+*
+* XXX: remove this if we are not going to support testing on
+* non-touch devices, e.g. B2G Desktop, Nightly, or,
+* make the creation of this object optional if we can reliably detect
+* touch support by evaluate (document instanceof DocumentTouch)
+*
+*/
+var Mouse2Touch = (function m2t() {
+ var Mouse2TouchEvent = {
+ 'mousedown': 'touchstart',
+ 'mousemove': 'touchmove',
+ 'mouseup': 'touchend'
+ };
+
+ var Touch2MouseEvent = {
+ 'touchstart': 'mousedown',
+ 'touchmove': 'mousemove',
+ 'touchend': 'mouseup'
+ };
+
+ var ForceOnWindow = {
+ 'touchmove': true,
+ 'touchend': true
+ };
+
+ var addEventHandler = function m2t_addEventHandlers(target,
+ name,
+ listener) {
+ target = ForceOnWindow[name] ? window : target;
+ name = Touch2MouseEvent[name] || name;
+ target.addEventListener(name, {
+ handleEvent: function m2t_handleEvent(evt) {
+ if (Mouse2TouchEvent[evt.type]) {
+ var original = evt;
+ evt = {
+ type: Mouse2TouchEvent[original.type],
+ target: original.target,
+ touches: [original],
+ preventDefault: function() {
+ original.preventDefault();
+ }
+ };
+ evt.changedTouches = evt.touches;
+ }
+ return listener.handleEvent(evt);
+ }
+ }, true);
+ };
+
+ var removeEventHandler = function m2t_removeEventHandler(target,
+ name,
+ listener) {
+ target = ForceOnWindow[name] ? window : target;
+ name = Touch2MouseEvent[name] || name;
+ target.removeEventListener(name, listener);
+ };
+
+ return {
+ addEventHandler: addEventHandler,
+ removeEventHandler: removeEventHandler
+ };
+})();
diff --git a/apps/system/js/notifications.js b/apps/system/js/notifications.js
new file mode 100644
index 0000000..dae4ab4
--- /dev/null
+++ b/apps/system/js/notifications.js
@@ -0,0 +1,410 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+(function appCacheIcons() {
+ // Caching the icon for notification if appCache is in effect
+ var appCache = window.applicationCache;
+ if (!appCache)
+ return;
+
+ var addIcons = function addIcons(app) {
+ var icons = app.manifest.icons;
+ if (icons) {
+ Object.keys(icons).forEach(function iconIterator(key) {
+ var url = app.origin + icons[key];
+ appCache.mozAdd(url);
+ });
+ }
+ };
+
+ var removeIcons = function removeIcons(app) {
+ var icons = app.manifest.icons;
+ if (icons) {
+ Object.keys(icons).forEach(function iconIterator(key) {
+ var url = app.origin + icons[key];
+ appCache.mozRemove(url);
+ });
+ }
+ };
+
+ window.addEventListener('applicationinstall', function bsm_oninstall(evt) {
+ addIcons(evt.detail.application);
+ });
+
+ window.addEventListener('applicationuninstall', function bsm_oninstall(evt) {
+ removeIcons(evt.detail.application);
+ });
+}());
+
+var NotificationScreen = {
+ TOASTER_TIMEOUT: 5000,
+ TRANSITION_SPEED: 1.8,
+ TRANSITION_FRACTION: 0.30,
+
+ _notification: null,
+ _containerWidth: null,
+ _toasterTimeout: null,
+ _toasterGD: null,
+
+ lockscreenPreview: true,
+ silent: false,
+ alerts: true,
+ vibrates: true,
+
+ init: function ns_init() {
+ window.addEventListener('mozChromeEvent', this);
+ this.container =
+ document.getElementById('desktop-notifications-container');
+ this.lockScreenContainer =
+ document.getElementById('notifications-lockscreen-container');
+ this.toaster = document.getElementById('notification-toaster');
+ this.toasterIcon = document.getElementById('toaster-icon');
+ this.toasterTitle = document.getElementById('toaster-title');
+ this.toasterDetail = document.getElementById('toaster-detail');
+ this.clearAllButton = document.getElementById('notification-clear');
+
+ this._toasterGD = new GestureDetector(this.toaster);
+ ['tap', 'mousedown', 'swipe'].forEach(function(evt) {
+ this.container.addEventListener(evt, this);
+ this.toaster.addEventListener(evt, this);
+ }, this);
+
+ this.clearAllButton.addEventListener('click', this.clearAll.bind(this));
+
+ // will hold the count of external contributors to the notification
+ // screen
+ this.externalNotificationsCount = 0;
+
+ window.addEventListener('utilitytrayshow', this);
+ window.addEventListener('unlock', this.clearLockScreen.bind(this));
+ window.addEventListener('mozvisibilitychange', this);
+ window.addEventListener('appopen', this.handleAppopen.bind(this));
+
+ this._sound = 'style/notifications/ringtones/notifier_exclamation.ogg';
+
+ var self = this;
+ SettingsListener.observe('notification.ringtone', '', function(value) {
+ self._sound = value;
+ });
+ },
+
+ handleEvent: function ns_handleEvent(evt) {
+ switch (evt.type) {
+ case 'mozChromeEvent':
+ var detail = evt.detail;
+ if (detail.type !== 'desktop-notification')
+ return;
+
+ this.addNotification(detail);
+ break;
+ case 'tap':
+ var target = evt.target;
+ this.tap(target);
+ break;
+ case 'mousedown':
+ this.mousedown(evt);
+ break;
+ case 'swipe':
+ this.swipe(evt);
+ break;
+ case 'utilitytrayshow':
+ this.updateTimestamps();
+ StatusBar.updateNotificationUnread(false);
+ break;
+ case 'mozvisibilitychange':
+ //update timestamps in lockscreen notifications
+ if (!document.mozHidden) {
+ this.updateTimestamps();
+ }
+ break;
+ }
+ },
+
+ handleAppopen: function ns_handleAppopen(evt) {
+ var manifestURL = evt.detail.manifestURL,
+ selector = '[data-manifest-u-r-l="' + manifestURL + '"]';
+
+ var nodes = this.container.querySelectorAll(selector);
+
+ for (var i = nodes.length - 1; i >= 0; i--) {
+ this.closeNotification(nodes[i]);
+ }
+ },
+
+ // Swipe handling
+ mousedown: function ns_mousedown(evt) {
+ if (!evt.target.dataset.notificationID)
+ return;
+
+ evt.preventDefault();
+ this._notification = evt.target;
+ this._containerWidth = this.container.clientWidth;
+ },
+
+ swipe: function ns_swipe(evt) {
+ var detail = evt.detail;
+ var distance = detail.start.screenX - detail.end.screenX;
+ var fastEnough = Math.abs(detail.vx) > this.TRANSITION_SPEED;
+ var farEnough = Math.abs(distance) >
+ this._containerWidth * this.TRANSITION_FRACTION;
+
+ // We only remove the notification if the swipe was
+ // - left to right
+ // - far or fast enough
+ if ((distance > 0) ||
+ !(farEnough || fastEnough)) {
+ // Werent far or fast enough to delete, restore
+ delete this._notification;
+ return;
+ }
+
+ this._notification.classList.add('disappearing');
+
+ var notification = this._notification;
+ this._notification = null;
+
+ var toaster = this.toaster;
+ var self = this;
+ notification.addEventListener('transitionend', function trListener() {
+ notification.removeEventListener('transitionend', trListener);
+
+ self.closeNotification(notification);
+
+ if (notification != toaster)
+ return;
+
+ // Putting back the toaster in a clean state for the next notification
+ toaster.style.display = 'none';
+ setTimeout(function nextLoop() {
+ toaster.style.MozTransition = '';
+ toaster.style.MozTransform = '';
+ toaster.classList.remove('displayed');
+ toaster.classList.remove('disappearing');
+
+ setTimeout(function nextLoop() {
+ toaster.style.display = 'block';
+ });
+ });
+ });
+ },
+
+ tap: function ns_tap(notificationNode) {
+ var notificationID = notificationNode.dataset.notificationID;
+
+ var event = document.createEvent('CustomEvent');
+ event.initCustomEvent('mozContentEvent', true, true, {
+ type: 'desktop-notification-click',
+ id: notificationID
+ });
+ window.dispatchEvent(event);
+
+ this.removeNotification(notificationNode.dataset.notificationID, false);
+
+ if (notificationNode == this.toaster) {
+ this.toaster.classList.remove('displayed');
+ } else {
+ UtilityTray.hide();
+ }
+ },
+
+ updateTimestamps: function ns_updateTimestamps() {
+ var timestamps = document.getElementsByClassName('timestamp');
+ for (var i = 0, l = timestamps.length; i < l; i++) {
+ timestamps[i].textContent =
+ this.prettyDate(new Date(timestamps[i].dataset.timestamp));
+ }
+ },
+
+ /**
+ * Display a human-readable relative timestamp.
+ */
+ prettyDate: function prettyDate(time) {
+ var date;
+ if (navigator.mozL10n) {
+ date = navigator.mozL10n.DateTimeFormat().fromNow(time, true);
+ } else {
+ date = time.toLocaleFormat();
+ }
+ return date;
+ },
+
+ addNotification: function ns_addNotification(detail) {
+ var notificationNode = document.createElement('div');
+ notificationNode.className = 'notification';
+
+ notificationNode.dataset.notificationID = detail.id;
+ notificationNode.dataset.manifestURL = detail.manifestURL;
+
+ if (detail.icon) {
+ var icon = document.createElement('img');
+ icon.src = detail.icon;
+ notificationNode.appendChild(icon);
+ this.toasterIcon.src = detail.icon;
+ this.toasterIcon.hidden = false;
+ } else {
+ this.toasterIcon.hidden = true
+ }
+
+ var time = document.createElement('span');
+ var timestamp = new Date();
+ time.classList.add('timestamp');
+ time.dataset.timestamp = timestamp;
+ time.textContent = this.prettyDate(timestamp);
+ notificationNode.appendChild(time);
+
+ var title = document.createElement('div');
+ title.textContent = detail.title;
+ notificationNode.appendChild(title);
+
+ this.toasterTitle.textContent = detail.title;
+
+ var message = document.createElement('div');
+ message.classList.add('detail');
+ message.textContent = detail.text;
+ notificationNode.appendChild(message);
+
+ this.toasterDetail.textContent = detail.text;
+
+ this.container.insertBefore(notificationNode,
+ this.container.firstElementChild);
+ new GestureDetector(notificationNode).startDetecting();
+
+ // We turn the screen on if needed in order to let
+ // the user see the notification toaster
+ if (typeof(ScreenManager) !== 'undefined' &&
+ !ScreenManager.screenEnabled) {
+ ScreenManager.turnScreenOn();
+ }
+
+ this.updateStatusBarIcon(true);
+
+ // Notification toaster
+ if (this.lockscreenPreview || !LockScreen.locked) {
+ this.toaster.dataset.notificationID = detail.id;
+
+ this.toaster.classList.add('displayed');
+ this._toasterGD.startDetecting();
+
+ if (this._toasterTimeout)
+ clearTimeout(this._toasterTimeout);
+
+ this._toasterTimeout = setTimeout((function() {
+ this.toaster.classList.remove('displayed');
+ this._toasterTimeout = null;
+ this._toasterGD.stopDetecting();
+ }).bind(this), this.TOASTER_TIMEOUT);
+ }
+
+ // Adding it to the lockscreen if locked and the privacy setting
+ // does not prevent it.
+ if (typeof(LockScreen) !== 'undefined' &&
+ LockScreen.locked && this.lockscreenPreview) {
+ var lockScreenNode = notificationNode.cloneNode(true);
+ this.lockScreenContainer.insertBefore(lockScreenNode,
+ this.lockScreenContainer.firstElementChild);
+ }
+
+ if (this.alerts && !this.silent) {
+ var ringtonePlayer = new Audio();
+ ringtonePlayer.src = this._sound;
+ ringtonePlayer.mozAudioChannelType = 'notification';
+ ringtonePlayer.play();
+ window.setTimeout(function smsRingtoneEnder() {
+ ringtonePlayer.pause();
+ ringtonePlayer.src = '';
+ }, 2000);
+ }
+
+ if (this.vibrates) {
+ if (document.mozHidden) {
+ window.addEventListener('mozvisibilitychange', function waitOn() {
+ window.removeEventListener('mozvisibilitychange', waitOn);
+ navigator.vibrate([200, 200, 200, 200]);
+ });
+ } else {
+ navigator.vibrate([200, 200, 200, 200]);
+ }
+ }
+
+ return notificationNode;
+ },
+
+ closeNotification: function ns_closeNotification(notificationNode) {
+ var notificationID = notificationNode.dataset.notificationID;
+
+ var event = document.createEvent('CustomEvent');
+ event.initCustomEvent('mozContentEvent', true, true, {
+ type: 'desktop-notification-close',
+ id: notificationID
+ });
+ window.dispatchEvent(event);
+
+ this.removeNotification(notificationNode.dataset.notificationID);
+ },
+
+ removeNotification: function ns_removeNotification(notificationID) {
+ var notifSelector = '[data-notification-i-d="' + notificationID + '"]';
+ var notificationNode = this.container.querySelector(notifSelector);
+
+ notificationNode.parentNode.removeChild(notificationNode);
+ this.updateStatusBarIcon();
+ },
+
+ clearAll: function ns_clearAll() {
+ while (this.container.firstElementChild) {
+ this.closeNotification(this.container.firstElementChild);
+ }
+ },
+
+ clearLockScreen: function ns_clearLockScreen() {
+ while (this.lockScreenContainer.firstElementChild) {
+ var element = this.lockScreenContainer.firstElementChild;
+ this.lockScreenContainer.removeChild(element);
+ }
+ },
+
+ updateStatusBarIcon: function ns_updateStatusBarIcon(unread) {
+ var nbTotalNotif = this.container.children.length +
+ this.externalNotificationsCount;
+ StatusBar.updateNotification(nbTotalNotif);
+
+ if (unread)
+ StatusBar.updateNotificationUnread(true);
+ },
+
+ incExternalNotifications: function ns_incExternalNotifications() {
+ this.externalNotificationsCount++;
+ this.updateStatusBarIcon(true);
+ },
+
+ decExternalNotifications: function ns_decExternalNotifications() {
+ this.externalNotificationsCount--;
+ if (this.externalNotificationsCount < 0) {
+ this.externalNotificationsCount = 0;
+ }
+ this.updateStatusBarIcon();
+ }
+
+};
+
+NotificationScreen.init();
+
+SettingsListener.observe(
+ 'lockscreen.notifications-preview.enabled', true, function(value) {
+
+ NotificationScreen.lockscreenPreview = value;
+});
+
+SettingsListener.observe('alert-sound.enabled', true, function(value) {
+ NotificationScreen.alerts = value;
+});
+
+SettingsListener.observe('ring.enabled', true, function(value) {
+ NotificationScreen.silent = !value;
+});
+
+SettingsListener.observe('alert-vibration.enabled', true, function(value) {
+ NotificationScreen.vibrates = value;
+});
diff --git a/apps/system/js/operator_variant/operator_variant.js b/apps/system/js/operator_variant/operator_variant.js
new file mode 100644
index 0000000..13ad257
--- /dev/null
+++ b/apps/system/js/operator_variant/operator_variant.js
@@ -0,0 +1,168 @@
+/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+(function OperatorVariant() {
+ /**
+ * Get the mcc/mnc info that has been stored in the settings.
+ */
+
+ var settings = window.navigator.mozSettings;
+ if (!settings)
+ return;
+
+ var iccSettings = { mcc: -1, mnc: -1 };
+
+ // Read the mcc/mnc settings, then trigger callback.
+ function getICCSettings(callback) {
+ var transaction = settings.createLock();
+ var mccKey = 'operatorvariant.mcc';
+ var mncKey = 'operatorvariant.mnc';
+
+ var mccRequest = transaction.get(mccKey);
+ mccRequest.onsuccess = function() {
+ iccSettings.mcc = parseInt(mccRequest.result[mccKey], 10) || 0;
+ var mncRequest = transaction.get(mncKey);
+ mncRequest.onsuccess = function() {
+ iccSettings.mnc = parseInt(mncRequest.result[mncKey], 10) || 0;
+ callback();
+ };
+ };
+ }
+
+
+ /**
+ * Compare the cached mcc/mnc info with the one in the SIM card,
+ * and retrieve/apply APN settings if they differ.
+ */
+
+ var mobileConnection = window.navigator.mozMobileConnection;
+ if (!mobileConnection)
+ return;
+
+ // Check the mcc/mnc information on the SIM card.
+ function checkICCInfo() {
+ if (!mobileConnection.iccInfo || mobileConnection.cardState !== 'ready')
+ return;
+
+ // ensure that the iccSettings have been retrieved
+ if ((iccSettings.mcc < 0) || (iccSettings.mnc < 0))
+ return;
+
+ // XXX sometimes we get 0/0 for mcc/mnc, even when cardState === 'ready'...
+ var mcc = parseInt(mobileConnection.iccInfo.mcc, 10) || 0;
+ var mnc = parseInt(mobileConnection.iccInfo.mnc, 10) || 0;
+ if (!mcc || !mnc)
+ return;
+
+ // same SIM card => do nothing
+ if ((mcc == iccSettings.mcc) && (mnc == iccSettings.mnc))
+ return;
+
+ // new SIM card => cache iccInfo, load and apply new APN settings
+ iccSettings.mcc = mcc;
+ iccSettings.mnc = mnc;
+ retrieveOperatorVariantSettings(applyOperatorVariantSettings);
+ };
+
+ // Load and query APN database, then trigger callback on results.
+ function retrieveOperatorVariantSettings(callback) {
+ var OPERATOR_VARIANT_FILE = 'shared/resources/apn.json';
+
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', OPERATOR_VARIANT_FILE, true);
+ xhr.responseType = 'json';
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState == 4 && (xhr.status == 200 || xhr.status === 0)) {
+ var apn = xhr.response;
+ var mcc = iccSettings.mcc;
+ var mnc = iccSettings.mnc;
+ // get a list of matching APNs
+ var compatibleAPN = apn[mcc] ? (apn[mcc][mnc] || []) : [];
+ callback(compatibleAPN);
+ }
+ };
+ xhr.send();
+ }
+
+ // Store APN settings for the first carrier matching the mcc/mnc info.
+ function applyOperatorVariantSettings(result) {
+ var apnPrefNames = {
+ 'default': {
+ 'ril.data.carrier': 'carrier',
+ 'ril.data.apn': 'apn',
+ 'ril.data.user': 'user',
+ 'ril.data.passwd': 'password',
+ 'ril.data.httpProxyHost': 'proxy',
+ 'ril.data.httpProxyPort': 'port'
+ },
+ 'supl': {
+ 'ril.supl.carrier': 'carrier',
+ 'ril.supl.apn': 'apn',
+ 'ril.supl.user': 'user',
+ 'ril.supl.passwd': 'password',
+ 'ril.supl.httpProxyHost': 'proxy',
+ 'ril.supl.httpProxyPort': 'port'
+ },
+ 'mms': {
+ 'ril.mms.carrier': 'carrier',
+ 'ril.mms.apn': 'apn',
+ 'ril.mms.user': 'user',
+ 'ril.mms.passwd': 'password',
+ 'ril.mms.httpProxyHost': 'proxy',
+ 'ril.mms.httpProxyPort': 'port',
+ 'ril.mms.mmsc': 'mmsc',
+ 'ril.mms.mmsproxy': 'mmsproxy',
+ 'ril.mms.mmsport': 'mmsport'
+ },
+ 'operatorvariant': {
+ 'ril.iccInfo.mbdn': 'voicemail',
+ 'ril.sms.strict7BitEncoding.enabled': 'enableStrict7BitEncodingForSms',
+ 'ril.cellbroadcast.searchlist': 'cellBroadcastSearchList'
+ }
+ };
+
+ var booleanPrefNames = [
+ 'ril.sms.strict7BitEncoding.enabled'
+ ];
+
+ // store relevant APN settings
+ var transaction = settings.createLock();
+ for (var type in apnPrefNames) {
+ var apn = {};
+ for (var i = 0; i < result.length; i++) {
+ if (result[i] && result[i].type.indexOf(type) != -1) {
+ apn = result[i];
+ break;
+ }
+ }
+ var prefNames = apnPrefNames[type];
+ for (var key in prefNames) {
+ var name = apnPrefNames[type][key];
+ var item = {};
+ if (booleanPrefNames.indexOf(key) != -1) {
+ item[key] = apn[name] || false;
+ } else {
+ item[key] = apn[name] || '';
+ }
+ transaction.set(item);
+ }
+ }
+
+ // store the current mcc/mnc info in the settings
+ transaction.set({
+ 'operatorvariant.mcc': iccSettings.mcc,
+ 'operatorvariant.mnc': iccSettings.mnc
+ });
+ }
+
+
+ /**
+ * Check the APN settings on startup and when the SIM card is changed.
+ */
+
+ getICCSettings(checkICCInfo);
+ mobileConnection.addEventListener('iccinfochange', checkICCInfo);
+})();
+
diff --git a/apps/system/js/payment.js b/apps/system/js/payment.js
new file mode 100644
index 0000000..a953f5c
--- /dev/null
+++ b/apps/system/js/payment.js
@@ -0,0 +1,151 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+// TODO: Blocked by [Payment] UX and visuals for the payment request
+// confirmation screen https://github.com/mozilla-b2g/gaia/issues/2692
+
+'use strict';
+
+const kPaymentConfirmationScreen = '../payment.html';
+
+var Payment = {
+ chromeEventId: null,
+ trustedUILayers: {},
+
+ init: function init() {
+ window.addEventListener('mozChromeEvent', this);
+ },
+
+ handleEvent: function onMozChromeEvent(e) {
+ // We save the mozChromeEvent identifiers to send replies back from content
+ // with this exact value.
+ this.chromeEventId = e.detail.id;
+ if (!this.chromeEventId)
+ return;
+
+ var requestId = e.detail.requestId;
+
+ switch (e.detail.type) {
+ // Chrome asks Gaia to show the payment request confirmation dialog.
+ case 'open-payment-confirmation-dialog':
+ var requests = e.detail.paymentRequests;
+ if (!requests)
+ return;
+
+ var returnSelection = (function returnSelection(selection) {
+ if (!selection)
+ return;
+
+ this._dispatchEvent({
+ id: this.chromeEventId,
+ userSelection: selection
+ });
+ }).bind(this);
+
+ // If there is only one request, we skip the confirmation dialog and
+ // send the request type back to the chrome as a user selection, so
+ // the payment flow can continue.
+ if (requests.length == 1) {
+ returnSelection(requests[0].type);
+ return;
+ }
+
+ var frame = document.createElement('iframe');
+ frame.setAttribute('mozbrowser', 'true');
+ frame.setAttribute('remote', true);
+ frame.classList.add('screen');
+ frame.src = kPaymentConfirmationScreen;
+ frame.addEventListener('mozbrowserloadend', function addReqs(evt) {
+ var frame = evt.target;
+ if (!frame || !requests)
+ return;
+
+ // TODO: Temp layout until issue #2692 is solved.
+ var frameDocument = frame.contentWindow.document;
+ var requestsList = frameDocument.getElementById('requests')
+ .getElementsByTagName('ul')[0];
+ for (var i in requests) {
+ var requestElement = frameDocument.createElement('li');
+ var button = frameDocument.createElement('button');
+ button.setAttribute('value', requests[i].type);
+ var requestText = 'Pay with ' + requests[i].providerName + '\n' +
+ requests[i].productName + '\n' +
+ requests[i].productDescription + '\n' +
+ requests[i].productPrice[0].amount + ' ' +
+ requests[i].productPrice[0].currency;
+ button.appendChild(frameDocument.createTextNode(requestText));
+ button.onclick = function selectRequest() {
+ // We send the selected request back to Chrome so it can start
+ // the appropriate payment flow.
+ returnSelection(this.getAttribute('value'));
+ };
+ requestElement.appendChild(button);
+ requestsList.appendChild(requestElement);
+ }
+ });
+
+ this._openTrustedUI(frame);
+ break;
+
+ // Chrome asks Gaia to show the payment flow according to the
+ // payment request selected by the user.
+ case 'open-payment-flow-dialog':
+ if (!e.detail.uri)
+ return;
+
+ // TODO: For now, known payment providers (BlueVia and Mozilla Market)
+ // only accepts the JWT by GET, so we just add it to the URI.
+ e.detail.uri += e.detail.jwt;
+
+ this.trustedUILayers[requestId] = this.chromeEventId;
+
+ var frame = document.createElement('iframe');
+ frame.setAttribute('mozbrowser', 'true');
+ frame.classList.add('screen');
+ frame.src = e.detail.uri;
+ frame.addEventListener('mozbrowserloadstart', (function loadStart(evt) {
+ // After creating the new frame containing the payment provider buy
+ // flow, we send it back to chrome so the payment callbacks can be
+ // injected.
+ this._dispatchEvent({
+ id: this.chromeEventId,
+ frame: evt.target
+ });
+ }).bind(this));
+
+ // The payment flow is shown within the trusted UI
+ this._openTrustedUI(frame);
+ break;
+
+ case 'close-payment-flow-dialog':
+ TrustedUIManager.close(this.trustedUILayers[requestId],
+ (function dialogClosed() {
+ this._dispatchEvent({ id: this.chromeEventId });
+ delete this.trustedUILayers[requestId];
+ }).bind(this));
+ break;
+ }
+ },
+
+ _openTrustedUI: function _openTrustedUI(frame) {
+ // The payment flow is shown within the trusted UI with the name of
+ // the mozPay caller application as title.
+ var title = WindowManager.getCurrentDisplayedApp().name;
+ title = title ? title : navigator.mozL10n.get('payment-flow');
+ TrustedUIManager.open(title, frame, this.chromeEventId);
+ },
+
+ _dispatchEvent: function _dispatchEvent(obj) {
+ var event = document.createEvent('CustomEvent');
+ event.initCustomEvent('mozContentEvent', true, true, obj);
+ window.dispatchEvent(event);
+ }
+};
+
+// Make sure L10n is ready before init
+if (navigator.mozL10n.readyState == 'complete' ||
+ navigator.mozL10n.readyState == 'interactive') {
+ Payment.init();
+} else {
+ window.addEventListener('localized', Payment.init.bind(Payment));
+}
diff --git a/apps/system/js/permission_manager.js b/apps/system/js/permission_manager.js
new file mode 100644
index 0000000..f3c013c
--- /dev/null
+++ b/apps/system/js/permission_manager.js
@@ -0,0 +1,203 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var PermissionManager = (function() {
+ var _ = navigator.mozL10n.get;
+
+ window.addEventListener('mozChromeEvent', function pm_chromeEventHandler(e) {
+ var detail = e.detail;
+ switch (detail.type) {
+ case 'permission-prompt':
+ overlay.dataset.type = detail.permission;
+ handlePermissionPrompt(detail);
+ break;
+ case 'fullscreenoriginchange':
+ delete overlay.dataset.type;
+ handleFullscreenOriginChange(detail);
+ break;
+ }
+ });
+
+ var fullscreenRequest = undefined;
+
+ var handleFullscreenOriginChange = function(detail) {
+ // If there's already a fullscreen request visible, cancel it,
+ // we'll show the request for the new domain.
+ if (fullscreenRequest != undefined) {
+ cancelRequest(fullscreenRequest);
+ fullscreenRequest = undefined;
+ }
+ if (detail.fullscreenorigin != WindowManager.getDisplayedApp()) {
+ // The message to be displayed on the approval UI.
+ var message = _('fullscreen-request', { 'origin': detail.fullscreenorigin });
+ fullscreenRequest = requestPermission(message,
+ /* yesCallback */ null,
+ /* noCallback */ function() {
+ document.mozCancelFullScreen();
+ });
+ }
+ };
+
+ var handlePermissionPrompt = function pm_handlePermissionPrompt(detail) {
+ remember.checked = detail.remember ? true : false;
+ var str = '';
+
+ var permissionID = 'perm-' + detail.permission.replace(':', '-');
+ if (detail.isApp) { // App
+ str = _(permissionID + '-appRequest', { 'app': detail.appName });
+ } else { // Web content
+ str = _(permissionID + '-webRequest', { 'site': detail.origin });
+ }
+
+ requestPermission(str, function pm_permYesCB() {
+ dispatchResponse(detail.id, 'permission-allow', remember.checked);
+ }, function pm_permNoCB() {
+ dispatchResponse(detail.id, 'permission-deny', remember.checked);
+ });
+ };
+
+ var dispatchResponse = function pm_dispatchResponse(id, type, remember) {
+ var event = document.createEvent('CustomEvent');
+ remember = remember ? true : false;
+
+ event.initCustomEvent('mozContentEvent', true, true, {
+ id: id,
+ type: type,
+ remember: remember
+ });
+ window.dispatchEvent(event);
+ };
+
+ // A queue of pending requests. Callers of requestPermission() must be
+ // careful not to create an infinite loop!
+ var pending = [];
+
+ // Div over in which the permission UI resides.
+ var overlay = document.getElementById('permission-screen');
+ var dialog = document.getElementById('permission-dialog');
+ var message = document.getElementById('permission-message');
+
+ // "Yes"/"No" buttons on the permission UI.
+ var yes = document.getElementById('permission-yes');
+ var no = document.getElementById('permission-no');
+
+ // Remember the choice checkbox
+ var remember = document.getElementById('permission-remember-checkbox');
+ var rememberSection = document.getElementById('permission-remember-section');
+
+ // The ID of the next permission request. This is incremented by one
+ // on every request, modulo some large number to prevent overflow problems.
+ var nextRequestID = 0;
+
+ // The ID of the request currently visible on the screen. This has the value
+ // "undefined" when there is no request visible on the screen.
+ var currentRequestId = undefined;
+
+ var hidePermissionPrompt = function() {
+ overlay.classList.remove('visible');
+ currentRequestId = undefined;
+ // Cleanup the event handlers.
+ yes.removeEventListener('click', clickHandler);
+ yes.callback = null;
+ no.removeEventListener('click', clickHandler);
+ no.callback = null;
+ };
+
+ // Show the next request, if we have one.
+ var showNextPendingRequest = function() {
+ if (pending.length == 0)
+ return;
+ var request = pending.shift();
+ showPermissionPrompt(request.id,
+ request.message,
+ request.yescallback,
+ request.nocallback);
+ };
+
+ // This is the event listener function for the yes/no buttons.
+ var clickHandler = function(evt) {
+ var callback = null;
+ if (evt.target === yes && yes.callback) {
+ callback = yes.callback;
+ } else if (evt.target === no && no.callback) {
+ callback = no.callback;
+ }
+ hidePermissionPrompt();
+
+ // Call the appropriate callback, if it is defined.
+ if (callback)
+ window.setTimeout(callback, 0);
+
+ showNextPendingRequest();
+ };
+
+ var requestPermission = function(msg,
+ yescallback, nocallback) {
+ var id = nextRequestID;
+ nextRequestID = (nextRequestID + 1) % 1000000;
+
+ if (currentRequestId != undefined) {
+ // There is already a permission request being shown, queue this one.
+ pending.push({
+ id: id,
+ message: msg,
+ yescallback: yescallback,
+ nocallback: nocallback
+ });
+ return id;
+ }
+
+ showPermissionPrompt(id, msg, yescallback, nocallback);
+
+ return id;
+ };
+
+ var showPermissionPrompt = function(id, msg,
+ yescallback, nocallback) {
+ // Put the message in the dialog.
+ // Note plain text since this may include text from
+ // untrusted app manifests, for example.
+ message.textContent = msg;
+
+ currentRequestId = id;
+
+ // Make the screen visible
+ overlay.classList.add('visible');
+
+ // Set event listeners for the yes and no buttons
+ yes.addEventListener('click', clickHandler);
+ yes.callback = yescallback;
+
+ no.addEventListener('click', clickHandler);
+ no.callback = nocallback;
+ };
+
+ // Cancels a request with a specfied id. Request can either be
+ // currently showing, or pending. If there are further pending requests,
+ // the next is shown.
+ var cancelRequest = function(id) {
+ if (currentRequestId === id) {
+ // Request is currently being displayed. Hide the permission prompt,
+ // and show the next request, if we have any.
+ hidePermissionPrompt();
+ showNextPendingRequest();
+ } else {
+ // The request is currently not being displayed. Search through the
+ // list of pending requests, and remove it from the list if present.
+ for (var i = 0; i < pending.length; i++) {
+ if (pending[i].id === id) {
+ pending.splice(i, 1);
+ break;
+ }
+ }
+ }
+ };
+
+ rememberSection.addEventListener('click', function onLabelClick() {
+ remember.checked = !remember.checked;
+ });
+
+}());
+
diff --git a/apps/system/js/popup_manager.js b/apps/system/js/popup_manager.js
new file mode 100644
index 0000000..583c9c8
--- /dev/null
+++ b/apps/system/js/popup_manager.js
@@ -0,0 +1,313 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+'use strict';
+
+var PopupManager = {
+ _currentPopup: {},
+ _currentOrigin: '',
+ _endTimes: 0,
+ _startTimes: 0,
+
+ throbber: document.getElementById('popup-throbber'),
+
+ overlay: document.getElementById('dialog-overlay'),
+
+ popupContainer: document.getElementById('popup-container'),
+
+ container: document.getElementById('frame-container'),
+
+ screen: document.getElementById('screen'),
+
+ closeButton: document.getElementById('popup-close'),
+
+ errorTitle: document.getElementById('popup-error-title'),
+
+ errorMessage: document.getElementById('popup-error-message'),
+
+ errorReload: document.getElementById('popup-error-reload'),
+
+ errorBack: document.getElementById('popup-error-back'),
+
+ init: function pm_init() {
+ this.title = document.getElementById('popup-title');
+ window.addEventListener('mozbrowseropenwindow', this);
+ window.addEventListener('mozbrowserclose', this);
+ window.addEventListener('appwillclose', this);
+ window.addEventListener('appopen', this);
+ window.addEventListener('appterminated', this);
+ window.addEventListener('home', this);
+ window.addEventListener('keyboardhide', this);
+ window.addEventListener('keyboardchange', this);
+ this.closeButton.addEventListener('click', this);
+ this.errorReload.addEventListener('click', this);
+ this.errorBack.addEventListener('click', this);
+ },
+
+ open: function pm_open(frame, origin) {
+ // Only one popup per origin at a time.
+ // If the popup is being shown, we swap frames.
+ if (this._currentPopup[origin]) {
+ this.container.removeChild(this._currentPopup[origin]);
+ delete this._currentPopup[origin];
+ }
+
+ this.title.textContent = this.getTitleFromUrl(frame.dataset.url);
+
+ // Reset overlay height
+ this.setHeight(window.innerHeight - StatusBar.height);
+
+ this._currentPopup[origin] = frame;
+
+ var popup = this._currentPopup[origin];
+ var dataset = popup.dataset;
+ dataset.frameType = 'popup';
+ dataset.frameName = name;
+ dataset.frameOrigin = origin;
+
+ // this seems needed, or an override to origin in close()
+ this._currentOrigin = origin;
+
+ this.container.appendChild(popup);
+
+ this.screen.classList.add('popup');
+
+ popup.addEventListener('mozbrowsererror', this);
+ popup.addEventListener('mozbrowserloadend', this);
+ popup.addEventListener('mozbrowserloadstart', this);
+ popup.addEventListener('mozbrowserlocationchange', this);
+ },
+
+ close: function pm_close(evt) {
+ if (evt && (!'frameType' in evt.target.dataset ||
+ evt.target.dataset.frameType !== 'popup'))
+ return;
+
+ var self = this;
+ this.popupContainer.addEventListener('transitionend', function wait(event) {
+ self.popupContainer.removeEventListener('transitionend', wait);
+ self.screen.classList.remove('popup');
+ self.popupContainer.classList.remove('disappearing');
+ self.container.removeChild(self._currentPopup[self._currentOrigin]);
+ delete self._currentPopup[self._currentOrigin];
+ });
+
+ this.popupContainer.classList.add('disappearing');
+
+ // We just removed the focused window leaving the system
+ // without any focused window, let's fix this.
+ window.focus();
+ },
+
+ backHandling: function pm_backHandling() {
+ if (!this._currentPopup[this._currentOrigin])
+ return;
+
+ this.close();
+ },
+
+ isVisible: function pm_isVisible() {
+ return (this._currentPopup[this._currentOrigin] != null);
+ },
+
+ setHeight: function pm_setHeight(height) {
+ if (this.isVisible())
+ this.overlay.style.height = height + 'px';
+ },
+
+ handleEvent: function pm_handleEvent(evt) {
+ switch (evt.type) {
+ case 'click':
+ switch (evt.target) {
+ case this.closeButton:
+ this.backHandling();
+ break;
+
+ case this.errorBack:
+ this.backHandling();
+ break;
+
+ case this.errorReload:
+ this.container.classList.remove('error');
+ delete this._currentPopup[this._currentOrigin].dataset.error;
+ this._currentPopup[this._currentOrigin].reload(true);
+ break;
+ }
+ break;
+
+ case 'mozbrowserloadstart':
+ this.throbber.classList.add('loading');
+ break;
+
+ case 'mozbrowserloadend':
+ this.throbber.classList.remove('loading');
+ break;
+
+ case 'mozbrowserlocationchange':
+ evt.target.dataset.url = evt.detail;
+ this.show();
+ break;
+
+ case 'mozbrowsererror':
+ this._currentPopup[evt.target.dataset.frameOrigin].dataset.error = true;
+ this.showError();
+ break;
+
+ case 'mozbrowseropenwindow':
+ var detail = evt.detail;
+ var openerType = evt.target.dataset.frameType;
+ var openerOrigin = evt.target.dataset.frameOrigin;
+
+ // Only app frame is allowed to launch popup
+ if (openerType !== 'window')
+ return;
+
+ // <a href="" target="_blank"> links should opened outside the app
+ // itself and fire an activity to be opened into a new browser window.
+ if (detail.name === '_blank') {
+ new MozActivity({ name: 'view',
+ data: { type: 'url', url: detail.url }});
+ return;
+ }
+
+ this.throbber.classList.remove('loading');
+
+ var frame = detail.frameElement;
+ frame.dataset.url = detail.url;
+
+ this.container.classList.remove('error');
+ this.open(frame, openerOrigin);
+
+ break;
+
+ case 'mozbrowserclose':
+ this.close(evt);
+ break;
+
+ case 'home':
+ // Reset overlay height before hiding
+ this.setHeight(window.innerHeight - StatusBar.height);
+ this.hide(this._currentOrigin);
+ break;
+
+ case 'appwillclose':
+ if (!this._currentPopup[evt.detail.origin])
+ return;
+
+ this.hide(evt.detail.origin);
+ break;
+
+ case 'appopen':
+ this._currentOrigin = evt.detail.origin;
+ this.show();
+ break;
+
+ case 'appterminated':
+ if (!this._currentPopup[evt.detail.origin])
+ return;
+ this.close(evt.detail.origin);
+ break;
+
+ case 'keyboardchange':
+ this.setHeight(window.innerHeight -
+ StatusBar.height - evt.detail.height);
+ break;
+
+ case 'keyboardhide':
+ this.setHeight(window.innerHeight - StatusBar.height);
+ break;
+ }
+ },
+
+ showError: function pm_showError() {
+ if (!('error' in this._currentPopup[this._currentOrigin].dataset)) {
+ this.container.classList.remove('error');
+ return;
+ }
+
+ var contentOrigin =
+ this.getTitleFromUrl(this._currentPopup[this._currentOrigin].dataset.url);
+ var _ = navigator.mozL10n.get;
+
+ if (AirplaneMode.enabled) {
+ this.errorTitle.textContent = _('airplane-is-on');
+ this.errorMessage.textContent = _('airplane-is-turned-on',
+ {name: contentOrigin});
+ } else if (!navigator.onLine) {
+ this.errorTitle.textContent = _('network-connection-unavailable');
+ this.errorMessage.textContent = _('network-error', {name: contentOrigin});
+ } else {
+ this.errorTitle.textContent = _('error-title', {name: contentOrigin});
+ this.errorMessage.textContent = _('error-message', {name: contentOrigin});
+ }
+ this.container.classList.add('error');
+ },
+
+ // This is for card view to request
+ // Return nothing if the content is the same origin as opener
+ // Return URL if the content is off-origin
+ getOpenedOriginFromOpener: function pm_getOpenedOriginOpener(origin) {
+ var opened = this._getOriginObject(this._currentPopup[origin].dataset.url);
+ var opener = this._getOriginObject(origin);
+ // Same origin means: Protocol, Domain, Port
+ if (opened.protocol == opener.protocol &&
+ opened.hostname == opener.hostname &&
+ opened.port == opener.port) {
+ return '';
+ } else {
+ return opened.protocol + '//' + opened.hostname;
+ }
+ },
+
+ getTitleFromUrl: function pm_getTitleFromUrl(url) {
+ var app = WindowManager.getCurrentDisplayedApp();
+ var opened = this._getOriginObject(url);
+ var opener = this._getOriginObject(app.frame.dataset.frameOrigin);
+ // Same origin means: Protocol, Domain, Port
+ if (opened.protocol == opener.protocol &&
+ opened.hostname == opener.hostname &&
+ opened.port == opener.port) {
+ return app.name;
+ } else {
+ return opened.protocol + '//' + opened.hostname;
+ }
+ },
+
+ _getOriginObject: function pm__getOriginObject(url) {
+ var parser = document.createElement('a');
+ parser.href = url;
+
+ return {
+ protocol: parser.protocol,
+ hostname: parser.hostname,
+ port: parser.port
+ };
+ },
+
+ getPopupFromOrigin: function pm_getPopupFromOrigin(origin) {
+ return this._currentPopup[origin];
+ },
+
+ show: function pm_show() {
+ if (!this._currentPopup[this._currentOrigin])
+ return;
+
+
+ this.showError();
+ this.screen.classList.add('popup');
+
+ var popup = this._currentPopup[this._currentOrigin];
+ this.title.textContent = this.getTitleFromUrl(popup.dataset.url);
+ popup.hidden = false;
+ },
+
+ hide: function pm_hide(origin) {
+ if (!this._currentPopup[origin])
+ return;
+
+ this.screen.classList.remove('popup');
+ this._currentPopup[origin].hidden = true;
+ }
+};
+
+PopupManager.init();
+
diff --git a/apps/system/js/quick_settings.js b/apps/system/js/quick_settings.js
new file mode 100644
index 0000000..328bb35
--- /dev/null
+++ b/apps/system/js/quick_settings.js
@@ -0,0 +1,279 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var QuickSettings = {
+ // Indicate setting status of geolocation.enabled
+ geolocationEnabled: false,
+ WIFI_STATUSCHANGE_TIMEOUT: 2000,
+
+ init: function qs_init() {
+ var settings = window.navigator.mozSettings;
+ var conn = window.navigator.mozMobileConnection;
+ if (!settings || !conn)
+ return;
+
+ this.getAllElements();
+
+ this.overlay.addEventListener('click', this);
+ window.addEventListener('utilitytrayshow', this);
+
+ var self = this;
+
+ /*
+ * Monitor data network icon
+ */
+ conn.addEventListener('datachange', function qs_onDataChange() {
+ var label = {
+ 'lte': '4G', // 4G LTE
+ 'ehrpd': '4G', // 4G CDMA
+ 'hspa+': 'H+', // 3.5G HSPA+
+ 'hsdpa': 'H', 'hsupa': 'H', 'hspa': 'H', // 3.5G HSDPA
+ 'evdo0': '3G', 'evdoa': '3G', 'evdob': '3G', '1xrtt': '3G', // 3G CDMA
+ 'umts': '3G', // 3G
+ 'edge': 'E', // EDGE
+ 'is95a': '2G', 'is95b': '2G', // 2G CDMA
+ 'gprs': '2G'
+ };
+ self.data.dataset.network = label[conn.data.type];
+ });
+
+ /* monitor data setting
+ * TODO prevent quickly tapping on it
+ */
+ SettingsListener.observe('ril.data.enabled', true, function(value) {
+ if (value) {
+ self.data.dataset.enabled = 'true';
+ } else {
+ delete self.data.dataset.enabled;
+ }
+ });
+
+ /* monitor bluetooth setting and initialization/disable ready event
+ * - when settings changed, update UI and lock toogle to prevent quickly
+ * tapping on it.
+ * - when got bluetooth initialization/disable ready, active toogle, so
+ * return the control to user.
+ */
+ var btFirstSet = true;
+ SettingsListener.observe('bluetooth.enabled', true, function(value) {
+ // check self.bluetooth.dataset.enabled and value are identical
+ if ((self.bluetooth.dataset.enabled && value) ||
+ (self.bluetooth.dataset.enabled === undefined && !value))
+ return;
+
+ if (value) {
+ self.bluetooth.dataset.enabled = 'true';
+ } else {
+ delete self.bluetooth.dataset.enabled;
+ }
+
+ // Set to the initializing state to block user interaction until the
+ // operation completes. (unless we are being called for the first time,
+ // where Bluetooth is already initialize
+ if (!btFirstSet)
+ self.bluetooth.dataset.initializing = 'true';
+ btFirstSet = false;
+ });
+ window.addEventListener('bluetooth-adapter-added', this);
+ window.addEventListener('bluetooth-disabled', this);
+
+
+ /* monitor wifi setting and initialization/disable ready event
+ * - when settings changed, update UI and lock toogle to prevent quickly
+ * tapping on it.
+ * - when got bluetooth initialization/disable ready, active toogle, so
+ * return the control to user.
+ */
+ var wifiFirstSet = true;
+ SettingsListener.observe('wifi.enabled', true, function(value) {
+ // check self.wifi.dataset.enabled and value are identical
+ if ((self.wifi.dataset.enabled && value) ||
+ (self.wifi.dataset.enabled === undefined && !value))
+ return;
+
+ if (value) {
+ self.wifi.dataset.enabled = 'true';
+ } else {
+ delete self.wifi.dataset.enabled;
+ }
+ // Set to the initializing state to block user interaction until the
+ // operation completes. (unless we are being called for the first time,
+ // where Wifi is already initialize
+ if (!wifiFirstSet)
+ self.wifi.dataset.initializing = 'true';
+ wifiFirstSet = false;
+ });
+ window.addEventListener('wifi-enabled', this);
+ window.addEventListener('wifi-disabled', this);
+ window.addEventListener('wifi-statuschange', this);
+
+ /* monitor geolocation setting
+ * TODO prevent quickly tapping on it
+ */
+ SettingsListener.observe('geolocation.enabled', true, function(value) {
+ self.geolocationEnabled = value;
+ });
+
+ // monitor airplane mode
+ SettingsListener.observe('ril.radio.disabled', false, function(value) {
+ self.data.dataset.airplaneMode = value;
+ if (value) {
+ self.data.classList.add('quick-settings-airplane-mode');
+ self.airplaneMode.dataset.enabled = 'true';
+ } else {
+ self.data.classList.remove('quick-settings-airplane-mode');
+ delete self.airplaneMode.dataset.enabled;
+ }
+ });
+ },
+
+ handleEvent: function qs_handleEvent(evt) {
+ evt.preventDefault();
+ switch (evt.type) {
+ case 'click':
+ switch (evt.target) {
+ case this.wifi:
+ // do nothing if wifi isn't ready
+ if (this.wifi.dataset.initializing)
+ return;
+ var enabled = !!this.wifi.dataset.enabled;
+ SettingsListener.getSettingsLock().set({
+ 'wifi.enabled': !enabled
+ });
+ if (!enabled)
+ this.toggleAutoConfigWifi = true;
+ break;
+
+ case this.data:
+ if (this.data.dataset.airplaneMode !== 'true') {
+ // TODO should ignore the action if data initialization isn't done
+ var enabled = !!this.data.dataset.enabled;
+
+ SettingsListener.getSettingsLock().set({
+ 'ril.data.enabled': !enabled
+ });
+ }
+
+ break;
+
+ case this.bluetooth:
+ // do nothing if bluetooth isn't ready
+ if (this.bluetooth.dataset.initializing)
+ return;
+
+ var enabled = !!this.bluetooth.dataset.enabled;
+ SettingsListener.getSettingsLock().set({
+ 'bluetooth.enabled': !enabled
+ });
+ break;
+
+ case this.airplaneMode:
+ var enabled = !!this.airplaneMode.dataset.enabled;
+ SettingsListener.getSettingsLock().set({
+ 'ril.radio.disabled': !enabled
+ });
+ break;
+
+ case this.fullApp:
+ // XXX: This should be replaced probably by Web Activities
+ var host = document.location.host;
+ var domain = host.replace(/(^[\w\d]+\.)?([\w\d]+\.[a-z]+)/, '$2');
+ var protocol = document.location.protocol + '//';
+ Applications.getByManifestURL(protocol + 'settings.' +
+ domain + '/manifest.webapp').launch();
+
+ UtilityTray.hide();
+ break;
+ }
+ break;
+
+ case 'utilitytrayshow':
+ break;
+
+ // unlock bluetooth toggle
+ case 'bluetooth-adapter-added':
+ case 'bluetooth-disabled':
+ delete this.bluetooth.dataset.initializing;
+ break;
+ // unlock wifi toggle
+ case 'wifi-enabled':
+ delete this.wifi.dataset.initializing;
+ if (this.toggleAutoConfigWifi) {
+ // Check whether it found a wifi to connect after a timeout.
+ this.wifiStatusTimer = setTimeout(this.autoConfigWifi.bind(this),
+ this.WIFI_STATUSCHANGE_TIMEOUT);
+ }
+ break;
+ case 'wifi-disabled':
+ delete this.wifi.dataset.initializing;
+ if (this.toggleAutoConfigWifi) {
+ clearTimeout(this.wifiStatusTimer);
+ this.wifiStatusTimer = null;
+ this.toggleAutoConfigWifi = false;
+ }
+ break;
+
+ case 'wifi-statuschange':
+ if (this.toggleAutoConfigWifi && !this.wifi.dataset.initializing)
+ this.autoConfigWifi();
+ break;
+ }
+ },
+
+ getAllElements: function qs_getAllElements() {
+ // ID of elements to create references
+ var elements = ['wifi', 'data', 'bluetooth', 'airplane-mode', 'full-app'];
+
+ var toCamelCase = function toCamelCase(str) {
+ return str.replace(/\-(.)/g, function replacer(str, p1) {
+ return p1.toUpperCase();
+ });
+ };
+
+ elements.forEach(function createElementRef(name) {
+ this[toCamelCase(name)] =
+ document.getElementById('quick-settings-' + name);
+ }, this);
+
+ this.overlay = document.getElementById('quick-settings');
+ },
+
+ // XXX Break down obj keys in a for each loop because mozSettings
+ // does not currently supports multiple keys in one set()
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=779381
+ setMozSettings: function qs_setter(keypairs) {
+ var setlock = SettingsListener.getSettingsLock();
+ for (var key in keypairs) {
+ var obj = {};
+ obj[key] = keypairs[key];
+ setlock.set(obj);
+ }
+ },
+
+ /* Auto-config wifi if user enabled wifi from quick settings bar.
+ * If there are no known networks around, wifi settings page
+ * will be opened. Otherwise nothing will be done.
+ */
+ autoConfigWifi: function qs_autoConfigWifi() {
+ clearTimeout(this.wifiStatusTimer);
+ this.wifiStatusTimer = null;
+ this.toggleAutoConfigWifi = false;
+
+ var wifiManager = window.navigator.mozWifiManager;
+ var status = wifiManager.connection.status;
+
+ if (status == 'disconnected') {
+ var activity = new MozActivity({
+ name: 'configure',
+ data: {
+ target: 'device',
+ section: 'wifi'
+ }
+ });
+ }
+ }
+};
+
+QuickSettings.init();
diff --git a/apps/system/js/remote_debugger.js b/apps/system/js/remote_debugger.js
new file mode 100644
index 0000000..51544bb
--- /dev/null
+++ b/apps/system/js/remote_debugger.js
@@ -0,0 +1,41 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var RemoteDebugger = (function() {
+
+ return {
+ init: function() {
+ window.addEventListener('mozChromeEvent', this);
+ },
+
+ handleEvent: function onMozChromeEvent(e) {
+ if (e.detail.type !== 'remote-debugger-prompt') {
+ return;
+ }
+
+ // Reusing the ModalDialog infrastructure.
+ ModalDialog.showWithPseudoEvent({
+ text: navigator.mozL10n.get('remoteDebuggerMessage'),
+ type: 'confirm',
+ callback: function() {
+ RemoteDebugger._dispatchEvent(true);
+ },
+ cancel: function() {
+ RemoteDebugger._dispatchEvent(false);
+ }
+ });
+ },
+
+ _dispatchEvent: function su_dispatchEvent(value) {
+ var event = document.createEvent('CustomEvent');
+ event.initCustomEvent('mozContentEvent', true, true,
+ { type: 'remote-debugger-prompt',
+ value: value });
+ window.dispatchEvent(event);
+ }
+ };
+})();
+
+RemoteDebugger.init();
diff --git a/apps/system/js/screen_manager.js b/apps/system/js/screen_manager.js
new file mode 100644
index 0000000..9a69e8c
--- /dev/null
+++ b/apps/system/js/screen_manager.js
@@ -0,0 +1,503 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var ScreenManager = {
+ /*
+ * return the current screen status
+ * Must not mutate directly - use toggleScreen/turnScreenOff/turnScreenOn.
+ * Listen to 'screenchange' event to properly handle status changes
+ * This value can be "out of sync" with real mozPower value,
+ * we do this to give screen some time to flash before actual turn off.
+ */
+ screenEnabled: false,
+
+ /*
+ * before idle-screen-off, invoke a nice dimming to the brightness
+ * to notify the user that the screen is about to be turn off.
+ * The user can cancel the idle-screen-off by touching the screen
+ * and by pressing a button (trigger onactive callback on Idle API)
+ *
+ */
+ _inTransition: false,
+
+ /*
+ * Whether the wake lock is enabled or not
+ */
+ _screenWakeLocked: false,
+
+ /*
+ * Whether the device light is enabled or not
+ * sync with setting 'screen.automatic-brightness'
+ */
+ _deviceLightEnabled: true,
+
+ /*
+ * Preferred brightness without considering device light nor dimming
+ * sync with setting 'screen.brightness'
+ */
+ _userBrightness: 1,
+ _savedBrightness: 1,
+
+ /*
+ * The auto-brightness algorithm will never set the screen brightness
+ * to a value smaller than this. 0.1 seems like a good screen brightness
+ * in a completely dark room on a Unagi.
+ */
+ AUTO_BRIGHTNESS_MINIMUM: 0.1,
+
+ /*
+ * This constant is used in the auto brightness algorithm. We take
+ * the base 10 logarithm of the incoming lux value from the light
+ * sensor and multiplied it by this constant. That value is used to
+ * compute a weighted average with the current brightness and
+ * finally that average brightess is and then clamped to the range
+ * [AUTO_BRIGHTNESS_MINIMUM, 1.0].
+ *
+ * Making this value larger will increase the brightness for a given
+ * ambient light level. At a value of about .25, the screen will be
+ * at full brightness in sunlight or in a well-lighted work area.
+ * At a value of about .3, the screen will typically be at maximum
+ * brightness in outdoor daylight conditions, even when overcast.
+ */
+ AUTO_BRIGHTNESS_CONSTANT: .27,
+
+ /*
+ * When we change brightness we animate it smoothly.
+ * This constant is the number of milliseconds between adjustments
+ */
+ BRIGHTNESS_ADJUST_INTERVAL: 20,
+
+ /*
+ * When brightening or dimming the screen, this is how much we adjust
+ * the brightness value at a time.
+ */
+ BRIGHTNESS_ADJUST_STEP: 0.04,
+
+ /*
+ * Wait for _dimNotice milliseconds during idle-screen-off
+ */
+ _dimNotice: 10 * 1000,
+
+ /*
+ * We track the value of the idle timeout pref in this variable.
+ */
+ _idleTimeout: 0,
+ _idleTimerId: 0,
+
+ /*
+ * If the screen off is triggered by promixity during phon call then
+ * we need wake it up while phone is ended.
+ */
+ _screenOffByProximity: false,
+
+ /*
+ * Request wakelock during in_call state.
+ * To ensure turnScreenOff by proximity event is protected by wakelock for
+ * early suspend only.
+ */
+ _cpuWakeLock: null,
+
+ init: function scm_init() {
+ window.addEventListener('sleep', this);
+ window.addEventListener('wake', this);
+
+ this.screen = document.getElementById('screen');
+
+ var self = this;
+ var power = navigator.mozPower;
+
+ if (power) {
+ power.addWakeLockListener(function scm_handleWakeLock(topic, state) {
+ switch (topic) {
+ case 'screen':
+ self._screenWakeLocked = (state == 'locked-foreground');
+
+ if (self._screenWakeLocked)
+ // Turn screen on if wake lock is acquire
+ self.turnScreenOn();
+ self._reconfigScreenTimeout();
+ break;
+
+ case 'cpu':
+ power.cpuSleepAllowed = (state != 'locked-foreground' &&
+ state != 'locked-background');
+ break;
+ }
+ });
+ }
+
+ this._firstOn = false;
+ SettingsListener.observe('screen.timeout', 60,
+ function screenTimeoutChanged(value) {
+ if (typeof value !== 'number')
+ value = parseInt(value);
+ self._idleTimeout = value;
+ self._setIdleTimeout(self._idleTimeout);
+
+ if (!self._firstOn) {
+ self._firstOn = true;
+
+ // During boot up, the brightness was set by bootloader as 0.5,
+ // Let's set the API value to that so setScreenBrightness() can
+ // dim nicely to value set by user.
+ power.screenBrightness = 0.5;
+
+ // Turn screen on with dim.
+ self.turnScreenOn(false);
+ }
+ });
+
+ SettingsListener.observe('screen.automatic-brightness', true,
+ function deviceLightSettingChanged(value) {
+ self.setDeviceLightEnabled(value);
+ });
+
+ SettingsListener.observe('screen.brightness', 1,
+ function brightnessSettingChanged(value) {
+ self._userBrightness = value;
+ self.setScreenBrightness(value, false);
+ });
+
+ var telephony = window.navigator.mozTelephony;
+ if (telephony) {
+ telephony.addEventListener('callschanged', this);
+ }
+ },
+
+ //
+ // Automatically adjust the screen brightness based on the ambient
+ // light (in lux) measured by the device light sensor
+ //
+ autoAdjustBrightness: function scm_adjustBrightness(lux) {
+ var currentBrightness = this._targetBrightness;
+
+ if (lux < 1) // Can't take the log of 0 or negative numbers
+ lux = 1;
+
+ var computedBrightness =
+ Math.log(lux) / Math.LN10 * this.AUTO_BRIGHTNESS_CONSTANT;
+
+ var clampedBrightness = Math.max(this.AUTO_BRIGHTNESS_MINIMUM,
+ Math.min(1.0, computedBrightness));
+
+ // If nothing changed, we're done.
+ if (clampedBrightness === currentBrightness)
+ return;
+
+ this.setScreenBrightness(clampedBrightness, false);
+ },
+
+ handleEvent: function scm_handleEvent(evt) {
+ switch (evt.type) {
+ case 'devicelight':
+ if (!this._deviceLightEnabled || !this.screenEnabled ||
+ this._inTransition)
+ return;
+ this.autoAdjustBrightness(evt.value);
+ break;
+
+ case 'sleep':
+ this.turnScreenOff(true);
+ break;
+
+ case 'wake':
+ this.turnScreenOn();
+ break;
+
+ case 'userproximity':
+ this._screenOffByProximity = evt.near;
+ if (evt.near) {
+ this.turnScreenOff(true);
+ } else {
+ this.turnScreenOn();
+ }
+ break;
+
+ case 'callschanged':
+ var telephony = window.navigator.mozTelephony;
+ if (!telephony.calls.length) {
+ if (this._screenOffByProximity) {
+ this.turnScreenOn();
+ }
+
+ window.removeEventListener('userproximity', this);
+ this._screenOffByProximity = false;
+
+ if (this._cpuWakeLock) {
+ this._cpuWakeLock.unlock();
+ this._cpuWakeLock = null;
+ }
+ break;
+ }
+
+ // If the _cpuWakeLock is already set we are in a multiple
+ // call setup, turning the screen on to let user see the
+ // notification.
+ if (this._cpuWakeLock) {
+ this.turnScreenOn();
+
+ break;
+ }
+
+ // Enable the user proximity sensor once the call is connected.
+ var call = telephony.calls[0];
+ call.addEventListener('statechange', this);
+
+ break;
+
+ case 'statechange':
+ var call = evt.target;
+ if (call.state !== 'connected') {
+ break;
+ }
+
+ // The call is connected. Remove the statechange listener
+ // and enable the user proximity sensor.
+ call.removeEventListener('statechange', this);
+
+ this._cpuWakeLock = navigator.requestWakeLock('cpu');
+ window.addEventListener('userproximity', this);
+ break;
+ }
+ },
+
+ toggleScreen: function scm_toggleScreen() {
+ if (this.screenEnabled) {
+ this.turnScreenOff();
+ } else {
+ this.turnScreenOn();
+ }
+ },
+
+ turnScreenOff: function scm_turnScreenOff(instant) {
+ if (!this.screenEnabled)
+ return false;
+
+ var self = this;
+
+ // Remember the current screen brightness. We will restore it when
+ // we turn the screen back on.
+ self._savedBrightness = navigator.mozPower.screenBrightness;
+
+ // Remove the cpuWakeLock if screen is not turned off by
+ // userproximity event.
+ if (!this._screenOffByProximity && this._cpuWakeLock) {
+ window.removeEventListener('userproximity', this);
+ this._cpuWakeLock.unlock();
+ this._cpuWakeLock = null;
+ }
+
+ var screenOff = function scm_screenOff() {
+ self._setIdleTimeout(0);
+
+ window.removeEventListener('devicelight', self);
+
+ self.screenEnabled = false;
+ self._inTransition = false;
+ self.screen.classList.add('screenoff');
+ setTimeout(function realScreenOff() {
+ self.setScreenBrightness(0, true);
+ // Sometimes the ScreenManager.screenEnabled and mozPower.screenEnabled
+ // values are out of sync. Since the rest of the world relies only on
+ // the value of ScreenManager.screenEnabled it can be some situations
+ // where the screen is off but ScreenManager think it is on... (see
+ // bug 822463). Ideally a callback should have been used, like
+ // ScreenManager.getScreenState(function(value) { ...} ); but there
+ // are too many places to change that for now.
+ self.screenEnabled = false;
+ navigator.mozPower.screenEnabled = false;
+ }, 20);
+
+ self.fireScreenChangeEvent();
+ };
+
+ if (instant) {
+ if (!WindowManager.isFtuRunning()) {
+ screenOff();
+ }
+ return true;
+ }
+
+ this.setScreenBrightness(0.1, false);
+ this._inTransition = true;
+ setTimeout(function noticeTimeout() {
+ if (!self._inTransition)
+ return;
+
+ screenOff();
+ }, self._dimNotice);
+
+ return true;
+ },
+
+ turnScreenOn: function scm_turnScreenOn(instant) {
+ if (this.screenEnabled) {
+ if (this._inTransition) {
+ // Cancel the dim out
+ this._inTransition = false;
+ this.setScreenBrightness(this._savedBrightness, true);
+ this._reconfigScreenTimeout();
+ }
+ return false;
+ }
+
+ // Set the brightness before the screen is on.
+ this.setScreenBrightness(this._savedBrightness, instant);
+
+ // If we are in a call and there is no cpuWakeLock,
+ // we would have to get one here.
+ var telephony = window.navigator.mozTelephony;
+ if (!this._cpuWakeLock && telephony && telephony.calls.length) {
+ telephony.calls.some(function checkCallConnection(call) {
+ if (call.state == 'connected') {
+ this._cpuWakeLock = navigator.requestWakeLock('cpu');
+ window.addEventListener('userproximity', this);
+ return true;
+ }
+ return false;
+ }, this);
+ }
+
+ // Actually turn the screen on.
+ var power = navigator.mozPower;
+ if (power)
+ power.screenEnabled = true;
+ this.screenEnabled = true;
+ this.screen.classList.remove('screenoff');
+
+ // Attaching the event listener effectively turn on the hardware
+ // device light sensor, which _must be_ done after power.screenEnabled.
+ if (this._deviceLightEnabled)
+ window.addEventListener('devicelight', this);
+
+ this._reconfigScreenTimeout();
+ this.fireScreenChangeEvent();
+
+ return true;
+ },
+
+ _reconfigScreenTimeout: function scm_reconfigScreenTimeout() {
+ // Remove idle timer if screen wake lock is acquired.
+ if (this._screenWakeLocked) {
+ this._setIdleTimeout(0);
+ // The screen should be turn off with shorter timeout if
+ // it was never unlocked.
+ } else if (LockScreen.locked) {
+ this._setIdleTimeout(10, true);
+ var self = this;
+ var stopShortIdleTimeout = function scm_stopShortIdleTimeout() {
+ window.removeEventListener('unlock', stopShortIdleTimeout);
+ window.removeEventListener('lockpanelchange', stopShortIdleTimeout);
+ self._setIdleTimeout(self._idleTimeout, false);
+ };
+
+ window.addEventListener('unlock', stopShortIdleTimeout);
+ window.addEventListener('lockpanelchange', stopShortIdleTimeout);
+ } else {
+ this._setIdleTimeout(this._idleTimeout, false);
+ }
+ },
+
+ setScreenBrightness: function scm_setScreenBrightness(brightness, instant) {
+ this._targetBrightness = brightness;
+ var power = navigator.mozPower;
+ if (!power)
+ return;
+
+ // Make sure we don't have another brightness change scheduled
+ if (this._transitionBrightnessTimer) {
+ clearTimeout(this._transitionBrightnessTimer);
+ this._transitionBrightnessTimer = null;
+ }
+
+ if (typeof instant !== 'boolean')
+ instant = true;
+
+ if (instant) {
+ power.screenBrightness = brightness;
+ return;
+ }
+
+ // transitionBrightness() is a looping function that will
+ // gracefully tune the brightness to _targetBrightness for us.
+ this.transitionBrightness();
+ },
+
+ transitionBrightness: function scm_transitionBrightness() {
+ var self = this;
+ var power = navigator.mozPower;
+ var screenBrightness = power.screenBrightness;
+ var delta = this.BRIGHTNESS_ADJUST_STEP;
+
+ // Is this the last time adjustment we need to make?
+ if (Math.abs(this._targetBrightness - screenBrightness) <= delta) {
+ power.screenBrightness = this._targetBrightness;
+ this._transitionBrightnessTimer = null;
+ return;
+ }
+
+ if (screenBrightness > this._targetBrightness)
+ delta *= -1;
+
+ screenBrightness += delta;
+ power.screenBrightness = screenBrightness;
+
+ this._transitionBrightnessTimer =
+ setTimeout(function transitionBrightnessTimeout() {
+ self.transitionBrightness();
+ }, this.BRIGHTNESS_ADJUST_INTERVAL);
+ },
+
+ setDeviceLightEnabled: function scm_setDeviceLightEnabled(enabled) {
+ if (!enabled && this._deviceLightEnabled) {
+ // Disabled -- set the brightness back to preferred brightness
+ this.setScreenBrightness(this._userBrightness, false);
+ }
+ this._deviceLightEnabled = enabled;
+
+ if (!this.screenEnabled)
+ return;
+
+ // Disable/enable device light sensor accordingly.
+ // This will also toggle the actual hardware, which
+ // must be done while the screen is on.
+ if (enabled) {
+ window.addEventListener('devicelight', this);
+ } else {
+ window.removeEventListener('devicelight', this);
+ }
+ },
+
+ _setIdleTimeout: function scm_setIdleTimeout(time, instant) {
+ window.clearIdleTimeout(this._idleTimerId);
+
+ // Reset the idled state back to false.
+ this._idled = false;
+
+ // 0 is the value used to disable idle timer by user and by us.
+ if (time === 0)
+ return;
+
+ var self = this;
+ var idleCallback = function idle_proxy() {
+ self.turnScreenOff(instant);
+ };
+ var activeCallback = function active_proxy() {
+ self.turnScreenOn(true);
+ };
+
+ this._idleTimerId = window.setIdleTimeout(idleCallback,
+ activeCallback, time * 1000);
+ },
+
+ fireScreenChangeEvent: function scm_fireScreenChangeEvent() {
+ var evt = new CustomEvent('screenchange',
+ { bubbles: true, cancelable: false,
+ detail: { screenEnabled: this.screenEnabled } });
+ window.dispatchEvent(evt);
+ }
+};
+
+ScreenManager.init();
diff --git a/apps/system/js/screenshot.js b/apps/system/js/screenshot.js
new file mode 100644
index 0000000..9e8e7f6
--- /dev/null
+++ b/apps/system/js/screenshot.js
@@ -0,0 +1,115 @@
+// screenshot.js: system screenshot module
+//
+// This system module takes a screenshot of the currently running app
+// or homescreen and stores it with DeviceStorage when the user
+// presses the home and sleep buttons at the same time. It communicates
+// with gecko code running in b2g/chrome/content/shell.js using a private
+// event-based API. It is the gecko code that creates the screenshot.
+//
+// This script must be used with the defer attribute.
+//
+//
+(function() {
+ window.addEventListener('home+sleep', takeScreenshot);
+
+ // Assume that the maximum screenshot size is 4 bytes per pixel
+ // plus a bit extra. In practice, with compression, our PNG files will be
+ // much smaller than this.
+ var MAX_SCREENSHOT_SIZE = window.innerWidth * window.innerHeight * 4 + 4096;
+
+ function takeScreenshot() {
+ // Give feedback that the screenshot request was received
+ navigator.vibrate(100);
+
+ // We don't need device storage here, but check to see that
+ // it is available before sending the screenshot request to chrome.
+ // If device storage is available, the callback will be called.
+ // Otherwise, an error message notification will be displayed.
+ getDeviceStorage(function() {
+ // Let chrome know we'd like a screenshot.
+ // This is a completely non-standard undocumented API
+ // for communicating with our chrome code.
+ var screenshotProps = {
+ detail: {
+ type: 'take-screenshot'
+ }
+ };
+ window.dispatchEvent(new CustomEvent('mozContentEvent', screenshotProps));
+ });
+ }
+
+ // Display a screenshot success or failure notification.
+ // Localize the first argument, and localize the third if the second is null
+ function notify(titleid, body, bodyid) {
+ var title = navigator.mozL10n.get(titleid) || titleid;
+ body = body || navigator.mozL10n.get(bodyid);
+ navigator.mozNotification.createNotification(title, body).show();
+ }
+
+ // Get a DeviceStorage object and pass it to the callback.
+ // Or, if device storage is not available, display a notification.
+ function getDeviceStorage(callback) {
+ var storage = navigator.getDeviceStorage('pictures');
+ var availreq = storage.available();
+ availreq.onsuccess = function() {
+ var state = availreq.result;
+ if (state === 'unavailable') {
+ notify('screenshotFailed', null, 'screenshotNoSDCard');
+ }
+ else if (state === 'shared') {
+ notify('screenshotFailed', null, 'screenshotSDCardInUse');
+ }
+ else if (state === 'available') {
+ var freereq = storage.freeSpace();
+ freereq.onsuccess = function() {
+ if (freereq.result < MAX_SCREENSHOT_SIZE) {
+ notify('screenshotFailed', null, 'screenshotSDCardLow');
+ }
+ else {
+ callback(storage);
+ }
+ };
+ freereq.onerror = function() {
+ notify('screenshotFailed', freereq.error && freereq.error.name);
+ };
+ }
+ }
+ availreq.onerror = function() {
+ notify('screenshotFailed', availreq.error && availreq.error.name);
+ }
+ }
+
+ // Handle the event we get from chrome with the screenshot
+ window.addEventListener('mozChromeEvent', function ss_onMozChromeEvent(e) {
+ try {
+ if (e.detail.type === 'take-screenshot-success') {
+ getDeviceStorage(function(storage) {
+ var filename = 'screenshots/' +
+ new Date().toISOString().slice(0, -5).replace(/[:T]/g, '-') +
+ '.png';
+
+ var saveRequest = storage.addNamed(e.detail.file, filename);
+
+ saveRequest.onsuccess = function ss_onsuccess() {
+ // Vibrate again when the screenshot is saved
+ navigator.vibrate(100);
+
+ // Display filename in a notification
+ notify('screenshotSaved', filename);
+ };
+
+ saveRequest.onerror = function ss_onerror() {
+ notify('screenshotFailed', saveRequest.error.name);
+ };
+ });
+ }
+ else if (e.detail.type === 'take-screenshot-error') {
+ notify('screenshotFailed', e.detail.error);
+ }
+ }
+ catch (e) {
+ console.log('exception in screenshot handler', e);
+ notify('screenshotFailed', e.toString());
+ }
+ });
+}());
diff --git a/apps/system/js/sim_lock.js b/apps/system/js/sim_lock.js
new file mode 100644
index 0000000..3a3d3e0
--- /dev/null
+++ b/apps/system/js/sim_lock.js
@@ -0,0 +1,110 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var SimLock = {
+ init: function sl_init() {
+ // Do not do anything if we can't have access to MobileConnection API
+ var conn = window.navigator.mozMobileConnection;
+ if (!conn)
+ return;
+
+ this.onClose = this.onClose.bind(this);
+
+ // Watch for apps that need a mobile connection
+ window.addEventListener('appwillopen', this);
+
+ // Display the dialog only after lockscreen is unlocked
+ // To prevent keyboard being displayed behind it.
+ window.addEventListener('unlock', this);
+
+ // always monitor card state change
+ conn.addEventListener('cardstatechange', this.showIfLocked.bind(this));
+ },
+
+ handleEvent: function sl_handleEvent(evt) {
+ switch (evt.type) {
+ case 'unlock':
+ this.showIfLocked();
+ break;
+ case 'appwillopen':
+ // if an app needs telephony or sms permission,
+ // we will launch the unlock screen if needed.
+
+ var app = Applications.getByManifestURL(
+ evt.target.getAttribute('mozapp'));
+
+ if (!app || !app.manifest.permissions)
+ return;
+
+ // Ignore first time usage app which already ask for SIM code
+ if (evt.target.classList.contains('ftu'))
+ return;
+
+ if (!('telephony' in app.manifest.permissions ||
+ 'sms' in app.manifest.permissions))
+ return;
+
+ // Ignore second `appwillopen` event when showIfLocked ends up
+ // eventually opening the app on valid pin code
+ var origin = evt.target.dataset.frameOrigin;
+ if (origin == this._lastOrigin) {
+ delete this._lastOrigin;
+ return;
+ }
+ this._lastOrigin = origin;
+
+ // if sim is locked, cancel app opening in order to display
+ // it after PIN dialog
+ if (this.showIfLocked())
+ evt.preventDefault();
+
+ break;
+ }
+ },
+
+ showIfLocked: function sl_showIfLocked() {
+ var conn = window.navigator.mozMobileConnection;
+ if (!conn)
+ return false;
+
+ if (LockScreen.locked)
+ return false;
+
+ switch (conn.cardState) {
+ // do nothing in absent and null card states
+ case null:
+ case 'absent':
+ break;
+ case 'pukRequired':
+ case 'pinRequired':
+ SimPinDialog.show('unlock', this.onClose);
+ return true;
+ case 'networkLocked':
+ // XXXX: After unlocking the SIM the cardState is
+ // 'networkLocked' but it changes inmediately to 'ready'
+ // if the phone is not SIM-locked. If the cardState
+ // is still 'networkLocked' after two seconds we unlock
+ // the network control key lock (network personalization).
+ setTimeout(function checkState() {
+ if (conn.cardState == 'networkLocked') {
+ SimPinDialog.show('unlock', SimLock.onClose);
+ }
+ }, 5000);
+ break;
+ }
+ return false;
+ },
+
+ onClose: function sl_onClose(reason) {
+ // Display the app only when PIN code is valid and when we click
+ // on `X` button
+ if (this._lastOrigin && (reason == 'success' || reason == 'skip'))
+ WindowManager.setDisplayedApp(this._lastOrigin);
+ delete this._lastOrigin;
+ }
+
+};
+
+SimLock.init();
diff --git a/apps/system/js/simcard_dialog.js b/apps/system/js/simcard_dialog.js
new file mode 100644
index 0000000..4176cbf
--- /dev/null
+++ b/apps/system/js/simcard_dialog.js
@@ -0,0 +1,355 @@
+/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var SimPinDialog = {
+ dialogTitle: document.querySelector('#simpin-dialog header h1'),
+ dialogDone: document.querySelector('#simpin-dialog button[type="submit"]'),
+ dialogClose: document.querySelector('#simpin-dialog button[type="reset"]'),
+
+ pinArea: document.getElementById('pinArea'),
+ pukArea: document.getElementById('pukArea'),
+ nckArea: document.getElementById('nckArea'),
+ newPinArea: document.getElementById('newPinArea'),
+ confirmPinArea: document.getElementById('confirmPinArea'),
+
+ pinInput: null,
+ pukInput: null,
+ nckInput: null,
+ newPinInput: null,
+ confirmPinInput: null,
+
+ errorMsg: document.getElementById('errorMsg'),
+ errorMsgHeader: document.getElementById('messageHeader'),
+ errorMsgBody: document.getElementById('messageBody'),
+
+ mobileConnection: null,
+
+ lockType: 'pin',
+ action: 'unlock',
+
+ // Now we don't have a number-password type for input field
+ // mimic one by binding one number input and one text input
+ getNumberPasswordInputField: function spl_wrapNumberInput(name) {
+ var valueEntered = '';
+ var inputField = document.querySelector('input[name="' + name + '"]');
+ var displayField = document.querySelector('input[name="' + name + 'Vis"]');
+ var codeMaxLength = parseInt(inputField.getAttribute('maxlength'), 10);
+ var self = this;
+
+ inputField.addEventListener('keypress', function(evt) {
+ if (evt.target !== inputField)
+ return;
+ evt.preventDefault();
+
+ var code = evt.charCode;
+ if (code !== 0 && (code < 0x30 || code > 0x39))
+ return;
+
+ if (code === 0) { // backspace
+ valueEntered = valueEntered.substr(0, valueEntered.length - 1);
+ } else {
+ if (valueEntered.length >= codeMaxLength)
+ return;
+ valueEntered += String.fromCharCode(code);
+ }
+ displayField.value = encryption(valueEntered);
+ if (displayField.value.length >= 4)
+ self.dialogDone.disabled = false;
+ else
+ self.dialogDone.disabled = true;
+ });
+
+ function encryption(str) {
+ return (new Array(str.length + 1)).join('*');
+ }
+
+ function setValue(value) {
+ valueEntered = value;
+ inputField.value = value;
+ displayField.value = encryption(valueEntered);
+ }
+
+ function setFocus() {
+ inputField.focus();
+ }
+
+ function blur() {
+ inputField.blur();
+ }
+
+ return {
+ get value() { return valueEntered; },
+ set value(value) { setValue(value) },
+ focus: setFocus,
+ blur: blur
+ };
+ },
+
+ handleCardState: function spl_handleCardState() {
+ var _ = navigator.mozL10n.get;
+
+ var cardState = this.mobileConnection.cardState;
+ switch (cardState) {
+ case 'pinRequired':
+ this.lockType = 'pin';
+ this.errorMsg.hidden = true;
+ this.inputFieldControl(true, false, false, false);
+ this.pinInput.focus();
+ break;
+ case 'pukRequired':
+ this.lockType = 'puk';
+ this.errorMsgHeader.textContent = _('simCardLockedMsg') || '';
+ this.errorMsgHeader.dataset.l10nId = 'simCardLockedMsg';
+ this.errorMsgBody.textContent = _('enterPukMsg') || '';
+ this.errorMsgBody.dataset.l10nId = 'enterPukMsg';
+ this.errorMsg.hidden = false;
+ this.inputFieldControl(false, true, false, true);
+ this.pukInput.focus();
+ break;
+ case 'networkLocked':
+ this.lockType = 'nck';
+ this.errorMsg.hidden = true;
+ this.inputFieldControl(false, false, true, false);
+ this.nckInput.focus();
+ break;
+ default:
+ this.skip();
+ break;
+ }
+ this.dialogTitle.textContent = _(this.lockType + 'Title') || '';
+ this.dialogTitle.dataset.l10nId = this.lockType + 'Title';
+ },
+
+ handleError: function spl_handleLockError(evt) {
+ var retry = (evt.retryCount) ? evt.retryCount : -1;
+ this.showErrorMsg(retry, evt.lockType);
+ if (retry === -1) {
+ this.skip();
+ return;
+ }
+ if (evt.lockType === 'pin') {
+ this.pinInput.focus();
+ } else if (evt.lockType === 'puk') {
+ this.pukInput.focus();
+ } else {
+ this.nckInput.focus();
+ }
+ },
+
+ showErrorMsg: function spl_showErrorMsg(retry, type) {
+ var _ = navigator.mozL10n.get;
+
+ this.errorMsgHeader.textContent = _(type + 'ErrorMsg');
+ this.errorMsgHeader.dataset.l10nId = type + 'ErrorMsg';
+
+ if (retry !== 1) {
+ var l10nArgs = { n: retry };
+ this.errorMsgBody.dataset.l10nId = type + 'AttemptMsg';
+ this.errorMsgBody.dataset.l10nArgs = JSON.stringify(l10nArgs);
+ this.errorMsgBody.textContent = _(type + 'AttemptMsg', l10nArgs);
+ } else {
+ this.errorMsgBody.dataset.l10nId = type + 'LastChanceMsg';
+ this.errorMsgBody.textContent = _(type + 'LastChanceMsg');
+ }
+
+ this.errorMsg.hidden = false;
+ },
+
+ unlockPin: function spl_unlockPin() {
+ var pin = this.pinInput.value;
+ if (pin === '')
+ return;
+
+ var options = {lockType: 'pin', pin: pin };
+ this.unlockCardLock(options);
+ this.clear();
+ },
+
+ unlockPuk: function spl_unlockPuk() {
+ var _ = navigator.mozL10n.get;
+
+ var puk = this.pukInput.value;
+ var newPin = this.newPinInput.value;
+ var confirmPin = this.confirmPinInput.value;
+ if (puk === '' || newPin === '' || confirmPin === '')
+ return;
+
+ if (newPin !== confirmPin) {
+ this.errorMsgHeader.textContent = _('newPinErrorMsg');
+ this.errorMsgHeader.dataset.l10nId = 'newPinErrorMsg';
+ this.errorMsgBody.textContent = '';
+ this.errorMsg.hidden = false;
+ return;
+ }
+ var options = {lockType: 'puk', puk: puk, newPin: newPin };
+ this.unlockCardLock(options);
+ this.clear();
+ },
+
+ unlockNck: function spl_unlockNck() {
+ var nck = this.nckInput.value;
+ if (nck === '')
+ return;
+
+ var options = {lockType: 'nck', pin: nck };
+ this.unlockCardLock(options);
+ this.clear();
+ },
+
+ unlockCardLock: function spl_unlockCardLock(options) {
+ var req = this.mobileConnection.unlockCardLock(options);
+ req.onsuccess = this.close.bind(this, 'success');
+ },
+
+ enableLock: function spl_enableLock() {
+ var pin = this.pinInput.value;
+ if (pin === '')
+ return;
+
+ var enabled = SimPinLock.simPinCheckBox.checked;
+ var options = {lockType: 'pin', pin: pin, enabled: enabled};
+ this.setCardLock(options);
+ this.clear();
+ },
+
+ changePin: function spl_changePin() {
+ var _ = navigator.mozL10n.get;
+
+ var pin = this.pinInput.value;
+ var newPin = this.newPinInput.value;
+ var confirmPin = this.confirmPinInput.value;
+ if (pin === '' || newPin === '' || confirmPin === '')
+ return;
+
+ if (newPin !== confirmPin) {
+ this.errorMsgHeader.textContent = _('newPinErrorMsg');
+ this.errorMsgHeader.dataset.l10nId = 'newPinErrorMsg';
+ this.errorMsgBody.textContent = '';
+ this.errorMsg.hidden = false;
+ return;
+ }
+ var options = {lockType: 'pin', pin: pin, newPin: newPin};
+ this.setCardLock(options);
+ this.clear();
+ },
+
+ setCardLock: function spl_setCardLock(options) {
+ var req = this.mobileConnection.setCardLock(options);
+ req.onsuccess = this.close.bind(this, 'success');
+ },
+ inputFieldControl: function spl_inputField(isPin, isPuk, isNck, isNewPin) {
+ this.pinArea.hidden = !isPin;
+ this.pukArea.hidden = !isPuk;
+ this.nckArea.hidden = !isNck;
+ this.newPinArea.hidden = !isNewPin;
+ this.confirmPinArea.hidden = !isNewPin;
+ },
+
+ verify: function spl_verify() {
+ switch (this.action) {
+ case 'unlock':
+ if (this.lockType === 'pin')
+ this.unlockPin();
+ else if (this.lockType === 'puk') {
+ this.unlockPuk();
+ } else {
+ this.unlockNck();
+ }
+ break;
+ case 'enable':
+ this.enableLock();
+ break;
+ case 'changePin':
+ this.changePin();
+ break;
+ }
+ return false;
+ },
+
+ onHide: function spl_onHide(reason) {
+ this.clear();
+ if (this.onclose)
+ this.onclose(reason);
+ },
+
+ clear: function spl_clear() {
+ this.errorMsg.hidden = true;
+ this.pinInput.value = '';
+ this.pinInput.blur();
+ this.pukInput.value = '';
+ this.pukInput.blur();
+ this.newPinInput.value = '';
+ this.confirmPinInput.value = '';
+ },
+
+ onclose: null,
+ /**
+ * Show the SIM pin dialog
+ * @param {String} action Name of the action to execute,
+ * either: unlock, enable or changePin.
+ * @param {Function} title Optional function called when dialog is closed.
+ * Receive a single argument being the reason of
+ * dialog closing: success, skip, home or holdhome.
+ */
+ show: function spl_show(action, onclose) {
+ var _ = navigator.mozL10n.get;
+
+ this.systemDialog.show();
+ this.dialogDone.disabled = true;
+ this.action = action;
+ this.lockType = 'pin';
+ switch (action) {
+ case 'unlock':
+ this.handleCardState();
+ break;
+ case 'enable':
+ this.inputFieldControl(true, false, false, false);
+ this.dialogTitle.textContent = _('pinTitle') || '';
+ this.dialogTitle.dataset.l10nId = 'pinTitle';
+ break;
+ case 'changePin':
+ this.inputFieldControl(true, false, false, true);
+ this.dialogTitle.textContent = _('newpinTitle') || '';
+ this.dialogTitle.dataset.l10nId = 'newpinTitle';
+ break;
+ }
+
+ if (onclose && typeof onclose === 'function')
+ this.onclose = onclose;
+ },
+
+ close: function spl_close(reason) {
+ this.systemDialog.hide(reason);
+ },
+
+ skip: function spl_skip() {
+ this.close('skip');
+ return false;
+ },
+
+ init: function spl_init() {
+ this.systemDialog = SystemDialog('simpin-dialog', {
+ onHide: this.onHide.bind(this)
+ });
+
+ this.mobileConnection = window.navigator.mozMobileConnection;
+ if (!this.mobileConnection)
+ return;
+
+ this.mobileConnection.addEventListener('icccardlockerror',
+ this.handleError.bind(this));
+
+ this.dialogDone.onclick = this.verify.bind(this);
+ this.dialogClose.onclick = this.skip.bind(this);
+ this.pinInput = this.getNumberPasswordInputField('simpin');
+ this.pukInput = this.getNumberPasswordInputField('simpuk');
+ this.nckInput = this.getNumberPasswordInputField('nckpin');
+ this.newPinInput = this.getNumberPasswordInputField('newSimpin');
+ this.confirmPinInput = this.getNumberPasswordInputField('confirmNewSimpin');
+ }
+};
+
+SimPinDialog.init();
+
diff --git a/apps/system/js/sleep_menu.js b/apps/system/js/sleep_menu.js
new file mode 100644
index 0000000..a97b772
--- /dev/null
+++ b/apps/system/js/sleep_menu.js
@@ -0,0 +1,276 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var SleepMenu = {
+ // Indicate setting status of ril.radio.disabled
+ isFlightModeEnabled: false,
+
+ // Indicate setting status of volume
+ isSilentModeEnabled: false,
+
+ elements: {},
+
+ get visible() {
+ return this.elements.overlay.classList.contains('visible');
+ },
+
+ getAllElements: function sm_getAllElements() {
+ this.elements.overlay = document.getElementById('sleep-menu');
+ this.elements.container =
+ document.querySelector('#sleep-menu-container ul');
+ this.elements.cancel = document.querySelector('#sleep-menu button');
+ },
+
+ init: function sm_init() {
+ this.getAllElements();
+ window.addEventListener('holdsleep', this.show.bind(this));
+ window.addEventListener('click', this, true);
+ window.addEventListener('screenchange', this, true);
+ window.addEventListener('home', this);
+ this.elements.cancel.addEventListener('click', this);
+
+ var self = this;
+ SettingsListener.observe('ril.radio.disabled', false, function(value) {
+ self.isFlightModeEnabled = value;
+ });
+
+ var settings = navigator.mozSettings;
+ SettingsListener.observe('audio.volume.notification', 7, function(value) {
+ settings.createLock().set({'ring.enabled': (value != 0)});
+ });
+
+ SettingsListener.observe('ring.enabled', true, function(value) {
+ self.isSilentModeEnabled = !value;
+ });
+ },
+
+ // Generate items
+ generateItems: function sm_generateItems() {
+ var items = [];
+ var _ = navigator.mozL10n.get;
+ var options = {
+ airplane: {
+ label: _('airplane'),
+ value: 'airplane',
+ icon: '/style/sleep_menu/images/airplane.png'
+ },
+ airplaneOff: {
+ label: _('airplaneOff'),
+ value: 'airplane'
+ },
+ silent: {
+ label: _('silent'),
+ value: 'silent',
+ icon: '/style/sleep_menu/images/vibration.png'
+ },
+ silentOff: {
+ label: _('normal'),
+ value: 'silentOff'
+ },
+ restart: {
+ label: _('restart'),
+ value: 'restart',
+ icon: '/style/sleep_menu/images/restart.png'
+ },
+ power: {
+ label: _('power'),
+ value: 'power',
+ icon: '/style/sleep_menu/images/power-off.png'
+ }
+ };
+
+ if (this.isFlightModeEnabled) {
+ items.push(options.airplaneOff);
+ } else {
+ items.push(options.airplane);
+ }
+
+ if (!this.isSilentModeEnabled) {
+ items.push(options.silent);
+ } else {
+ items.push(options.silentOff);
+ }
+
+ items.push(options.restart);
+ items.push(options.power);
+
+ return items;
+ },
+
+ show: function sm_show() {
+ this.elements.container.innerHTML = '';
+ this.buildMenu(this.generateItems());
+ this.elements.overlay.classList.add('visible');
+ },
+
+ buildMenu: function sm_buildMenu(items) {
+ items.forEach(function traveseItems(item) {
+ var item_li = document.createElement('li');
+ item_li.dataset.value = item.value;
+ item_li.textContent = item.label;
+ this.elements.container.appendChild(item_li);
+ }, this);
+ },
+
+ hide: function lm_hide() {
+ this.elements.overlay.classList.remove('visible');
+ },
+
+ handleEvent: function sm_handleEvent(evt) {
+ switch (evt.type) {
+ case 'screenchange':
+ if (!evt.detail.screenEnabled)
+ this.hide();
+ break;
+
+ case 'click':
+ if (!this.visible)
+ return;
+
+ if (evt.currentTarget === this.elements.cancel) {
+ this.hide();
+ return;
+ }
+
+ var action = evt.target.dataset.value;
+ if (!action) {
+ return;
+ }
+ this.hide();
+ this.handler(action);
+ break;
+
+ case 'home':
+ if (this.visible) {
+ this.hide();
+ }
+ break;
+ }
+ },
+
+ handler: function sm_handler(action) {
+ switch (action) {
+ case 'airplane':
+ // Airplane mode should turn off
+ //
+ // Radio ('ril.radio.disabled'`)
+ // Data ('ril.data.enabled'`)
+ // Wifi
+ // Bluetooth
+ // Geolocation
+ //
+ // It should also save the status of the latter 4 items
+ // so when leaving the airplane mode we could know which one to turn on.
+
+ if (!window.navigator.mozSettings)
+ return;
+
+ SettingsListener.getSettingsLock().set({
+ 'ril.radio.disabled': !this.isFlightModeEnabled
+ });
+
+ break;
+
+ // About silent and silentOff
+ // * Turn on silent mode will cause:
+ // * Turn off ringtone no matter if ring is on or off
+ // * for sms and incoming calls.
+ // * Turn off silent mode will cause:
+ // * Turn on ringtone no matter if ring is on or off
+ // * for sms and incoming calls.
+ case 'silent':
+ if (!window.navigator.mozSettings)
+ return;
+
+ SettingsListener.getSettingsLock().set({
+ 'ring.enabled': false
+ });
+ this.isSilentModeEnabled = true;
+
+ break;
+
+ case 'silentOff':
+ if (!window.navigator.mozSettings)
+ return;
+
+ SettingsListener.getSettingsLock().set({
+ 'ring.enabled': true
+ });
+ this.isSilentModeEnabled = false;
+
+ break;
+
+ case 'restart':
+ this.startPowerOff(true);
+
+ break;
+
+ case 'power':
+ this.startPowerOff(false);
+
+ break;
+ }
+ },
+
+ startPowerOff: function sm_startPowerOff(reboot) {
+ var power = navigator.mozPower;
+ if (!power)
+ return;
+
+ // Early return if we are already shutting down.
+ if (document.getElementById('poweroff-splash'))
+ return;
+
+ // Show shutdown animation before actually performing shutdown.
+ // * step1: fade-in poweroff-splash.
+ // * step2: The 3-rings animation is performed on the screen.
+ var div = document.createElement('div');
+ div.dataset.zIndexLevel = 'poweroff-splash';
+ div.id = 'poweroff-splash';
+
+ // The overall animation ends when the inner span of the bottom ring
+ // is animated, so we store it for detecting.
+ var inner;
+
+ for (var i = 1; i <= 3; i++) {
+ var outer = document.createElement('span');
+ outer.className = 'poweroff-ring';
+ outer.id = 'poweroff-ring-' + i;
+ div.appendChild(outer);
+
+ inner = document.createElement('span');
+ outer.appendChild(inner);
+ }
+
+ div.className = 'step1';
+
+ var nextAnimation = function nextAnimation(e) {
+ // Switch to next class
+ if (e.target == div)
+ div.className = 'step2';
+
+ if (e.target != inner)
+ return;
+
+ // Actual poweroff/reboot
+ setTimeout(function powerOffAnimated() {
+ if (reboot) {
+ power.reboot();
+ } else {
+ power.powerOff();
+ }
+ });
+
+ // Paint screen to black before reboot/poweroff
+ ScreenManager.turnScreenOff(true);
+ };
+
+ div.addEventListener('animationend', nextAnimation);
+
+ document.getElementById('screen').appendChild(div);
+ }
+};
+
+SleepMenu.init();
diff --git a/apps/system/js/sound_manager.js b/apps/system/js/sound_manager.js
new file mode 100644
index 0000000..52dfc06
--- /dev/null
+++ b/apps/system/js/sound_manager.js
@@ -0,0 +1,242 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+(function() {
+ window.addEventListener('volumeup', function() {
+ if (onBTEarphoneConnected() && onCall()) {
+ changeVolume(1, 'bt_sco');
+ } else {
+ changeVolume(1);
+ }
+ });
+ window.addEventListener('volumedown', function() {
+ if (onBTEarphoneConnected() && onCall()) {
+ changeVolume(-1, 'bt_sco');
+ } else {
+ changeVolume(-1);
+ }
+ });
+
+ // Store the current active channel;
+ // change with 'audio-channel-changed' mozChromeEvent
+ var currentChannel = 'notification';
+
+ var vibrationEnabled = true;
+
+ // This event is generated in shell.js in response to bluetooth headset.
+ // Bluetooth headset always assign audio volume to a specific value when
+ // pressing its volume-up/volume-down buttons.
+ window.addEventListener('mozChromeEvent', function(e) {
+ var type = e.detail.type;
+ if (type == 'bluetooth-volumeset') {
+ changeVolume(e.detail.value - currentVolume['bt_sco'], 'bt_sco');
+ } else if (type == 'audio-channel-changed') {
+ currentChannel = e.detail.channel;
+ }
+ });
+
+ function onCall() {
+ if (currentChannel == 'telephony')
+ return true;
+
+ // XXX: This work should be removed
+ // once we could get telephony channel change event
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=819858
+ var telephony = window.navigator.mozTelephony;
+ if (!telephony)
+ return false;
+
+ return telephony.calls.some(function callIterator(call) {
+ return (call.state == 'connected');
+ });
+ }
+
+ function onBTEarphoneConnected() {
+ var bluetooth = navigator.mozBluetooth;
+ if (!bluetooth)
+ return false;
+
+ // 0x111E is for querying earphone type.
+ return navigator.mozBluetooth.isConnected(0x111E);
+ };
+
+ // Platform doesn't provide the maximum value of each channel
+ // therefore, hard code here.
+ var MAX_VOLUME = {
+ 'alarm': 15,
+ 'notification': 15,
+ 'telephony': 5,
+ 'content': 15,
+ 'bt_sco': 15
+ };
+
+ // Please refer https://wiki.mozilla.org/WebAPI/AudioChannels > Settings
+ var currentVolume = {
+ 'alarm': 15,
+ 'notification': 15,
+ 'telephony': 5,
+ 'content': 15,
+ 'bt_sco': 15
+ };
+ var pendingRequestCount = 0;
+
+ // We have three virtual states here:
+ // OFF -> VIBRATION -> MUTE
+ var muteState = 'OFF';
+
+ for (var channel in currentVolume) {
+ (function(channel) {
+ var setting = 'audio.volume.' + channel;
+ SettingsListener.observe(setting, 5, function onSettingsChange(volume) {
+ if (pendingRequestCount)
+ return;
+
+ var max = MAX_VOLUME[channel];
+ currentVolume[channel] =
+ parseInt(Math.max(0, Math.min(max, volume)), 10);
+ });
+ })(channel);
+ }
+
+ SettingsListener.observe('vibration.enabled', true, function(vibration) {
+ if (pendingRequestCount)
+ return;
+
+ vibrationEnabled = vibration;
+ });
+
+ var activeTimeout = 0;
+
+ // When hardware volume key is pressed, we need to decide which channel we
+ // should toggle.
+ // This method returns the string for setting key 'audio.volume.*' represents
+ // that.
+ // Note: this string does not always equal to currentChannel since some
+ // different channels are grouped together to listen to the same setting.
+ function getChannel() {
+ if (onCall())
+ return 'telephony';
+
+ switch (currentChannel) {
+ case 'normal':
+ case 'content':
+ return 'content';
+ case 'telephony':
+ return 'telephony';
+ case 'alarm':
+ return 'alarm';
+ case 'notification':
+ case 'ringer':
+ default:
+ return 'notification';
+ }
+ }
+
+ function getVolumeState(currentVolume, delta, channel) {
+ if (channel == 'notification') {
+ if (currentVolume + delta <= 0) {
+ if (currentVolume == 0 && vibrationEnabled) {
+ vibrationEnabled = false;
+ } else if (currentVolume > 0 && !vibrationEnabled) {
+ vibrationEnabled = true;
+ }
+ return 'MUTE';
+ } else {
+ return 'OFF';
+ }
+ } else {
+ if (currentVolume + delta <= 0) {
+ return 'MUTE';
+ } else {
+ return 'OFF';
+ }
+ }
+ }
+
+ function changeVolume(delta, channel) {
+ channel = channel ? channel : getChannel();
+
+ muteState = getVolumeState(currentVolume[channel], delta, channel);
+
+ var volume = currentVolume[channel] + delta;
+
+ currentVolume[channel] = volume =
+ Math.max(0, Math.min(MAX_VOLUME[channel], volume));
+
+ var overlay = document.getElementById('system-overlay');
+ var notification = document.getElementById('volume');
+ var overlayClasses = overlay.classList;
+ var classes = notification.classList;
+
+ switch (muteState) {
+ case 'OFF':
+ classes.remove('mute');
+ if (vibrationEnabled) {
+ classes.add('vibration');
+ } else {
+ classes.remove('vibration');
+ }
+ break;
+ case 'MUTE':
+ classes.add('mute');
+ if (channel == 'notification') {
+ if (vibrationEnabled) {
+ classes.add('vibration');
+ SettingsListener.getSettingsLock().set({
+ 'vibration.enabled': true
+ });
+ } else {
+ classes.remove('vibration');
+ SettingsListener.getSettingsLock().set({
+ 'vibration.enabled': false
+ });
+ }
+ }
+ break;
+ }
+
+ var steps =
+ Array.prototype.slice.call(notification.querySelectorAll('div'), 0);
+
+ for (var i = 0; i < steps.length; i++) {
+ var step = steps[i];
+ if (i < volume) {
+ step.classList.add('active');
+ } else {
+ step.classList.remove('active');
+ }
+ }
+
+ overlayClasses.add('volume');
+ classes.add('visible');
+ window.clearTimeout(activeTimeout);
+ activeTimeout = window.setTimeout(function hideSound() {
+ overlayClasses.remove('volume');
+ classes.remove('visible');
+ }, 1500);
+
+ if (!window.navigator.mozSettings)
+ return;
+
+ pendingRequestCount++;
+ var req;
+
+ notification.dataset.channel = channel;
+
+ var settingObject = {};
+ settingObject['audio.volume.' + channel] = volume;
+
+ req = SettingsListener.getSettingsLock().set(settingObject);
+
+ req.onsuccess = function onSuccess() {
+ pendingRequestCount--;
+ };
+
+ req.onerror = function onError() {
+ pendingRequestCount--;
+ };
+ }
+})();
+
diff --git a/apps/system/js/source_view.js b/apps/system/js/source_view.js
new file mode 100644
index 0000000..dc6b8e4
--- /dev/null
+++ b/apps/system/js/source_view.js
@@ -0,0 +1,67 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var SourceView = {
+ get viewer() {
+ return document.getElementById('appViewsource');
+ },
+
+ get active() {
+ return !this.viewer ? false : this.viewer.style.visibility === 'visible';
+ },
+
+ init: function sv_init() {
+ window.addEventListener('home+volume', function() {
+ if (ScreenManager.screenEnabled)
+ SourceView.toggle();
+ });
+ window.addEventListener('locked', function() {
+ SourceView.hide();
+ });
+ },
+
+ show: function sv_show() {
+ var viewsource = this.viewer;
+ if (!viewsource) {
+ var style = '#appViewsource { ' +
+ ' position: absolute;' +
+ ' top: -moz-calc(10%);' +
+ ' left: -moz-calc(10%);' +
+ ' width: -moz-calc(80% - 2 * 15px);' +
+ ' height: -moz-calc(80% - 2 * 15px);' +
+ ' visibility: hidden;' +
+ ' margin: 15px;' +
+ ' background-color: white;' +
+ ' opacity: 0.92;' +
+ ' color: black;' +
+ ' z-index: 9999;' +
+ '}';
+ document.styleSheets[0].insertRule(style, 0);
+
+ viewsource = document.createElement('iframe');
+ viewsource.id = 'appViewsource';
+ document.body.appendChild(viewsource);
+ }
+
+ var url = WindowManager.getDisplayedApp();
+ if (!url)
+ // Assume the home screen is the visible app.
+ url = window.location.toString();
+ viewsource.src = 'view-source: ' + url;
+ viewsource.style.visibility = 'visible';
+ },
+
+ hide: function sv_hide() {
+ var viewsource = this.viewer;
+ if (viewsource) {
+ viewsource.style.visibility = 'hidden';
+ viewsource.src = 'about:blank';
+ }
+ },
+
+ toggle: function sv_toggle() {
+ this.active ? this.hide() : this.show();
+ }
+};
diff --git a/apps/system/js/statusbar.js b/apps/system/js/statusbar.js
new file mode 100644
index 0000000..1d95d99
--- /dev/null
+++ b/apps/system/js/statusbar.js
@@ -0,0 +1,618 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var StatusBar = {
+ /* all elements that are children nodes of the status bar */
+ ELEMENTS: ['notification', 'time',
+ 'battery', 'wifi', 'data', 'flight-mode', 'signal', 'network-activity',
+ 'tethering', 'alarm', 'bluetooth', 'mute', 'headphones',
+ 'recording', 'sms', 'geolocation', 'usb', 'label', 'system-downloads',
+ 'call-forwarding'],
+
+ /* Timeout for 'recently active' indicators */
+ kActiveIndicatorTimeout: 60 * 1000,
+
+ /* Whether or not status bar is actively updating or not */
+ active: true,
+
+ /* Some values that sync from mozSettings */
+ settingValues: {},
+
+ /* Keep the DOM element references here */
+ icons: {},
+
+ /* A mapping table between technology names
+ we would get from API v.s. the icon we want to show. */
+ mobileDataIconTypes: {
+ 'lte': '4G', // 4G LTE
+ 'ehrpd': '4G', // 4G CDMA
+ 'hspa+': 'H+', // 3.5G HSPA+
+ 'hsdpa': 'H', 'hsupa': 'H', 'hspa': 'H', // 3.5G HSDPA
+ 'evdo0': '3G', 'evdoa': '3G', 'evdob': '3G', '1xrtt': '3G', // 3G CDMA
+ 'umts': '3G', // 3G
+ 'edge': 'E', // EDGE
+ 'is95a': '2G', 'is95b': '2G', // 2G CDMA
+ 'gprs': '2G'
+ },
+
+ geolocationActive: false,
+ geolocationTimer: null,
+
+ recordingActive: false,
+ recordingTimer: null,
+
+ umsActive: false,
+
+ headphonesActive: false,
+
+ /**
+ * this keeps how many current installs/updates we do
+ * it triggers the icon "systemDownloads"
+ */
+ systemDownloadsCount: 0,
+
+ /* For other modules to acquire */
+ get height() {
+ if (this.screen.classList.contains('fullscreen-app') ||
+ document.mozFullScreen) {
+ return 0;
+ } else if (this.screen.classList.contains('active-statusbar')) {
+ return this.attentionBar.offsetHeight;
+ } else {
+ return this._cacheHeight ||
+ (this._cacheHeight = this.element.getBoundingClientRect().height);
+ }
+ },
+
+ init: function sb_init() {
+ this.getAllElements();
+
+ var settings = {
+ 'ril.radio.disabled': ['signal', 'data'],
+ 'ril.data.enabled': ['data'],
+ 'wifi.enabled': ['wifi'],
+ 'bluetooth.enabled': ['bluetooth'],
+ 'tethering.usb.enabled': ['tethering'],
+ 'tethering.wifi.enabled': ['tethering'],
+ 'tethering.wifi.connectedClients': ['tethering'],
+ 'tethering.usb.connectedClients': ['tethering'],
+ 'ring.enabled': ['mute'],
+ 'alarm.enabled': ['alarm'],
+ 'vibration.enabled': ['vibration'],
+ 'ril.cf.enabled': ['callForwarding']
+ };
+
+ var self = this;
+ for (var settingKey in settings) {
+ (function sb_setSettingsListener(settingKey) {
+ SettingsListener.observe(settingKey, false,
+ function sb_settingUpdate(value) {
+ self.settingValues[settingKey] = value;
+ settings[settingKey].forEach(
+ function sb_callUpdate(name) {
+ self.update[name].call(self);
+ }
+ );
+ }
+ );
+ self.settingValues[settingKey] = false;
+ })(settingKey);
+ }
+
+ // Listen to 'screenchange' from screen_manager.js
+ window.addEventListener('screenchange', this);
+
+ // Listen to 'geolocation-status' and 'recording-status' mozChromeEvent
+ window.addEventListener('mozChromeEvent', this);
+
+ // Listen to 'bluetoothconnectionchange' from bluetooth.js
+ window.addEventListener('bluetoothconnectionchange', this);
+
+ // Listen to 'moztimechange'
+ window.addEventListener('moztimechange', this);
+
+ this.systemDownloadsCount = 0;
+ this.setActive(true);
+ },
+
+ handleEvent: function sb_handleEvent(evt) {
+ switch (evt.type) {
+ case 'screenchange':
+ this.setActive(evt.detail.screenEnabled);
+ break;
+
+ case 'chargingchange':
+ case 'levelchange':
+ case 'statuschange':
+ this.update.battery.call(this);
+ break;
+
+ case 'voicechange':
+ this.update.signal.call(this);
+ this.update.label.call(this);
+ break;
+
+ case 'cardstatechange':
+ this.update.signal.call(this);
+ this.update.label.call(this);
+ this.update.data.call(this);
+ break;
+
+ case 'callschanged':
+ this.update.signal.call(this);
+ break;
+
+ case 'iccinfochange':
+ this.update.label.call(this);
+ break;
+
+ case 'datachange':
+ this.update.data.call(this);
+ break;
+
+ case 'bluetoothconnectionchange':
+ this.update.bluetooth.call(this);
+ break;
+
+ case 'moztimechange':
+ this.update.time.call(this);
+ break;
+
+ case 'mozChromeEvent':
+ switch (evt.detail.type) {
+ case 'geolocation-status':
+ this.geolocationActive = evt.detail.active;
+ this.update.geolocation.call(this);
+ break;
+
+ case 'recording-status':
+ this.recordingActive = evt.detail.active;
+ this.update.recording.call(this);
+ break;
+
+ case 'volume-state-changed':
+ this.umsActive = evt.detail.active;
+ this.update.usb.call(this);
+ break;
+
+ case 'headphones-status-changed':
+ this.headphonesActive = (evt.detail.state != 'off');
+ this.update.headphones.call(this);
+ break;
+ }
+
+ break;
+
+ case 'moznetworkupload':
+ case 'moznetworkdownload':
+ this.update.networkActivity.call(this);
+ break;
+ }
+ },
+
+ setActive: function sb_setActive(active) {
+ this.active = active;
+ if (active) {
+ this.update.time.call(this);
+
+ var battery = window.navigator.battery;
+ if (battery) {
+ battery.addEventListener('chargingchange', this);
+ battery.addEventListener('levelchange', this);
+ battery.addEventListener('statuschange', this);
+ this.update.battery.call(this);
+ }
+
+ var conn = window.navigator.mozMobileConnection;
+ if (conn) {
+ conn.addEventListener('voicechange', this);
+ conn.addEventListener('iccinfochange', this);
+ conn.addEventListener('datachange', this);
+ this.update.signal.call(this);
+ this.update.data.call(this);
+ }
+
+ window.addEventListener('wifi-statuschange',
+ this.update.wifi.bind(this));
+ this.update.wifi.call(this);
+
+ window.addEventListener('moznetworkupload', this);
+ window.addEventListener('moznetworkdownload', this);
+ } else {
+ clearTimeout(this._clockTimer);
+
+ var battery = window.navigator.battery;
+ if (battery) {
+ battery.removeEventListener('chargingchange', this);
+ battery.removeEventListener('levelchange', this);
+ battery.removeEventListener('statuschange', this);
+ }
+
+ var conn = window.navigator.mozMobileConnection;
+ if (conn) {
+ conn.removeEventListener('voicechange', this);
+ conn.removeEventListener('iccinfochange', this);
+ conn.removeEventListener('datachange', this);
+ }
+
+ window.removeEventListener('moznetworkupload', this);
+ window.removeEventListener('moznetworkdownload', this);
+ }
+ },
+
+ update: {
+ label: function sb_updateLabel() {
+ var conn = window.navigator.mozMobileConnection;
+ var label = this.icons.label;
+ var l10nArgs = JSON.parse(label.dataset.l10nArgs || '{}');
+
+ if (!conn || !conn.voice || !conn.voice.connected ||
+ conn.voice.emergencyCallsOnly) {
+ delete l10nArgs.operator;
+ label.dataset.l10nArgs = JSON.stringify(l10nArgs);
+
+ label.dataset.l10nId = '';
+ label.textContent = l10nArgs.date;
+
+ return;
+ }
+
+ var operatorInfos = MobileOperator.userFacingInfo(conn);
+ l10nArgs.operator = operatorInfos.operator;
+
+ if (operatorInfos.region) {
+ l10nArgs.operator += ' ' + operatorInfos.region;
+ }
+
+ label.dataset.l10nArgs = JSON.stringify(l10nArgs);
+
+ label.dataset.l10nId = 'statusbarLabel';
+ label.textContent = navigator.mozL10n.get('statusbarLabel', l10nArgs);
+ },
+
+ time: function sb_updateTime() {
+ // Schedule another clock update when a new minute rolls around
+ var _ = navigator.mozL10n.get;
+ var f = new navigator.mozL10n.DateTimeFormat();
+ var now = new Date();
+ var sec = now.getSeconds();
+ if (this._clockTimer)
+ window.clearTimeout(this._clockTimer);
+ this._clockTimer =
+ window.setTimeout((this.update.time).bind(this), (59 - sec) * 1000);
+
+ var formated = f.localeFormat(now, _('shortTimeFormat'));
+ formated = formated.replace(/\s?(AM|PM)\s?/i, '<span>$1</span>');
+ this.icons.time.innerHTML = formated;
+
+ var label = this.icons.label;
+ var l10nArgs = JSON.parse(label.dataset.l10nArgs || '{}');
+ l10nArgs.date = f.localeFormat(now, _('statusbarDateFormat'));
+ label.dataset.l10nArgs = JSON.stringify(l10nArgs);
+ this.update.label.call(this);
+ },
+
+ battery: function sb_updateBattery() {
+ var battery = window.navigator.battery;
+ if (!battery)
+ return;
+
+ var icon = this.icons.battery;
+
+ icon.hidden = false;
+ icon.dataset.charging = battery.charging;
+ icon.dataset.level = Math.floor(battery.level * 10) * 10;
+ },
+
+ networkActivity: function sb_updateNetworkActivity() {
+ // Each time we receive an update, make network activity indicator
+ // show up for 500ms.
+
+ var icon = this.icons.networkActivity;
+
+ clearTimeout(this._networkActivityTimer);
+ icon.hidden = false;
+
+ this._networkActivityTimer = setTimeout(function hideNetActivityIcon() {
+ icon.hidden = true;
+ }, 500);
+ },
+
+ signal: function sb_updateSignal() {
+ var conn = window.navigator.mozMobileConnection;
+ if (!conn || !conn.voice)
+ return;
+
+ var voice = conn.voice;
+ var icon = this.icons.signal;
+ var flightModeIcon = this.icons.flightMode;
+ var _ = navigator.mozL10n.get;
+
+ if (this.settingValues['ril.radio.disabled']) {
+ // "Airplane Mode"
+ icon.hidden = true;
+ flightModeIcon.hidden = false;
+ return;
+ }
+
+ flightModeIcon.hidden = true;
+ icon.hidden = false;
+
+ if (conn.cardState === 'absent') {
+ // no SIM
+ delete icon.dataset.level;
+ delete icon.dataset.emergency;
+ delete icon.dataset.searching;
+ delete icon.dataset.roaming;
+ } else if (voice.connected || this.hasActiveCall()) {
+ // "Carrier" / "Carrier (Roaming)"
+ icon.dataset.level = Math.ceil(voice.relSignalStrength / 20); // 0-5
+ icon.dataset.roaming = voice.roaming;
+
+ delete icon.dataset.emergency;
+ delete icon.dataset.searching;
+ } else {
+ // "No Network" / "Emergency Calls Only (REASON)" / trying to connect
+ icon.dataset.level = -1;
+ // logically, we should have "&& !voice.connected" as well but we
+ // already know this.
+ icon.dataset.searching = (!voice.emergencyCallsOnly &&
+ voice.state !== 'notSearching');
+ icon.dataset.emergency = (voice.emergencyCallsOnly);
+ delete icon.dataset.roaming;
+ }
+
+ if (voice.emergencyCallsOnly) {
+ this.addCallListener();
+ } else {
+ this.removeCallListener();
+ }
+
+ },
+
+ data: function sb_updateSignal() {
+ var conn = window.navigator.mozMobileConnection;
+ if (!conn || !conn.data)
+ return;
+
+ var data = conn.data;
+ var icon = this.icons.data;
+
+ if (this.settingValues['ril.radio.disabled'] ||
+ !this.settingValues['ril.data.enabled'] ||
+ !this.icons.wifi.hidden || !data.connected) {
+ icon.hidden = true;
+
+ return;
+ }
+
+ icon.hidden = false;
+ icon.dataset.type =
+ this.mobileDataIconTypes[data.type] || 'circle';
+ },
+
+
+ wifi: function sb_updateWifi() {
+ var wifiManager = window.navigator.mozWifiManager;
+ if (!wifiManager)
+ return;
+
+ var icon = this.icons.wifi;
+ var wasHidden = icon.hidden;
+
+ if (!this.settingValues['wifi.enabled']) {
+ icon.hidden = true;
+ if (!wasHidden)
+ this.update.data.call(this);
+
+ return;
+ }
+
+ switch (wifiManager.connection.status) {
+ case 'disconnected':
+ icon.hidden = true;
+
+ break;
+
+ case 'connecting':
+ case 'associated':
+ icon.hidden = false;
+ icon.dataset.connecting = true;
+ icon.dataset.level = 0;
+
+ break;
+
+ case 'connected':
+ icon.hidden = false;
+
+ var relSignalStrength =
+ wifiManager.connectionInformation.relSignalStrength;
+ icon.dataset.level = Math.floor(relSignalStrength / 25);
+
+ break;
+ }
+
+ if (icon.hidden !== wasHidden)
+ this.update.data.call(this);
+ },
+
+ tethering: function sb_updateTethering() {
+ var icon = this.icons.tethering;
+ icon.hidden = !(this.settingValues['tethering.usb.enabled'] ||
+ this.settingValues['tethering.wifi.enabled']);
+
+ icon.dataset.active =
+ (this.settingValues['tethering.wifi.connectedClients'] !== 0) ||
+ (this.settingValues['tethering.usb.connectedClients'] !== 0);
+ },
+
+ bluetooth: function sb_updateBluetooth() {
+ var icon = this.icons.bluetooth;
+
+ icon.hidden = !this.settingValues['bluetooth.enabled'];
+ icon.dataset.active = Bluetooth.connected;
+ },
+
+ alarm: function sb_updateAlarm() {
+ this.icons.alarm.hidden = !this.settingValues['alarm.enabled'];
+ },
+
+ mute: function sb_updateMute() {
+ this.icons.mute.hidden =
+ (this.settingValues['ring.enabled'] == true);
+ },
+
+ vibration: function sb_vibration() {
+ var vibrate = (this.settingValues['vibration.enabled'] == true);
+ if (vibrate) {
+ this.icons.mute.classList.add('vibration');
+ } else {
+ this.icons.mute.classList.remove('vibration');
+ }
+ },
+
+ recording: function sb_updateRecording() {
+ window.clearTimeout(this.recordingTimer);
+
+ var icon = this.icons.recording;
+ icon.dataset.active = this.recordingActive;
+
+ if (this.recordingActive) {
+ // Geolocation is currently active, show the active icon.
+ icon.hidden = false;
+ return;
+ }
+
+ // Geolocation is currently inactive.
+ // Show the inactive icon and hide it after kActiveIndicatorTimeout
+ this.recordingTimer = window.setTimeout(function hideGeoIcon() {
+ icon.hidden = true;
+ }, this.kActiveIndicatorTimeout);
+ },
+
+ sms: function sb_updateSms() {
+ // We are not going to show this for v1
+
+ // this.icon.sms.hidden = ?
+ // this.icon.sms.dataset.num = ?;
+ },
+
+ geolocation: function sb_updateGeolocation() {
+ window.clearTimeout(this.geolocationTimer);
+
+ var icon = this.icons.geolocation;
+ icon.dataset.active = this.geolocationActive;
+
+ if (this.geolocationActive) {
+ // Geolocation is currently active, show the active icon.
+ icon.hidden = false;
+ return;
+ }
+
+ // Geolocation is currently inactive.
+ // Show the inactive icon and hide it after kActiveIndicatorTimeout
+ this.geolocationTimer = window.setTimeout(function hideGeoIcon() {
+ icon.hidden = true;
+ }, this.kActiveIndicatorTimeout);
+ },
+
+ usb: function sb_updateUsb() {
+ var icon = this.icons.usb;
+ icon.hidden = !this.umsActive;
+ },
+
+ headphones: function sb_updateHeadphones() {
+ var icon = this.icons.headphones;
+ icon.hidden = !this.headphonesActive;
+ },
+
+ systemDownloads: function sb_updatesystemDownloads() {
+ var icon = this.icons.systemDownloads;
+ icon.hidden = (this.systemDownloadsCount === 0);
+ },
+
+ callForwarding: function sb_updateCallForwarding() {
+ var icon = this.icons.callForwarding;
+ icon.hidden = !this.settingValues['ril.cf.enabled'];
+ }
+ },
+
+ hasActiveCall: function sb_hasActiveCall() {
+ var telephony = navigator.mozTelephony;
+
+ // will return true as soon as we begin dialing
+ return !!(telephony && telephony.active);
+ },
+
+ addCallListener: function sb_addCallListener() {
+ var telephony = navigator.mozTelephony;
+ if (telephony) {
+ telephony.addEventListener('callschanged', this);
+ }
+ },
+
+ removeCallListener: function sb_addCallListener() {
+ var telephony = navigator.mozTelephony;
+ if (telephony) {
+ telephony.removeEventListener('callschanged', this);
+ }
+ },
+
+ updateNotification: function sb_updateNotification(count) {
+ var icon = this.icons.notification;
+ if (!count) {
+ icon.hidden = true;
+ return;
+ }
+
+ icon.hidden = false;
+ icon.dataset.num = count;
+ },
+
+ updateNotificationUnread: function sb_updateNotificationUnread(unread) {
+ this.icons.notification.dataset.unread = unread;
+ },
+
+ incSystemDownloads: function sb_incSystemDownloads() {
+ this.systemDownloadsCount++;
+ this.update.systemDownloads.call(this);
+ },
+
+ decSystemDownloads: function sb_decSystemDownloads() {
+ if (--this.systemDownloadsCount < 0) {
+ this.systemDownloadsCount = 0;
+ }
+
+ this.update.systemDownloads.call(this);
+ },
+
+ getAllElements: function sb_getAllElements() {
+ // ID of elements to create references
+
+ var toCamelCase = function toCamelCase(str) {
+ return str.replace(/\-(.)/g, function replacer(str, p1) {
+ return p1.toUpperCase();
+ });
+ };
+
+ this.ELEMENTS.forEach((function createElementRef(name) {
+ this.icons[toCamelCase(name)] =
+ document.getElementById('statusbar-' + name);
+ }).bind(this));
+
+ this.element = document.getElementById('statusbar');
+ this.screen = document.getElementById('screen');
+ this.attentionBar = document.getElementById('attention-bar');
+ }
+};
+
+if (navigator.mozL10n.readyState == 'complete' ||
+ navigator.mozL10n.readyState == 'interactive') {
+ StatusBar.init();
+} else {
+ window.addEventListener('localized', StatusBar.init.bind(StatusBar));
+}
+
+
diff --git a/apps/system/js/storage.js b/apps/system/js/storage.js
new file mode 100644
index 0000000..ff7b10e
--- /dev/null
+++ b/apps/system/js/storage.js
@@ -0,0 +1,60 @@
+var Storage = {
+
+ automounterDisable: 0,
+ automounterEnable: 1,
+ automounterDisableWhenUnplugged: 2,
+
+ umsEnabled: 'ums.enabled',
+ umsMode: 'ums.mode',
+
+ init: function storageInit() {
+ this.setMode(this.automounterDisable, 'init');
+ window.addEventListener('lock', this);
+ window.addEventListener('unlock', this);
+
+ SettingsListener.observe(this.umsEnabled, false, function umsChanged(val) {
+ if (LockScreen.locked) {
+ // covers startup
+ Storage.setMode(Storage.automounterDisable, 'screen locked');
+ } else {
+ Storage.setMode(Storage.modeFromBool(val), 'change in ums.enabled');
+ }
+ });
+ },
+
+ modeFromBool: function storageModeFromBool(val) {
+ return val ? this.automounterEnable : this.automounterDisable;
+ },
+
+ setMode: function storageSetMode(val, reason) {
+ if (!window.navigator.mozSettings)
+ return;
+
+ //console.info('Setting', this.umsMode, 'to', val, 'due to', reason);
+ var param = {};
+ param[this.umsMode] = val;
+ SettingsListener.getSettingsLock().set(param);
+ },
+
+ handleEvent: function storageHandleEvent(e) {
+ switch (e.type) {
+ case 'lock':
+ this.setMode(this.automounterDisableWhenUnplugged, 'screen locked');
+ break;
+ case 'unlock':
+ if (!window.navigator.mozSettings)
+ return;
+
+ var req = SettingsListener.getSettingsLock().get(this.umsEnabled);
+ req.onsuccess = function umsEnabledFetched() {
+ var mode = Storage.modeFromBool(req.result[Storage.umsEnabled]);
+ Storage.setMode(mode, 'screen unlocked');
+ };
+ break;
+ default:
+ return;
+ }
+ }
+};
+
+Storage.init();
diff --git a/apps/system/js/system_banner.js b/apps/system/js/system_banner.js
new file mode 100644
index 0000000..89028e2
--- /dev/null
+++ b/apps/system/js/system_banner.js
@@ -0,0 +1,33 @@
+'use strict';
+
+var SystemBanner = {
+ banner: document.getElementById('system-banner'),
+
+ // Shows a banner with a given message.
+ // Optionally shows a button with a given label/callback.
+ // buttonParams = { label: ..., callback: ... }
+ show: function sb_show(message, buttonParams) {
+ var banner = this.banner;
+ banner.firstElementChild.textContent = message;
+
+ var button = banner.querySelector('button');
+ if (buttonParams) {
+ banner.dataset.button = true;
+ button.textContent = buttonParams.label;
+ button.addEventListener('click', buttonParams.callback);
+ }
+
+ banner.addEventListener('animationend', function animationend() {
+ banner.removeEventListener('animationend', animationend);
+ banner.classList.remove('visible');
+
+ if (buttonParams) {
+ banner.dataset.button = false;
+ button.removeEventListener('click', buttonParams.callback);
+ button.classList.remove('visible');
+ }
+ });
+
+ banner.classList.add('visible');
+ }
+};
diff --git a/apps/system/js/system_dialog.js b/apps/system/js/system_dialog.js
new file mode 100644
index 0000000..b0c32d8
--- /dev/null
+++ b/apps/system/js/system_dialog.js
@@ -0,0 +1,113 @@
+/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+'use strict';
+
+/**
+ * System app is made of a top-level `<div ="screen"></div>` DOM element
+ * which contain all possible screens displayed by the app.
+ * Multiple screens can be displayed at a time. We store the list of currently
+ * visible screens into this DOM element class attribute.
+ */
+var SystemScreen = {
+ screen: document.getElementById('screen'),
+
+ show: function ss_show(screenName) {
+ this.screen.classList.add(screenName);
+ },
+
+ hide: function ss_show(screenName) {
+ this.screen.classList.remove(screenName);
+ },
+
+ isVisible: function ss_isVisible(screenName) {
+ return this.screen.classList.contains(screenName);
+ }
+};
+
+/**
+ * System app displays various kind of dialogs.
+ * A dialog is a system app 'screen' that has a high z-index and is used to be
+ * displayed on top of other apps. But it doesn't display over the status bar,
+ * nor the eventually displayed keyboard.
+ *
+ * `SystemDialog` except the dialog DOM Element `id`.
+ * This DOM Element has to have a DOM attribute 'role' set to 'dialog'.
+ *
+ * It also supports a second `options` object with following attributes:
+ * `onHide`: function called when dialog is hidden, either when `hide()`
+ * method is called, or when dialog is automatically hidden on
+ * home button press
+ */
+function SystemDialog(id, options) {
+ var overlay = document.getElementById('dialog-overlay');
+ var dialog = document.getElementById(id);
+ var screenName = 'dialog';
+
+ // Listen to keyboard visibility changes and window resizing
+ // in order to resize the dialog accordingly
+ function updateHeight(keyboardHeight) {
+ if (SystemScreen.isVisible(screenName)) {
+ var height = window.innerHeight -
+ (keyboardHeight ? keyboardHeight : 0) -
+ StatusBar.height;
+ overlay.style.height = height + 'px';
+ }
+ };
+ function handleEvent(evt) {
+ switch (evt.type) {
+ case 'resize':
+ case 'keyboardhide':
+ updateHeight();
+ break;
+ case 'keyboardchange':
+ updateHeight(evt.detail.height);
+ break;
+ case 'home':
+ case 'holdhome':
+ // Automatically hide the dialog on home button press
+ if (SystemScreen.isVisible(screenName)) {
+ hide(evt.type);
+ // Prevent WindowManager to shift homescreen to the first page
+ // when the dialog is on top of the homescreen
+ var displayedApp = WindowManager.getDisplayedApp();
+ var displayedAppFrame = WindowManager.getAppFrame(displayedApp);
+ if (evt.type == 'home' &&
+ displayedAppFrame.classList.contains('homescreen'))
+ evt.stopImmediatePropagation();
+ }
+ break;
+ }
+ };
+ window.addEventListener('resize', handleEvent);
+ window.addEventListener('keyboardchange', handleEvent);
+ window.addEventListener('keyboardhide', handleEvent);
+ window.addEventListener('home', handleEvent);
+ window.addEventListener('holdhome', handleEvent);
+
+ function show() {
+ dialog.hidden = false;
+ dialog.classList.add(id);
+ SystemScreen.show(screenName);
+ updateHeight();
+ }
+
+ function hide(reason) {
+ dialog.hidden = true;
+ dialog.classList.remove(id);
+ SystemScreen.hide(screenName);
+ if (typeof(options.onHide) == 'function')
+ options.onHide(reason);
+ }
+
+ function isVisible() {
+ return SystemScreen.isVisible(screenName) &&
+ overlay.classList.contains(id);
+ }
+
+ return {
+ show: show,
+ hide: hide,
+ isVisible: isVisible
+ };
+}
+
diff --git a/apps/system/js/trusted_ui.js b/apps/system/js/trusted_ui.js
new file mode 100644
index 0000000..43bea21
--- /dev/null
+++ b/apps/system/js/trusted_ui.js
@@ -0,0 +1,334 @@
+/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil -*- */
+/* vim: set ft=javascript sw=2 ts=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var TrustedUIManager = {
+
+ get currentStack() {
+ if (!this._dialogStacks[this._lastDisplayedApp]) {
+ this._dialogStacks[this._lastDisplayedApp] = [];
+ }
+ return this._dialogStacks[this._lastDisplayedApp];
+ },
+
+ _dialogStacks: {},
+ _lastDisplayedApp: null,
+
+ overlay: document.getElementById('dialog-overlay'),
+
+ popupContainer: document.getElementById('trustedui-container'),
+
+ popupContainerInner: document.getElementById('trustedui-inner'),
+
+ container: document.getElementById('trustedui-frame-container'),
+
+ dialogTitle: document.getElementById('trustedui-title'),
+
+ screen: document.getElementById('screen'),
+
+ loadingIcon: document.getElementById('statusbar-loading'),
+
+ throbber: document.getElementById('trustedui-throbber'),
+
+ closeButton: document.getElementById('trustedui-close'),
+
+ hasTrustedUI: function trui_hasTrustedUI(origin) {
+ return (this._dialogStacks[origin] && this._dialogStacks[origin].length);
+ },
+
+ getDialogFromOrigin: function trui_getDialogFromOrigin(origin) {
+ if (!origin || !this.hasTrustedUI(origin))
+ return false;
+ var stack = this._dialogStacks[origin];
+ return stack[stack.length - 1];
+ },
+
+ init: function trui_init() {
+ window.addEventListener('home', this);
+ window.addEventListener('holdhome', this);
+ window.addEventListener('appwillopen', this);
+ window.addEventListener('appopen', this);
+ window.addEventListener('appwillclose', this);
+ window.addEventListener('appterminated', this);
+ window.addEventListener('keyboardhide', this);
+ window.addEventListener('keyboardchange', this);
+ window.addEventListener('mozbrowserloadstart', this);
+ window.addEventListener('mozbrowserloadend', this);
+ this.closeButton.addEventListener('click', this);
+ },
+
+ hideTrustedApp: function trui_hideTrustedApp() {
+ var self = this;
+ this.popupContainer.classList.add('closing');
+ this.popupContainer.addEventListener('transitionend', function hide() {
+ this.removeEventListener('transitionend', hide);
+ self.hide();
+ });
+ },
+
+ reopenTrustedApp: function trui_reopenTrustedApp() {
+ this._hideAllFrames();
+ var dialog = this._getTopDialog();
+ this._makeDialogVisible(dialog);
+ this.popupContainer.classList.add('closing');
+ this.show();
+ this.popupContainer.classList.remove('closing');
+ },
+
+ open: function trui_open(name, frame, chromeEventId, onCancelCB) {
+ screen.mozLockOrientation('portrait');
+ this._hideAllFrames();
+ if (this.currentStack.length) {
+ this._makeDialogHidden(this._getTopDialog());
+ this._pushNewDialog(name, frame, chromeEventId, onCancelCB);
+ } else {
+ // first time, spin back to home screen first
+ this.popupContainer.classList.add('up');
+ this.popupContainer.classList.remove('closing');
+ WindowManager.hideCurrentApp(function openTrustedUI() {
+ this.popupContainer.classList.remove('up');
+ this._pushNewDialog(name, frame, chromeEventId, onCancelCB);
+ }.bind(this));
+ }
+ },
+
+ close: function trui_close(chromeEventId, callback) {
+ var stackSize = this.currentStack.length;
+
+ this._restoreOrientation();
+
+ if (callback)
+ callback();
+
+ if (stackSize === 0) {
+ // nothing to close. what are you doing?
+ return;
+ } else if (stackSize === 1) {
+ // only one dialog, so transition back to main app
+ var self = this;
+ var container = this.popupContainer;
+ if (!CardsView.cardSwitcherIsShown()) {
+ WindowManager.restoreCurrentApp();
+ container.addEventListener('transitionend', function wait(event) {
+ this.removeEventListener('transitionend', wait);
+ self._closeDialog(chromeEventId);
+ });
+ } else {
+ WindowManager.restoreCurrentApp(this._lastDisplayedApp);
+ this._closeDialog(chromeEventId);
+ }
+
+ // The css transition caused by the removal of the trustedui
+ // class by the hide() method will trigger a 'transitionend'
+ // event ultimately to be fired.
+ this.hide();
+
+ window.focus();
+ } else {
+ this._closeDialog(chromeEventId);
+ }
+ },
+
+ _dispatchCloseEvent: function dispatchCloseEvent(eventId) {
+ var _ = navigator.mozL10n.get;
+ if (!eventId)
+ return;
+ var event = document.createEvent('customEvent');
+ var details = {
+ id: eventId,
+ type: 'cancel',
+ errorMsg: _('dialog-closed')
+ };
+ event.initCustomEvent('mozContentEvent', true, true, details);
+ window.dispatchEvent(event);
+ },
+
+ _getTopDialog: function trui_getTopDialog() {
+ // get the topmost dialog for the _lastDisplayedApp or null
+ return this.currentStack[this.currentStack.length - 1];
+ },
+
+ _pushNewDialog: function trui_PushNewDialog(name, frame, chromeEventId,
+ onCancelCB) {
+ // add some data attributes to the frame
+ var dataset = frame.dataset;
+ dataset.frameType = 'popup';
+ dataset.frameName = frame.name;
+ dataset.frameOrigin = this._lastDisplayedApp;
+
+ // make a shiny new dialog object
+ var dialog = {
+ name: name,
+ frame: frame,
+ chromeEventId: chromeEventId,
+ onCancelCB: onCancelCB
+ };
+
+ // push and show
+ this.currentStack.push(dialog);
+ this.dialogTitle.textContent = dialog.name;
+ this.container.appendChild(dialog.frame);
+ this._makeDialogVisible(dialog);
+ },
+
+ _makeDialogVisible: function trui_makeDialogVisible(dialog) {
+ // make sure the trusty ui is visible
+ this.popupContainer.classList.remove('closing');
+ this.show();
+
+ // ensure the frame is visible and the dialog title is correct.
+ dialog.frame.classList.add('selected');
+ this.dialogTitle.textContent = dialog.name;
+ },
+
+ _makeDialogHidden: function trui_makeDialogHidden(dialog) {
+ if (!dialog)
+ return;
+ this._restoreOrientation();
+ dialog.frame.classList.remove('selected');
+ },
+
+ _restoreOrientation: function trui_restoreOrientation() {
+ var app = WindowManager.getDisplayedApp();
+ WindowManager.setOrientationForApp(app);
+ },
+
+ /**
+ * close the dialog identified by the chromeEventId
+ */
+ _closeDialog: function trui_closeDialog(chromeEventId) {
+ if (this.currentStack.length === 0)
+ return;
+
+ var found = false;
+ for (var i = 0; i < this.currentStack.length; i++) {
+ if (this.currentStack[i].chromeEventId === chromeEventId) {
+ var dialog = this.currentStack.splice(i, 1)[0];
+ this.container.removeChild(dialog.frame);
+ found = true;
+ break;
+ }
+ }
+
+ if (found && this.currentStack.length) {
+ this._makeDialogVisible(this._getTopDialog());
+ }
+ },
+
+ hide: function trui_hide() {
+ this.screen.classList.remove('trustedui');
+ },
+
+ show: function trui_show() {
+ this.screen.classList.add('trustedui');
+ },
+
+ isVisible: function trui_show() {
+ return this.screen.classList.contains('trustedui');
+ },
+
+ setHeight: function trui_setHeight(height) {
+ this.overlay.style.height = height + 'px';
+ },
+
+ /*
+ * _destroyDialog: internal method called when the dialog is closed
+ * by user action (canceled), or when 'appterminated' is received.
+ * In either case, notify the caller.
+ */
+ _destroyDialog: function trui_destroyDialog(origin) {
+ var stack = this.currentStack;
+ if (origin)
+ stack = this._dialogStacks[origin];
+
+ if (stack.length === 0)
+ return;
+
+ // If the user closed a trusty UI dialog, they probably meant
+ // to close every dialog.
+ for (var i = 0, toClose = stack.length; i < toClose; i++) {
+ var dialog = this._getTopDialog();
+
+ // First, send a chrome event saying we've been canceled
+ this._dispatchCloseEvent(dialog.chromeEventId);
+
+ // Now close and fire the cancel callback, if it exists
+ this.close(dialog.chromeEventId, dialog.onCancelCB);
+ }
+ this.hide();
+ this.popupContainer.classList.remove('closing');
+ },
+
+ _hideAllFrames: function trui_hideAllFrames() {
+ var selectedFrames = this.container.querySelectorAll('iframe.selected');
+ for (var i = 0; i < selectedFrames.length; i++) {
+ selectedFrames[i].classList.remove('selected');
+ }
+ },
+
+ handleEvent: function trui_handleEvent(evt) {
+ switch (evt.type) {
+ case 'home':
+ case 'holdhome':
+ if (!this.isVisible())
+ return;
+
+ this.hideTrustedApp();
+ break;
+ case 'click':
+ // Close-button clicked
+ this._destroyDialog();
+ break;
+ case 'appterminated':
+ this._destroyDialog(evt.detail.origin);
+ break;
+ case 'appwillopen':
+ // Hiding trustedUI when coming from Activity
+ if (this.isVisible())
+ this.hideTrustedApp();
+
+ // Ignore homescreen
+ if (evt.target.classList.contains('homescreen'))
+ return;
+ this._lastDisplayedApp = evt.detail.origin;
+ if (this.currentStack.length) {
+ // Reopening an app with trustedUI
+ this.popupContainer.classList.remove('up');
+ this._makeDialogVisible(this._getTopDialog());
+ WindowManager.hideCurrentApp();
+ this.reopenTrustedApp();
+ }
+ break;
+ case 'appopen':
+ if (this.currentStack.length) {
+ screen.mozLockOrientation('portrait');
+ }
+ break;
+ case 'appwillclose':
+ if (this.isVisible())
+ return;
+ var dialog = this._getTopDialog();
+ this._makeDialogHidden(dialog);
+ this.hide();
+ break;
+ case 'keyboardchange':
+ this.setHeight(window.innerHeight -
+ StatusBar.height - evt.detail.height);
+ break;
+ case 'keyboardhide':
+ this.setHeight(window.innerHeight - StatusBar.height);
+ break;
+ case 'mozbrowserloadstart':
+ this.throbber.classList.add('loading');
+ break;
+ case 'mozbrowserloadend':
+ this.throbber.classList.remove('loading');
+ break;
+ }
+ }
+
+};
+
+TrustedUIManager.init();
+
diff --git a/apps/system/js/ttlview.js b/apps/system/js/ttlview.js
new file mode 100644
index 0000000..81503af
--- /dev/null
+++ b/apps/system/js/ttlview.js
@@ -0,0 +1,50 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var TTLView = {
+ element: null,
+
+ get visible() {
+ return this.element && this.element.style.display === 'block';
+ },
+
+ hide: function tv_hide() {
+ if (this.element)
+ this.element.style.visibility = 'hidden';
+ },
+
+ show: function tv_show() {
+ var element = this.element;
+ if (!element) {
+ element = document.createElement('div');
+ element.id = 'debug-ttl';
+ element.innerHTML = '00000';
+ element.dataset.zIndexLevel = 'debug-ttl';
+
+ this.element = element;
+ document.getElementById('screen').appendChild(element);
+
+ // this is fired when the app launching is initialized
+ window.addEventListener('appwillopen', function willopen(e) {
+ element.innerHTML = '00000';
+ });
+
+ window.addEventListener('apploadtime', function apploadtime(e) {
+ element.innerHTML = e.detail.time + ' [' + e.detail.type + ']';
+ });
+ }
+
+ element.style.visibility = 'visible';
+ },
+
+ toggle: function tv_toggle() {
+ this.visible ? this.hide() : this.show();
+ }
+};
+
+SettingsListener.observe('debug.ttl.enabled', false, function(value) {
+ !!value ? TTLView.show() : TTLView.hide();
+});
+
diff --git a/apps/system/js/updatable.js b/apps/system/js/updatable.js
new file mode 100644
index 0000000..57ce737
--- /dev/null
+++ b/apps/system/js/updatable.js
@@ -0,0 +1,269 @@
+'use strict';
+
+/*
+ * An Updatable object represents an application *or* system update.
+ * It takes care of the interaction with the UpdateManager and observes
+ * the update itself to handle success/error cases.
+ *
+ * - name of the update
+ * - size of the update
+ * - download() to start the download
+ * - cancelDownload() to cancel it
+ */
+
+/* === App Updates === */
+function AppUpdatable(app) {
+ this._mgmt = navigator.mozApps.mgmt;
+ this.app = app;
+
+ var manifest = app.manifest ? app.manifest : app.updateManifest;
+ this.name = new ManifestHelper(manifest).name;
+
+ this.size = app.downloadSize;
+ this.progress = null;
+
+ UpdateManager.addToUpdatableApps(this);
+ app.ondownloadavailable = this.availableCallBack.bind(this);
+ if (app.downloadAvailable) {
+ this.availableCallBack();
+ }
+ if (app.readyToApplyDownload) {
+ this.applyUpdate();
+ }
+}
+
+AppUpdatable.prototype.download = function() {
+ UpdateManager.addToDownloadsQueue(this);
+ this.progress = 0;
+
+ this.app.download();
+};
+
+AppUpdatable.prototype.cancelDownload = function() {
+ this.app.cancelDownload();
+};
+
+AppUpdatable.prototype.uninit = function() {
+ this.app.ondownloadavailable = null;
+ this.clean();
+};
+
+AppUpdatable.prototype.clean = function() {
+ this.app.ondownloaderror = null;
+ this.app.ondownloadsuccess = null;
+ this.app.ondownloadapplied = null;
+ this.app.onprogress = null;
+
+ this.progress = null;
+};
+
+AppUpdatable.prototype.availableCallBack = function() {
+ this.size = this.app.downloadSize;
+
+ if (this.app.installState === 'installed') {
+ UpdateManager.addToUpdatesQueue(this);
+
+ // we add these callbacks only now to prevent interfering
+ // with other modules (especially the AppInstallManager)
+ this.app.ondownloaderror = this.errorCallBack.bind(this);
+ this.app.ondownloadsuccess = this.successCallBack.bind(this);
+ this.app.ondownloadapplied = this.appliedCallBack.bind(this);
+ this.app.onprogress = this.progressCallBack.bind(this);
+ }
+};
+
+AppUpdatable.prototype.successCallBack = function() {
+ var app = this.app;
+ if (WindowManager.getDisplayedApp() !== app.origin) {
+ this.applyUpdate();
+ } else {
+ var self = this;
+ window.addEventListener('appwillclose', function waitClose() {
+ window.removeEventListener('appwillclose', waitClose);
+ self.applyUpdate();
+ });
+ }
+
+ UpdateManager.removeFromDownloadsQueue(this);
+ UpdateManager.removeFromUpdatesQueue(this);
+};
+
+AppUpdatable.prototype.applyUpdate = function() {
+ WindowManager.kill(this.app.origin);
+ this._mgmt.applyDownload(this.app);
+};
+
+AppUpdatable.prototype.appliedCallBack = function() {
+ this.clean();
+};
+
+AppUpdatable.prototype.errorCallBack = function(e) {
+ var errorName = e.application.downloadError.name;
+ console.info('downloadError event, error code is', errorName);
+ UpdateManager.requestErrorBanner();
+ UpdateManager.removeFromDownloadsQueue(this);
+ this.progress = null;
+};
+
+AppUpdatable.prototype.progressCallBack = function() {
+ if (this.progress === null) {
+ // this is the first progress
+ UpdateManager.addToDownloadsQueue(this);
+ this.progress = 0;
+ }
+
+ var delta = this.app.progress - this.progress;
+
+ this.progress = this.app.progress;
+ UpdateManager.downloadProgressed(delta);
+};
+
+/*
+ * System Updates
+ * Will be instanciated only once by the UpdateManager
+ *
+ */
+function SystemUpdatable() {
+ var _ = navigator.mozL10n.get;
+ this.name = _('systemUpdate');
+ this.size = 0;
+ this.downloading = false;
+ this.paused = false;
+
+ // XXX: this state should be kept on the platform side
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=827090
+ this.checkKnownUpdate(UpdateManager.checkForUpdates.bind(UpdateManager));
+
+ window.addEventListener('mozChromeEvent', this);
+}
+
+SystemUpdatable.KNOWN_UPDATE_FLAG = 'known-sysupdate';
+
+SystemUpdatable.prototype.download = function() {
+ if (this.downloading) {
+ return;
+ }
+
+ this.downloading = true;
+ this.paused = false;
+ this._dispatchEvent('update-available-result', 'download');
+ UpdateManager.addToDownloadsQueue(this);
+ this.progress = 0;
+};
+
+SystemUpdatable.prototype.cancelDownload = function() {
+ this._dispatchEvent('update-download-cancel');
+ this.downloading = false;
+ this.paused = false;
+};
+
+SystemUpdatable.prototype.uninit = function() {
+ window.removeEventListener('mozChromeEvent', this);
+};
+
+SystemUpdatable.prototype.handleEvent = function(evt) {
+ if (evt.type !== 'mozChromeEvent')
+ return;
+
+ var detail = evt.detail;
+ if (!detail.type)
+ return;
+
+ switch (detail.type) {
+ case 'update-error':
+ this.errorCallBack();
+ break;
+ case 'update-download-started':
+ // TODO UpdateManager glue
+ this.paused = false;
+ break;
+ case 'update-download-progress':
+ var delta = detail.progress - this.progress;
+ this.progress = detail.progress;
+
+ UpdateManager.downloadProgressed(delta);
+ break;
+ case 'update-download-stopped':
+ // TODO UpdateManager glue
+ this.paused = detail.paused;
+ if (!this.paused) {
+ UpdateManager.startedUncompressing();
+ }
+ break;
+ case 'update-downloaded':
+ this.downloading = false;
+ this.showApplyPrompt();
+ break;
+ case 'update-prompt-apply':
+ this.showApplyPrompt();
+ break;
+ }
+};
+
+SystemUpdatable.prototype.errorCallBack = function() {
+ UpdateManager.requestErrorBanner();
+ UpdateManager.removeFromDownloadsQueue(this);
+ this.downloading = false;
+};
+
+SystemUpdatable.prototype.showApplyPrompt = function() {
+ var _ = navigator.mozL10n.get;
+
+ // Update will be completed after restart
+ this.forgetKnownUpdate();
+
+ var cancel = {
+ title: _('later'),
+ callback: this.declineInstall.bind(this)
+ };
+
+ var confirm = {
+ title: _('installNow'),
+ callback: this.acceptInstall.bind(this)
+ };
+
+ UtilityTray.hide();
+ CustomDialog.show(_('systemUpdateReady'), _('wantToInstall'),
+ cancel, confirm);
+};
+
+SystemUpdatable.prototype.declineInstall = function() {
+ CustomDialog.hide();
+ this._dispatchEvent('update-prompt-apply-result', 'wait');
+
+ UpdateManager.removeFromDownloadsQueue(this);
+};
+
+SystemUpdatable.prototype.acceptInstall = function() {
+ CustomDialog.hide();
+ this._dispatchEvent('update-prompt-apply-result', 'restart');
+};
+
+SystemUpdatable.prototype.rememberKnownUpdate = function() {
+ asyncStorage.setItem(SystemUpdatable.KNOWN_UPDATE_FLAG, true);
+};
+
+SystemUpdatable.prototype.checkKnownUpdate = function(callback) {
+ if (typeof callback !== 'function') {
+ return;
+ }
+
+ asyncStorage.getItem(SystemUpdatable.KNOWN_UPDATE_FLAG, function(value) {
+ callback(!!value);
+ });
+};
+
+SystemUpdatable.prototype.forgetKnownUpdate = function() {
+ asyncStorage.removeItem(SystemUpdatable.KNOWN_UPDATE_FLAG);
+};
+
+SystemUpdatable.prototype._dispatchEvent = function(type, result) {
+ var event = document.createEvent('CustomEvent');
+ var data = { type: type };
+ if (result) {
+ data.result = result;
+ }
+
+ event.initCustomEvent('mozContentEvent', true, true, data);
+ window.dispatchEvent(event);
+};
diff --git a/apps/system/js/update_manager.js b/apps/system/js/update_manager.js
new file mode 100644
index 0000000..8e37733
--- /dev/null
+++ b/apps/system/js/update_manager.js
@@ -0,0 +1,623 @@
+'use strict';
+
+/*
+ * The UpdateManager is a central component for apps *and* system updates.
+ * The user can start or cancel all downloads at once.
+ * This component also makes sure of bothering the user to a minimum by
+ * showing active notifications for new updates/errors only once.
+ *
+ * It maintains 2 queues of Updatable objects.
+ * - updatesQueue for available updates
+ * - downloadsQueue for active downloads
+ */
+
+var UpdateManager = {
+ _mgmt: null,
+ _downloading: false,
+ _uncompressing: false,
+ _downloadedBytes: 0,
+ _errorTimeout: null,
+ _wifiLock: null,
+ _systemUpdateDisplayed: false,
+ _isDataConnectionWarningDialogEnabled: true,
+ _settings: null,
+ _conn: null,
+ NOTIFICATION_BUFFERING_TIMEOUT: 30 * 1000,
+ TOASTER_TIMEOUT: 1200,
+
+ container: null,
+ message: null,
+ toaster: null,
+ toasterMessage: null,
+ laterButton: null,
+ notnowButton: null,
+ downloadButton: null,
+ downloadViaDataConnectionButton: null,
+ downloadDialog: null,
+ downloadViaDataConnectionDialog: null,
+ downloadDialogTitle: null,
+ downloadDialogList: null,
+ lastUpdatesAvailable: 0,
+ _notificationTimeout: null,
+
+ updatableApps: [],
+ systemUpdatable: null,
+ updatesQueue: [],
+ downloadsQueue: [],
+
+ init: function um_init() {
+ if (!this._mgmt) {
+ this._mgmt = navigator.mozApps.mgmt;
+ }
+
+ this._mgmt.getAll().onsuccess = (function gotAll(evt) {
+ var apps = evt.target.result;
+ apps.forEach(function appIterator(app) {
+ new AppUpdatable(app);
+ });
+ }).bind(this);
+
+ this._settings = navigator.mozSettings;
+
+ this.systemUpdatable = new SystemUpdatable();
+
+ this.container = document.getElementById('update-manager-container');
+ this.message = this.container.querySelector('.message');
+
+ this.toaster = document.getElementById('update-manager-toaster');
+ this.toasterMessage = this.toaster.querySelector('.message');
+
+ this.laterButton = document.getElementById('updates-later-button');
+ this.notnowButton =
+ document.getElementById('updates-viaDataConnection-notnow-button');
+ this.downloadButton = document.getElementById('updates-download-button');
+ this.downloadViaDataConnectionButton =
+ document.getElementById('updates-viaDataConnection-download-button');
+ this.downloadDialog = document.getElementById('updates-download-dialog');
+ this.downloadDialogTitle = this.downloadDialog.querySelector('h1');
+ this.downloadDialogList = this.downloadDialog.querySelector('ul');
+ this.downloadViaDataConnectionDialog =
+ document.getElementById('updates-viaDataConnection-dialog');
+
+ this.container.onclick = this.containerClicked.bind(this);
+ this.laterButton.onclick = this.cancelPrompt.bind(this);
+ this.downloadButton.onclick = this.requestDownloads.bind(this);
+ this.downloadDialogList.onchange = this.updateDownloadButton.bind(this);
+ this.notnowButton.onclick =
+ this.cancelDataConnectionUpdatesPrompt.bind(this);
+ this.downloadViaDataConnectionButton.onclick =
+ this.requestDownloads.bind(this);
+
+ window.addEventListener('mozChromeEvent', this);
+ window.addEventListener('applicationinstall', this);
+ window.addEventListener('applicationuninstall', this);
+ window.addEventListener('online', this);
+ window.addEventListener('offline', this);
+
+ SettingsListener.observe('gaia.system.checkForUpdates', false,
+ this.checkForUpdates.bind(this));
+
+ // We maintain the the edge and nowifi data attributes to show
+ // a warning on the download dialog
+ window.addEventListener('wifi-statuschange', this);
+ this.updateWifiStatus();
+ this.updateOnlineStatus();
+
+ this._conn = window.navigator.mozMobileConnection;
+ if (this._conn) {
+ this._conn.addEventListener('datachange', this);
+ this.updateDataStatus();
+ }
+
+ window.asyncStorage.
+ getItem('gaia.system.isDataConnectionWarningDialogEnabled',
+ (function(value) {
+ value = value || true;
+ this._isDataConnectionWarningDialogEnabled = true;
+ this.downloadDialog.dataset.dataConnectionInlineWarning = !value;
+ }).bind(this));
+ },
+
+ requestDownloads: function um_requestDownloads(evt) {
+ evt.preventDefault();
+
+ if (evt.target == this.downloadViaDataConnectionButton) {
+ window.asyncStorage.
+ setItem('gaia.system.isDataConnectionWarningDialogEnabled', false);
+ this._isDataConnectionWarningDialogEnabled = false;
+ this.downloadDialog.dataset.dataConnectionInlineWarning = true;
+ this.startDownloads();
+ } else {
+ if (this._isDataConnectionWarningDialogEnabled &&
+ this.downloadDialog.dataset.nowifi) {
+ this.downloadViaDataConnectionDialog.classList.add('visible');
+ } else {
+ this.startDownloads();
+ }
+ }
+ },
+
+ startDownloads: function um_startDownloads() {
+ this.downloadDialog.classList.remove('visible');
+ this.downloadViaDataConnectionDialog.classList.remove('visible');
+
+ UtilityTray.show();
+
+ var checkValues = {};
+ var dialog = this.downloadDialogList;
+ var checkboxes = dialog.querySelectorAll('input[type="checkbox"]');
+ for (var i = 0; i < checkboxes.length; i++) {
+ var checkbox = checkboxes[i];
+ checkValues[checkbox.dataset.position] = checkbox.checked;
+ }
+
+ this.updatesQueue.forEach(function(updatable, index) {
+ // The user opted out of the download
+ if (updatable.app && !checkValues[index]) {
+ return;
+ }
+
+ updatable.download();
+ });
+
+ this._downloadedBytes = 0;
+ this.render();
+ },
+
+ cancelAllDownloads: function um_cancelAllDownloads() {
+ CustomDialog.hide();
+
+ // We're emptying the array while iterating
+ while (this.downloadsQueue.length) {
+ var updatable = this.downloadsQueue[0];
+ updatable.cancelDownload();
+ this.removeFromDownloadsQueue(updatable);
+ }
+ },
+
+ requestErrorBanner: function um_requestErrorBanner() {
+ if (this._errorTimeout)
+ return;
+
+ var _ = navigator.mozL10n.get;
+ var self = this;
+ this._errorTimeout = setTimeout(function waitForMore() {
+ SystemBanner.show(_('downloadError'));
+ self._errorTimeout = null;
+ }, this.NOTIFICATION_BUFFERING_TIMEOUT);
+ },
+
+ containerClicked: function um_containerClicker() {
+ var _ = navigator.mozL10n.get;
+
+ if (this._downloading) {
+ var cancel = {
+ title: _('no'),
+ callback: this.cancelPrompt.bind(this)
+ };
+
+ var confirm = {
+ title: _('yes'),
+ callback: this.cancelAllDownloads.bind(this)
+ };
+
+ CustomDialog.show(_('cancelAllDownloads'), _('wantToCancelAll'),
+ cancel, confirm);
+ } else {
+ this.showDownloadPrompt();
+ }
+
+ UtilityTray.hide();
+ },
+
+ showDownloadPrompt: function um_showDownloadPrompt() {
+ var _ = navigator.mozL10n.get;
+
+ this._systemUpdateDisplayed = false;
+ this.downloadDialogTitle.textContent = _('numberOfUpdates', {
+ n: this.updatesQueue.length
+ });
+
+ var updateList = '';
+
+ // System update should always be on top
+ this.updatesQueue.sort(function sortUpdates(updatable, otherUpdatable) {
+ if (!updatable.app)
+ return -1;
+ if (!otherUpdatable.app)
+ return 1;
+
+ if (updatable.name < otherUpdatable.name)
+ return -1;
+ if (updatable.name > otherUpdatable.name)
+ return 1;
+ return 0;
+ });
+
+ this.downloadDialogList.innerHTML = '';
+ this.updatesQueue.forEach(function updatableIterator(updatable, index) {
+ var listItem = document.createElement('li');
+
+ // The user can choose not to update an app
+ var checkContainer = document.createElement('label');
+ if (updatable instanceof SystemUpdatable) {
+ checkContainer.textContent = _('required');
+ checkContainer.classList.add('required');
+ this._systemUpdateDisplayed = true;
+ } else {
+ var checkbox = document.createElement('input');
+ checkbox.type = 'checkbox';
+ checkbox.dataset.position = index;
+ checkbox.checked = true;
+
+ var span = document.createElement('span');
+
+ checkContainer.appendChild(checkbox);
+ checkContainer.appendChild(span);
+ }
+ listItem.appendChild(checkContainer);
+
+ var name = document.createElement('div');
+ name.classList.add('name');
+ name.textContent = updatable.name;
+ listItem.appendChild(name);
+
+ if (updatable.size) {
+ var sizeItem = document.createElement('div');
+ sizeItem.textContent = this._humanizeSize(updatable.size);
+ listItem.appendChild(sizeItem);
+ } else {
+ listItem.classList.add('nosize');
+ }
+
+ this.downloadDialogList.appendChild(listItem);
+ }, this);
+
+ this.downloadDialog.classList.add('visible');
+ },
+
+ updateDownloadButton: function() {
+ if (this._systemUpdateDisplayed) {
+ this.downloadButton.disabled = false;
+ return;
+ }
+
+ var disabled = true;
+
+ var dialog = this.downloadDialogList;
+ var checkboxes = dialog.querySelectorAll('input[type="checkbox"]');
+ for (var i = 0; i < checkboxes.length; i++) {
+ if (checkboxes[i].checked) {
+ disabled = false;
+ break;
+ }
+ }
+
+ this.downloadButton.disabled = disabled;
+ },
+
+ cancelPrompt: function um_cancelPrompt() {
+ CustomDialog.hide();
+ this.downloadDialog.classList.remove('visible');
+ },
+
+ cancelDataConnectionUpdatesPrompt: function um_cancelDCUpdatesPrompt() {
+ CustomDialog.hide();
+ this.downloadViaDataConnectionDialog.classList.remove('visible');
+ this.downloadDialog.classList.remove('visible');
+ },
+
+ downloadProgressed: function um_downloadProgress(bytes) {
+ if (bytes > 0) {
+ this._downloadedBytes += bytes;
+ this.render();
+ }
+ },
+
+ startedUncompressing: function um_startedUncompressing() {
+ this._uncompressing = true;
+ this.render();
+ },
+
+ render: function um_render() {
+ var _ = navigator.mozL10n.get;
+
+ this.toasterMessage.innerHTML =
+ _('updateAvailableInfo', {
+ n: this.updatesQueue.length - this.lastUpdatesAvailable
+ });
+
+ var message = '';
+ if (this._downloading) {
+ if (this._uncompressing && this.downloadsQueue.length === 1) {
+ message = _('uncompressingMessage');
+ } else {
+ var humanProgress = this._humanizeSize(this._downloadedBytes);
+ message = _('downloadingUpdateMessage', {
+ progress: humanProgress
+ });
+ }
+ } else {
+ message = _('updateAvailableInfo', {
+ n: this.updatesQueue.length
+ });
+ }
+
+ this.message.innerHTML = message;
+ var css = this.container.classList;
+ this._downloading ? css.add('downloading') : css.remove('downloading');
+ },
+
+ addToUpdatableApps: function um_addtoUpdatableapps(updatableApp) {
+ this.updatableApps.push(updatableApp);
+ },
+
+ removeFromAll: function um_removeFromAll(updatableApp) {
+ var removeIndex = this.updatableApps.indexOf(updatableApp);
+ if (removeIndex === -1)
+ return;
+
+ var removedApp = this.updatableApps[removeIndex];
+ this.removeFromUpdatesQueue(removedApp);
+
+ removedApp.uninit();
+ this.updatableApps.splice(removeIndex, 1);
+ },
+
+ addToUpdatesQueue: function um_addToUpdatesQueue(updatable) {
+ if (this._downloading) {
+ return;
+ }
+
+ if (updatable.app &&
+ updatable.app.installState !== 'installed') {
+ return;
+ }
+
+ if (updatable.app &&
+ this.updatableApps.indexOf(updatable) === -1) {
+ return;
+ }
+
+ var alreadyThere = this.updatesQueue.some(function lookup(u) {
+ return (u.app === updatable.app);
+ });
+ if (alreadyThere) {
+ return;
+ }
+
+ this.updatesQueue.push(updatable);
+
+ if (this._notificationTimeout === null) {
+ this._notificationTimeout = setTimeout(this.displayNotificationAndToaster.bind(this),
+ this.NOTIFICATION_BUFFERING_TIMEOUT);
+ }
+ this.render();
+ },
+
+ displayNotificationAndToaster: function um_displayNotificationAndToaster() {
+ this._notificationTimeout = null;
+ if (this.updatesQueue.length && !this._downloading) {
+ this.lastUpdatesAvailable = this.updatesQueue.length;
+ StatusBar.updateNotificationUnread(true);
+ this.displayNotificationIfHidden();
+ this.toaster.classList.add('displayed');
+ var self = this;
+ setTimeout(function waitToHide() {
+ self.toaster.classList.remove('displayed');
+ }, this.TOASTER_TIMEOUT);
+ }
+ },
+
+ removeFromUpdatesQueue: function um_removeFromUpdatesQueue(updatable) {
+ var removeIndex = this.updatesQueue.indexOf(updatable);
+ if (removeIndex === -1)
+ return;
+
+ this.updatesQueue.splice(removeIndex, 1);
+ this.lastUpdatesAvailable = this.updatesQueue.length;
+
+ if (this.updatesQueue.length === 0) {
+ this.hideNotificationIfDisplayed();
+ }
+
+ this.render();
+ },
+
+ addToDownloadsQueue: function um_addToDownloadsQueue(updatable) {
+ if (updatable.app &&
+ this.updatableApps.indexOf(updatable) === -1) {
+ return;
+ }
+
+ var alreadyThere = this.downloadsQueue.some(function lookup(u) {
+ return (u.app === updatable.app);
+ });
+ if (alreadyThere) {
+ return;
+ }
+
+ this.downloadsQueue.push(updatable);
+
+ if (this.downloadsQueue.length === 1) {
+ this._downloading = true;
+ StatusBar.incSystemDownloads();
+ this._wifiLock = navigator.requestWakeLock('wifi');
+
+ this.displayNotificationIfHidden();
+ this.render();
+ }
+ },
+
+ removeFromDownloadsQueue: function um_removeFromDownloadsQueue(updatable) {
+ var removeIndex = this.downloadsQueue.indexOf(updatable);
+ if (removeIndex === -1)
+ return;
+
+ this.downloadsQueue.splice(removeIndex, 1);
+
+ if (this.downloadsQueue.length === 0) {
+ this._downloading = false;
+ StatusBar.decSystemDownloads();
+ this._downloadedBytes = 0;
+ this.checkStatuses();
+
+ if (this._wifiLock) {
+ try {
+ this._wifiLock.unlock();
+ } catch (e) {
+ // this can happen if the lock is already unlocked
+ console.error('error during unlock', e);
+ }
+
+ this._wifiLock = null;
+ }
+
+ this.render();
+ }
+ },
+
+ hideNotificationIfDisplayed: function() {
+ if (this.container.classList.contains('displayed')) {
+ this.container.classList.remove('displayed');
+ NotificationScreen.decExternalNotifications();
+ }
+ },
+
+ displayNotificationIfHidden: function() {
+ if (!this.container.classList.contains('displayed')) {
+ this.container.classList.add('displayed');
+ NotificationScreen.incExternalNotifications();
+ }
+ },
+
+ checkStatuses: function um_checkStatuses() {
+ this.updatableApps.forEach(function(updatableApp) {
+ var app = updatableApp.app;
+ if (app.downloadAvailable) {
+ this.addToUpdatesQueue(updatableApp);
+ }
+ }, this);
+ },
+
+ oninstall: function um_oninstall(evt) {
+ var app = evt.application;
+ var updatableApp = new AppUpdatable(app);
+ },
+
+ onuninstall: function um_onuninstall(evt) {
+ this.updatableApps.some(function appIterator(updatableApp, index) {
+ // The application object we get from the event
+ // has only origin and manifestURL properties
+ if (updatableApp.app.manifestURL === evt.application.manifestURL) {
+ this.removeFromAll(updatableApp);
+ return true;
+ }
+ return false;
+ }, this);
+ },
+
+ handleEvent: function um_handleEvent(evt) {
+ if (!evt.type)
+ return;
+
+ switch (evt.type) {
+ case 'applicationinstall':
+ this.oninstall(evt.detail);
+ break;
+ case 'applicationuninstall':
+ this.onuninstall(evt.detail);
+ break;
+ case 'datachange':
+ this.updateDataStatus();
+ break;
+ case 'offline':
+ this.updateOnlineStatus();
+ break;
+ case 'online':
+ this.updateOnlineStatus();
+ break;
+ case 'wifi-statuschange':
+ this.updateWifiStatus();
+ break;
+ }
+
+ if (evt.type !== 'mozChromeEvent')
+ return;
+
+ var detail = evt.detail;
+
+ if (detail.type && detail.type === 'update-available') {
+ this.systemUpdatable.size = detail.size;
+ this.systemUpdatable.rememberKnownUpdate();
+ this.addToUpdatesQueue(this.systemUpdatable);
+ }
+ },
+
+ updateOnlineStatus: function su_updateOnlineStatus() {
+ var online = (navigator && 'onLine' in navigator) ? navigator.onLine : true;
+ this.downloadDialog.dataset.online = online;
+
+ if (online) {
+ this.laterButton.classList.remove('full');
+ } else {
+ this.laterButton.classList.add('full');
+ }
+ },
+
+ updateWifiStatus: function su_updateWifiStatus() {
+ var wifiManager = window.navigator.mozWifiManager;
+ if (!wifiManager)
+ return;
+
+ this.downloadDialog.dataset.nowifi =
+ (wifiManager.connection.status != 'connected');
+ },
+
+ checkForUpdates: function su_checkForUpdates(shouldCheck) {
+ if (!shouldCheck) {
+ return;
+ }
+
+ this._dispatchEvent('force-update-check');
+
+ if (!this._settings) {
+ return;
+ }
+
+ var lock = this._settings.createLock();
+ lock.set({
+ 'gaia.system.checkForUpdates': false
+ });
+ },
+
+ _dispatchEvent: function um_dispatchEvent(type, result) {
+ var event = document.createEvent('CustomEvent');
+ var data = { type: type };
+ if (result) {
+ data.result = result;
+ }
+
+ event.initCustomEvent('mozContentEvent', true, true, data);
+ window.dispatchEvent(event);
+ },
+
+ // This is going to be part of l10n.js
+ _humanizeSize: function um_humanizeSize(bytes) {
+ var _ = navigator.mozL10n.get;
+ var units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'];
+
+ if (!bytes)
+ return '0.00 ' + _(units[0]);
+
+ var e = Math.floor(Math.log(bytes) / Math.log(1024));
+ return (bytes / Math.pow(1024, Math.floor(e))).toFixed(2) + ' ' +
+ _(units[e]);
+ }
+};
+
+window.addEventListener('localized', function startup(evt) {
+ window.removeEventListener('localized', startup);
+
+ UpdateManager.init();
+});
diff --git a/apps/system/js/utility_tray.js b/apps/system/js/utility_tray.js
new file mode 100644
index 0000000..13f124d
--- /dev/null
+++ b/apps/system/js/utility_tray.js
@@ -0,0 +1,148 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var UtilityTray = {
+ shown: false,
+
+ active: false,
+
+ overlay: document.getElementById('utility-tray'),
+
+ statusbar: document.getElementById('statusbar'),
+
+ screen: document.getElementById('screen'),
+
+ init: function ut_init() {
+ var touchEvents = ['touchstart', 'touchmove', 'touchend'];
+
+ // XXX: Always use Mouse2Touch here.
+ // We cannot reliably detect touch support normally
+ // by evaluate (document instanceof DocumentTouch) on Desktop B2G.
+ touchEvents.forEach(function bindEvents(name) {
+ // window.addEventListener(name, this);
+ Mouse2Touch.addEventHandler(window, name, this);
+ }, this);
+
+ window.addEventListener('screenchange', this);
+ window.addEventListener('home', this);
+
+ this.overlay.addEventListener('transitionend', this);
+ },
+
+ handleEvent: function ut_handleEvent(evt) {
+ switch (evt.type) {
+ case 'home':
+ if (this.shown) {
+ this.hide();
+ }
+ break;
+
+ case 'screenchange':
+ if (this.shown && !evt.detail.screenEnabled)
+ this.hide(true);
+
+ break;
+
+ case 'touchstart':
+ if (LockScreen.locked)
+ return;
+ if (evt.target !== this.overlay &&
+ evt.target !== this.statusbar)
+ return;
+
+ this.active = true;
+ // XXX: required for Mouse2Touch fake events to function
+ evt.target.setCapture(true);
+
+ this.onTouchStart(evt.touches[0]);
+ break;
+
+ case 'touchmove':
+ if (!this.active)
+ return;
+
+ this.onTouchMove(evt.touches[0]);
+ break;
+
+ case 'touchend':
+ if (!this.active)
+ return;
+
+ this.active = false;
+ // XXX: required for Mouse2Touch fake events to function
+ document.releaseCapture();
+
+ this.onTouchEnd(evt.changedTouches[0]);
+ break;
+
+ case 'transitionend':
+ if (!this.shown)
+ this.screen.classList.remove('utility-tray');
+ break;
+ }
+ },
+
+ onTouchStart: function ut_onTouchStart(touch) {
+ this.startX = touch.pageX;
+ this.startY = touch.pageY;
+ this.screen.classList.add('utility-tray');
+ this.onTouchMove({ pageY: touch.pageY + this.statusbar.offsetHeight });
+ },
+
+ onTouchMove: function ut_onTouchMove(touch) {
+ var screenHeight = this.overlay.getBoundingClientRect().height;
+ var y = touch.pageY;
+ if (y > this.lastY)
+ this.opening = true;
+ else if (y < this.lastY)
+ this.opening = false;
+ this.lastY = y;
+ var dy = -(this.startY - y);
+ if (this.shown)
+ dy += screenHeight;
+ dy = Math.min(screenHeight, dy);
+
+ var style = this.overlay.style;
+ style.MozTransition = '';
+ style.MozTransform = 'translateY(' + dy + 'px)';
+ },
+
+ onTouchEnd: function ut_onTouchEnd(touch) {
+ this.opening ? this.show() : this.hide();
+ },
+
+ hide: function ut_hide(instant) {
+ var alreadyHidden = !this.shown;
+ var style = this.overlay.style;
+ style.MozTransition = instant ? '' : '-moz-transform 0.2s linear';
+ style.MozTransform = 'translateY(0)';
+ this.shown = false;
+ if (instant)
+ this.screen.classList.remove('utility-tray');
+
+ if (!alreadyHidden) {
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('utilitytrayhide', true, true, null);
+ window.dispatchEvent(evt);
+ }
+ },
+
+ show: function ut_show(dy) {
+ var alreadyShown = this.shown;
+ var style = this.overlay.style;
+ style.MozTransition = '-moz-transform 0.2s linear';
+ style.MozTransform = 'translateY(100%)';
+ this.shown = true;
+ this.screen.classList.add('utility-tray');
+
+ if (!alreadyShown) {
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('utilitytrayshow', true, true, null);
+ window.dispatchEvent(evt);
+ }
+ }
+};
+
+UtilityTray.init();
diff --git a/apps/system/js/value_selector/date_picker.js b/apps/system/js/value_selector/date_picker.js
new file mode 100644
index 0000000..9cdd484
--- /dev/null
+++ b/apps/system/js/value_selector/date_picker.js
@@ -0,0 +1,568 @@
+/**
+ * DatePicker is a html/js "widget" which will display
+ * all the days of a given month and allow selection of
+ * one specific day. It also implements controls to travel
+ * between months and jump into arbitrary time.
+ *
+ * The DatePicker itself contains no UI for the controls.
+ *
+ * Example usage:
+ *
+ * // the container will have elements for the month
+ * // added and removed from it.
+ * var picker = new DatePicker(container);
+ *
+ * // EVENTS:
+ *
+ * // called when the user clicks a day in the calendar.
+ * picker.onvaluechange = function(date) {}
+ *
+ * // called when the month of the calendar changes.
+ * // NOTE: at this time this can only happen programmatically
+ * // so there is only for control flow.
+ * picker.onmonthchange = function(date) {}
+ *
+ * // display a given year/month/date on the calendar the month
+ * // is zero based just like the JS date constructor.
+ * picker.display(2012, 0, 2);
+ *
+ * // move to the next month.
+ * picker.next();
+ *
+ * // move to the previous month
+ * picker.previous();
+ *
+ */
+var DatePicker = (function() {
+ 'use strict';
+
+ const SELECTED = 'selected';
+
+ var Calc = {
+
+ NEXT_MONTH: 'next-month',
+
+ OTHER_MONTH: 'other-month',
+
+ PRESENT: 'present',
+
+ FUTURE: 'future',
+
+ PAST: 'past',
+
+ get today() {
+ return new Date();
+ },
+
+ daysInWeek: function() {
+ //XXX: We need to localize this...
+ return 7;
+ },
+
+ /**
+ * Checks is given date is today.
+ *
+ * @param {Date} date compare.
+ * @return {Boolean} true when today.
+ */
+ isToday: function(date) {
+ return Calc.isSameDate(date, Calc.today);
+ },
+
+ /**
+ * Checks if two date objects occur
+ * on the same date (in the same month, year, day).
+ * Disregards time.
+ *
+ * @param {Date} first date.
+ * @param {Date} second date.
+ * @return {Boolean} true when they are the same date.
+ */
+ isSameDate: function(first, second) {
+ return first.getMonth() == second.getMonth() &&
+ first.getDate() == second.getDate() &&
+ first.getFullYear() == second.getFullYear();
+ },
+
+ /**
+ * Returns an identifier for a specific
+ * date in time for a given date
+ *
+ * @param {Date} date to get id for.
+ * @return {String} identifier.
+ */
+ getDayId: function(date) {
+ return [
+ date.getFullYear(),
+ date.getMonth(),
+ date.getDate()
+ ].join('-');
+ },
+
+ /**
+ * Returns a date object from
+ * a string id for a date.
+ *
+ * @param {String} id identifier for date.
+ * @return {Date} date output.
+ */
+ dateFromId: function(id) {
+ var parts = id.split('-');
+ return new Date(parts[0], parts[1], parts[2]);
+ },
+
+ createDay: function(date, day, month, year) {
+ return new Date(
+ typeof year !== 'undefined' ? year : date.getFullYear(),
+ typeof month !== 'undefined' ? month : date.getMonth(),
+ typeof day !== 'undefined' ? day : date.getDate()
+ );
+ },
+
+ /**
+ * Finds localized week start date of given date.
+ *
+ * @param {Date} date any day the week.
+ * @return {Date} first date in the week of given date.
+ */
+ getWeekStartDate: function(date) {
+ var currentDay = date.getDay();
+ var startDay = date.getDate() - currentDay;
+
+ return Calc.createDay(date, startDay);
+ },
+
+ getWeekEndDate: function(date) {
+ // TODO: There are localization problems
+ // with this approach as we assume a 7 day week.
+ var start = Calc.getWeekStartDate(date);
+ start.setDate(start.getDate() + 7);
+ start.setMilliseconds(-1);
+
+ return start;
+ },
+
+ /**
+ * Returns an array of dates objects.
+ * Inclusive. First and last are
+ * the given instances.
+ *
+ * @param {Date} start starting day.
+ * @param {Date} end ending day.
+ * @param {Boolean} includeTime include times start/end ?
+ */
+ daysBetween: function(start, end, includeTime) {
+ if (!(start instanceof Date)) {
+ throw new Error('start date must be an instanceof Date');
+ }
+
+ if (!(end instanceof Date)) {
+ throw new Error('end date must be an instanceof Date');
+ }
+
+ if (start > end) {
+ var tmp = end;
+ end = start;
+ start = tmp;
+ tmp = null;
+ }
+
+ var list = [];
+ var last = start.getDate();
+ var cur;
+
+ // for infinite loop protection.
+ var max = 500;
+ var macInc = 0;
+
+ while (macInc++ < max) {
+ var next = new Date(
+ start.getFullYear(),
+ start.getMonth(),
+ ++last
+ );
+
+ if (next > end) {
+ throw new Error(
+ 'sanity fails next is greater then end'
+ );
+ }
+
+ if (!Calc.isSameDate(next, end)) {
+ list.push(next);
+ continue;
+ }
+
+ break;
+ }
+
+ if (includeTime) {
+ list.unshift(start);
+ list.push(end);
+ } else {
+ list.unshift(this.createDay(start));
+ list.push(this.createDay(end));
+ }
+
+ return list;
+ },
+
+ /**
+ * Checks if date is in the past
+ *
+ * @param {Date} date to check.
+ * @return {Boolean} true when date is in the past.
+ */
+ isPast: function(date) {
+ return (date.valueOf() < Calc.today.valueOf());
+ },
+
+ /**
+ * Checks if date is in the future
+ *
+ * @param {Date} date to check.
+ * @return {Boolean} true when date is in the future.
+ */
+ isFuture: function(date) {
+ return !Calc.isPast(date);
+ },
+
+ /**
+ * Based on the input date
+ * will return one of the following states
+ *
+ * past, present, future
+ *
+ * @param {Date} day for compare.
+ * @param {Date} month comparison month.
+ * @return {String} state.
+ */
+ relativeState: function(day, month) {
+ var states;
+ //var today = Calc.today;
+
+ // 1. the date is today (real time)
+ if (Calc.isToday(day)) {
+ return Calc.PRESENT;
+ }
+
+ // 2. the date is in the past (real time)
+ if (Calc.isPast(day)) {
+ states = Calc.PAST;
+ // 3. the date is in the future (real time)
+ } else {
+ states = Calc.FUTURE;
+ }
+
+ // 4. the date is not in the current month (relative time)
+ if (day.getMonth() !== month.getMonth()) {
+ states += ' ' + Calc.OTHER_MONTH;
+ }
+
+ return states;
+ }
+
+ };
+
+ /* expose calc */
+ DatePicker.Calc = Calc;
+
+ /**
+ * Initialize a date picker widget.
+ *
+ * @param {HTMLELement} element target of widget creation.
+ */
+ function DatePicker(element) {
+ this.element = element;
+ // default time is set so next/previous work
+ // but we do not render the initial display here.
+ this._position = new Date();
+
+ // register events
+ element.addEventListener('click', this);
+
+ //XXX: When the document is localized again
+ // we must also re-render the month because
+ // the week days may have changed?
+ // This will only happen when we change timezones
+ // unless we add this information to the locales.
+ }
+
+ DatePicker.prototype = {
+
+ /**
+ * Internal value not exposed so we can fire events
+ * when the getter/setter's are used.
+ *
+ * @type Date
+ */
+ _value: null,
+
+ SELECTED: 'selected',
+
+ /**
+ * Gets current value
+ *
+ * @return {Null|Date} date or null.
+ */
+ get value() {
+ return this._value;
+ },
+
+ /**
+ * Sets the current value of the date picker.
+ * When value differs from the currently set the
+ * `onvaluechange` event will be fired with the new/old value.
+ */
+ set value(value) {
+ var old = this._value;
+ if (old !== value) {
+ this._value = value;
+ this._clearSelectedDay(value);
+ this.onvaluechange(value, old);
+ }
+ },
+
+ /**
+ * Clears the currently selected date of its 'selected' class.
+ * @private
+ */
+ _clearSelectedDay: function(value) {
+ var target = this.element.querySelector('.' + SELECTED);
+ if (target) {
+ target.classList.remove(SELECTED);
+ }
+ },
+
+ handleEvent: function(e) {
+ switch (e.type) {
+ case 'click':
+ var target = e.target;
+ //XXX: if the html of the date elements changes
+ // this may also need to be altered as it
+ // assumes that there is no nesting of elements.
+ if (target.dataset.date) {
+ var date = Calc.dateFromId(target.dataset.date);
+ // order here is important as setting value will
+ // clear all the past selected dates...
+ this.value = date;
+ this._position = date;
+ // must come after setting selected date
+ target.classList.add(SELECTED);
+ }
+ break;
+ }
+ },
+
+ /**
+ * Getter is used for date normalization.
+ */
+ get year() {
+ return this._position.getFullYear();
+ },
+
+ /**
+ * Getter is used for date normalization.
+ */
+ get month() {
+ return this._position.getMonth();
+ },
+
+ get date() {
+ return this._position.getDate();
+ },
+
+ /**
+ * Find the number of days in the given month/year.
+ * Month is zero based like the JS date constructor.
+ *
+ * @param {Numeric} year year value.
+ * @param {Numeric} month month value.
+ * @return {Numeric} number of days in month.
+ */
+ _daysInMonth: function(year, month) {
+ var end = new Date(year, month + 1);
+ end.setMilliseconds(-1);
+ return end.getDate();
+ },
+
+ /**
+ * Build the container for a day element.
+ * Each element has classes added to it based
+ * on what date it is created for.
+ *
+ * _today_ is based on today's actual date.
+ * Each date element also contains a data-date attribute
+ * with its current date as a string represented in
+ * the following format: "yyyy-mm-dd".
+ *
+ * Possible classes:
+ * - past
+ * - present (today)
+ * - future
+ * - other-month (day of another month but falls within same week)
+ *
+ * @param {Date} date date desired.
+ * @return {HTMLElement} dom element for day.
+ */
+ _renderDay: function(date) {
+ var dayContainer = document.createElement('li');
+ var dayEl = document.createElement('span');
+
+ dayContainer.className = Calc.relativeState(
+ date,
+ this._position
+ );
+
+ dayEl.dataset.date = Calc.getDayId(date);
+ dayEl.textContent = date.getDate();
+
+ dayContainer.appendChild(dayEl);
+
+ return dayContainer;
+ },
+
+ /**
+ * Renders a set of dates and returns an ol element
+ * containing each date.
+ *
+ * @private
+ * @param {Array[Date]} dates array of dates.
+ * @return {HTMLELement} container for week.
+ */
+ _renderWeek: function(dates) {
+ var container = document.createElement('ol');
+ var i = 0;
+ var len = dates.length;
+
+ for (; i < len; i++) {
+ container.appendChild(
+ this._renderDay(dates[i])
+ );
+ }
+
+ return container;
+ },
+
+ /**
+ * Finds all dates in a given month by week.
+ * Includes leading and trailing days that occur
+ * outside the given year/month combination.
+ *
+ * @private
+ * @param {Numeric} year target year.
+ * @param {Numeric} month target month.
+ * @return {Array[Date]} array of dates.
+ */
+ _getMonthDays: function(year, month) {
+ var date = new Date(year, month);
+ var dateEnd = new Date(year, month + 1);
+ dateEnd.setMilliseconds(-1);
+
+ var start = Calc.getWeekStartDate(date);
+ var end = Calc.getWeekEndDate(dateEnd);
+ return Calc.daysBetween(start, end);
+ },
+
+ /**
+ * Returns a section element with all
+ * the days of the given month/year pair.
+ *
+ * Each month has a class for the number of weeks
+ * it contains.
+ *
+ * Possible values:
+ * - weeks-4
+ * - weeks-5
+ * - weeks-6
+ *
+ * @private
+ */
+ _renderMonth: function(year, month) {
+ var container = document.createElement('section');
+ var days = this._getMonthDays(year, month);
+ var daysInWeek = Calc.daysInWeek();
+ var weeks = days.length / daysInWeek;
+ var i = 0;
+
+ container.classList.add('weeks-' + weeks);
+
+ for (; i < weeks; i++) {
+ container.appendChild(this._renderWeek(
+ days.splice(0, daysInWeek)
+ ));
+ }
+
+ return container;
+ },
+
+ /**
+ * Moves calendar one month into the future.
+ */
+ next: function() {
+ this.display(this.year, this.month + 1, this.date);
+ },
+
+ /**
+ * Moves calendar one month into the past.
+ */
+ previous: function() {
+ this.display(this.year, this.month - 1, this.date);
+ },
+
+ /**
+ * Primary method to display given month.
+ * Will remove the current display and replace
+ * it with the given month.
+ *
+ * @param {Numeric} year year to display.
+ * @param {Numeric} month month to display.
+ * @param {Numeric} date date to display.
+ */
+ display: function(year, month, date) {
+
+ // reset the date to the last date if overflow
+ var lastDate = new Date(year, month + 1, 0).getDate();
+ if (lastDate < date)
+ date = lastDate;
+
+ // Should come before render month
+ this._position = new Date(year, month, date);
+
+ var element = this._renderMonth(year, month);
+
+ if (this.monthDisplay) {
+ this.monthDisplay.parentNode.removeChild(
+ this.monthDisplay
+ );
+ }
+
+ this.monthDisplay = element;
+ this.element.appendChild(this.monthDisplay);
+
+ this.onmonthchange(this._position);
+
+ // Set the date as selected if presented
+ this._clearSelectedDay();
+ if (date) {
+ var dayId = Calc.getDayId(this._position);
+ this.value = this._position;
+ var selector = '[data-date="' + dayId + '"]';
+ var dateElement = document.querySelector(selector);
+ dateElement.classList.add(SELECTED);
+ }
+ },
+
+ /**
+ * Called when the month is changed.
+ */
+ onmonthchange: function(month, year) {},
+
+ /**
+ * Called when the selected day changes.
+ */
+ onvaluechange: function(date) {}
+ };
+
+ return DatePicker;
+}());
diff --git a/apps/system/js/value_selector/input_parser.js b/apps/system/js/value_selector/input_parser.js
new file mode 100644
index 0000000..5eef320
--- /dev/null
+++ b/apps/system/js/value_selector/input_parser.js
@@ -0,0 +1,160 @@
+/**
+ * Stateless object for input parser functions..
+ * The intent is the methods here will only relate to the parsing
+ * of input[type="date|time"]
+ */
+
+ValueSelector.InputParser = (function() {
+
+ var InputParser = {
+ _dateParts: ['year', 'month', 'date'],
+ _timeParts: ['hours', 'minutes', 'seconds'],
+
+ /**
+ * Import HTML5 input[type="time"] string value
+ *
+ * @param {String} value 23:20:50.52, 17:39:57.
+ * @return {Object} { hours: 23, minutes: 20, seconds: 50 }.
+ */
+ importTime: function(value) {
+ var result = {
+ hours: 0,
+ minutes: 0,
+ seconds: 0
+ };
+
+ var parts = value.split(':');
+ var part;
+ var partName;
+
+ var i = 0;
+ var len = InputParser._timeParts.length;
+
+ for (; i < len; i++) {
+ partName = InputParser._timeParts[i];
+ part = parts[i];
+ if (part) {
+ result[partName] = parseInt(part.slice(0, 2), 10) || 0;
+ }
+ }
+
+ return result;
+ },
+
+ /**
+ * Export date to HTML5 input[type="time"]
+ *
+ * @param {Date} value export value.
+ * @return {String} 17:39:57.
+ */
+ exportTime: function(value) {
+ var hour = value.getHours();
+ var minute = value.getMinutes();
+ var second = value.getSeconds();
+
+ var result = '';
+
+ result += InputParser.padNumber(hour) + ':';
+ result += InputParser.padNumber(minute) + ':';
+ result += InputParser.padNumber(second);
+
+ return result;
+ },
+
+ /**
+ * Import HTML5 input[type="time"] to object.
+ *
+ * @param {String} value 1997-12-19.
+ * @return {Object} { year: 1997, month: 12, date: 19 }.
+ */
+ importDate: function(value) {
+ var result = {
+ year: 0,
+ month: 0,
+ date: 0
+ };
+
+ var parts = value.split('-');
+ var part;
+ var partName;
+
+ var i = 0;
+ var len = InputParser._dateParts.length;
+
+ for (; i < len; i++) {
+ partName = InputParser._dateParts[i];
+ part = parts[i];
+ if (part) {
+ result[partName] = parseInt(part, 10);
+ }
+ }
+
+ if (result.month > 0) {
+ result.month = result.month - 1;
+ }
+
+ result.date = result.date || 1;
+
+ return result;
+ },
+
+ /**
+ * Export js date to HTML5 input[type="date"]
+ *
+ * @param {Date} value export value.
+ * @return {String} date string (1997-12-19).
+ */
+ exportDate: function(value) {
+ var year = value.getFullYear();
+ var month = value.getMonth() + 1;
+ var date = value.getDate();
+
+ var result = '';
+
+ result += InputParser.padNumber(year) + '-';
+ result += InputParser.padNumber(month) + '-';
+ result += InputParser.padNumber(date);
+
+ return result;
+ },
+
+ /**
+ * Designed to take a date & time value from
+ * html5 input types and returns a JS Date.
+ *
+ * @param {String} date input date.
+ * @param {String} time input time.
+ *
+ * @return {Date} full date object from date/time.
+ */
+ formatInputDate: function(date, time) {
+ time = InputParser.importTime(time);
+ date = InputParser.importDate(date);
+
+ return new Date(
+ date.year,
+ date.month,
+ date.date,
+ time.hours,
+ time.minutes,
+ time.seconds
+ );
+ },
+
+ /**
+ * @param {Numeric} numeric value.
+ * @return {String} Pad the numeric with a leading zero if < 10.
+ */
+ padNumber: function(numeric) {
+ var value = String(numeric);
+ if (numeric < 10) {
+ return '0' + value;
+ }
+
+ return value;
+ }
+ };
+
+ return InputParser;
+
+}());
diff --git a/apps/system/js/value_selector/spin_date_picker.js b/apps/system/js/value_selector/spin_date_picker.js
new file mode 100644
index 0000000..feb0602
--- /dev/null
+++ b/apps/system/js/value_selector/spin_date_picker.js
@@ -0,0 +1,341 @@
+/**
+ * SpinDatePicker is a html/js "widget" which enables users
+ * pick a specific date. It display the date in the way based
+ * on the language setting.
+ *
+ * The SpinDatePicker itself contains no UI for the controls.
+ *
+ * Example usage:
+ *
+ * // All necessary UI elements are contained in the root element.
+ * var picker = new SpinDatePicker(root);
+ * picker.value = new Date();
+ * // after users pick a date
+ * var newDate = picker.value;
+ */
+var SpinDatePicker = (function SpinDatePicker() {
+ 'use strict';
+
+ var FIRST_YEAR = 1900;
+ var LAST_YEAR = 2099;
+
+ function getYearText() {
+ var yearText = [];
+ var dateTimeFormat = navigator.mozL10n.DateTimeFormat();
+
+ for (var i = FIRST_YEAR; i <= LAST_YEAR; i++) {
+ var date = new Date(i, 0, 1);
+ yearText.push(dateTimeFormat.localeFormat(date, '%Y'));
+ }
+
+ return yearText;
+ }
+
+ function getMonthText() {
+ var monthText = [];
+ var date = new Date(0);
+ var dateTimeFormat = navigator.mozL10n.DateTimeFormat();
+
+ for (var i = 0; i < 12; i++) {
+ date.setMonth(i);
+ monthText.push(dateTimeFormat.localeFormat(date, '%B'));
+ }
+
+ return monthText;
+ }
+
+ function getDateText(days) {
+ var dateText = [];
+ var date = new Date(0);
+ var dateTimeFormat = navigator.mozL10n.DateTimeFormat();
+
+ for (var i = 1; i <= days; i++) {
+ date.setDate(i);
+ dateText.push(dateTimeFormat.localeFormat(date, '%d'));
+ }
+
+ return dateText;
+ }
+
+ function getDaysInMonth(year, month) {
+ var date = new Date(year, month + 1, 0);
+ return date.getDate();
+ }
+
+ /**
+ * Get the order of date components.
+ *
+ * @param {String} date format.
+ */
+ function getDateComponentOrder(format) {
+ var format = navigator.mozL10n.get('dateTimeFormat_%x');
+ var order = '';
+ var tokens = format.match(/(%E.|%O.|%.)/g);
+
+ if (tokens) {
+ tokens.forEach(function(token) {
+ switch (token) {
+ case '%Y':
+ case '%y':
+ case '%Oy':
+ case 'Ey':
+ case 'EY':
+ order += 'Y';
+ break;
+ case '%B':
+ case '%b':
+ case '%m':
+ case '%Om':
+ order += 'M';
+ break;
+ case '%d':
+ case '%e':
+ case '%Od':
+ case '%Oe':
+ order += 'D';
+ break;
+ }
+ });
+ }
+
+ if (order.length != 3)
+ order = 'DMY';
+
+ return order;
+ }
+
+ /**
+ * Initialize a date picker widget.
+ *
+ * @param {HTMLELement} element target of widget creation.
+ */
+ function SpinDatePicker(element) {
+ this.element = element;
+
+ this.yearPicker = null;
+ this.monthPicker = null;
+ this.datePickers = {
+ '28': null,
+ '29': null,
+ '30': null,
+ '31': null
+ };
+
+ //XXX: When the document is localized again
+ // we must also re-render the month because
+ // the week days may have changed?
+ // This will only happen when we change timezones
+ // unless we add this information to the locales.
+
+ var pickerContainer =
+ element.querySelector('.picker-container');
+ var yearPickerContainer =
+ element.querySelector('.value-picker-year');
+ var monthPickerContainer =
+ element.querySelector('.value-picker-month');
+ var tmpDatePickerContainers =
+ element.querySelectorAll('.value-picker-date');
+ var datePickerContainers = {
+ '28': tmpDatePickerContainers[0],
+ '29': tmpDatePickerContainers[1],
+ '30': tmpDatePickerContainers[2],
+ '31': tmpDatePickerContainers[3]
+ };
+
+ var updateCurrentValue = function spd_updateCurrentValue() {
+ var selectedYear = this.yearPicker.getSelectedIndex() + FIRST_YEAR;
+ var selectedMonth = this.monthPicker.getSelectedIndex();
+ var days = getDaysInMonth(selectedYear, selectedMonth);
+ var datePicker = this.datePickers[days];
+ var selectedDate = datePicker.getSelectedIndex() + 1;
+
+ this._value = new Date(selectedYear, selectedMonth, selectedDate);
+ };
+
+ var updateDatePickerVisibility =
+ function spd_updateDatePickerVisibility() {
+ var days = getDaysInMonth(this.yearPicker.getSelectedIndex() +
+ FIRST_YEAR, this.monthPicker.getSelectedIndex());
+ for (var i = 28; i <= 31; i++) {
+ datePickerContainers[i].hidden = true;
+ this.datePickers[i].setSelectedIndex(this._currentSelectedDateIndex);
+ }
+ datePickerContainers[days].hidden = false;
+ };
+
+ var onvaluechangeInternal =
+ function spd_onvaluechangeInternal(newDateValue) {
+ this.yearPicker.setSelectedIndex(newDateValue.getFullYear() - FIRST_YEAR);
+ this.monthPicker.setSelectedIndex(newDateValue.getMonth());
+ for (var i = 28; i <= 31; i++) {
+ this.datePickers[i].setSelectedIndex(newDateValue.getDate() - 1);
+ }
+ updateDatePickerVisibility.apply(this);
+ updateCurrentValue.apply(this);
+ };
+
+ var onSelectedYearChanged =
+ function spd_onSelectedYearChanged(selectedYear) {
+ updateDatePickerVisibility.apply(this);
+ updateCurrentValue.apply(this);
+ };
+
+ var onSelectedMonthChanged =
+ function spd_onSelectedMonthChanged(selectedMonth) {
+ updateDatePickerVisibility.apply(this);
+ updateCurrentValue.apply(this);
+ };
+
+ var onSelectedDateChanged =
+ function spd_onSelectedDateChanged(selectedDate) {
+ this._currentSelectedDateIndex = selectedDate;
+ updateCurrentValue.apply(this);
+ };
+
+ var unitClassName = 'picker-unit';
+
+ // year value picker
+ var yearUnitStyle = {
+ valueDisplayedText: getYearText(),
+ className: unitClassName
+ };
+ if (this.yearPicker)
+ this.yearPicker.uninit();
+ this.yearPicker = new ValuePicker(yearPickerContainer, yearUnitStyle);
+ this.yearPicker.onselectedindexchange = onSelectedYearChanged.bind(this);
+
+ // month value picker
+ var monthUnitStyle = {
+ valueDisplayedText: getMonthText(),
+ className: unitClassName
+ };
+ if (this.monthPicker)
+ this.monthPicker.uninit();
+ this.monthPicker =
+ new ValuePicker(monthPickerContainer, monthUnitStyle);
+ this.monthPicker.onselectedindexchange = onSelectedMonthChanged.bind(this);
+
+ // date value picker
+ for (var i = 28; i <= 31; i++) {
+ var datePickerContainer = datePickerContainers[i];
+ var dateUnitStyle = {
+ valueDisplayedText: getDateText(i),
+ className: unitClassName
+ };
+ var datePicker = this.datePickers[i];
+
+ if (datePicker)
+ datePicker.uninit();
+ datePickerContainer.hidden = false;
+ this.datePickers[i] = new ValuePicker(datePickerContainer, dateUnitStyle);
+ this.datePickers[i].onselectedindexchange =
+ onSelectedDateChanged.bind(this);
+ }
+
+ // set component order
+ var dateComponentOrder = getDateComponentOrder();
+ var pickerClassList = pickerContainer.classList;
+ pickerClassList.remove('YMD');
+ pickerClassList.remove('DMY');
+ pickerClassList.remove('MDY');
+ pickerClassList.add(dateComponentOrder);
+
+ // Prevent focus being taken away by us for time picker.
+ // The event listener on outer box will not be triggered cause
+ // there is a evt.stopPropagation() in value_picker.js
+ this.pickerElements = [monthPickerContainer, yearPickerContainer];
+ for (var i = 28; i <= 31; i++) {
+ this.pickerElements.push(datePickerContainers[i]);
+ }
+
+ this.pickerElements.forEach((function pickerElements_forEach(picker) {
+ picker.addEventListener('mousedown', this);
+ }).bind(this));
+
+ this.onvaluechangeInternal = onvaluechangeInternal.bind(this);
+ }
+
+ SpinDatePicker.prototype = {
+
+ /**
+ * Internal value not exposed so we can fire events
+ * when the getter/setter's are used.
+ *
+ * @type Date
+ */
+ _value: null,
+
+ /**
+ * Gets current value
+ *
+ * @return {Null|Date} date or null.
+ */
+ get value() {
+ return this._value;
+ },
+
+ /**
+ * Sets the current value of the date picker.
+ * When value differs from the currently set the
+ * `onvaluechange` event will be fired with the new/old value.
+ */
+ set value(value) {
+ var old = this._value;
+ if (old !== value) {
+ this._value = value;
+ this.onvaluechangeInternal(value);
+ }
+ },
+
+ /**
+ * Getter is used for date normalization.
+ */
+ get year() {
+ return this._value.getFullYear();
+ },
+
+ /**
+ * Getter is used for date normalization.
+ */
+ get month() {
+ return this._value.getMonth();
+ },
+
+ get date() {
+ return this._value.getDate();
+ },
+
+ handleEvent: function vs_handleEvent(evt) {
+ switch (evt.type) {
+ case 'mousedown':
+ // Prevent focus being taken away by us.
+ evt.preventDefault();
+ break;
+ }
+ },
+
+ uninit: function() {
+ if (this.yearPicker)
+ this.yearPicker.uninit();
+ if (this.monthPicker)
+ this.monthPicker.uninit();
+ if (this.datePickers) {
+ for (var i = 28; i <= 31; i++) {
+ var datePicker = this.datePickers[i];
+ datePicker.uninit();
+ }
+ }
+
+ this.pickerElements.forEach((function pickerElements_forEach(picker) {
+ picker.removeEventListener('mousedown', this);
+ }).bind(this));
+ },
+
+ /**
+ * Called when the selected date changes.
+ */
+ onvaluechangeInternal: function(date) {}
+ };
+
+ return SpinDatePicker;
+}());
diff --git a/apps/system/js/value_selector/value_picker.js b/apps/system/js/value_selector/value_picker.js
new file mode 100644
index 0000000..34e686f
--- /dev/null
+++ b/apps/system/js/value_selector/value_picker.js
@@ -0,0 +1,222 @@
+var ValuePicker = (function() {
+ //
+ // Constructor
+ //
+ function VP(e, unitStyle) {
+ this.element = e;
+ this._valueDisplayedText = unitStyle.valueDisplayedText;
+ this._unitClassName = unitStyle.className;
+ this._lower = 0;
+ this._upper = unitStyle.valueDisplayedText.length - 1;
+ this._range = unitStyle.valueDisplayedText.length;
+ this._currentIndex = 0;
+ this.init();
+ }
+
+ //
+ // Public methods
+ //
+ VP.prototype.getSelectedIndex = function() {
+ var selectedIndex = this._currentIndex;
+ return selectedIndex;
+ };
+
+ VP.prototype.getSelectedDisplayedText = function() {
+ var displayedText = this._valueDisplayedText[this._currentIndex];
+ return displayedText;
+ };
+
+ VP.prototype.setSelectedIndex = function(tunedIndex, ignorePicker) {
+ if ((tunedIndex % 1) > 0.5) {
+ tunedIndex = Math.floor(tunedIndex) + 1;
+ } else {
+ tunedIndex = Math.floor(tunedIndex);
+ }
+
+ if (tunedIndex < this._lower) {
+ tunedIndex = this._lower;
+ }
+
+ if (tunedIndex > this._upper) {
+ tunedIndex = this._upper;
+ }
+
+ if (this._currentIndex != tunedIndex) {
+ this._currentIndex = tunedIndex;
+ this.onselectedindexchange(this._currentIndex);
+ }
+ this.updateUI(tunedIndex, ignorePicker);
+
+ return tunedIndex;
+ };
+
+ VP.prototype.setSelectedIndexByDisplayedText = function(displayedText) {
+ var newIndex = this._valueDisplayedText.indexOf(displayedText);
+ if (newIndex != -1) {
+ if (this._currentIndex != newIndex) {
+ this._currentIndex = newIndex;
+ this.onselectedindexchange(this._currentIndex);
+ }
+ this.updateUI(newIndex);
+ }
+ };
+
+ //
+ // Internal methods
+ //
+ VP.prototype.init = function() {
+ this.initUI();
+ this.setSelectedIndex(0); // Default Index is zero
+ this.mousedonwHandler = vp_mousedown.bind(this);
+ this.mousemoveHandler = vp_mousemove.bind(this);
+ this.mouseupHandler = vp_mouseup.bind(this);
+ this.addEventListeners();
+ };
+
+ VP.prototype.initUI = function() {
+ var lower = this._lower;
+ var upper = this._upper;
+ var unitCount = this._valueDisplayedText.length;
+ for (var i = 0; i < unitCount; ++i) {
+ this.addPickerUnit(i);
+ }
+ // cache the size of picker
+ this._pickerUnits = this.element.children;
+ this._pickerUnitsHeight = this._pickerUnits[0].clientHeight;
+ this._pickerHeight = this._pickerUnits[0].clientHeight *
+ this._pickerUnits.length;
+ this._space = this._pickerHeight / this._range;
+ };
+
+ VP.prototype.addPickerUnit = function(index) {
+ var html = this._valueDisplayedText[index];
+ var unit = document.createElement('div');
+ unit.className = this._unitClassName;
+ unit.innerHTML = html;
+ this.element.appendChild(unit);
+ };
+
+ VP.prototype.updateUI = function(index, ignorePicker) {
+ if (true !== ignorePicker) {
+ this.element.style.top =
+ (this._lower - index) * this._space + 'px';
+ }
+ };
+
+ VP.prototype.addEventListeners = function() {
+ this.element.addEventListener('mousedown', this.mousedonwHandler, false);
+ };
+
+ VP.prototype.removeEventListeners = function() {
+ this.element.removeEventListener('mouseup', this.mouseupHandler, false);
+ this.element.removeEventListener('mousemove', this.mousemoveHandler, false);
+ };
+
+ VP.prototype.uninit = function() {
+ this.element.removeEventListener('mousedown', this.mousedonwHandler, false);
+ this.element.removeEventListener('mouseup', this.mouseupHandler, false);
+ this.element.removeEventListener('mousemove', this.mousemoveHandler, false);
+ this.element.style.top = '0px';
+ this.onselectedindexchange = null;
+ empty(this.element);
+ };
+
+ VP.prototype.onselectedindexchange = function(index) {};
+
+ function cloneEvent(evt) {
+ if ('touches' in evt) {
+ evt = evt.touches[0];
+ }
+ return { x: evt.pageX, y: evt.pageY, timestamp: evt.timeStamp };
+ }
+
+ function empty(element) {
+ while (element.hasChildNodes())
+ element.removeChild(element.lastChild);
+ element.innerHTML = '';
+ }
+
+ //
+ // Tuneable parameters
+ //
+ var SPEED_THRESHOLD = 0.1;
+ var currentEvent, startEvent, currentSpeed;
+ var tunedIndex = 0;
+
+ function toFixed(value) {
+ return parseFloat(value.toFixed(1));
+ }
+
+ function calcSpeed() {
+ var movingSpace = startEvent.y - currentEvent.y;
+ var deltaTime = currentEvent.timestamp - startEvent.timestamp;
+ var speed = movingSpace / deltaTime;
+ currentSpeed = parseFloat(speed.toFixed(2));
+ }
+
+ function calcTargetIndex(space) {
+ return tunedIndex - getMovingSpace() / space;
+ }
+
+ // If the user swap really slow, narrow down the moving space
+ // So the user can fine tune value.
+ function getMovingSpace() {
+ var movingSpace = currentEvent.y - startEvent.y;
+ var reValue = Math.abs(currentSpeed) > SPEED_THRESHOLD ?
+ movingSpace : movingSpace / 4;
+ return reValue;
+ }
+
+ function vp_mousemove(event) {
+ event.stopPropagation();
+ event.target.setCapture(true);
+ currentEvent = cloneEvent(event);
+
+ calcSpeed();
+
+ // move selected index
+ this.element.style.top = parseFloat(this.element.style.top) +
+ getMovingSpace() + 'px';
+
+ tunedIndex = calcTargetIndex(this._space);
+ var roundedIndex = Math.round(tunedIndex * 10) / 10;
+
+ if (roundedIndex != this._currentIndex) {
+ this.setSelectedIndex(toFixed(roundedIndex), true);
+ }
+
+ startEvent = currentEvent;
+ }
+
+ function vp_mouseup(event) {
+ event.stopPropagation();
+ this.removeEventListeners();
+
+ // Add animation back
+ this.element.classList.add('animation-on');
+
+ // Add momentum if speed is higher than a given threshold.
+ if (Math.abs(currentSpeed) > SPEED_THRESHOLD) {
+ var direction = currentSpeed > 0 ? 1 : -1;
+ tunedIndex += Math.min(Math.abs(currentSpeed) * 5, 5) * direction;
+ }
+ tunedIndex = this.setSelectedIndex(toFixed(tunedIndex));
+ currentSpeed = 0;
+ }
+
+ function vp_mousedown(event) {
+ event.stopPropagation();
+
+ // Stop animation
+ this.element.classList.remove('animation-on');
+
+ startEvent = currentEvent = cloneEvent(event);
+ tunedIndex = this._currentIndex;
+
+ this.removeEventListeners();
+ this.element.addEventListener('mousemove', this.mousemoveHandler, false);
+ this.element.addEventListener('mouseup', this.mouseupHandler, false);
+ }
+
+ return VP;
+}());
diff --git a/apps/system/js/value_selector/value_selector.js b/apps/system/js/value_selector/value_selector.js
new file mode 100644
index 0000000..b3381b3
--- /dev/null
+++ b/apps/system/js/value_selector/value_selector.js
@@ -0,0 +1,526 @@
+/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var ValueSelector = {
+
+ _containers: {},
+ _popups: {},
+ _buttons: {},
+ _datePicker: null,
+
+ debug: function(msg) {
+ var debugFlag = false;
+ if (debugFlag) {
+ console.log('[ValueSelector] ', msg);
+ }
+ },
+
+ init: function vs_init() {
+
+ var self = this;
+
+ window.navigator.mozKeyboard.onfocuschange = function onfocuschange(evt) {
+ var typeToHandle = ['select-one', 'select-multiple', 'date',
+ 'time', 'datetime', 'datetime-local', 'blur'];
+
+ var type = evt.detail.type;
+ // handle the <select> element and inputs with type of date/time
+ // in system app for now
+ if (typeToHandle.indexOf(type) == -1)
+ return;
+
+ var currentValue = evt.detail.value;
+
+ switch (evt.detail.type) {
+ case 'select-one':
+ case 'select-multiple':
+ self.debug('select triggered' + JSON.stringify(evt.detail));
+ self._currentPickerType = evt.detail.type;
+ self.showOptions(evt.detail);
+ break;
+
+ case 'date':
+ self.showDatePicker(currentValue);
+ break;
+
+ case 'time':
+ self.showTimePicker(currentValue);
+ break;
+
+ case 'datetime':
+ case 'datetime-local':
+ // TODO
+ break;
+ case 'blur':
+ self.hide();
+ break;
+ }
+ };
+
+ this._element = document.getElementById('value-selector');
+ this._element.addEventListener('mousedown', this);
+ this._containers['select'] =
+ document.getElementById('value-selector-container');
+ this._containers['select'].addEventListener('click', this);
+ ActiveEffectHelper.enableActive(this._containers['select']);
+
+ this._popups['select'] =
+ document.getElementById('select-option-popup');
+ this._popups['select'].addEventListener('submit', this);
+ this._popups['time'] =
+ document.getElementById('time-picker-popup');
+ this._popups['date'] =
+ document.getElementById('spin-date-picker-popup');
+
+ this._buttons['select'] = document.getElementById('select-options-buttons');
+ this._buttons['select'].addEventListener('click', this);
+
+ this._buttons['time'] = document.getElementById('time-picker-buttons');
+ this._buttons['time'].addEventListener('click', this);
+ this._buttons['date'] = document.getElementById('spin-date-picker-buttons');
+
+ this._buttons['date'].addEventListener('click', this);
+
+ this._containers['time'] = document.getElementById('picker-bar');
+ this._containers['date'] = document.getElementById('spin-date-picker');
+
+ ActiveEffectHelper.enableActive(this._buttons['select']);
+ ActiveEffectHelper.enableActive(this._buttons['time']);
+ ActiveEffectHelper.enableActive(this._buttons['date']);
+
+ // Prevent focus being taken away by us for time picker.
+ // The event listener on outer box will not be triggered cause
+ // there is a evt.stopPropagation() in value_picker.js
+ var pickerElements = ['value-picker-hours', 'value-picker-minutes',
+ 'value-picker-hour24-state'];
+
+ pickerElements.forEach((function pickerElements_forEach(id) {
+ var element = document.getElementById(id);
+ element.addEventListener('mousedown', this);
+ }).bind(this));
+
+ window.addEventListener('appopen', this);
+ window.addEventListener('appwillclose', this);
+
+ // invalidate the current spin date picker when language setting changes
+ navigator.mozSettings.addObserver('language.current',
+ (function language_change(e) {
+ if (this._datePicker) {
+ this._datePicker.uninit();
+ this._datePicker = null;
+ }}).bind(this));
+ },
+
+ handleEvent: function vs_handleEvent(evt) {
+ switch (evt.type) {
+ case 'appopen':
+ case 'appwillclose':
+ this.hide();
+ break;
+
+ case 'click':
+ var currentTarget = evt.currentTarget;
+ switch (currentTarget) {
+ case this._buttons['select']:
+ case this._buttons['time']:
+ case this._buttons['date']:
+ var target = evt.target;
+ if (target.dataset.type == 'cancel') {
+ this.cancel();
+ } else if (target.dataset.type == 'ok') {
+ this.confirm();
+ }
+ break;
+
+ case this._containers['select']:
+ this.handleSelect(evt.target);
+ break;
+ }
+ break;
+
+ case 'submit':
+ // Prevent the form from submit.
+ case 'mousedown':
+ // Prevent focus being taken away by us.
+ evt.preventDefault();
+ break;
+
+ default:
+ this.debug('no event handler defined for' + evt.type);
+ break;
+ }
+ },
+
+ handleSelect: function vs_handleSelect(target) {
+
+ if (target.dataset === undefined ||
+ (target.dataset.optionIndex === undefined &&
+ target.dataset.optionValue === undefined))
+ return;
+
+ if (this._currentPickerType === 'select-one') {
+ var selectee = this._containers['select'].
+ querySelectorAll('[aria-checked="true"]');
+ for (var i = 0; i < selectee.length; i++) {
+ selectee[i].removeAttribute('aria-checked');
+ }
+
+ target.setAttribute('aria-checked', 'true');
+ } else if (target.getAttribute('aria-checked') === 'true') {
+ target.removeAttribute('aria-checked');
+ } else {
+ target.setAttribute('aria-checked', 'true');
+ }
+
+ // setValue here to trigger change event
+ var singleOptionIndex;
+ var optionIndices = [];
+
+ var selectee = this._containers['select'].
+ querySelectorAll('[aria-checked="true"]');
+
+ if (this._currentPickerType === 'select-one') {
+
+ if (selectee.length > 0)
+ singleOptionIndex = selectee[0].dataset.optionIndex;
+
+ window.navigator.mozKeyboard.setSelectedOption(singleOptionIndex);
+
+ } else if (this._currentPickerType === 'select-multiple') {
+ // Multiple select case
+ for (var i = 0; i < selectee.length; i++) {
+
+ var index = parseInt(selectee[i].dataset.optionIndex);
+ optionIndices.push(index);
+ }
+
+ window.navigator.mozKeyboard.setSelectedOptions(optionIndices);
+ }
+
+ },
+
+ show: function vs_show(detail) {
+ this._element.hidden = false;
+ },
+
+ showPanel: function vs_showPanel(type) {
+ for (var p in this._containers) {
+ if (p === type) {
+ this._popups[p].hidden = false;
+ } else {
+ this._popups[p].hidden = true;
+ }
+ }
+ },
+
+ hide: function vs_hide() {
+ this._element.hidden = true;
+ },
+
+ cancel: function vs_cancel() {
+ this.debug('cancel invoked');
+ window.navigator.mozKeyboard.removeFocus();
+ this.hide();
+ },
+
+ confirm: function vs_confirm() {
+
+ if (this._currentPickerType === 'time') {
+
+ var timeValue = TimePicker.getTimeValue();
+ this.debug('output value: ' + timeValue);
+
+ window.navigator.mozKeyboard.setValue(timeValue);
+ } else if (this._currentPickerType === 'date') {
+ var dateValue = this._datePicker.value;
+ // The format should be 2012-09-19
+ dateValue = dateValue.toLocaleFormat('%Y-%m-%d');
+ this.debug('output value: ' + dateValue);
+ window.navigator.mozKeyboard.setValue(dateValue);
+ }
+
+ window.navigator.mozKeyboard.removeFocus();
+ this.hide();
+ },
+
+ showOptions: function vs_showOptions(detail) {
+
+ var options = null;
+ if (detail.choices && detail.choices.choices)
+ options = detail.choices.choices;
+
+ if (options)
+ this.buildOptions(options);
+
+ this.show();
+ this.showPanel('select');
+ },
+
+ buildOptions: function(options) {
+
+ var optionHTML = '';
+
+ function escapeHTML(str) {
+ var span = document.createElement('span');
+ span.textContent = str;
+ return span.innerHTML;
+ }
+
+ for (var i = 0, n = options.length; i < n; i++) {
+
+ var checked = options[i].selected ? ' aria-checked="true"' : '';
+
+ // This for attribute is created only to avoid applying
+ // a general rule in building block
+ var forAttribute = ' for="gaia-option-' + options[i].optionIndex + '"';
+
+ optionHTML += '<li data-option-index="' + options[i].optionIndex + '"' +
+ checked + '>' +
+ '<label' + forAttribute + '> <span>' +
+ escapeHTML(options[i].text) +
+ '</span></label>' +
+ '</li>';
+ }
+
+ var optionsContainer = document.querySelector(
+ '#value-selector-container ol');
+ if (!optionsContainer)
+ return;
+
+ optionsContainer.innerHTML = optionHTML;
+
+
+ // Apply different style when the options are more than 1 page
+ if (options.length > 5) {
+ this._containers['select'].classList.add('scrollable');
+ } else {
+ this._containers['select'].classList.remove('scrollable');
+ }
+
+ // Change the title for multiple select
+ var titleL10nId = 'choose-options';
+ if (this._currentPickerType === 'select-one')
+ titleL10nId = 'choose-option';
+
+ var optionsTitle = document.querySelector(
+ '#value-selector-container h1');
+
+ if (optionsTitle) {
+ optionsTitle.dataset.l10nId = titleL10nId;
+ optionsTitle.textContent = navigator.mozL10n.get(titleL10nId);
+ }
+ },
+
+ showTimePicker: function vs_showTimePicker(currentValue) {
+ this._currentPickerType = 'time';
+ this.show();
+ this.showPanel('time');
+
+ if (!this._timePickerInitialized) {
+ TimePicker.initTimePicker();
+ this._timePickerInitialized = true;
+ }
+
+ var time;
+ if (!currentValue) {
+ var now = new Date();
+ time = {
+ hours: now.getHours(),
+ minutes: now.getMinutes()
+ };
+ } else {
+ var inputParser = ValueSelector.InputParser;
+ if (!inputParser)
+ console.error('Cannot get input parser for value selector');
+
+ time = inputParser.importTime(currentValue);
+ }
+
+ var timePicker = TimePicker.timePicker;
+ // Set the value of time picker according to the current value
+ if (timePicker.is12hFormat) {
+ var hour = (time.hours % 12);
+ hour = (hour == 0) ? 12 : hour;
+ // 24-hour state value selector: AM = 0, PM = 1
+ var hour24State = (time.hours >= 12) ? 1 : 0;
+ timePicker.hour.setSelectedIndexByDisplayedText(hour);
+ timePicker.hour24State.setSelectedIndex(hour24State);
+ } else {
+ timePicker.hour.setSelectedIndex(time.hours);
+ }
+
+ timePicker.minute.setSelectedIndex(time.minutes);
+ },
+
+ showDatePicker: function vs_showDatePicker(currentValue) {
+ this._currentPickerType = 'date';
+ this.show();
+ this.showPanel('date');
+
+ if (!this._datePicker) {
+ this._datePicker = new SpinDatePicker(this._containers['date']);
+ }
+
+ // Show current date as default value
+ var date = new Date();
+ if (currentValue) {
+ var inputParser = ValueSelector.InputParser;
+ if (!inputParser)
+ console.error('Cannot get input parser for value selector');
+
+ date = inputParser.formatInputDate(currentValue, '');
+ }
+ this._datePicker.value = date;
+ }
+
+};
+
+var TimePicker = {
+ timePicker: {
+ hour: null,
+ minute: null,
+ hour24State: null,
+ is12hFormat: false
+ },
+
+ get hourSelector() {
+ delete this.hourSelector;
+ return this.hourSelector =
+ document.getElementById('value-picker-hours');
+ },
+
+ get minuteSelector() {
+ delete this.minuteSelector;
+ return this.minuteSelector =
+ document.getElementById('value-picker-minutes');
+ },
+
+ get hour24StateSelector() {
+ delete this.hour24StateSelector;
+ return this.hour24StateSelector =
+ document.getElementById('value-picker-hour24-state');
+ },
+
+ initTimePicker: function tp_initTimePicker() {
+ var localeTimeFormat = navigator.mozL10n.get('dateTimeFormat_%X');
+ var is12hFormat = (localeTimeFormat.indexOf('%p') >= 0);
+ this.timePicker.is12hFormat = is12hFormat;
+ this.setTimePickerStyle();
+ var startHour = is12hFormat ? 1 : 0;
+ var endHour = is12hFormat ? (startHour + 12) : (startHour + 12 * 2);
+ var unitClassName = 'picker-unit';
+ var hourDisplayedText = [];
+ for (var i = startHour; i < endHour; i++) {
+ var value = i;
+ hourDisplayedText.push(value);
+ }
+ var hourUnitStyle = {
+ valueDisplayedText: hourDisplayedText,
+ className: unitClassName
+ };
+ this.timePicker.hour = new ValuePicker(this.hourSelector, hourUnitStyle);
+
+ var minuteDisplayedText = [];
+ for (var i = 0; i < 60; i++) {
+ var value = (i < 10) ? '0' + i : i;
+ minuteDisplayedText.push(value);
+ }
+ var minuteUnitStyle = {
+ valueDisplayedText: minuteDisplayedText,
+ className: unitClassName
+ };
+ this.timePicker.minute =
+ new ValuePicker(this.minuteSelector, minuteUnitStyle);
+
+ if (is12hFormat) {
+ var hour24StateUnitStyle = {
+ valueDisplayedText: ['AM', 'PM'],
+ className: unitClassName
+ };
+ this.timePicker.hour24State =
+ new ValuePicker(this.hour24StateSelector, hour24StateUnitStyle);
+ }
+ },
+
+ setTimePickerStyle: function tp_setTimePickerStyle() {
+ var style = (this.timePicker.is12hFormat) ? 'format12h' : 'format24h';
+ document.getElementById('picker-bar').classList.add(style);
+ },
+
+ // return a string for the time value, format: "16:37"
+ getTimeValue: function tp_getTimeValue() {
+ var hour = 0;
+ if (this.timePicker.is12hFormat) {
+ var hour24Offset = 12 * this.timePicker.hour24State.getSelectedIndex();
+ hour = this.timePicker.hour.getSelectedDisplayedText();
+ hour = (hour == 12) ? 0 : hour;
+ hour = hour + hour24Offset;
+ } else {
+ hour = this.timePicker.hour.getSelectedIndex();
+ }
+ var minute = this.timePicker.minute.getSelectedDisplayedText();
+
+ return hour + ':' + minute;
+ }
+};
+
+var ActiveEffectHelper = (function() {
+
+ var lastActiveElement = null;
+
+ function _setActive(element, isActive) {
+ if (isActive) {
+ element.classList.add('active');
+ lastActiveElement = element;
+ } else {
+ element.classList.remove('active');
+ if (lastActiveElement) {
+ lastActiveElement.classList.remove('active');
+ lastActiveElement = null;
+ }
+ }
+ }
+
+ function _onMouseDown(evt) {
+ var target = evt.target;
+
+ _setActive(target, true);
+ target.addEventListener('mouseleave', _onMouseLeave);
+ }
+
+ function _onMouseUp(evt) {
+ var target = evt.target;
+
+ _setActive(target, false);
+ target.removeEventListener('mouseleave', _onMouseLeave);
+ }
+
+ function _onMouseLeave(evt) {
+ var target = evt.target;
+ _setActive(target, false);
+ target.removeEventListener('mouseleave', _onMouseLeave);
+ }
+
+ var _events = {
+ 'mousedown': _onMouseDown,
+ 'mouseup': _onMouseUp
+ };
+
+ function _enableActive(element) {
+ // Attach event listeners
+ for (var event in _events) {
+ var callback = _events[event] || null;
+ if (callback)
+ element.addEventListener(event, callback);
+ }
+ }
+
+ return {
+ enableActive: _enableActive
+ };
+
+})();
+
+ValueSelector.init();
diff --git a/apps/system/js/voicemail.js b/apps/system/js/voicemail.js
new file mode 100644
index 0000000..dea5116
--- /dev/null
+++ b/apps/system/js/voicemail.js
@@ -0,0 +1,93 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+// Custom voicemail notification -- This can be removed once DesktopNotification
+// supports removing notifications via API
+var Voicemail = {
+
+ icon: null,
+ notification: null,
+ // A random starting point that is unlikely to be used by other notifications
+ notificationId: 3000 + Math.floor(Math.random() * 999),
+
+ init: function vm_init() {
+ var voicemail = window.navigator.mozVoicemail;
+ if (!voicemail)
+ return;
+
+ voicemail.addEventListener('statuschanged', this);
+
+ this.icon = window.location.protocol + '//' +
+ window.location.hostname + '/style/icons/voicemail.png';
+ },
+
+ handleEvent: function vm_handleEvent(evt) {
+ var voicemail = window.navigator.mozVoicemail;
+ if (!voicemail.status)
+ return;
+
+ this.updateNotification(voicemail.status);
+ },
+
+ updateNotification: function vm_updateNotification(status) {
+ var _ = window.navigator.mozL10n.get;
+ var title = status.returnMessage;
+ var showCount = status.hasMessages && status.messageCount > 0;
+
+ if (!title) {
+ title = showCount ? _('newVoicemails', { n: status.messageCount }) :
+ _('newVoicemailsUnknown');
+ }
+
+ var text = title;
+ var voicemailNumber = navigator.mozVoicemail.number;
+ if (voicemailNumber) {
+ text = _('dialNumber', { number: voicemailNumber });
+ }
+
+ this.hideNotification();
+ if (status.hasMessages) {
+ this.showNotification(title, text, voicemailNumber);
+ }
+ },
+
+ showNotification: function vm_showNotification(title, text, voicemailNumber) {
+ this.notificationId++;
+ this.notification = NotificationScreen.addNotification({
+ id: this.notificationId, title: title, text: text, icon: this.icon
+ });
+
+ if (!voicemailNumber) {
+ return;
+ }
+
+ var self = this;
+ function vmNotification_onTap(event) {
+ self.notification.removeEventListener('tap', vmNotification_onTap);
+
+ var telephony = window.navigator.mozTelephony;
+ if (!telephony) {
+ return;
+ }
+
+ telephony.dial(voicemailNumber);
+ }
+
+ this.notification.addEventListener('tap', vmNotification_onTap);
+ },
+
+ hideNotification: function vm_hideNotification() {
+ if (!this.notification) {
+ return;
+ }
+
+ if (this.notification.parentNode) {
+ NotificationScreen.removeNotification(this.notificationId);
+ }
+
+ this.notification = null;
+ this.notificationId = 0;
+ }
+};
+
+Voicemail.init();
diff --git a/apps/system/js/wifi.js b/apps/system/js/wifi.js
new file mode 100644
index 0000000..3456fdf
--- /dev/null
+++ b/apps/system/js/wifi.js
@@ -0,0 +1,223 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var Wifi = {
+ wifiWakeLocked: false,
+
+ wifiEnabled: true,
+
+ wifiDisabledByWakelock: false,
+
+ // Without wake lock, wait for kOffTime milliseconds and turn wifi off
+ // after the conditions are met.
+ kOffTime: 60 * 1000,
+
+ // if Wifi is enabled but disconnected, try to scan for networks every
+ // kScanInterval ms.
+ kScanInterval: 20 * 1000,
+
+ _scanTimer: null,
+
+ init: function wf_init() {
+ window.addEventListener('screenchange', this);
+
+ var battery = window.navigator.battery;
+ battery.addEventListener('chargingchange', this);
+
+ if (!window.navigator.mozSettings)
+ return;
+
+ // If wifi is turned off by us and phone got rebooted,
+ // bring wifi back.
+ var name = 'wifi.disabled_by_wakelock';
+ var req = SettingsListener.getSettingsLock().get(name);
+ req.onsuccess = function gotWifiDisabledByWakelock() {
+ if (!req.result[name])
+ return;
+
+ // Re-enable wifi and reset wifi.disabled_by_wakelock
+ // SettingsListener.getSettingsLock() always return invalid lock
+ // in our usage here.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=793239
+ var lock = navigator.mozSettings.createLock();
+ lock.set({ 'wifi.enabled': true });
+ lock.set({ 'wifi.disabled_by_wakelock': false });
+ };
+
+ var self = this;
+ var wifiManager = window.navigator.mozWifiManager;
+ // when wifi is really enabled, emit event to notify QuickSettings
+ wifiManager.onenabled = function onWifiEnabled() {
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('wifi-enabled',
+ /* canBubble */ true, /* cancelable */ false, null);
+ window.dispatchEvent(evt);
+ };
+
+ // when wifi is really disabled, emit event to notify QuickSettings
+ wifiManager.ondisabled = function onWifiDisabled() {
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('wifi-disabled',
+ /* canBubble */ true, /* cancelable */ false, null);
+ window.dispatchEvent(evt);
+ };
+
+ // when wifi status change, emit event to notify StatusBar/UpdateManager
+ wifiManager.onstatuschange = function onWifiDisabled() {
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('wifi-statuschange',
+ /* canBubble */ true, /* cancelable */ false, null);
+ window.dispatchEvent(evt);
+ };
+
+ // Track the wifi.enabled mozSettings value
+ SettingsListener.observe('wifi.enabled', true, function(value) {
+ if (!wifiManager && value) {
+ self.wifiEnabled = false;
+
+ // roll back the setting value to notify the UIs
+ // that wifi interface is not available
+ if (value) {
+ SettingsListener.getSettingsLock().set({
+ 'wifi.enabled': false
+ });
+ }
+
+ return;
+ }
+
+ self.wifiEnabled = value;
+
+ clearTimeout(self._scanTimer);
+ if (!value)
+ return;
+
+ // If wifi is enabled but disconnected.
+ // we would need to call getNetworks() continuously
+ // so we could join known wifi network
+ self._scanTimer = setInterval(function wifi_scan() {
+ if (wifiManager.connection.status == 'disconnected')
+ wifiManager.getNetworks();
+ });
+ });
+
+ var power = navigator.mozPower;
+ power.addWakeLockListener(function wifi_handleWakeLock(topic, state) {
+ if (topic !== 'wifi')
+ return;
+
+ self.wifiWakeLocked = (state == 'locked-foreground' ||
+ state == 'locked-background');
+
+ self.maybeToggleWifi();
+ });
+ },
+
+ handleEvent: function wifi_handleEvent(evt) {
+ this.maybeToggleWifi();
+ },
+
+ // Check the status of screen, wifi wake lock and power source
+ // and turn on/off wifi accordingly
+ maybeToggleWifi: function wifi_maybeToggleWifi() {
+ var battery = window.navigator.battery;
+ var wifiManager = window.navigator.mozWifiManager;
+ if (!battery || !wifiManager ||
+ (!this.wifiEnabled && !this.wifiDisabledByWakelock))
+ return;
+
+
+ // Let's quietly turn off wifi if there is no wake lock and
+ // the screen is off and we are not on a power source.
+ if (!ScreenManager.screenEnabled &&
+ !this.wifiWakeLocked && !battery.charging) {
+ // We don't need to do anything if wifi is not enabled currently
+ if (!this.wifiEnabled)
+ return;
+
+ // We still need to turn of wifi even if there is no Alarm API
+ if (!navigator.mozAlarms) {
+ console.warn('Turning off wifi without sleep timer because' +
+ ' Alarm API is not available');
+ this.sleep();
+
+ return;
+ }
+
+ // Set System Message Handler, so we will be notified when alarm goes off.
+ this.setSystemMessageHandler();
+
+ // Start with a timer, only turn off wifi till timeout.
+ var date = new Date(Date.now() + this.kOffTime);
+ var self = this;
+ var req = navigator.mozAlarms.add(date, 'ignoreTimezone', 'wifi-off');
+ req.onsuccess = function wifi_offAlarmSet() {
+ self._alarmId = req.result;
+ };
+ req.onerror = function wifi_offAlarmSetFailed() {
+ console.warn('Fail to set wifi sleep timer on Alarm API. ' +
+ 'Turn off wifi immediately.');
+ self.sleep();
+ };
+ }
+ // ... and quietly turn it back on or cancel the timer otherwise
+ else {
+ if (this._alarmId) {
+ navigator.mozAlarms.remove(this._alarmId);
+ this._alarmId = null;
+ }
+
+ // If wifi is enabled but disconnected.
+ // we would need to call getNetworks() so we could join known wifi network
+ if (this.wifiEnabled && wifiManager.connection.status == 'disconnected') {
+ wifiManager.getNetworks();
+ }
+
+ // We don't need to do anything if we didn't disable wifi at first place.
+ if (!this.wifiDisabledByWakelock)
+ return;
+
+ var lock = SettingsListener.getSettingsLock();
+ // turn wifi back on.
+ lock.set({ 'wifi.enabled': true });
+
+ this.wifiDisabledByWakelock = false;
+ lock.set({ 'wifi.disabled_by_wakelock': false });
+ }
+ },
+
+ // Quietly turn off wifi for real, set wifiDisabledByWakelock to true
+ // so we will turn it back on.
+ sleep: function wifi_sleep() {
+ var lock = SettingsListener.getSettingsLock();
+ // Actually turn off the wifi
+ lock.set({ 'wifi.enabled': false });
+
+ // Remember that it was turned off by us.
+ this.wifiDisabledByWakelock = true;
+
+ // Keep this value in disk so if the phone reboots we'll
+ // be able to turn the wifi back on.
+ lock.set({ 'wifi.disabled_by_wakelock': true });
+ },
+
+ // Register for handling system message,
+ // this cannot be done during |init()| because of bug 797803
+ setSystemMessageHandler: function wifi_setSystemMessageHandler() {
+ if (this._systemMessageHandlerRegistered)
+ return;
+
+ this._systemMessageHandlerRegistered = true;
+ var self = this;
+ navigator.mozSetMessageHandler('alarm', function gotAlarm(message) {
+ if (message.data !== 'wifi-off')
+ return;
+
+ self.sleep();
+ });
+ }
+};
+
+Wifi.init();
diff --git a/apps/system/js/window.js b/apps/system/js/window.js
new file mode 100644
index 0000000..a9109dd
--- /dev/null
+++ b/apps/system/js/window.js
@@ -0,0 +1,152 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+(function(window){
+
+ var _ = navigator.mozL10n.get;
+
+ var ENABLE_LOG = false;
+
+ // Use mutation observer to monitor appWindow status change
+ window.AppLog = function AppLog(app) {
+ // select the target node
+ var target = app.frame;
+
+ // create an observer instance
+ var observer = new MutationObserver(function(mutations) {
+ mutations.forEach(function(mutation) {
+ console.log(mutation.target.id,
+ mutation.target.className,
+ mutation.attributeName);
+ });
+ });
+
+ // configuration of the observer:
+ var config = { attributes: true };
+
+ // pass in the target node, as well as the observer options
+ observer.observe(target, config);
+ }
+
+ window.AppError = function AppError(app) {
+ var self = this;
+ this.app = app;
+ this.app.frame.addEventListener('mozbrowsererror', function (evt) {
+ if (evt.detail.type != 'other')
+ return;
+
+ console.warn('app of [' + self.app.origin + '] got a mozbrowsererror event.');
+
+ if (self.injected) {
+ self.update();
+ } else {
+ self.render();
+ }
+ self.show();
+ self.injected = true;
+ });
+ return this;
+ };
+
+ AppError.className = 'appError';
+
+ AppError.prototype.hide = function() {
+ this.element.classList.remove('visible');
+ }
+
+ AppError.prototype.show = function() {
+ this.element.classList.add('visible');
+ }
+
+ AppError.prototype.render = function() {
+ this.app.frame.insertAdjacentHTML('beforeend', this.view());
+ this.closeButton = this.app.frame.querySelector('.' + AppError.className + ' .close');
+ this.reloadButton = this.app.frame.querySelector('.' + AppError.className + ' .reload');
+ this.titleElement = this.app.frame.querySelector('.' + AppError.className + ' .title');
+ this.messageElement = this.app.frame.querySelector('.' + AppError.className + ' .message');
+ this.element = this.app.frame.querySelector('.' + AppError.className);
+ var self = this;
+ this.closeButton.onclick = function() {
+ self.app.kill();
+ }
+
+ this.reloadButton.onclick = function() {
+ self.hide();
+ self.app.reload();
+ }
+ }
+
+ AppError.prototype.update = function() {
+ this.titleElement.textContent = this.getTitle();
+ this.messageElement.textContent = this.getMessage();
+ }
+
+ AppError.prototype.id = function() {
+ return AppError.className + '-' + this.app.frame.id;
+ }
+
+ AppError.prototype.getTitle = function() {
+ if (AirplaneMode.enabled) {
+ return _('airplane-is-on');
+ } else if (!navigator.onLine) {
+ return _('network-connection-unavailable');
+ } else {
+ return _('error-title', { name: this.app.name });
+ }
+ }
+
+ AppError.prototype.getMessage = function() {
+ if (AirplaneMode.enabled) {
+ return _('airplane-is-turned-on', { name: this.app.name });
+ } else if (!navigator.onLine) {
+ return _('network-error', { name: this.app.name });
+ } else {
+ return _('error-message', { name: this.app.name });
+ }
+ }
+
+ AppError.prototype.view = function() {
+ return '<div id="' + this.id() + '" class="' + AppError.className + ' visible" role="dialog">' +
+ '<div class="modal-dialog-message-container inner">' +
+ '<h3 data-l10n-id="error-title" class="title">' + this.getTitle() + '</h3>' +
+ '<p>' +
+ '<span data-l10n-id="error-message" class="message">' + this.getMessage() + '</span>' +
+ '</p>' +
+ '</div>' +
+ '<menu data-items="2">' +
+ '<button class="close" data-l10n-id="try-again">' + _('close') + '</button>' +
+ '<button class="reload" data-l10n-id="try-again">' + _('try-again') + '</button>' +
+ '</menu>' +
+ '</div>';
+ }
+
+ window.AppWindow = function AppWindow(configuration) {
+ for (var key in configuration) {
+ this[key] = configuration[key];
+ }
+
+ // We keep the appError object here for the purpose that
+ // we may need to export the error state of AppWindow instance to the other module
+ // in the future.
+ this.appError = new AppError(this);
+ if (ENABLE_LOG)
+ this.appLog = new AppLog(this);
+
+ return this;
+ };
+
+ AppWindow.prototype.reload = function() {
+ this.iframe.reload(true);
+ }
+
+ AppWindow.prototype.kill = function() {
+ // XXX: A workaround because a AppWindow instance shouldn't reference Window Manager directly here.
+ // In the future we should make every app maintain and execute the events in itself.
+ // Like resize, setVisibility...
+ // And Window Manager is in charge of cross app management.
+ WindowManager.kill(this.origin);
+ }
+
+}(this));
diff --git a/apps/system/js/window_manager.js b/apps/system/js/window_manager.js
new file mode 100644
index 0000000..e53605e
--- /dev/null
+++ b/apps/system/js/window_manager.js
@@ -0,0 +1,2011 @@
+/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+//
+// This file calls getElementById without waiting for an onload event, so it
+// must have a defer attribute or be included at the end of the <body>.
+//
+// This module is responsible for launching apps and for allowing
+// the user to switch among apps and kill apps. Specifically, it handles:
+// launching apps,
+// killing apps
+// keeping track of the set of running apps (which we call tasks here)
+// keeping track of which task is displayed (the foreground task)
+// changing the foreground task
+// hiding all apps to display the homescreen
+// displaying the app switcher to allow the user to switch and kill apps
+// performing appropriate transition animations between:
+// the homescreen and an app
+// the homescreen and the switcher
+// an app and the homescreen
+// the switcher and the homescreen
+// the switcher and the current foreground task
+// the switcher and a different task
+// Handling Home key events to switch to the homescreen and the switcher
+//
+// The public API of the module is small. It defines an WindowManager object
+// with these methods:
+//
+// launch(origin): switch to the specified running app
+// kill(origin, callback): stop specified app
+// reload(origin): reload the given app
+// getDisplayedApp(): return the origin of the currently displayed app
+// setOrientationForApp(origin): set the phone to orientation to a given app
+// getAppFrame(origin): returns the iframe element for the specified origin
+// which is assumed to be running. This is only currently used
+// for tests and chrome stuff: see the end of the file
+// getRunningApps(): get the app references of the running apps.
+//
+// TODO
+// The "origin" does not actually refer to app's origin but rather a identifier
+// of the app reference that one gets from |getDisplayedApp()| or
+// iterates |getRunningApps|. The string is make up of the specified
+// launching entry point, origin, or the website url launched by wrapper.
+// It would be ideal if the variable get correctly named and it's rule is being
+// properly documented.
+// See https://bugzilla.mozilla.org/show_bug.cgi?id=796629
+//
+
+var WindowManager = (function() {
+ 'use strict';
+
+ function debug(str) {
+ dump('WindowManager: ' + str + '\n');
+ }
+
+ // Holds the origin of the home screen, which should be the first
+ // app we launch through web activity during boot
+ var homescreen = null;
+ var homescreenURL = '';
+ var homescreenManifestURL = '';
+ var ftu = null;
+ var ftuManifestURL = '';
+ var ftuURL = '';
+ var isRunningFirstRunApp = false;
+ // keep the reference of inline activity frame here
+ var inlineActivityFrames = [];
+ var activityCallerOrigin = '';
+
+ // Some document elements we use
+ var windows = document.getElementById('windows');
+ var screenElement = document.getElementById('screen');
+ var wrapperHeader = document.querySelector('#wrapper-activity-indicator');
+ var wrapperFooter = document.querySelector('#wrapper-footer');
+ var kTransitionTimeout = 1000;
+
+ // Set this to true to debugging the transitions and state change
+ var slowTransition = false;
+ if (slowTransition) {
+ windows.classList.add('slow-transition');
+ }
+
+ //
+ // The set of running apps.
+ // This is a map from app origin to an object like this:
+ // {
+ // name: the app's name
+ // manifest: the app's manifest object
+ // frame: the iframe element that the app is displayed in
+ // launchTime: last time when app gets active
+ // }
+ //
+ var runningApps = {};
+ var numRunningApps = 0; // appendFrame() and removeFrame() maintain this count
+ var nextAppId = 0; // to give each app's iframe a unique id attribute
+
+ // The origin of the currently displayed app, or null if there isn't one
+ var displayedApp = null;
+
+ // Function to hide init starting logo
+ function handleInitlogo(callback) {
+ var initlogo = document.getElementById('initlogo');
+ initlogo.classList.add('hide');
+ initlogo.addEventListener('transitionend', function delInitlogo() {
+ initlogo.removeEventListener('transitionend', delInitlogo);
+ initlogo.parentNode.removeChild(initlogo);
+ if (callback) {
+ callback();
+ }
+ });
+ };
+
+ // Public function. Return the origin of the currently displayed app
+ // or null if there is none.
+ function getDisplayedApp() {
+ return displayedApp || null;
+ }
+
+ function requireFullscreen(origin) {
+ var app = runningApps[origin];
+ if (!app)
+ return false;
+
+ var manifest = app.manifest;
+ if (manifest.entry_points && manifest.type == 'certified') {
+ var entryPoint = manifest.entry_points[origin.split('/')[3]];
+ if (entryPoint)
+ return entryPoint.fullscreen;
+ return false;
+ } else {
+ return manifest.fullscreen;
+ }
+ }
+
+ // Make the specified app the displayed app.
+ // Public function. Pass null to make the homescreen visible
+ function launch(origin) {
+ // If the origin is indeed valid we make that app as the displayed app.
+ if (isRunning(origin)) {
+ setDisplayedApp(origin);
+ return;
+ }
+
+ // If the origin is null, make the homescreen visible.
+ if (origin == null) {
+ setDisplayedApp(homescreen);
+ return;
+ }
+
+ // At this point, we have no choice but to show the homescreen.
+ // We cannot launch/relaunch a given app based on the "origin" because
+ // we would need the manifest URL and the specific entry point.
+ console.warn('No running app is being identified as "' + origin + '". ' +
+ 'Showing home screen instead.');
+ setDisplayedApp(homescreen);
+ }
+
+ function isRunning(origin) {
+ return runningApps.hasOwnProperty(origin);
+ }
+
+ function getAppFrame(origin) {
+ if (isRunning(origin))
+ return runningApps[origin].frame;
+ else
+ return null;
+ }
+
+ // Set the size of the app's iframe to match the size of the screen.
+ // We have to call this on resize events (which happen when the
+ // phone orientation is changed). And also when an app is launched
+ // and each time an app is brought to the front, since the
+ // orientation could have changed since it was last displayed
+ function setAppSize(origin, changeActivityFrame) {
+ var app = runningApps[origin];
+ if (!app)
+ return;
+
+ var frame = app.frame;
+ var manifest = app.manifest;
+
+ var cssWidth = window.innerWidth + 'px';
+ var cssHeight = window.innerHeight - StatusBar.height;
+ if ('wrapper' in frame.dataset) {
+ cssHeight -= 10;
+ }
+ cssHeight += 'px';
+
+ if (!screenElement.classList.contains('attention') &&
+ requireFullscreen(origin)) {
+ cssHeight = window.innerHeight + 'px';
+ }
+
+ frame.style.width = cssWidth;
+ frame.style.height = cssHeight;
+
+ // We will call setInlineActivityFrameSize()
+ // if changeActivityFrame is not explicitly set to false.
+ if (changeActivityFrame !== false)
+ setInlineActivityFrameSize();
+ }
+
+ // App's height is relevant to keyboard height
+ function setAppHeight(keyboardHeight) {
+ var app = runningApps[displayedApp];
+ if (!app)
+ return;
+
+ var frame = app.frame;
+ var manifest = app.manifest;
+
+ var cssHeight =
+ window.innerHeight - StatusBar.height - keyboardHeight + 'px';
+
+ if (!screenElement.classList.contains('attention') &&
+ requireFullscreen(displayedApp)) {
+ cssHeight = window.innerHeight - keyboardHeight + 'px';
+ }
+
+ frame.style.height = cssHeight;
+
+ setInlineActivityFrameSize();
+ }
+
+ // Copy the dimension of the currently displayed app
+ function setInlineActivityFrameSize() {
+ if (!inlineActivityFrames.length)
+ return;
+
+ var app = runningApps[displayedApp];
+ var appFrame = app.frame;
+ var frame = inlineActivityFrames[inlineActivityFrames.length - 1];
+
+ frame.style.width = appFrame.style.width;
+
+ if (document.mozFullScreen) {
+ frame.style.height = window.innerHeight + 'px';
+ frame.style.top = '0px';
+ } else {
+ if ('wrapper' in appFrame.dataset) {
+ frame.style.height = window.innerHeight - StatusBar.height + 'px';
+ } else {
+ frame.style.height = appFrame.style.height;
+ }
+ frame.style.top = appFrame.offsetTop + 'px';
+ }
+ }
+
+ function setFrameBackgroundBlob(frame, blob, transparent) {
+ URL.revokeObjectURL(frame.dataset.bgObjectURL);
+ delete frame.dataset.bgObjectURL;
+
+ var objectURL = URL.createObjectURL(blob);
+ frame.dataset.bgObjectURL = objectURL;
+ var backgroundCSS =
+ '-moz-linear-gradient(top, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0.5) 100%),' +
+ 'url(' + objectURL + '),' +
+ ((transparent) ? 'transparent' : '#fff');
+
+ frame.style.background = backgroundCSS;
+ }
+
+ function clearFrameBackground(frame) {
+ if (!('bgObjectURL' in frame.dataset))
+ return;
+
+ URL.revokeObjectURL(frame.dataset.bgObjectURL);
+ delete frame.dataset.bgObjectURL;
+ frame.style.background = '';
+ }
+
+ var openFrame = null;
+ var closeFrame = null;
+ var openCallback = null;
+ var closeCallback = null;
+ var transitionOpenCallback = null;
+ var transitionCloseCallback = null;
+
+ // Use setOpenFrame() to reset the CSS classes set
+ // to the current openFrame (before overwriting the reference)
+ function setOpenFrame(frame) {
+ if (openFrame) {
+ removeFrameClasses(openFrame);
+ }
+
+ openFrame = frame;
+ }
+
+ // Use setCloseFrame() to reset the CSS classes set
+ // to the current closeFrame (before overwriting the reference)
+ function setCloseFrame(frame) {
+ if (closeFrame) {
+ removeFrameClasses(closeFrame);
+ // closeFrame should not be set to active
+ closeFrame.classList.remove('active');
+ }
+
+ closeFrame = frame;
+ }
+
+ // Remove these visible className from frame so we will not ended
+ // up having a frozen frame in the middle of the transition
+ function removeFrameClasses(frame) {
+ var classNames = ['opening', 'closing', 'opening-switching',
+ 'opening-card', 'closing-card'];
+
+ var classList = frame.classList;
+
+ classNames.forEach(function removeClass(className) {
+ classList.remove(className);
+ });
+ }
+
+ windows.addEventListener('transitionend', function frameTransitionend(evt) {
+ var prop = evt.propertyName;
+ var frame = evt.target;
+ if (prop !== 'transform')
+ return;
+
+ var classList = frame.classList;
+
+ if (classList.contains('inlineActivity')) {
+ if (classList.contains('active')) {
+ if (openFrame)
+ openFrame.firstChild.focus();
+
+ setOpenFrame(null);
+ } else {
+ windows.removeChild(frame);
+ }
+
+ return;
+ }
+
+ if (screenElement.classList.contains('switch-app')) {
+ if (classList.contains('closing')) {
+ classList.remove('closing');
+ classList.add('closing-card');
+
+ if (openFrame) {
+ if (openFrame.classList.contains('opening-card')) {
+ openFrame.classList.remove('opening-card');
+ openFrame.classList.add('opening-switching');
+ } else {
+ // Skip the opening-card and opening-switching transition
+ // because the closing-card transition had already finished here.
+ if (openFrame.classList.contains('fullscreen-app')) {
+ screenElement.classList.add('fullscreen-app');
+ }
+ openFrame.classList.add('opening');
+ }
+ }
+ } else if (classList.contains('closing-card')) {
+ windowClosed(frame);
+ setTimeout(closeCallback);
+ closeCallback = null;
+
+ } else if (classList.contains('opening-switching')) {
+ // If the opening app need to be full screen, switch to full screen
+ if (classList.contains('fullscreen-app')) {
+ screenElement.classList.add('fullscreen-app');
+ }
+
+ classList.remove('opening-switching');
+ classList.add('opening');
+ } else if (classList.contains('opening')) {
+ windowOpened(frame);
+
+ setTimeout(openCallback);
+ openCallback = null;
+
+ setCloseFrame(null);
+ setOpenFrame(null);
+ screenElement.classList.remove('switch-app');
+ }
+
+ return;
+ }
+
+ if (classList.contains('opening')) {
+ windowOpened(frame);
+
+ setTimeout(openCallback);
+ openCallback = null;
+
+ setOpenFrame(null);
+ } else if (classList.contains('closing')) {
+ windowClosed(frame);
+
+ setTimeout(closeCallback);
+ closeCallback = null;
+
+ setCloseFrame(null);
+ }
+ });
+
+ // Executes when the opening transition scale the app
+ // to full size.
+ function windowOpened(frame) {
+ var iframe = frame.firstChild;
+
+ frame.classList.add('active');
+ windows.classList.add('active');
+
+ if ('wrapper' in frame.dataset) {
+ wrapperFooter.classList.add('visible');
+ }
+
+ // Take the focus away from the currently displayed app
+ var app = runningApps[displayedApp];
+ if (app && app.iframe)
+ app.iframe.blur();
+
+ // Give the focus to the frame
+ iframe.focus();
+
+ if (!TrustedUIManager.isVisible() && !isRunningFirstRunApp) {
+ // Set homescreen visibility to false
+ toggleHomescreen(false);
+ }
+
+ // Set displayedApp to the new value
+ displayedApp = iframe.dataset.frameOrigin;
+
+ // Set orientation for the new app
+ setOrientationForApp(displayedApp);
+
+ // Dispatch an 'appopen' event.
+ var manifestURL = runningApps[displayedApp].manifestURL;
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('appopen', true, false, {
+ manifestURL: manifestURL,
+ origin: displayedApp
+ });
+ frame.dispatchEvent(evt);
+ }
+
+ // Executes when app closing transition finishes.
+ function windowClosed(frame) {
+ var iframe = frame.firstChild;
+
+ // If the FTU is closing, make sure we save this state
+ if (iframe.src == ftuURL) {
+ isRunningFirstRunApp = false;
+ document.getElementById('screen').classList.remove('ftu');
+ window.asyncStorage.setItem('ftu.enabled', false);
+ // Done with FTU, letting everyone know
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('ftudone',
+ /* canBubble */ true, /* cancelable */ false, {});
+ window.dispatchEvent(evt);
+ }
+
+ frame.classList.remove('active');
+ windows.classList.remove('active');
+
+ // set the closed frame visibility to false
+ if ('setVisible' in iframe)
+ iframe.setVisible(false);
+
+ screenElement.classList.remove('fullscreen-app');
+ }
+
+ // The following things needs to happen when firstpaint happens.
+ // We centralize all that here but not all of them applies.
+ windows.addEventListener('mozbrowserfirstpaint', function firstpaint(evt) {
+ var iframe = evt.target;
+ var frame = iframe.parentNode;
+
+ // remove the unpainted flag
+ delete iframe.dataset.unpainted;
+
+ setTimeout(function firstpainted() {
+ // Save the screenshot
+ // Remove the background only until we actually got the screenshot,
+ // because the getScreenshot() call will be pushed back by
+ // painting/loading in the child process; when we got the screenshot,
+ // that means the app is mostly loaded.
+ // (as opposed to plain white firstpaint)
+ saveAppScreenshot(frame, function screenshotTaken() {
+ // Remove the default background
+ frame.classList.remove('default-background');
+
+ // Remove the screenshot from frame
+ clearFrameBackground(frame);
+ });
+ });
+ });
+
+ // setFrameBackground() will attach the screenshot background to
+ // the given frame.
+ // The callback could be sync or async (depend on whether we need
+ // the screenshot from database or not)
+ function setFrameBackground(frame, callback, transparent) {
+ var iframe = frame.firstChild;
+ // If the frame is painted, or there is already background image present
+ // start the transition right away.
+ if (!('unpainted' in iframe.dataset) ||
+ ('bgObjectURL' in frame.dataset)) {
+ callback();
+ return;
+ }
+
+ // Get the screenshot from the database
+ getAppScreenshotFromDatabase(iframe.src || iframe.dataset.frameOrigin,
+ function(screenshot) {
+ // If firstpaint is faster than database, we will not transition
+ // with screenshot.
+ if (!('unpainted' in iframe.dataset)) {
+ callback();
+ return;
+ }
+
+ if (!screenshot) {
+ // put a default background
+ frame.classList.add('default-background');
+ callback();
+ return;
+ }
+
+ // set the screenshot as the background of the frame itself.
+ // we are safe to do so since there is nothing on it yet.
+ setFrameBackgroundBlob(frame, screenshot, transparent);
+
+ // start the transition
+ callback();
+ });
+ }
+
+ // On-disk database for window manager.
+ // It's only for app screenshots right now.
+ var database = null;
+ var DB_SCREENSHOT_OBJSTORE = 'screenshots';
+
+ (function openDatabase() {
+ var DB_VERSION = 2;
+ var DB_NAME = 'window_manager';
+
+ var req = window.indexedDB.open(DB_NAME, DB_VERSION);
+ req.onerror = function() {
+ console.error('Window Manager: opening database failed.');
+ };
+ req.onupgradeneeded = function databaseUpgradeneeded() {
+ database = req.result;
+
+ if (database.objectStoreNames.contains(DB_SCREENSHOT_OBJSTORE))
+ database.deleteObjectStore(DB_SCREENSHOT_OBJSTORE);
+
+ var store = database.createObjectStore(
+ DB_SCREENSHOT_OBJSTORE, { keyPath: 'url' });
+ };
+
+ req.onsuccess = function databaseSuccess() {
+ database = req.result;
+ };
+ })();
+
+ function putAppScreenshotToDatabase(url, data) {
+ if (!database)
+ return;
+
+ var txn = database.transaction(DB_SCREENSHOT_OBJSTORE, 'readwrite');
+ txn.onerror = function() {
+ console.warn(
+ 'Window Manager: transaction error while trying to save screenshot.');
+ };
+ var store = txn.objectStore(DB_SCREENSHOT_OBJSTORE);
+ var req = store.put({
+ url: url,
+ screenshot: data
+ });
+ req.onerror = function(evt) {
+ console.warn(
+ 'Window Manager: put error while trying to save screenshot.');
+ };
+ }
+
+ function getAppScreenshotFromDatabase(url, callback) {
+ if (!database) {
+ console.warn(
+ 'Window Manager: Neither database nor app frame is ' +
+ 'ready for getting screenshot.');
+
+ callback();
+ return;
+ }
+
+ var req = database.transaction(DB_SCREENSHOT_OBJSTORE)
+ .objectStore(DB_SCREENSHOT_OBJSTORE).get(url);
+ req.onsuccess = function() {
+ if (!req.result) {
+ console.log('Window Manager: No screenshot in database. ' +
+ 'This is expected from a fresh installed app.');
+ callback();
+
+ return;
+ }
+
+ callback(req.result.screenshot, true);
+ }
+ req.onerror = function(evt) {
+ console.warn('Window Manager: get screenshot from database failed.');
+ callback();
+ };
+ }
+
+ function deleteAppScreenshotFromDatabase(url) {
+ var txn = database.transaction(DB_SCREENSHOT_OBJSTORE);
+ var store = txn.objectStore(DB_SCREENSHOT_OBJSTORE);
+
+ store.delete(url);
+ }
+
+ function getAppScreenshotFromFrame(frame, callback) {
+ if (!frame) {
+ callback();
+ return;
+ }
+
+ var iframe = frame.firstChild;
+ var req = iframe.getScreenshot(iframe.offsetWidth, iframe.offsetHeight);
+
+ req.onsuccess = function gotScreenshotFromFrame(evt) {
+ var result = evt.target.result;
+ callback(result, false);
+ };
+
+ req.onerror = function gotScreenshotFromFrameError(evt) {
+ console.warn('Window Manager: getScreenshot failed.');
+ callback();
+ };
+ }
+
+ // Meta method for get the screenshot from the app frame,
+ // and save it to database.
+ function saveAppScreenshot(frame, callback) {
+ getAppScreenshotFromFrame(frame, function gotScreenshot(screenshot) {
+ if (callback)
+ callback(screenshot);
+
+ if (!screenshot)
+ return;
+
+ var iframe = frame.firstChild;
+ putAppScreenshotToDatabase(iframe.src || iframe.dataset.frameOrigin,
+ screenshot);
+ });
+ }
+
+ // Perform an "open" animation for the app's iframe
+ function openWindow(origin, callback) {
+ var app = runningApps[origin];
+ setOpenFrame(app.frame);
+
+ openCallback = callback || function() {};
+
+ // set the size of the opening app
+ setAppSize(origin);
+
+ if (origin === homescreen) {
+ // We cannot apply background screenshot to home screen app since
+ // the screenshot is encoded in JPEG and the alpha channel is
+ // not perserved. See
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=801676#c33
+ // If that resolves,
+ // setFrameBackground(openFrame, gotBackground, true);
+ // will simply work here.
+
+ // Call the openCallback only once. We have to use tmp var as
+ // openCallback can be a method calling the callback
+ // (like the `removeFrame` callback in `kill()` ).
+ var tmpCallback = openCallback;
+ openCallback = null;
+ tmpCallback();
+
+ windows.classList.add('active');
+ openFrame.classList.add('homescreen');
+ openFrame.firstChild.focus();
+ setOpenFrame(null);
+ displayedApp = origin;
+
+ return;
+ }
+
+ if (requireFullscreen(origin))
+ screenElement.classList.add('fullscreen-app');
+
+ transitionOpenCallback = function startOpeningTransition() {
+ // We have been canceled by another transition.
+ if (!openFrame || transitionOpenCallback != startOpeningTransition)
+ return;
+
+ // Make sure we're not called twice.
+ transitionOpenCallback = null;
+
+ if (!screenElement.classList.contains('switch-app')) {
+ openFrame.classList.add('opening');
+ } else if (!openFrame.classList.contains('opening')) {
+ openFrame.classList.add('opening-card');
+ }
+ };
+
+ if ('unpainted' in openFrame.firstChild.dataset) {
+ waitForNextPaintOrBackground(openFrame, transitionOpenCallback);
+ } else {
+ waitForNextPaint(openFrame, transitionOpenCallback);
+ }
+
+ // Set the frame to be visible.
+ if ('setVisible' in openFrame.firstChild) {
+ if (!AttentionScreen.isFullyVisible()) {
+ openFrame.firstChild.setVisible(true);
+ } else {
+ // If attention screen is fully visible now,
+ // don't give the open frame visible.
+ // This is the case that homescreen is restarted behind attention screen
+ openFrame.firstChild.setVisible(false);
+ }
+ }
+ }
+
+ function waitForNextPaintOrBackground(frame, callback) {
+ var waiting = true;
+ function proceed() {
+ if (waiting) {
+ waiting = false;
+ callback();
+ }
+ }
+
+ waitForNextPaint(frame, proceed);
+ setFrameBackground(frame, proceed);
+ }
+
+ function waitForNextPaint(frame, callback) {
+ function onNextPaint() {
+ clearTimeout(timeout);
+ callback();
+ }
+
+ var iframe = frame.firstChild;
+
+ // Register a timeout in case we don't receive
+ // nextpaint in an acceptable time frame.
+ var timeout = setTimeout(function() {
+ if ('removeNextPaintListener' in iframe)
+ iframe.removeNextPaintListener(onNextPaint);
+ callback();
+ }, kTransitionTimeout);
+
+ if ('addNextPaintListener' in iframe)
+ iframe.addNextPaintListener(onNextPaint);
+ }
+
+ // Perform a "close" animation for the app's iframe
+ function closeWindow(origin, callback) {
+ var app = runningApps[origin];
+ setCloseFrame(app.frame);
+ closeCallback = callback || function() {};
+
+ // Animate the window close. Ensure the homescreen is in the
+ // foreground since it will be shown during the animation.
+ var homescreenFrame = ensureHomescreen();
+
+ // invoke openWindow to show homescreen here
+ openWindow(homescreen, null);
+
+ // Take keyboard focus away from the closing window
+ closeFrame.firstChild.blur();
+
+ // set orientation for homescreen app
+ setOrientationForApp(homescreen);
+
+ // Set the size of both homescreen app and the closing app
+ // since the orientation had changed.
+ setAppSize(homescreen);
+ setAppSize(origin);
+
+ // Send a synthentic 'appwillclose' event.
+ // The keyboard uses this and the appclose event to know when to close
+ // See https://github.com/andreasgal/gaia/issues/832
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('appwillclose', true, false, { origin: origin });
+ closeFrame.dispatchEvent(evt);
+
+ if ('wrapper' in closeFrame.dataset) {
+ wrapperHeader.classList.remove('visible');
+ wrapperFooter.classList.remove('visible');
+ }
+
+ transitionCloseCallback = function startClosingTransition() {
+ // We have been canceled by another transition.
+ if (!closeFrame || transitionCloseCallback != startClosingTransition)
+ return;
+
+ // Make sure we're not called twice.
+ transitionCloseCallback = null;
+
+ // Start the transition
+ closeFrame.classList.add('closing');
+ closeFrame.classList.remove('active');
+ };
+
+ waitForNextPaint(homescreenFrame, transitionCloseCallback);
+ }
+
+ // Perform a "switching" animation for the closing frame and the opening frame
+ function switchWindow(origin, callback) {
+ // This will trigger different transition to both openWindow()
+ // and closeWindow() transition.
+ screenElement.classList.add('switch-app');
+
+ // Ask closeWindow() to start closing the displayedApp
+ closeWindow(displayedApp, callback);
+
+ // Ask openWindow() to show a card on the right waiting to be opened
+ openWindow(origin);
+ }
+
+ // Ensure the homescreen is loaded and return its frame. Restarts
+ // the homescreen app if it was killed in the background.
+ // Note: this function would not invoke openWindow(homescreen),
+ // which should be handled in setDisplayedApp and in closeWindow()
+ function ensureHomescreen(reset) {
+ // If the url of the homescreen is not known at this point do nothing.
+ if (!homescreen || !homescreenManifestURL) {
+ return null;
+ }
+
+ if (!isRunning(homescreen)) {
+ var app = Applications.getByManifestURL(homescreenManifestURL);
+ appendFrame(null, homescreen, homescreenURL,
+ app.manifest.name, app.manifest, app.manifestURL);
+ runningApps[homescreen].iframe.dataset.start = Date.now();
+ setAppSize(homescreen);
+ } else if (reset) {
+ runningApps[homescreen].iframe.src = homescreenURL;
+ setAppSize(homescreen);
+ }
+
+ return runningApps[homescreen].frame;
+ }
+
+ function retrieveHomescreen(callback) {
+ var lock = navigator.mozSettings.createLock();
+ var setting = lock.get('homescreen.manifestURL');
+ setting.onsuccess = function() {
+ var app =
+ Applications.getByManifestURL(this.result['homescreen.manifestURL']);
+
+ // XXX This is a one-day workaround to not break everybody and make sure
+ // work can continue.
+ if (!app) {
+ var tmpURL = document.location.toString()
+ .replace('system', 'homescreen')
+ .replace('index.html', 'manifest.webapp');
+ app = Applications.getByManifestURL(tmpURL);
+ }
+
+ if (app) {
+ homescreenManifestURL = app.manifestURL;
+ homescreen = app.origin;
+ homescreenURL = app.origin + '/index.html#root';
+
+ callback(app);
+ }
+ }
+ }
+
+ function skipFTU() {
+ document.getElementById('screen').classList.remove('ftuStarting');
+ handleInitlogo();
+ setDisplayedApp(homescreen);
+ }
+
+ // Check if the FTU was executed or not, if not, get a
+ // reference to the app and launch it.
+ function retrieveFTU() {
+ window.asyncStorage.getItem('ftu.enabled', function getItem(launchFTU) {
+ document.getElementById('screen').classList.add('ftuStarting');
+ if (launchFTU === false) {
+ skipFTU();
+ return;
+ }
+ var lock = navigator.mozSettings.createLock();
+ var req = lock.get('ftu.manifestURL');
+ req.onsuccess = function() {
+ ftuManifestURL = this.result['ftu.manifestURL'];
+ if (!ftuManifestURL) {
+ dump('FTU manifest cannot be found skipping.\n');
+ skipFTU();
+ return;
+ }
+ ftu = Applications.getByManifestURL(ftuManifestURL);
+ if (!ftu) {
+ dump('Opps, bogus FTU manifest.\n');
+ skipFTU();
+ return;
+ }
+ ftuURL = ftu.origin + ftu.manifest.entry_points['ftu'].launch_path;
+ ftu.launch('ftu');
+ };
+ req.onerror = function() {
+ dump('Couldn\'t get the ftu manifestURL.\n');
+ skipFTU();
+ };
+ });
+ }
+
+ // Hide current app
+ function hideCurrentApp(callback) {
+ if (displayedApp == null || displayedApp == homescreen)
+ return;
+
+ toggleHomescreen(true);
+ var frame = getAppFrame(displayedApp);
+ frame.classList.add('back');
+ frame.classList.remove('restored');
+ if (callback) {
+ frame.addEventListener('transitionend', function execCallback() {
+ frame.style.visibility = 'hidden';
+ frame.removeEventListener('transitionend', execCallback);
+ callback();
+ });
+ }
+ }
+
+ // If app parameter is passed,
+ // it means there's a specific app needs to be restored
+ // instead of current app
+ function restoreCurrentApp(app) {
+ if (app) {
+ // Restore app visibility immediately but don't open it.
+ var frame = getAppFrame(app);
+ frame.style.visibility = 'visible';
+ frame.classList.remove('back');
+ } else {
+ app = displayedApp;
+ toggleHomescreen(false);
+ var frame = getAppFrame(app);
+ frame.style.visibility = 'visible';
+ frame.classList.remove('back');
+ frame.classList.add('restored');
+ frame.addEventListener('transitionend', function removeRestored() {
+ frame.removeEventListener('transitionend', removeRestored);
+ frame.classList.remove('restored');
+ });
+ }
+ }
+
+ function toggleHomescreen(visible) {
+ var homescreenFrame = ensureHomescreen();
+ if (homescreenFrame && 'setVisible' in homescreenFrame.firstChild)
+ homescreenFrame.firstChild.setVisible(visible);
+ }
+
+ // Switch to a different app
+ function setDisplayedApp(origin, callback) {
+ var currentApp = displayedApp, newApp = origin || homescreen;
+ var isFirstRunApplication = !currentApp && (origin == ftuURL);
+
+ var homescreenFrame = null;
+ if (!isFirstRunApplication) {
+ // Returns the frame reference of the home screen app.
+ // Restarts the homescreen app if it was killed in the background.
+ homescreenFrame = ensureHomescreen();
+ }
+
+ // Cancel transitions waiting to be started.
+ transitionOpenCallback = null;
+ transitionCloseCallback = null;
+
+ // Discard any existing activity
+ stopInlineActivity(true);
+
+ // Before starting a new transition, let's make sure current transitions
+ // are stopped and the state classes are cleaned up.
+ // visibility status should also be reset.
+ if (openFrame && 'setVisible' in openFrame.firstChild)
+ openFrame.firstChild.setVisible(false);
+ if (closeFrame && 'setVisible' in closeFrame.firstChild)
+ closeFrame.firstChild.setVisible(false);
+
+ if (!isFirstRunApplication && newApp == homescreen && !AttentionScreen.isFullyVisible()) {
+ toggleHomescreen(true);
+ }
+
+ setOpenFrame(null);
+ setCloseFrame(null);
+ screenElement.classList.remove('switch-app');
+ screenElement.classList.remove('fullscreen-app');
+
+ // Dispatch an appwillopen event only when we open an app
+ if (newApp != currentApp) {
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('appwillopen', true, true, { origin: newApp });
+
+ var app = runningApps[newApp];
+ // Allows listeners to cancel app opening and so stay on homescreen
+ if (!app.frame.dispatchEvent(evt)) {
+ if (typeof(callback) == 'function')
+ callback();
+ return;
+ }
+
+ var iframe = app.iframe;
+
+ // unpainted means that the app is cold booting
+ // if it is, we're going to listen for Browser API's loadend event
+ // which indicates that the iframe's document load is complete
+ //
+ // if the app is not cold booting (is in memory) we will listen
+ // to appopen event, which is fired when the transition to the
+ // app window is complete.
+ //
+ // [w] - warm boot (app is in memory, just transition to it)
+ // [c] - cold boot (app has to be booted, we show it's document load
+ // time)
+ var type;
+ if ('unpainted' in iframe.dataset) {
+ type = 'mozbrowserloadend';
+ } else {
+ iframe.dataset.start = Date.now();
+ type = 'appopen';
+ }
+
+ app.frame.addEventListener(type, function apploaded(e) {
+ e.target.removeEventListener(e.type, apploaded);
+
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('apploadtime', true, false, {
+ time: parseInt(Date.now() - iframe.dataset.start),
+ type: (e.type == 'appopen') ? 'w' : 'c'
+ });
+ iframe.dispatchEvent(evt);
+ });
+ }
+
+ // Case 1: the app is already displayed
+ if (currentApp && currentApp == newApp) {
+ if (newApp == homescreen) {
+ // relaunch homescreen
+ openWindow(homescreen, callback);
+ } else if (callback) {
+ // Just run the callback right away if it is not homescreen
+ callback();
+ }
+ }
+ // Case 2: null --> app
+ else if (isFirstRunApplication) {
+ isRunningFirstRunApp = true;
+ openWindow(newApp, function windowOpened() {
+ handleInitlogo(function() {
+ var mainScreen = document.getElementById('screen');
+ mainScreen.classList.add('ftu');
+ mainScreen.classList.remove('ftuStarting');
+ });
+ });
+ }
+ // Case 3: null->homescreen || homescreen->app
+ else if ((!currentApp && newApp == homescreen) ||
+ (currentApp == homescreen && newApp)) {
+ openWindow(newApp, callback);
+ }
+ // Case 4: app->homescreen
+ else if (currentApp && currentApp != homescreen && newApp == homescreen) {
+ // For screenshot to catch current window size
+ closeWindow(currentApp, callback);
+ }
+ // Case 5: app-to-app transition
+ else {
+ switchWindow(newApp, callback);
+ }
+ // Set homescreen as active,
+ // to control the z-index between homescreen & keyboard iframe
+ if ((newApp == homescreen) && homescreenFrame) {
+ homescreenFrame.classList.add('active');
+ } else {
+ homescreenFrame.classList.remove('active');
+ }
+
+ // Record the time when app was launched,
+ // need this to display apps in proper order on CardsView.
+ // We would also need this to determined the freshness of the frame
+ // for making screenshots.
+ if (newApp)
+ runningApps[newApp].launchTime = Date.now();
+
+ // If the app has a attention screen open, displaying it
+ AttentionScreen.showForOrigin(newApp);
+ }
+
+ function setOrientationForApp(origin) {
+ if (origin == null) { // No app is currently running.
+ screen.mozLockOrientation('portrait-primary');
+ return;
+ }
+
+ var app = runningApps[origin];
+ if (!app)
+ return;
+ var manifest = app.manifest;
+
+ if (manifest.orientation) {
+ var rv = screen.mozLockOrientation(manifest.orientation);
+ if (rv === false) {
+ console.warn('screen.mozLockOrientation() returned false for',
+ origin, 'orientation', manifest.orientation);
+ }
+ }
+ else { // If no orientation was requested, then let it rotate
+ screen.mozUnlockOrientation();
+ }
+ }
+
+ var isOutOfProcessDisabled = false;
+ SettingsListener.observe('debug.oop.disabled', false, function(value) {
+ isOutOfProcessDisabled = value;
+ });
+
+ function createFrame(origFrame, origin, url, name, manifest, manifestURL) {
+ var iframe = origFrame || document.createElement('iframe');
+ iframe.setAttribute('mozallowfullscreen', 'true');
+
+ var frame = document.createElement('div');
+ frame.appendChild(iframe);
+ frame.className = 'appWindow';
+
+ iframe.dataset.frameOrigin = origin;
+ // Save original frame URL in order to restore it on frame load error
+ iframe.dataset.frameURL = url;
+
+ // Note that we don't set the frame size here. That will happen
+ // when we display the app in setDisplayedApp()
+
+ // frames are began unpainted.
+ iframe.dataset.unpainted = true;
+
+ if (!manifestURL) {
+ frame.setAttribute('data-wrapper', 'true');
+ return frame;
+ }
+
+ // Most apps currently need to be hosted in a special 'mozbrowser' iframe.
+ // They also need to be marked as 'mozapp' to be recognized as apps by the
+ // platform.
+ iframe.setAttribute('mozbrowser', 'true');
+
+ // These apps currently have bugs preventing them from being
+ // run out of process. All other apps will be run OOP.
+ //
+ var outOfProcessBlackList = [
+ 'Browser'
+ // Requires nested content processes (bug 761935). This is not
+ // on the schedule for v1.
+ ];
+
+ if (!isOutOfProcessDisabled &&
+ outOfProcessBlackList.indexOf(name) === -1) {
+ // FIXME: content shouldn't control this directly
+ iframe.setAttribute('remote', 'true');
+ }
+
+ iframe.setAttribute('mozapp', manifestURL);
+ iframe.src = url;
+ return frame;
+ }
+
+ function appendFrame(origFrame, origin, url, name, manifest, manifestURL) {
+ // Create the <iframe mozbrowser mozapp> that hosts the app
+ var frame =
+ createFrame(origFrame, origin, url, name, manifest, manifestURL);
+ var iframe = frame.firstChild;
+ frame.id = 'appframe' + nextAppId++;
+ iframe.dataset.frameType = 'window';
+
+ // Give a name to the frame for differentiating between main frame and
+ // inline frame. With the name we can get frames of the same app using the
+ // window.open method.
+ iframe.name = 'main';
+
+ // If this frame corresponds to the homescreen, set mozapptype=homescreen
+ // so we're less likely to kill this frame's process when we're running low
+ // on memory.
+ //
+ // We must do this before we the appendChild() call below. Once
+ // we add this frame to the document, we can't change its app type.
+ if (origin === homescreen) {
+ iframe.setAttribute('mozapptype', 'homescreen');
+ }
+
+ // Add the iframe to the document
+ windows.appendChild(frame);
+
+ // And map the app origin to the info we need for the app
+ var app = new AppWindow({
+ origin: origin,
+ name: name,
+ manifest: manifest,
+ manifestURL: manifestURL,
+ frame: frame,
+ iframe: iframe,
+ launchTime: 0
+ });
+ runningApps[origin] = app;
+
+ if (requireFullscreen(origin)) {
+ frame.classList.add('fullscreen-app');
+ }
+ if (origin === ftuURL) {
+ // Add a way to identify ftu app
+ // (Used by SimLock)
+ frame.classList.add('ftu');
+ }
+
+ // A frame should start with visible false
+ if ('setVisible' in iframe)
+ iframe.setVisible(false);
+
+ numRunningApps++;
+
+ return app;
+ }
+
+ function startInlineActivity(origin, url, name, manifest, manifestURL) {
+ // Create the <iframe mozbrowser mozapp> that hosts the app
+ var frame = createFrame(null, origin, url, name, manifest, manifestURL);
+ var iframe = frame.firstChild;
+ frame.classList.add('inlineActivity');
+ iframe.dataset.frameType = 'inline-activity';
+
+ // Give a name to the frame for differentiating between main frame and
+ // inline frame. With the name we can get frames of the same app using the
+ // window.open method.
+ iframe.name = 'inline';
+
+ // Save the reference
+ inlineActivityFrames.push(frame);
+
+ // Set the size
+ setInlineActivityFrameSize();
+
+ // Add the iframe to the document
+ windows.appendChild(frame);
+
+ // Open the frame, first, store the reference
+ openFrame = frame;
+
+ // set the frame to visible state
+ if ('setVisible' in iframe)
+ iframe.setVisible(true);
+
+ setFrameBackground(openFrame, function gotBackground() {
+ // Start the transition when this async/sync callback is called.
+ openFrame.classList.add('active');
+ if (inlineActivityFrames.length == 1)
+ activityCallerOrigin = displayedApp;
+ if ('wrapper' in runningApps[displayedApp].frame.dataset) {
+ wrapperFooter.classList.remove('visible');
+ wrapperHeader.classList.remove('visible');
+ }
+ });
+ }
+
+ function removeFrame(origin) {
+ var app = runningApps[origin];
+ var frame = app.frame;
+
+ if (frame) {
+ windows.removeChild(frame);
+ clearFrameBackground(frame);
+ }
+
+ if (openFrame == frame) {
+ setOpenFrame(null);
+ setTimeout(openCallback);
+ openCallback = null;
+ }
+ if (closeFrame == frame) {
+ setCloseFrame(null);
+ setTimeout(closeCallback);
+ closeCallback = null;
+ }
+
+ delete runningApps[origin];
+ numRunningApps--;
+ }
+
+ function removeInlineFrame(frame) {
+ // If frame is transitioning we should remove the reference
+ if (openFrame == frame)
+ setOpenFrame(null);
+
+ // If frame is never set visible, we can remove the frame directly
+ // without closing transition
+ if (!frame.classList.contains('active')) {
+ windows.removeChild(frame);
+ return;
+ }
+ // Take keyboard focus away from the closing window
+ frame.firstChild.blur();
+ // Remove the active class and start the closing transition
+ frame.classList.remove('active');
+ }
+
+ // If all is not specified,
+ // remove the top most frame
+ function stopInlineActivity(all) {
+ if (!inlineActivityFrames.length)
+ return;
+
+ if (!all) {
+ var frame = inlineActivityFrames.pop();
+ removeInlineFrame(frame);
+ } else {
+ // stop all activity frames
+ // Remore the inlineActivityFrame reference
+ for (var frame of inlineActivityFrames) {
+ removeInlineFrame(frame);
+ }
+ inlineActivityFrames = [];
+ }
+
+ if (!inlineActivityFrames.length) {
+ // Give back focus to the displayed app
+ var app = runningApps[displayedApp];
+ if (app && app.iframe) {
+ app.iframe.focus();
+ if ('wrapper' in app.frame.dataset) {
+ wrapperFooter.classList.add('visible');
+ }
+ }
+ screenElement.classList.remove('inline-activity');
+ }
+ }
+
+ // Watch activity completion here instead of activity.js
+ // Because we know when and who to re-launch when activity ends.
+ window.addEventListener('mozChromeEvent', function(e) {
+ if (e.detail.type == 'activity-done') {
+ // Remove the top most frame every time we get an 'activity-done' event.
+ stopInlineActivity();
+ if (!inlineActivityFrames.length) {
+ setDisplayedApp(activityCallerOrigin);
+ activityCallerOrigin = '';
+ }
+ }
+ });
+
+ // There are two types of mozChromeEvent we need to handle
+ // in order to launch the app for Gecko
+ window.addEventListener('mozChromeEvent', function(e) {
+ var startTime = Date.now();
+
+ var manifestURL = e.detail.manifestURL;
+ if (!manifestURL)
+ return;
+
+ var app = Applications.getByManifestURL(manifestURL);
+ if (!app)
+ return;
+
+ var manifest = app.manifest;
+ var name = new ManifestHelper(manifest).name;
+ var origin = app.origin;
+
+ // Check if it's a virtual app from a entry point.
+ // If so, change the app name and origin to the
+ // entry point.
+ var entryPoints = manifest.entry_points;
+ if (entryPoints && manifest.type == 'certified') {
+ var givenPath = e.detail.url.substr(origin.length);
+
+ // Workaround here until the bug (to be filed) is fixed
+ // Basicly, gecko is sending the URL without launch_path sometimes
+ for (var ep in entryPoints) {
+ var currentEp = entryPoints[ep];
+ var path = givenPath;
+ if (path.indexOf('?') != -1) {
+ path = path.substr(0, path.indexOf('?'));
+ }
+
+ //Remove the origin and / to find if if the url is the entry point
+ if (path.indexOf('/' + ep) == 0 &&
+ (currentEp.launch_path == path)) {
+ origin = origin + currentEp.launch_path;
+ name = new ManifestHelper(currentEp).name;
+ }
+ }
+ }
+ switch (e.detail.type) {
+ // mozApps API is asking us to launch the app
+ // We will launch it in foreground
+ case 'webapps-launch':
+ if (origin == homescreen) {
+ // No need to append a frame if is homescreen
+ setDisplayedApp();
+ } else {
+ if (!isRunning(origin)) {
+ appendFrame(null, origin, e.detail.url,
+ name, app.manifest, app.manifestURL);
+ }
+ runningApps[origin].iframe.dataset.start = startTime;
+ setDisplayedApp(origin, null, 'window');
+ }
+ break;
+ // System Message Handler API is asking us to open the specific URL
+ // that handles the pending system message.
+ // We will launch it in background if it's not handling an activity.
+ case 'open-app':
+ // If the system message goes to System app,
+ // we should not be launching that in a frame.
+ if (e.detail.url === window.location.href)
+ return;
+
+ if (e.detail.isActivity && e.detail.target.disposition &&
+ e.detail.target.disposition == 'inline') {
+ // Inline activities behaves more like a dialog,
+ // let's deal them here.
+
+ startInlineActivity(origin, e.detail.url,
+ name, manifest, app.manifestURL);
+
+ return;
+ }
+
+ if (isRunning(origin)) {
+ // If the app is in foreground, it's too risky to change it's
+ // URL. We'll ignore this request.
+ if (displayedApp !== origin) {
+ var iframe = getAppFrame(origin).firstChild;
+
+ // If the app is opened and it is loaded to the correct page,
+ // then there is nothing to do.
+ if (iframe.src !== e.detail.url) {
+ // Rewrite the URL of the app frame to the requested URL.
+ // XXX: We could ended opening URls not for the app frame
+ // in the app frame. But we don't care.
+ iframe.src = e.detail.url;
+ }
+ }
+ } else if (origin !== homescreen) {
+ // XXX: We could ended opening URls not for the app frame
+ // in the app frame. But we don't care.
+ appendFrame(null, origin, e.detail.url,
+ name, manifest, app.manifestURL);
+
+ // set the size of the iframe
+ // so Cards View will get a correct screenshot of the frame
+ if (!e.detail.isActivity)
+ setAppSize(origin, false);
+ } else {
+ ensureHomescreen();
+ }
+
+ // We will only bring web activity handling apps to the foreground
+ if (!e.detail.isActivity)
+ return;
+
+ // XXX: the correct way would be for UtilityTray to close itself
+ // when there is a appwillopen/appopen event.
+ UtilityTray.hide();
+
+ setDisplayedApp(origin);
+
+ break;
+ }
+ });
+
+ // If the application tried to close themselves by calling window.close()
+ // we will handle that here.
+ // XXX: this event is fired twice:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=814583
+ window.addEventListener('mozbrowserclose', function(e) {
+ if (!'frameType' in e.target.dataset)
+ return;
+
+ switch (e.target.dataset.frameType) {
+ case 'window':
+ kill(e.target.dataset.frameOrigin);
+ break;
+
+ case 'inline-activity':
+ stopInlineActivity(true);
+ break;
+ }
+ });
+
+ // Deal with locationchange
+ window.addEventListener('mozbrowserlocationchange', function(e) {
+ if (!'frameType' in e.target.dataset)
+ return;
+
+ e.target.dataset.url = e.detail;
+ });
+
+ // Deal with application uninstall event
+ // if the application is being uninstalled, we ensure it stop running here.
+ window.addEventListener('applicationuninstall', function(e) {
+ kill(e.detail.application.origin);
+
+ deleteAppScreenshotFromDatabase(e.detail.application.origin);
+ });
+
+ // When an UI layer is overlapping the current app,
+ // WindowManager should set the visibility of app iframe to false
+ // And reset to true when the layer is gone.
+ // We may need to handle windowclosing, windowopened in the future.
+ var attentionScreenTimer = null;
+
+ var overlayEvents = [
+ 'lock',
+ 'will-unlock',
+ 'attentionscreenshow',
+ 'attentionscreenhide',
+ 'status-active',
+ 'status-inactive'
+ ];
+
+ function overlayEventHandler(evt) {
+ if (attentionScreenTimer)
+ clearTimeout(attentionScreenTimer);
+ switch (evt.type) {
+ case 'status-active':
+ case 'attentionscreenhide':
+ case 'will-unlock':
+ if (LockScreen.locked)
+ return;
+ if (inlineActivityFrames.length) {
+ setVisibilityForInlineActivity(true);
+ } else {
+ setVisibilityForCurrentApp(true);
+ }
+ break;
+ case 'lock':
+ setVisibilityForCurrentApp(false);
+ break;
+
+ /*
+ * Because in-transition is needed in attention screen,
+ * We set a timer here to deal with visibility change
+ */
+ case 'status-inactive':
+ if (!AttentionScreen.isVisible())
+ return;
+ case 'attentionscreenshow':
+ if (evt.detail && evt.detail.origin &&
+ evt.detail.origin != displayedApp) {
+ attentionScreenTimer = setTimeout(function setVisibility() {
+ if (inlineActivityFrames.length) {
+ setVisibilityForInlineActivity(false);
+ } else {
+ setVisibilityForCurrentApp(false);
+ }
+ }, 3000);
+
+ // Immediatly blur the frame in order to ensure hiding the keyboard
+ var app = runningApps[displayedApp];
+ if (app)
+ app.iframe.blur();
+ }
+ break;
+ }
+ }
+
+ overlayEvents.forEach(function overlayEventIterator(event) {
+ window.addEventListener(event, overlayEventHandler);
+ });
+
+ function setVisibilityForInlineActivity(visible) {
+ if (!inlineActivityFrames.length)
+ return;
+
+ var topFrame = inlineActivityFrames[inlineActivityFrames.length - 1].firstChild;
+ if ('setVisible' in topFrame) {
+ topFrame.setVisible(visible);
+ }
+
+ // Restore/give away focus on visiblity change
+ // so that the app can take back its focus
+ if (visible) {
+ topFrame.focus();
+ } else {
+ topFrame.blur();
+ }
+ }
+
+ function setVisibilityForCurrentApp(visible) {
+ var app = runningApps[displayedApp];
+ if (!app)
+ return;
+ if ('setVisible' in app.iframe)
+ app.iframe.setVisible(visible);
+
+ // Restore/give away focus on visiblity change
+ // so that the app can take back its focus
+ if (visible)
+ app.iframe.focus();
+ else
+ app.iframe.blur();
+ }
+
+ function handleAppCrash(origin, manifestURL) {
+ if (origin && manifestURL) {
+ // When inline activity frame crashes,
+ // query the localized name from manifest
+ var app = Applications.getByManifestURL(manifestURL);
+ CrashReporter.setAppName(getAppName(origin, app.manifest));
+ } else {
+ var app = runningApps[displayedApp];
+ CrashReporter.setAppName(app.name);
+ }
+ }
+
+ function getAppName(origin, manifest) {
+ if (!manifest)
+ return '';
+
+ if (manifest.entry_points && manifest.type == 'certified') {
+ var entryPoint = manifest.entry_points[origin.split('/')[3]];
+ return new ManifestHelper(entryPoint).name;
+ }
+ return new ManifestHelper(manifest).name;
+ }
+
+ // Deal with crashed apps
+ window.addEventListener('mozbrowsererror', function(e) {
+ if (!'frameType' in e.target.dataset)
+ return;
+
+ var origin = e.target.dataset.frameOrigin;
+ var manifestURL = e.target.getAttribute('mozapp');
+
+ if (e.target.dataset.frameType == 'inline-activity') {
+ stopInlineActivity(true);
+ handleAppCrash(origin, manifestURL);
+ return;
+ }
+
+ if (e.target.dataset.frameType !== 'window')
+ return;
+
+ /*
+ detail.type = error (Server Not Found case)
+ is handled in Modal Dialog
+ */
+ if (e.detail.type !== 'fatal')
+ return;
+
+ // If the crashing app is currently displayed, we will present
+ // the user with a banner notification.
+ if (displayedApp == origin)
+ handleAppCrash();
+
+ // If the crashing app is the home screen app and it is the displaying app
+ // we will need to relaunch it right away.
+ // Alternatively, if home screen is not the displaying app,
+ // we will not relaunch it until the foreground app is closed.
+ // (to be dealt in setDisplayedApp(), not here)
+ if (displayedApp == homescreen) {
+ kill(origin, function relaunchHomescreen() {
+ setDisplayedApp(homescreen);
+ });
+ return;
+ }
+
+ // Actually remove the frame, and trigger the closing transition
+ // if the app is currently displaying
+ kill(origin);
+ });
+
+
+ function hasPermission(app, permission) {
+ var mozPerms = navigator.mozPermissionSettings;
+ if (!mozPerms)
+ return false;
+
+ var value = mozPerms.get(permission, app.manifestURL, app.origin, false);
+
+ return (value === 'allow');
+ }
+
+ // Use a setting in order to be "called" by settings app
+ navigator.mozSettings.addObserver(
+ 'clear.remote-windows.data',
+ function clearRemoteWindowsData(setting) {
+ var shouldClear = setting.settingValue;
+ if (!shouldClear)
+ return;
+
+ // Delete all storage and cookies from our content processes
+ var request = navigator.mozApps.getSelf();
+ request.onsuccess = function() {
+ request.result.clearBrowserData();
+ };
+
+ // Reset the setting value to false
+ var lock = navigator.mozSettings.createLock();
+ lock.set({'clear.remote-windows.data': false});
+ });
+
+ // Watch for window.open usages in order to open wrapper frames
+ window.addEventListener('mozbrowseropenwindow', function handleWrapper(evt) {
+ var detail = evt.detail;
+ var features;
+ try {
+ features = JSON.parse(detail.features);
+ } catch (e) {
+ features = {};
+ }
+
+ // Handles only call to window.open with `{remote: true}` feature.
+ if (!features.remote)
+ return;
+
+ // XXX bug 819882: for now, only allows homescreen to open oop windows
+ var callerIframe = evt.target;
+ var callerFrame = callerIframe.parentNode;
+ var manifestURL = callerIframe.getAttribute('mozapp');
+ var callerApp = Applications.getByManifestURL(manifestURL);
+ if (!callerApp || !callerFrame.classList.contains('homescreen'))
+ return;
+ var callerOrigin = callerApp.origin;
+
+ // So, we are going to open a remote window.
+ // Now, avoid PopupManager listener to be fired.
+ evt.stopImmediatePropagation();
+
+ var name = detail.name;
+ var url = detail.url;
+
+ // Use fake origin for named windows in order to be able to reuse them,
+ // otherwise always open a new window for '_blank'.
+ var origin = null;
+ var app = null;
+ if (name == '_blank') {
+ origin = url;
+
+ // Just bring on top if a wrapper window is already running with this url
+ if (origin in runningApps &&
+ runningApps[origin].windowName == '_blank') {
+ setDisplayedApp(origin);
+ return;
+ }
+ } else {
+ origin = 'window:' + name + ',source:' + callerOrigin;
+
+ var runningApp = runningApps[origin];
+ if (runningApp && runningApp.windowName === name) {
+ if (runningApp.iframe.src === url) {
+ // If the url is already loaded, just display the app
+ setDisplayedApp(origin);
+ return;
+ } else {
+ // Wrapper context shouldn't be shared between two apps -> killing
+ kill(origin);
+ }
+ }
+ }
+
+ var title = '', icon = '', remote = false, useAsyncPanZoom = false;
+ var originName, originURL, searchName, searchURL;
+
+ try {
+ var features = JSON.parse(detail.features);
+ var regExp = new RegExp('&nbsp;', 'g');
+
+ title = features.name.replace(regExp, ' ') || url;
+ icon = features.icon || '';
+
+ if (features.origin) {
+ originName = features.origin.name.replace(regExp, ' ');
+ originURL = decodeURIComponent(features.origin.url);
+ }
+
+ if (features.search) {
+ searchName = features.search.name.replace(regExp, ' ');
+ searchURL = decodeURIComponent(features.search.url);
+ }
+
+ if (features.useAsyncPanZoom)
+ useAsyncPanZoom = true;
+ } catch (ex) { }
+
+ // If we don't reuse an existing app, open a brand new one
+ var iframe;
+ if (!app) {
+ // Bug 807438: Move new window document OOP
+ // Ignore `event.detail.frameElement` for now in order
+ // to create a remote system app frame.
+ // So that new window documents are going to share
+ // system app content processes data jar.
+ iframe = document.createElement('iframe');
+ iframe.setAttribute('mozbrowser', 'true');
+ iframe.setAttribute('remote', 'true');
+
+ iframe.addEventListener('mozbrowserloadstart', function start() {
+ iframe.dataset.loading = true;
+ wrapperHeader.classList.add('visible');
+ });
+
+ iframe.addEventListener('mozbrowserloadend', function end() {
+ delete iframe.dataset.loading;
+ wrapperHeader.classList.remove('visible');
+ });
+
+ // `mozasyncpanzoom` only works when added before attaching the iframe
+ // node to the document.
+ if (useAsyncPanZoom) {
+ iframe.dataset.useAsyncPanZoom = true;
+ iframe.setAttribute('mozasyncpanzoom', 'true');
+ }
+
+ var app = appendFrame(iframe, origin, url, title, {
+ 'name': title
+ }, null);
+
+ // Set the window name in order to reuse this app if we try to open
+ // a new window with same name
+ app.windowName = name;
+ } else {
+ iframe = app.iframe;
+
+ // Update app name for the card view
+ app.manifest.name = title;
+ }
+
+ iframe.dataset.name = title;
+ iframe.dataset.icon = icon;
+
+ if (originName)
+ iframe.dataset.originName = originName;
+ if (originURL)
+ iframe.dataset.originURL = originURL;
+
+ if (searchName)
+ iframe.dataset.searchName = searchName;
+ if (searchURL)
+ iframe.dataset.searchURL = searchURL;
+
+ // First load blank page in order to hide previous website
+ iframe.src = url;
+
+ setDisplayedApp(origin);
+ }, true); // Use capture in order to catch the event before PopupManager does
+
+
+ // Stop running the app with the specified origin
+ function kill(origin, callback) {
+ if (!isRunning(origin))
+ return;
+
+ // As we can't immediatly remove runningApps entry,
+ // we flag it as being killed in order to avoid trying to remove it twice.
+ // (Check required because of bug 814583)
+ if (runningApps[origin].killed)
+ return;
+ runningApps[origin].killed = true;
+
+ // If the app is the currently displayed app, switch to the homescreen
+ if (origin === displayedApp) {
+ // when the homescreen is displayed and being
+ // killed we need to forcibly restart it...
+ if (origin === homescreen) {
+ removeFrame(origin);
+
+ // XXX workaround bug 810431.
+ // we need this here and not in other situations
+ // as it is expected that homescreen frame is available.
+ setTimeout(function() {
+ setDisplayedApp();
+
+ if (callback) {
+ callback();
+ }
+ });
+ } else {
+ setDisplayedApp(homescreen, function() {
+ removeFrame(origin);
+ if (callback)
+ setTimeout(callback);
+ });
+ }
+
+ } else {
+ removeFrame(origin);
+ }
+
+ // Send a synthentic 'appterminated' event.
+ // Let other system app module know an app is
+ // being killed, removed or crashed.
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('appterminated', true, false, { origin: origin });
+ window.dispatchEvent(evt);
+ }
+
+ // Reload the frame of the running app
+ function reload(origin) {
+ if (!isRunning(origin))
+ return;
+
+ var app = runningApps[origin];
+ app.reload();
+ }
+
+ // When a resize event occurs, resize the running app, if there is one
+ // When the status bar is active it doubles in height so we need a resize
+ var appResizeEvents = ['resize', 'status-active', 'status-inactive',
+ 'keyboardchange', 'keyboardhide',
+ 'attentionscreenhide'];
+ appResizeEvents.forEach(function eventIterator(event) {
+ window.addEventListener(event, function on(evt) {
+ if (event == 'keyboardchange') {
+ // Cancel fullscreen if keyboard pops
+ if (document.mozFullScreen)
+ document.mozCancelFullScreen();
+
+ setAppHeight(evt.detail.height);
+ } else if (displayedApp) {
+ setAppSize(displayedApp);
+ }
+ });
+ });
+
+ window.addEventListener('home', function(e) {
+ // If the lockscreen is active, it will stop propagation on this event
+ // and we'll never see it here. Similarly, other overlays may use this
+ // event to hide themselves and may prevent the event from getting here.
+ // Note that for this to work, the lockscreen and other overlays must
+ // be included in index.html before this one, so they can register their
+ // event handlers before we do.
+
+ // If we are currently transitioning, the user would like to cancel
+ // it instead of toggling homescreen panels.
+ var inTransition = !!(openFrame || closeFrame);
+
+ if (document.mozFullScreen) {
+ document.mozCancelFullScreen();
+ }
+
+ if (displayedApp !== homescreen || inTransition) {
+ if (displayedApp != ftuURL) {
+ setDisplayedApp(homescreen);
+ } else {
+ e.preventDefault();
+ }
+ } else {
+ stopInlineActivity(true);
+ ensureHomescreen(true);
+ }
+ });
+
+ // Cancel dragstart event to workaround
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=783076
+ // which stops OOP home screen pannable with left mouse button on
+ // B2G/Desktop.
+ windows.addEventListener('dragstart', function(evt) {
+ evt.preventDefault();
+ }, true);
+
+ // With all important event handlers in place, we can now notify
+ // Gecko that we're ready for certain system services to send us
+ // messages (e.g. the radio).
+ // Note that shell.js starts listen for the mozContentEvent event at
+ // mozbrowserloadstart, which sometimes does not happen till window.onload.
+ window.addEventListener('load', function wm_loaded() {
+ window.removeEventListener('load', wm_loaded);
+
+ var evt = new CustomEvent('mozContentEvent',
+ { bubbles: true, cancelable: false,
+ detail: { type: 'system-message-listener-ready' } });
+ window.dispatchEvent(evt);
+ });
+
+ // This is code copied from
+ // http://dl.dropbox.com/u/8727858/physical-events/index.html
+ // It appears to workaround the Nexus S bug where we're not
+ // getting orientation data. See:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=753245
+ // It seems it needs to be in both window_manager.js and bootstrap.js.
+ function dumbListener2(event) {}
+ window.addEventListener('devicemotion', dumbListener2);
+
+ window.setTimeout(function() {
+ window.removeEventListener('devicemotion', dumbListener2);
+ }, 2000);
+
+ // Return the object that holds the public API
+ return {
+ isFtuRunning: function() {
+ return isRunningFirstRunApp;
+ },
+ launch: launch,
+ kill: kill,
+ reload: reload,
+ getDisplayedApp: getDisplayedApp,
+ setOrientationForApp: setOrientationForApp,
+ getAppFrame: getAppFrame,
+ getRunningApps: function() {
+ return runningApps;
+ },
+ setDisplayedApp: setDisplayedApp,
+ getCurrentDisplayedApp: function() {
+ return runningApps[displayedApp];
+ },
+ hideCurrentApp: hideCurrentApp,
+ restoreCurrentApp: restoreCurrentApp,
+ retrieveHomescreen: retrieveHomescreen,
+ retrieveFTU: retrieveFTU
+ };
+}());
+
diff --git a/apps/system/js/wrapper.js b/apps/system/js/wrapper.js
new file mode 100644
index 0000000..160d70b
--- /dev/null
+++ b/apps/system/js/wrapper.js
@@ -0,0 +1,198 @@
+/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var Launcher = (function() {
+ function log(str) {
+ dump(' -+- Launcher -+-: ' + str + '\n');
+ }
+
+ function currentAppFrame() {
+ return WindowManager.getAppFrame(WindowManager.getDisplayedApp());
+ }
+
+
+ function currentAppIframe() {
+ return currentAppFrame().firstChild;
+ }
+
+ var _ = navigator.mozL10n.get;
+
+ var BUTTONBAR_TIMEOUT = 5000;
+ var BUTTONBAR_INITIAL_OPEN_TIMEOUT = 1500;
+
+ var footer = document.querySelector('#wrapper-footer');
+ window.addEventListener('appopen', function onAppOpen(e) {
+ if ('wrapper' in currentAppFrame().dataset) {
+ window.addEventListener('mozbrowserlocationchange', onLocationChange);
+ onLocationChange();
+ onDisplayedApplicationChange();
+ }
+ });
+
+ window.addEventListener('appwillclose', function onAppClose(e) {
+ if ('wrapper' in currentAppFrame().dataset) {
+ window.removeEventListener('mozbrowserlocationchange', onLocationChange);
+ clearTimeout(buttonBarTimeout);
+ footer.classList.add('closed');
+ isButtonBarDisplayed = false;
+ }
+ });
+
+ window.addEventListener('keyboardchange', function onKeyboardChange(e) {
+ if ('wrapper' in currentAppFrame().dataset) {
+ if (footer.classList.contains('visible')) {
+ footer.classList.remove('visible');
+ }
+ }
+ });
+
+ window.addEventListener('keyboardhide', function onKeyboardChange(e) {
+ if ('wrapper' in currentAppFrame().dataset) {
+ if (!footer.classList.contains('visible')) {
+ footer.classList.add('visible');
+ }
+ }
+ });
+
+ var buttonBarTimeout;
+
+ var isButtonBarDisplayed = false;
+ function toggleButtonBar(time) {
+ clearTimeout(buttonBarTimeout);
+ footer.classList.toggle('closed');
+ isButtonBarDisplayed = !isButtonBarDisplayed;
+ if (isButtonBarDisplayed) {
+ buttonBarTimeout = setTimeout(toggleButtonBar, time || BUTTONBAR_TIMEOUT);
+ }
+ }
+
+ function clearButtonBarTimeout() {
+ clearTimeout(buttonBarTimeout);
+ buttonBarTimeout = setTimeout(toggleButtonBar, BUTTONBAR_TIMEOUT);
+ }
+
+ document.getElementById('handler').
+ addEventListener('mousedown', function open() { toggleButtonBar() });
+
+ document.getElementById('close-button').
+ addEventListener('mousedown', function close() { toggleButtonBar() });
+
+ var reload = document.getElementById('reload-button');
+ reload.addEventListener('click', function doReload(evt) {
+ clearButtonBarTimeout();
+ currentAppIframe().reload(true);
+ });
+
+ var back = document.getElementById('back-button');
+ back.addEventListener('click', function goBack() {
+ clearButtonBarTimeout();
+ currentAppIframe().goBack();
+ });
+
+ var forward = document.getElementById('forward-button');
+ forward.addEventListener('click', function goForward() {
+ clearButtonBarTimeout();
+ currentAppIframe().goForward();
+ });
+
+ function onLocationChange() {
+ currentAppIframe().getCanGoForward().onsuccess = function forwardSuccess(e) {
+ if (e.target.result === true) {
+ delete forward.dataset.disabled;
+ } else {
+ forward.dataset.disabled = true;
+ }
+ }
+
+ currentAppIframe().getCanGoBack().onsuccess = function backSuccess(e) {
+ if (e.target.result === true) {
+ delete back.dataset.disabled;
+ } else {
+ back.dataset.disabled = true;
+ }
+ }
+ }
+
+ window.addEventListener('mozbrowserlocationchange', onLocationChange);
+
+ var bookmarkButton = document.getElementById('bookmark-button');
+ function onDisplayedApplicationChange() {
+ toggleButtonBar(BUTTONBAR_INITIAL_OPEN_TIMEOUT);
+
+ var dataset = currentAppIframe().dataset;
+ if (dataset.originURL || dataset.searchURL) {
+ delete bookmarkButton.dataset.disabled;
+ return;
+ }
+
+ bookmarkButton.dataset.disabled = true;
+ }
+
+ bookmarkButton.addEventListener('click', function doBookmark(evt) {
+ if (bookmarkButton.dataset.disabled)
+ return;
+
+ clearButtonBarTimeout();
+ var dataset = currentAppIframe().dataset;
+
+ function selected(value) {
+ if (!value)
+ return;
+
+ var name, url;
+ if (value === 'origin') {
+ name = dataset.originName;
+ url = dataset.originURL;
+ }
+
+ if (value === 'search') {
+ name = dataset.searchName;
+ url = dataset.searchURL;
+ }
+
+ var activity = new MozActivity({
+ name: 'save-bookmark',
+ data: {
+ type: 'url',
+ url: url,
+ name: name,
+ icon: dataset.icon,
+ useAsyncPanZoom: dataset.useAsyncPanZoom,
+ iconable: false
+ }
+ });
+
+ activity.onsuccess = function onsuccess() {
+ if (value === 'origin') {
+ delete currentAppIframe().dataset.originURL;
+ }
+
+ if (value === 'search') {
+ delete currentAppIframe().dataset.searchURL;
+ }
+
+ if (!currentAppIframe().dataset.originURL &&
+ !currentAppIframe().dataset.searchURL) {
+ bookmarkButton.dataset.disabled = true;
+ }
+ }
+ }
+
+ var data = {
+ title: _('add-to-home-screen'),
+ options: []
+ };
+
+ if (dataset.originURL) {
+ data.options.push({ id: 'origin', text: dataset.originName });
+ }
+
+ if (dataset.searchURL) {
+ data.options.push({ id: 'search', text: dataset.searchName });
+ }
+
+ ModalDialog.selectOne(data, selected);
+ });
+}());
diff --git a/apps/system/locales/locales.ini b/apps/system/locales/locales.ini
new file mode 100644
index 0000000..c6d5fc2
--- /dev/null
+++ b/apps/system/locales/locales.ini
@@ -0,0 +1,11 @@
+@import url(system.en-US.properties)
+
+[ar]
+@import url(system.ar.properties)
+
+[fr]
+@import url(system.fr.properties)
+
+[zh-TW]
+@import url(system.zh-TW.properties)
+
diff --git a/apps/system/locales/system.ar.properties b/apps/system/locales/system.ar.properties
new file mode 100644
index 0000000..9a8b5f9
--- /dev/null
+++ b/apps/system/locales/system.ar.properties
@@ -0,0 +1,290 @@
+ok=موافق
+back=العودة
+cancel=إلغاء
+close=إغلاق
+
+# utility tray mobile states
+airplaneMode=وضع الطيران
+searching=جارِ البحث...
+noNetwork=لا يوجد شبكة
+roaming={{operator}} (تجوال)
+emergencyCallsOnly = مكالمات الطوارئ فقط
+emergencyCallsOnly-noSIM = (لايوجد SIM)
+emergencyCallsOnly-pinRequired = (مطلوب كود PIN)
+emergencyCallsOnly-pukRequired = (مطلوب كود PUK)
+emergencyCallsOnly-networkLocked = (الشبكة مُغلقة)
+enterPIN=أدخل كود PIN
+
+# status bar text label
+statusbarLabel={{operator}} ،{{date}}
+# see http://pubs.opengroup.org/onlinepubs/007908799/xsh/strftime.html
+statusbarDateFormat=%e .%b
+# either 24-hour format (%H:%M) or 12-hour without AM/PM (%I:%M)
+statusbarTimeFormat=%I:%M
+
+# notifications
+notifications=التنبيهات
+clear-all=إزالة الكل
+
+# voice mails
+newVoicemails={[ plural(n) ]}
+newVoicemails[zero] = لا يوجد بريد صوتي جديد
+newVoicemails[one] = رسالة صوتية جديدة
+newVoicemails[two] = {{n}} رسائل صوتية جديدة
+newVoicemails[few] = {{n}} رسائل صوتية جديدة
+newVoicemails[many] = {{n}} رسائل صوتية جديدة
+newVoicemails[other] = {{n}} رسائل صوتية جديدة
+newVoicemailsUnknown = رسالة صوتية جديدة
+dialNumber = إتصال ب {{number}}
+
+# sleep menu
+deviceMenu=الهاتف
+airplane=تفعيل وضع الطيران
+airplaneOff=تعطيل وضع الطيران
+silent=إسكات المكالمات الواردة
+normal=رنين المكالمات الواردة
+restart=إعادة التشغيل
+power=إيقاف التشغيل
+
+# permissions
+# LOCALIZATION NOTE: this is displayed in a permission request dialog box,
+# see shared/permissions/permissions.properties for more context.
+yes=نعم
+no=لا
+deny=عدم السماح
+allow=سماح
+remember-my-choice=تذكر إختياري
+
+# App install
+install-app=تثبيت {{name}}?
+size=الحجم
+author=المُبرمج
+unknown=غير معروف
+install=تثبيت {{name}} من {{origin}}؟
+bytes=بايت
+kB=كيلو بايت
+MB=ميجا بايت
+GB=جيجا بايت
+TB=تيرا بايت
+PB=بيتا بايت
+app-install-generic-error=توقف تنزيل {{&nbsp;appName }}
+app-install-download-failed=فشل في تنزيل {{ appName }}
+app-install-install-failed=فشل في تثبيت {{ appName }}
+cancelling-will-not-refund=الالغاء لن يعيد النقود للمشترى. إعادة النقود للمحتوى المُشترى فقط من خلال البائع الاصلي.
+apps-can-be-installed-later=يمكن تثبيت التطبيقات لاحقا من مصدر التثبيت الاصلي.
+are-you-sure-you-want-to-cancel=هل أنت متأكد من إلغاء التثبيت؟
+cancel-install=إلغاء التثبيت
+resume=إستئناف
+continue=متابعة
+stopDownloading=إيقاف تنزيل {{ app }}؟
+app-download-can-be-restarted=يمكنك إعادة بدء التنزيل لاحقا.
+app-download-stop-button=إيقاف التنزيل
+not-enough-space=لا توجد مساحة كافية
+not-enough-space-message=لا توجد مساحة كافية. اخذف التطبيقات القديمة أو الوسائط لتوفير مساحة وحاول التنزيل مرة اخرى من المصدر الرئيسي.
+app-install-success={{ appName }} تم تثبيته
+
+
+# update manager
+later=لاحقاً
+download=تنزيل
+wantToDownload=تنزيل التحديث الآن؟
+wantToInstall=تثبيت التحديث الأن؟
+installNow=التثبيت الآن
+# the <span> element will be displayed when the download finishes
+downloadingUpdateMessage=جارِ تنزيل التحديثات...<span>{{ progress }} تم تنزيله.</span>
+downloadingAppMessage=جاري تنزيل {{ appName }}
+downloadingAppProgressNoMax=تم تنزيل {{ progress }}
+downloadingAppProgressIndeterminate=جاري التنزيل
+downloadingAppProgress=تم تنزيل {{ progress }} / {{ max }}
+cancelAllDownloads=إلغاء جميع التنزيلات
+wantToCancelAll=هل أنت متأكد من إلغاء جميع التنزيلات؟
+downloadError=حدث خطأ عند تنزيل التحديثات.
+updateAvailableInfo={[ plural(n) ]}
+updateAvailableInfo[one]={{ n }} تحديثات متاحة. <span> اضغط للتحميل.</span>
+updateAvailableInfo[two]={{ n }} تحديثات متاحة. <span> اضغط للتحميل.</span>
+updateAvailableInfo[few]={{ n }} تحديثات متاحة. <span> اضغط للتحميل.</span>
+updateAvailableInfo[many]={{ n }} تحديثات متاحة. <span> اضغط للتحميل.</span>
+updateAvailableInfo[other]={{ n }} تحديثات متاحة. <span> اضغط للتحميل.</span>
+numberOfUpdates={[ plural(n) ]}
+numberOfUpdates[one]=متوفر {{ n }} تحديث
+numberOfUpdates[two]=متوفر {{ n }} تحديثات
+numberOfUpdates[few]=متوفر {{ n }} تحديثات
+numberOfUpdates[many]=متوفر {{ n }} تحديثات
+numberOfUpdates[other]=متوفر {{ n }} تحديثات
+systemUpdate=تحديث النظام
+required=مطلوب
+uncompressingMessage=جارِ فك الضغط… <span>ربما يحتاج لبضعة دقائق.</span>
+downloadNoWifiWarning=اللاسلكي غير متاح. التحديث باستخدام هذه الشبكة ربما سيترتب عليه رسوم اضافية.
+downloadEdgeWarning=اللاسلكي غير متاح. التحديث باستخدام هذه الشبكة سيحجب الكالمات الواردة و ربما سيترتب عليه رسوم اضافية.
+downloadOfflineWarning=لا يوجد اتصال. اتصل بشبكة لتنزيل التحديثات.
+
+# screenshots
+screenshotSaved = تم حفظ لقطة الشاشة في الأستوديو
+screenshotFailed = لم يتم حفظ لقطة الشاشة
+screenshotNoSDCard = لم يتم العثور على بطاقة ذاكرة
+screenshotSDCardInUse = بطاقة الذاكرة قيد الاستعمال
+screenshotSDCardLow = لا توجد مساحة كافية على بطاقة الذاكرة
+
+# lock screen
+# Note: the space between "security" and "code" is non-break space in Unicode
+enter-security-code=أدخل الكود الأمني
+emergency-call-button=مكالمة الطوارئ
+unlock-a11y-button.ariaLabel=فتح القفل
+camera-a11y-button.ariaLabel=الكاميرا
+# see http://pubs.opengroup.org/onlinepubs/007908799/xsh/strftime.html
+longDateFormat=%A, %B %e
+
+# system dialogs
+login=تسجيل الدخول
+username=اسم المستخدم
+password=كلمة السِّر
+http-authentication-message=تم طلب اسم مستخدم وكلمة سِّر من قبل {{host}}. الموقع يقول: "{{realm}}"
+error-message=لم يتم تحميل التطبيق {{name}} صورة صحيحة
+error-title=يواجه التطبيق {{name}} مشاكل
+airplane-is-on=وضع الطيران مُفعّل
+airplane-is-turned-on={{name}} يتطلب اتصال بالشبكة. أوقف وضعية الطيران من الاعدادات واتصل إما بالواي فاي أو بشبكة بيانات الجواّل.
+network-connection-unavailable=لا يوجد اتصال بالشبكة
+network-error={{name}} يتطلب اتصال بالشبكة. حاول الاتصال بالواي فاي او شبكة بيانات الجوّال.
+try-again=حاول مرة أخرى
+dialog-closed = تم إغلاق النافذة من قبل المستخدم
+
+# value selector, time/date picker
+select-time=حدد وقت
+choose-option=حدد
+choose-options=حدد
+select-day=حدد يوم
+
+# system overlay
+battery-almost-empty=البطارية فارغة تقريبا
+
+# wrapper
+add-to-home-screen=أضف إلى الشاشة الرئيسية
+
+# crash report dialog
+crash-dialog2-os={{brandShortName}} تم استرجاعه من الانهيار
+crash-dialog-app={{name}} توقف عن العمل
+crash-dialog-message=هل ترغب في ارسال تقرير عن الانهيار لموزيلا للمساعدة في حل المشكلة؟ (ترسل التقارير عبر الواي فاي فقط.)
+crash-info-link=ماذا يحتوي تقرير الانهيار؟
+crash-always-report=دائما قم بإرسال تقرير لموزيلا عند حدوث انهيار.
+crash-dont-send=لا ترسل
+crash-end=أرسل التقرير
+
+# "Crash Reports" information page
+crashReports=تقارير الانهيارات
+done=تم
+# Localization note (crash-reports-description-*): These strings are also included in settings.properties
+crash-reports-description-1=تقرير الانهيار يحتوي على تفاصيل الانهيار و جوّالك ولمحة عن حالة جوّالك لحظه حدوث الانهيار.
+crash-reports-description-2=ربما يحتوي على الصفحات المفتوحة والتطبيقات، بعض النصوص المطبوعة ومحتوى الرسائل المفتوحة، اخر تأريخ التصفح أو موقعك الجغرافي المستخدم في التطبيق المفتوح.
+# Localization note (crash-reports-description-3-*): These strings are a paragraph, with a "privacy policy"
+# link in the middle. Include trailing spaces as needed.
+crash-reports-description-3-start=نحن نستخدم تقارير الانهيار لمحاولة حل المشاكل وتحسين منتجاتنا. نحن نتعامل مع معلوماتك كما هي موضحة في
+crash-reports-description-3-privacy=سياسة الخصوصية
+crash-reports-description-3-end=.
+
+# crash notification banner
+crash-banner-os2={{brandShortName}} انهار الآن.
+crash-banner-app={{name}} توقف عن العمل .
+crash-banner-report=التقرير
+
+# bluetooth transfer
+bluetooth-sendingTitle=جاري الإرسال من البلوتوث
+bluetooth-receivingTitle=جارِ الاستلام من البلوتوث..
+bluetooth-sending-progress=جارِ الإرسال من البلوتوث
+bluetooth-receiving-progress=جارِ الاستلام من البلوتوث..
+acceptFileTransfer=قبول نقل الملف عبر البلوتوث؟
+wantToReceiveFile=يرغب {{deviceName}} بنقل {{fileName}} ({{fileSize}}).
+byteUnit-B = بايت
+byteUnit-KB = كيلو بايت
+byteUnit-MB = ميجا بايت
+byteUnit-GB = جيجا بايت
+byteUnit-TB = تيرا بايت
+byteUnit-PB = PB
+byteUnit-EB = EB
+byteUnit-ZB = ZB
+byteUnit-YB = YB
+# Localization note (fileSize*): The string is a float.
+fileSize = {{size}} {{unit}}
+deny=عدم السماح
+transfer=نقل
+cancelFileTransfer=إلغاء نقل الملف عبر البلوتوث؟
+continue=متابعة
+bluetooth-file-transfer-result={{status}} نقل الملف عبر البلوتوث.
+complete=إكتمل
+failed=فشل
+transferFinished-sentSuccessful-title=أُرسِل الملف
+transferFinished-receivedSuccessful-title=أُستُقبِل الملف
+transferFinished-sentFailed-title=لم يتم ارسال الملف
+transferFinished-receivedFailed-title=لم بتم استقبال الملف
+unknown-file=ملف غير مُعرَّف
+unknown-device=جهاز مجهول
+cannotOpenFile=لايمكن فتح الملف المُستلم
+unknownMediaTypeToOpen=لا يمكن فتح نوع وسائط غير مُعرَّف:
+cannotReceiveFile=لا يمكن استلام الملف
+cannotGetStorageState=لايمكن الوصول الى حالة تخزين الجوّال
+sdcard-not-exist=لم يتم العثور على بطاقة ذاكرة (الرجاء ادخال واحدة)
+sdcard-in-use=لا يمكن استعمال البلوتوث يبنما الجوَّال موصول. افصل الجوّال لاستلام الملف او الملفات.
+sdcard-no-space2=لا يمكن اتمام عملية النقل: بطاقة الذاكرة ملىء
+unknown-error=خطأ غير معروف
+confirm=تأكيد
+
+# Security :: SIM PIN lock
+noSimCard=لا توجد بطاقة SIM
+simPin=كود SIM PIN
+pukCode=كود PUK
+nckCode=كود NCK
+whatIsSimPin=ماهو كود SIM PIN؟
+simPinIntro1=كود SIM PIN يمنع الوصول إلى شبكات البيانات الخلوية لبطاقة SIM. عندما تكون مفعلة، أي جهاز يحتوي على بطاقة SIM سوف يطلب كود PIN عند إعادة التشغيل.
+simPinIntro2=كود SIM PIN ليس هو نفسه رمز المرور المستخدم لفتح الجهاز.
+changeSimPin=تغيير كود PIN
+pinTitle=أدخل الكود SIM PIN
+pukTitle=أدخل كود PUK
+nckTitle=ادخل كود NCK
+newpinTitle=كود PIN جديد
+pinErrorMsg=كود PIN خاطئ.
+pinAttemptMsg={[ plural(n) ]}
+pinAttemptMsg[one]=محاولة أخيرة متبقية.
+pinAttemptMsg[two]={{n}} محاولة متبقية .
+pinAttemptMsg[few]={{n}} محاولة متبقية .
+pinAttemptMsg[many]={{n}} محاولة متبقية .
+pinAttemptMsg[other]={{n}} محاولة متبقية .
+pinLastChanceMsg=هذه آخر فرصة لك لإدخال رقم التعريف الشخصي الصحيح. خلاف ذلك، يجب عليك إدخال مفتاح إلغاء التأمين الشخصي (PUK) لإستخدام هذه الشريحة.
+simCardLockedMsg=بطاقة SIM مقفلة.
+enterPukMsg=يجب عليك إدخال مفتاح إلغاء التأمين الشخصي (PUK) للشريحة. ارجع إلى وثائق الشريحة أو اتصل بالناقل الخاص بك للحصول على مزيد من المعلومات.
+pukErrorMsg=كود PUK غير صحيح.
+pukAttemptMsg=لديك {{n}} محاولة لإدخال الرمز الصحيح قبل أن تصبح هذه الشريحة غير صالحة للإستعمال بشكل دائم. ارجع إلى وثائق الشريحة أو اتصل بالناقل الخاص بك للحصول على مزيد من المعلومات.
+pukLastChanceMsg=آخر فرصة لإدخال مفتاح إلغاء التأمين الشخصي (PUK) الصحيح. شريحتك ستصبح غير صالحة للإستعمال بشكل دائم إذا أدخلت مفتاح إلغاء تأمين شخصي (PUK) خاطىء. ارجع إلى وثائق الشريحة أو اتصل بالناقل الخاص بك للحصول على مزيد من المعلومات.
+newSimPinMsg=أنشىء رقم تعريف شخصي (يجب أن يحتوي من 4 إلى 8 أرقام)
+confirmNewSimPinMsg=قم بتأكيد كود PIN
+newPinErrorMsg=كودين PIN غير متطابقين
+nckErrorMsg=كود NCK غير صحيح.
+nckAttemptMsg=فشل طلب عدم تخصيص الشبكة. لديك {{n}} محاولات متبقية لادخال كود NCL. راجع وثائق جهازك أو اتصل مع مزود الخدمة لمعلومات اضافية.
+nckLastChanceMsg=فشل طلب عدم تخصيص الشبكة. لديك محاولة اخيرة لادخال كود NCL. راجع وثائق جهازك أو اتصل مع مزود الخدمة لمعلومات اضافية.
+
+# Activity selection menu titles
+activity-pick= اختر من:
+activity-view= شاهد بـ:
+activity-test= افحص بـ:
+activity-share= شارك بـ:
+activity-new= انشىء بـ:
+activity-open= افتح بـ:
+activity-save-bookmark=مرجعية إلى:
+activity-record=سجِّل من:
+activity-browse=تصفح بـ:
+activity-configure=ضبط بـ:
+activity-dial=اتصل من:
+
+# Persona dialog and Identity
+persona-signin=تسجيل الدخول
+
+# Payment dialog
+payment-flow=اشتري
+
+# Remote Debugger Connection Dialog
+remoteDebuggerMessage=تم الكشف عن طلب اتصال للتدقيق البعيد . هل تسمح بالاتصال؟
+
+# fullscreen permission
+# LOCALIZATION NOTE {{origin}} is the origin of the site which has requested fullscreen, e.g. http://example.com is now fullscreen. It includes protocol + hostname.
+fullscreen-request={{origin}} الآن ملء الشاشة.
+
+#captive wifi
+captive-wifi-available=تم العثور على الشبكة. {{neworkName}} هل تريد الانضمام اليها؟
diff --git a/apps/system/locales/system.en-US.properties b/apps/system/locales/system.en-US.properties
new file mode 100644
index 0000000..077c353
--- /dev/null
+++ b/apps/system/locales/system.en-US.properties
@@ -0,0 +1,296 @@
+ok=OK
+back=Back
+cancel=Cancel
+close=Close
+
+# utility tray mobile states
+airplaneMode=Airplane mode
+searching=Searching…
+noNetwork=No network
+roaming={{operator}} (Roaming)
+emergencyCallsOnly = Emergency calls only
+emergencyCallsOnly-noSIM = (no SIM)
+emergencyCallsOnly-pinRequired = (SIM PIN required)
+emergencyCallsOnly-pukRequired = (SIM PUK required)
+emergencyCallsOnly-networkLocked = (network locked)
+enterPIN=Enter PIN
+
+# status bar text label
+statusbarLabel={{date}}, {{operator}}
+# see http://pubs.opengroup.org/onlinepubs/007908799/xsh/strftime.html
+statusbarDateFormat=%b. %e
+# either 24-hour format (%H:%M) or 12-hour without AM/PM (%I:%M)
+statusbarTimeFormat=%I:%M
+
+# notifications
+notifications=Notifications
+clear-all=Clear all
+
+# voice mails
+newVoicemails={[ plural(n) ]}
+newVoicemails[zero] = No new voicemail
+newVoicemails[one] = One new voicemail
+newVoicemails[two] = {{n}} new voicemails
+newVoicemails[few] = {{n}} new voicemails
+newVoicemails[many] = {{n}} new voicemails
+newVoicemails[other] = {{n}} new voicemails
+newVoicemailsUnknown = New voicemail
+dialNumber = Dial {{number}}
+
+# sleep menu
+deviceMenu=Phone
+airplane=Turn on airplane mode
+airplaneOff=Turn off airplane mode
+silent=Silence incoming calls
+normal=Ring incoming calls
+restart=Restart
+power=Power off
+
+# permissions
+# LOCALIZATION NOTE: this is displayed in a permission request dialog box,
+# see shared/permissions/permissions.properties for more context.
+yes=Yes
+no=No
+deny=Don't allow
+allow=Allow
+remember-my-choice=Remember my choice
+
+# App install
+install-app=Install {{name}}?
+size=Size
+author=Author
+unknown=Unknown
+install=Install
+bytes=bytes
+kB=kB
+MB=MB
+GB=GB
+TB=TB
+PB=PB
+app-install-generic-error={{ appName }} download stopped
+app-install-download-failed={{ appName }} download failed
+app-install-install-failed={{ appName }} install failed
+cancelling-will-not-refund=Cancelling will not refund a purchase. Refunds for paid content are provided by the original seller.
+apps-can-be-installed-later=Apps can be installed later from the original installation source.
+are-you-sure-you-want-to-cancel=Are you sure you want to cancel this install?
+cancel-install=Cancel Install
+resume=Resume
+continue=Continue
+stopDownloading=Stop downloading {{ app }}?
+app-download-can-be-restarted=The download can be restarted later.
+app-download-stop-button=Stop Download
+not-enough-space=Not enough space
+not-enough-space-message=There is not enough space to install this app. Free up space by deleting old apps or media, and try installing again from the original source.
+app-install-success={{ appName }} installed
+
+
+# update manager
+later=Later
+download=Download
+wantToDownload=Download update now?
+wantToInstall=Install update now?
+installNow=Install Now
+notNow=Not Now
+# the <span> element will be displayed when the download finishes
+downloadingUpdateMessage=Downloading updates… <span>{{ progress }} downloaded.</span>
+downloadingAppMessage=Downloading {{ appName }}
+downloadingAppProgressNoMax={{ progress }} downloaded
+downloadingAppProgressIndeterminate=Downloading
+downloadingAppProgress={{ progress }} / {{ max }} downloaded
+cancelAllDownloads=Cancel all downloads
+wantToCancelAll=Are you sure you want to cancel all downloads?
+downloadError=There was an error while downloading the updates.
+updateAvailableInfo={[ plural(n) ]}
+updateAvailableInfo[one]={{ n }} update available. <span>Tap for more info.</span>
+updateAvailableInfo[two]={{ n }} updates available. <span>Tap for more info.</span>
+updateAvailableInfo[few]={{ n }} updates available. <span>Tap for more info.</span>
+updateAvailableInfo[many]={{ n }} updates available. <span>Tap for more info.</span>
+updateAvailableInfo[other]={{ n }} updates available. <span>Tap for more info.</span>
+numberOfUpdates={[ plural(n) ]}
+numberOfUpdates[one]={{ n }} update available
+numberOfUpdates[two]={{ n }} updates available
+numberOfUpdates[few]={{ n }} updates available
+numberOfUpdates[many]={{ n }} updates available
+numberOfUpdates[other]={{ n }} updates available
+systemUpdate=System update
+systemUpdateReady=System Update Ready
+required=Required
+uncompressingMessage=Uncompressing… <span>This can take a few minutes.</span>
+downloadNoWifiWarning=Wi-Fi unavailable. Updating over current connection may incur additional charges.
+downloadOfflineWarning=Connection unavailable. Connect to a network to download updates.
+downloadDataConnectionWarning=Updates are downloaded via data connection when Wi-Fi is not available. Additional data charges may apply.
+downloadUpdatesViaDataConnection=Download updates via data connection?
+downloadUpdatesViaDataConnectionMessage=Updates are downloaded via data connection when Wi-Fi is not available. When using data connection, phone calls may be blocked and additional charges may also apply.
+
+# screenshots
+screenshotSaved = Screenshot saved to Gallery
+screenshotFailed = The screenshot could not be saved
+screenshotNoSDCard = No memory card found
+screenshotSDCardInUse = Memory card is in use
+screenshotSDCardLow = Not enough space on memory card
+
+# lock screen
+# Note: the space between "security" and "code" is non-break space in Unicode
+enter-security-code=Enter security code
+emergency-call-button=Emergency Call
+unlock-a11y-button.ariaLabel=Unlock
+camera-a11y-button.ariaLabel=Camera
+# see http://pubs.opengroup.org/onlinepubs/007908799/xsh/strftime.html
+longDateFormat=%A, %B %e
+
+# system dialogs
+login=Sign in
+username=Username
+password=Password
+http-authentication-message=A username and password are being requested by {{host}}. The site says: “{{realm}}”
+error-message={{name}} is not loading properly
+error-title={{name}} is having problems
+airplane-is-on=Airplane mode is on
+airplane-is-turned-on={{name}} requires a network connection. Turn off airplane mode from Settings and connect to a Wi-Fi or mobile data network.
+network-connection-unavailable=Network connection unavailable
+network-error={{name}} requires a network connection. Try connecting to a Wi-Fi or mobile data network.
+try-again=Try again
+dialog-closed = Dialog closed by the user
+
+# value selector, time/date picker
+select-time=Select time
+choose-option=Select
+choose-options=Select
+select-day=Select day
+
+# system overlay
+battery-almost-empty=Battery almost empty
+
+# wrapper
+add-to-home-screen=Add to Home Screen
+
+# crash report dialog
+crash-dialog2-os={{brandShortName}} just recovered from a crash
+crash-dialog-app={{name}} just crashed
+crash-dialog-message=Would you like to send Mozilla a report about the crash to help us fix the problem? (Reports are sent over Wi-Fi only.)
+crash-info-link=What's in a crash report?
+crash-always-report=Always send Mozilla a report when a crash occurs.
+crash-dont-send=Don't Send
+crash-end=Send Report
+
+# "Crash Reports" information page
+crashReports=Crash Reports
+done=Done
+# Localization note (crash-reports-description-*): These strings are also included in settings.properties
+crash-reports-description-1=A crash report contains some details about the crash and your device, as well as a snapshot of the state of your device when it crashed.
+crash-reports-description-2=This may include things like open pages and apps, text typed into forms and the content of open messages, recent browsing history, or geolocation used by an open app.
+# Localization note (crash-reports-description-3-*): These strings are a paragraph, with a "privacy policy"
+# link in the middle. Include trailing spaces as needed.
+crash-reports-description-3-start=We use crash reports to try to fix problems and improve our products. We handle your information as we describe in our
+crash-reports-description-3-privacy=privacy policy
+crash-reports-description-3-end=.
+
+# crash notification banner
+crash-banner-os2={{brandShortName}} just crashed.
+crash-banner-app={{name}} just crashed.
+crash-banner-report=Report
+
+# bluetooth transfer
+bluetooth-sendingTitle=Sending Bluetooth transfer..
+bluetooth-receivingTitle=Receiving Bluetooth transfer..
+bluetooth-sending-progress=Sending Bluetooth transfer..
+bluetooth-receiving-progress=Receiving Bluetooth transfer..
+acceptFileTransfer=Accept Bluetooth file transfer?
+wantToReceiveFile={{deviceName}} would like to transfer {{fileName}} ({{fileSize}}).
+notification-fileTransfer-title=Receive file?
+notification-fileTransfer-description=File incoming from another device
+byteUnit-B = Bytes
+byteUnit-KB = KB
+byteUnit-MB = MB
+byteUnit-GB = GB
+byteUnit-TB = TB
+byteUnit-PB = PB
+byteUnit-EB = EB
+byteUnit-ZB = ZB
+byteUnit-YB = YB
+# Localization note (fileSize*): The string is a float.
+fileSize = {{size}} {{unit}}
+deny=Deny
+transfer=Transfer
+cancelFileTransfer=Cancel Bluetooth file transfer?
+continue=Continue
+bluetooth-file-transfer-result=Bluetooth file transfer {{status}}.
+complete=complete
+failed=failed
+transferFinished-sentSuccessful-title=File sent
+transferFinished-receivedSuccessful-title=File received
+transferFinished-sentFailed-title=File could not be sent
+transferFinished-receivedFailed-title=File could not be received
+unknown-file=Unknown file
+unknown-device=Unknown device
+cannotOpenFile=Can not open received file
+unknownMediaTypeToOpen=Can not open unknown media type:
+cannotReceiveFile=Can not receive file
+cannotGetStorageState=Can not get device storage state
+sdcard-not-exist=No memory card found (Please insert a memory card)
+sdcard-in-use=Bluetooth can not be used while phone is plugged in. Please unplug the phone to receive file(s).
+sdcard-no-space2=Can not complete transfer: memory card is full.
+unknown-error=Unknown error
+confirm=Confirm
+
+# Security :: SIM PIN lock
+noSimCard=No SIM card
+simPin=SIM PIN
+pukCode=PUK code
+nckCode=NCK code
+whatIsSimPin=What is a SIM PIN?
+simPinIntro1=A SIM PIN prevents access to the SIM card cellular data networks. When it’s enabled, any device containing the SIM card will request the PIN upon restart.
+simPinIntro2=A SIM PIN is not the same as the passcode used to unlock the device.
+changeSimPin=Change PIN
+pinTitle=Enter SIM PIN
+pukTitle=Enter PUK code
+nckTitle=Enter NCK code
+newpinTitle=New PIN
+pinErrorMsg=The PIN was incorrect.
+pinAttemptMsg={[ plural(n) ]}
+pinAttemptMsg[one]=one last try.
+pinAttemptMsg[two]={{n}} tries left.
+pinAttemptMsg[few]={{n}} tries left.
+pinAttemptMsg[many]={{n}} tries left.
+pinAttemptMsg[other]={{n}} tries left.
+pinLastChanceMsg=This is your last chance to enter the correct PIN. Otherwise, you must enter the PUK code to use this SIM card.
+simCardLockedMsg=The SIM card is locked.
+enterPukMsg=You must enter the Personal Unlocking Key (PUK) code for the SIM card. Refer to your SIM card documentation or contact your carrier for more information.
+pukErrorMsg=The PUK code is incorrect.
+pukAttemptMsg=You have {{n}} tries left to enter the correct code before this SIM card will be permanently unusable. Refer to your SIM card documentation or contact your carrier for more information.
+pukLastChanceMsg=Last chance to enter the correct PUK code. Your SIM card will be permanently unusable if you enter in the wrong PUK code. Refer to your SIM card documentation or contact your carrier for more information.
+newSimPinMsg=Create PIN (must contain 4 to 8 digits)
+confirmNewSimPinMsg=Confirm new PIN
+newPinErrorMsg=PINs don’t match.
+nckErrorMsg=The NCK code is incorrect.
+nckAttemptMsg=Network depersonalization request failure. You have {{n}} tries left to enter the correct code. Refer to your device documentation or contact your carrier for more information.
+nckLastChanceMsg=Network depersonalization request failure. Last chance to enter the correct NCK code. Refer to your device documentation or contact your carrier for more information.
+
+# Activity selection menu titles
+activity-pick= Select from:
+activity-view= View with:
+activity-test= Test with:
+activity-share= Share with:
+activity-new= Create with:
+activity-open= Open with:
+activity-save-bookmark=Bookmark to:
+activity-record=Record from:
+activity-browse=Browse with:
+activity-configure=Configure with:
+activity-dial=Dial from:
+
+# Persona dialog and Identity
+persona-signin=Sign In
+
+# Payment dialog
+payment-flow=Purchase
+
+# Remote Debugger Connection Dialog
+remoteDebuggerMessage=An incoming request to permit remote debugging connection was detected. Allow connection?
+
+# fullscreen permission
+# LOCALIZATION NOTE {{origin}} is the origin of the site which has requested fullscreen, e.g. http://example.com is now fullscreen. It includes protocol + hostname.
+fullscreen-request={{origin}} is now fullscreen.
+
+#captive wifi
+captive-wifi-available=The network {{networkName}} was found. Join network?
diff --git a/apps/system/locales/system.fr.properties b/apps/system/locales/system.fr.properties
new file mode 100644
index 0000000..4269f75
--- /dev/null
+++ b/apps/system/locales/system.fr.properties
@@ -0,0 +1,289 @@
+ok=OK
+back=Retour
+cancel=Annuler
+close=Fermer
+
+# utility tray mobile states
+airplaneMode=Mode avion
+searching=Recherche…
+noNetwork=Pas de réseau
+roaming={{operator}} (itinérance)
+emergencyCallsOnly = Appels d’urgence uniquement
+emergencyCallsOnly-noSIM = (aucune carte SIM)
+emergencyCallsOnly-pinRequired = (code PIN requis)
+emergencyCallsOnly-pukRequired = (code PUK requis)
+emergencyCallsOnly-networkLocked = (verrouillage réseau)
+enterPIN=Code PIN
+
+# status bar text label
+statusbarLabel={{date}}, {{operator}}
+# see http://pubs.opengroup.org/onlinepubs/007908799/xsh/strftime.html
+statusbarDateFormat=%e %b.
+# either 24-hour format (%H:%M) or 12-hour without AM/PM (%I:%M)
+statusbarTimeFormat=%H:%M
+
+# notifications
+notifications=Notifications
+clear-all=Tout effacer
+
+# voice mails
+newVoicemails={[ plural(n) ]}
+newVoicemails[zero] = Aucun nouveau message vocal
+newVoicemails[one] = Vous avez un nouveau message vocal
+newVoicemails[two] = Vous avez {{n}} nouveaux messages vocaux
+newVoicemails[few] = Vous avez {{n}} nouveaux messages vocaux
+newVoicemails[many] = Vous avez {{n}} nouveaux messages vocaux
+newVoicemails[other] = Vous avez {{n}} nouveaux messages vocaux
+newVoicemailsUnknown = Nouveau message vocal
+dialNumber = Appeler {{number}}
+
+# sleep menu
+deviceMenu=Menu système
+airplane=Mode avion
+airplaneOff=Désactiver le mode avion
+silent=Mode silencieux
+normal=Mode normal
+restart=Redémarrer
+power=Éteindre
+
+# permissions
+# LOCALIZATION NOTE: this is displayed in a permission request dialog box,
+# see shared/permissions/permissions.properties for more context.
+yes=Oui
+no=Non
+deny=Refuser
+allow=Autoriser
+remember-my-choice=Se souvenir de mon choix
+
+# App install
+install-app=Installer {{name}} ?
+size=Taille
+author=Auteur
+unknown=Inconnu
+install=Installer
+bytes=octets
+kB=Kio
+MB=Mio
+GB=Gio
+TB=Tio
+PB=Pio
+app-install-generic-error=Téléchargement de {{ appName }} arrêté
+app-install-download-failed=Échec du téléchargement de {{ appName }}
+app-install-install-failed=Échec de l’installation de {{ appName }}
+cancelling-will-not-refund=Annuler ne remboursera pas un achat. Les remboursements de contenus payants sont effectués par le marchand d’origine.
+apps-can-be-installed-later=Les applications peuvent être installées plus tard depuis la source d’origine.
+are-you-sure-you-want-to-cancel=Voulez-vous vraiment annuler l’installation ?
+cancel-install=Annuler l’installation
+resume=Reprendre
+continue=Continuer
+stopDownloading=Arrêter le téléchargement de {{ app }} ?
+app-download-can-be-restarted=Le téléchargement peut être redémarré plus tard.
+app-download-stop-button=Arrêter le téléchargement
+not-enough-space=Espace insuffisant
+not-enough-space-message=Il n’y a pas assez d’espace pour installer cette application. Libérez de l’espace en supprimant des médias ou d’anciennes applications puis essayez à nouveau de l’installer depuis la source d’origine.
+app-install-success={{ appName }} installé
+
+# update manager
+later=Plus tard
+download=Télécharger
+
+wantToDownload=Télécharger une mise à jour maintenant ?
+wantToInstall=Installer la mise à jour maintenant ?
+installNow=Installer maintenant
+# the <span> element will be displayed when the download finishes
+downloadingUpdateMessage=Téléchargement des mises à jour… <span>{{ progress }} téléchargés.</span>
+downloadingAppMessage=Téléchargement de {{ appName }}
+downloadingAppProgressNoMax={{ progress }} téléchargés
+downloadingAppProgressIndeterminate=Téléchargement en cours
+downloadingAppProgress={{ progress }} / {{ max }} téléchargés
+cancelAllDownloads=Annuler tous les téléchargements
+wantToCancelAll=Voulez-vous vraiment annuler tous les téléchargements ?
+downloadError=Une erreur s’est produite lors du téléchargement des mises à jour.
+updateAvailableInfo={[ plural(n) ]}
+updateAvailableInfo[one]={{ n }} mise à jour disponible. <span>Toucher pour plus d’informations.</span>
+updateAvailableInfo[two]={{ n }} mises à jour disponibles. <span>Toucher pour plus d’informations.</span>
+updateAvailableInfo[few]={{ n }} mises à jour disponibles. <span>Toucher pour plus d’informations.</span>
+updateAvailableInfo[many]={{ n }} mises à jour disponibles. <span>Toucher pour plus d’informations.</span>
+updateAvailableInfo[other]={{ n }} mises à jour disponibles. <span>Toucher pour plus d’informations.</span>
+numberOfUpdates={[ plural(n) ]}
+numberOfUpdates[one]={{ n }} mise à jour disponible
+numberOfUpdates[two]={{ n }} mises à jour disponibles
+numberOfUpdates[few]={{ n }} mises à jour disponibles
+numberOfUpdates[many]={{ n }} mises à jour disponibles
+numberOfUpdates[other]={{ n }} mises à jour disponibles
+systemUpdate=Mise à jour du système
+required=Requise
+uncompressingMessage=Décompression… <span>Cela peut prendre plusieurs minutes.</span>
+downloadNoWifiWarning=Wi-Fi non disponible. Mettre à jour depuis votre connexion actuelle pourra entraîner des coûts supplémentaires.
+downloadEdgeWarning=Wi-Fi non disponible. Mettre à jour depuis votre connexion actuelle bloquera les appels entrants et pourra entraîner des coûts supplémentaires.
+
+# screenshots
+screenshotSaved = Capture d’écran enregistrée
+screenshotFailed = Impossible d’enregistrer la capture d’écran
+screenshotNoSDCard = Aucune carte mémoire installée
+screenshotSDCardInUse = Carte mémoire utilisée
+screenshotSDCardLow = Espace libre sur la carte mémoire insuffisant
+
+# lock screen
+# Note: the space between "security" and "code" is non-break space in Unicode
+enter-security-code=Saisissez le code de sécurité
+emergency-call-button=Appel d’urgence
+unlock-a11y-button.ariaLabel=Déverrouiller
+camera-a11y-button.ariaLabel=Appareil photo
+# see http://pubs.opengroup.org/onlinepubs/007908799/xsh/strftime.html
+longDateFormat=%A %e %B
+
+# system dialogs
+login=Connexion
+username=Utilisateur
+password=Mot de passe
+http-authentication-message=Un identifiant et un mot de passe sont demandés par {{host}}. Le site se présente comme : « {{realm}} »
+error-message={{name}} ne se charge pas correctement.
+error-title={{name}} rencontre des problèmes.
+airplane-is-on=Le mode avion est activé
+airplane-is-turned-on={{name}} nécessite une connexion réseau. Désactivez le mode avion depuis l’application Paramètres et connectez-vous à un réseau Wi-Fi ou à un réseau de données mobiles.
+network-connection-unavailable=Aucune connexion réseau disponible
+network-error={{name}} nécessite une connexion réseau. Essayez de vous connecter à un réseau Wi-Fi ou à un réseau de données mobiles.
+try-again=Réessayer
+dialog-closed = Dialogue fermé par l’utilisateur
+
+# value selector, time/date picker
+select-time=Sélectionner l’heure
+choose-option=Choisir une option
+choose-options=Choisir des options
+select-day=Sélectionner le jour
+
+# system overlay
+battery-almost-empty=Batterie presque vide
+battery-low=Batterie faible
+battery-full=Batterie pleine
+
+# wrapper
+add-to-home-screen=Ajouter à l’écran d’accueil
+
+# crash report dialog
+crash-dialog2-os={{brandShortName}} a récupéré d’un plantage
+crash-dialog-app={{name}} a planté
+crash-dialog-message=Voulez-vous envoyer un rapport à Mozilla concernant ce plantage pour nous aider à résoudre ce problème ? (Les rapports ne sont envoyés que par Wi-Fi.)
+crash-info-link=Que contient un rapport de plantage ?
+crash-always-report=Toujours envoyer un rapport à Mozilla lorsqu’un plantage se produit.
+crash-dont-send=Ne pas envoyer
+crash-end=Envoyer le rapport
+
+# "Crash Reports" information page
+crashReports=Rapports de plantage
+done=Fini
+# Localization note (crash-reports-description-*): These strings are also included in system.properties
+crash-reports-description-1=Un rapport de plantage contient des détails concernant le plantage et votre appareil, ainsi qu’un instantané de l’état de votre appareil lors du plantage.
+crash-reports-description-2=Il peut inclure différentes choses comme les applications et pages ouvertes, le texte saisi dans les formulaires et le contenu des messages ouverts, l’historique de navigation récent, ou la géolocalisation utilisée par une application ouverte.
+# Localization note (crash-reports-description-3-*): These strings are a paragraph, with a "privacy policy"
+# link in the middle. Include trailing spaces as needed.
+crash-reports-description-3-start=Nous utilisons les rapports de plantage pour essayer de résoudre les problèmes et améliorer nos produits. Nous gérons vos informations comme décrit dans notre
+crash-reports-description-3-privacy=politique de confidentialité
+crash-reports-description-3-end=.
+
+# crash notification banner
+crash-banner-os2={{brandShortName}} a planté.
+crash-banner-app={{name}} a planté.
+crash-banner-report=Rapporter
+
+# bluetooth transfer
+bluetooth-sendingTitle=Début d’un transfert par Bluetooth…
+bluetooth-receivingTitle=Réception d’un transfert par Bluetooth…
+bluetooth-sending-progress=Début d’un transfert par Bluetooth…
+bluetooth-receiving-progress=Réception d’un transfert par Bluetooth…
+acceptFileTransfer=Accepter le transfert de fichier par Bluetooth ?
+wantToReceiveFile={{deviceName}} souhaite transférer {{fileName}} ({{fileSize}}).
+byteUnit-B = octets
+byteUnit-KB = Kio
+byteUnit-MB = Mio
+byteUnit-GB = Gio
+byteUnit-TB = Tio
+byteUnit-PB = Pio
+byteUnit-EB = Eio
+byteUnit-ZB = Zio
+byteUnit-YB = Yio
+# Localization note (fileSize*): The string is a float.
+fileSize = {{size}} {{unit}}
+deny=Refuser
+transfer=Transférer
+cancelFileTransfer=Annuler le transfert de fichier par Bluetooth ?
+continue=Continuer
+bluetooth-file-transfer-result={{status}} du transfert de fichier par Bluetooth.
+complete=Réussite
+failed=Échec
+transferFinished-sendingCompletedTitle=Envoi de fichier par Bluetooth terminé
+transferFinished-receivedCompletedTitle=Fichier reçu par Bluetooth prêt à être ouvert
+transferFinished-sendingFailedTitle=Échec de l’envoi de fichier par Bluetooth
+transferFinished-receivedFailedTitle=L’application Bluetooth a reçu un fichier qu‘elle ne peut pas ouvrir
+transferFinished-completedBody=Transfert terminé
+transferFinished-failedBody=Échec du transfert
+unknown-device=Appareil inconnu
+cannotOpenFile=Impossible d’ouvrir le fichier reçu
+unknownMediaTypeToOpen=Impossible d’ouvrir le média au format inconnu :
+cannotReceiveFile=Impossible de recevoir le fichier
+cannotGetStorageState=Impossible d’obtenir des informations sur l’espace de stockage
+sdcard-not-exist=Aucune carte mémoire trouvée (Veuillez insérer une carte mémoire)
+sdcard-in-use=L’application Bluetooth ne peut pas être utilisée tant que le téléphone est branché. Débranchez le téléphone pour recevoir des fichiers.
+sdcard-no-space2=Impossible de terminer le transfert : la carte mémoire est pleine.
+unknown-error=Erreur inconnue
+confirm=Confirmer
+
+# Security :: SIM PIN Lock
+noSimCard=Aucune carte SIM
+simPin=Code PIN de la carte SIM
+pukCode=Code PUK
+nckCode=Code NCK
+whatIsSimPin=Qu’est-ce qu’un code PIN ?
+simPinIntro1=Le code PIN de la carte SIM empêche l’accès à vos données de réseau sur votre carte SIM. Quand il est activé, tout appareil contenant une carte SIM réclamera le code PIN au redémarrage.
+simPinIntro2=Un code PIN de carte SIM est différent du code destiné à déverrouiller l’appareil.
+changeSimPin=Changer de code PIN
+pinTitle=Saisir le code PIN
+pukTitle=Saisir le code PUK
+nckTitle=Saisir le code NCK
+newpinTitle=Nouveau code PIN
+pinErrorMsg=Code PIN incorrect.
+pinAttemptMsg={[ plural(n) ]}
+pinAttemptMsg[one]=dernier essai.
+pinAttemptMsg[two]={{n}} essais restants.
+pinAttemptMsg[few]={{n}} essais restants.
+pinAttemptMsg[many]={{n}} essais restants.
+pinAttemptMsg[other]={{n}} essais restants.
+pinLastChanceMsg=C’est votre dernier essai autorisé pour saisir le code PIN correct. Sinon, vous devrez saisir le code PUK pour utiliser cette carte SIM.
+simCardLockedMsg=La carte SIM est verrouillée.
+enterPukMsg=Vous devez saisir la Personal Unlocking Key (PUK, soit Clé personnelle de déblocage) pour la carte SIM. Veuillez vous référer à la documentation associée à votre carte SIM ou contacter votre opérateur pour en savoir plus.
+pukErrorMsg=Le code PUK est incorrect.
+pukAttemptMsg=Il ne vous reste que {{n}} essais pour saisir le code correct avant que cette carte SIM ne devienne définitivement inutilisable. Veuillez vous référer à la documentation associée à votre carte SIM ou contacter votre opérateur pour en savoir plus.
+pukLastChanceMsg=C’est votre dernier essai autorisé pour saisir le code PUK correct. Votre carte SIM deviendra définitivement inutilisable si vous saisissez un code PUK incorrect. Veuillez vous référer à la documentation associée à votre carte SIM ou contacter votre opérateur pour en savoir plus.
+newSimPinMsg=Nouveau code PIN (il doit comprendre entre 4 et 8 chiffres)
+confirmNewSimPinMsg=Confirmer le nouveau code PIN
+newPinErrorMsg=Le code PIN est incorrect.
+nckErrorMsg=Le code NCK est incorrect.
+nckAttemptMsg=La dépersonnalisation du réseau a échoué. Il ne vous reste que {{n}} essais pour saisir le code correct. Veuillez vous référer à la documentation de l’appareil ou contacter votre opérateur pour en savoir plus.
+nckLastChanceMsg=La dépersonnalisation du réseau a échoué. C’est votre dernier essai autorisé pour saisir le code NCK correct. Veuillez vous référer à la documentation de l’appareil ou contacter votre opérateur pour en savoir plus.
+
+# Activity selection menu titles
+activity-pick= Sélectionner depuis :
+activity-view= Afficher avec :
+activity-test= Tester avec :
+activity-share= Partager avec :
+activity-new= Créer avec :
+activity-open= Ouvrir avec :
+activity-save-bookmark=Ajouter marque-page pour :
+activity-record=Enregistrer depuis :
+activity-browse=Ouvrir avec :
+activity-configure=Configurer avec :
+activity-dial=Composer depuis :
+
+# Persona dialog and Identity
+persona-signin=Identification
+
+# Payment dialog
+payment-flow=Acheter
+
+# Remote Debugger Connection Dialog
+remoteDebuggerMessage=Une requête entrante pour permettre une connexion de débogage à distance a été détectée. Autoriser la connexion ?
+
+# fullscreen permission
+# LOCALIZATION NOTE {{origin}} is the origin of the site which has requested fullscreen, e.g. http://example.com is now fullscreen. It includes protocol + hostname.
+fullscreen-request={{origin}} est maintenant affiché en mode plein écran.
diff --git a/apps/system/locales/system.zh-TW.properties b/apps/system/locales/system.zh-TW.properties
new file mode 100644
index 0000000..5e50ee5
--- /dev/null
+++ b/apps/system/locales/system.zh-TW.properties
@@ -0,0 +1,288 @@
+ok=確定
+back=上一頁
+cancel=取消
+close=關閉
+
+# utility tray mobile states
+airplaneMode=飛航模式
+searching=搜尋中…
+noNetwork=無網路
+roaming={{operator}}(漫遊中)
+emergencyCallsOnly = 僅可撥打緊急電話
+emergencyCallsOnly-noSIM = (無 SIM 卡)
+emergencyCallsOnly-pinRequired = (需要輸入 SIM PIN)
+emergencyCallsOnly-pukRequired = (需要輸入 SIM PUK)
+emergencyCallsOnly-networkLocked = (網路已鎖定)
+enterPIN=數入 PIN
+
+# status bar text label
+statusbarLabel={{date}}, {{operator}}
+# see http://pubs.opengroup.org/onlinepubs/007908799/xsh/strftime.html
+statusbarDateFormat=%c
+# either 24-hour format (%H:%M) or 12-hour without AM/PM (%I:%M)
+statusbarTimeFormat=%I:%M
+
+# notifications
+notifications=通知
+clear-all=清除全部
+
+# voice mails
+newVoicemails={[ plural(n) ]}
+newVoicemails[zero] = 沒有新的語音留言
+newVoicemails[one] = 一通新的語音留言
+newVoicemails[two] = {{n}} 通新的語音留言
+newVoicemails[few] = {{n}} 通新的語音留言
+newVoicemails[many] = {{n}} 通新的語音留言
+newVoicemails[other] = {{n}} 通新的語音留言
+newVoicemailsUnknown = 有新的語音留言
+dialNumber = 撥給 {{number}}
+
+# sleep menu
+deviceMenu=電話
+airplane=開啟飛航模式
+airplaneOff=關閉飛航模式
+silent=來電靜音
+normal=取消來電靜音
+restart=重新啟動
+power=關機
+
+# permissions
+# LOCALIZATION NOTE: this is displayed in a permission request dialog box,
+# see shared/permissions/permissions.properties for more context.
+yes=是
+no=否
+deny=拒絕
+allow=允許
+remember-my-choice=記住我的選擇
+
+# App install
+install-app=要安裝 {{name}} 嗎?
+size=大小
+author=作者
+unknown=未知
+install=安裝
+bytes=位元組
+kB=kB
+MB=MB
+GB=GB
+TB=TB
+PB=PB
+app-install-generic-error={{ appName }} 下載已停止
+app-install-download-failed={{ appName }} 下載失敗
+app-install-install-failed={{ appName }} 安裝失敗
+cancelling-will-not-refund=現在取消將不會退費。付費內容的退費機制將由原內容銷售商提供。
+apps-can-be-installed-later=您之後可以於原始安裝來源再次安裝應用程式
+are-you-sure-you-want-to-cancel=您確定要取消這次的安裝嗎?
+cancel-install=取消安裝
+resume=繼續
+continue=繼續
+stopDownloading=要停止下載 {{ app }} 嗎?
+app-download-can-be-restarted=可在稍後重新開始下載。
+app-download-stop-button=停止下載
+not-enough-space=空間不足
+not-enough-space-message=沒有足夠的空間可安裝此程式。您可以刪除舊程式或媒體檔案來釋放空間,接著再從原始來源嘗試安裝。
+app-install-success=已安裝 {{ appName }}
+
+
+# update manager
+later=稍候
+download=下載
+wantToDownload=現在開始下載更新?
+wantToInstall=現在開始安裝更新?
+installNow=立刻安裝
+# the <span> element will be displayed when the download finishes
+downloadingUpdateMessage=正在下載更新… <span>已下載 {{ progress }}。</span>
+downloadingAppMessage=正在下載 {{ appName }}
+downloadingAppProgressNoMax=已下載 {{ progress }}
+downloadingAppProgressIndeterminate=下載中
+downloadingAppProgress=已下載 {{ progress }} / {{ max }}
+cancelAllDownloads=取消所有下載
+wantToCancelAll=您確定要取消所有下載?
+downloadError=下載更新時發生錯誤。
+updateAvailableInfo={[ plural(n) ]}
+updateAvailableInfo[one]=有 {{ n }} 個可用更新。<span>點擊這裡取得更多資訊。</span>
+updateAvailableInfo[two]=有 {{ n }} 個可用更新。<span>點擊這裡取得更多資訊。</span>
+updateAvailableInfo[few]=有 {{ n }} 個可用更新。<span>點擊這裡取得更多資訊。</span>
+updateAvailableInfo[many]=有 {{ n }} 個可用更新。<span>點擊這裡取得更多資訊。</span>
+updateAvailableInfo[other]=有 {{ n }} 個可用更新。<span>點擊這裡取得更多資訊。</span>
+numberOfUpdates={[ plural(n) ]}
+numberOfUpdates[one]=有 {{ n }} 個更新可用
+numberOfUpdates[two]=有 {{ n }} 個更新可用
+numberOfUpdates[few]=有 {{ n }} 個更新可用
+numberOfUpdates[many]=有 {{ n }} 個更新可用
+numberOfUpdates[other]=有 {{ n }} 個更新可用
+systemUpdate=系統更新
+required=必需
+uncompressingMessage=正在解壓縮… <span>可能會花上幾分鐘。</span>
+downloadNoWifiWarning=無法使用 Wi-Fi。透過目前的連線更新可能需要另外付費。
+downloadEdgeWarning=無法使用 Wi-Fi。透過目前的連線更新將會封鎖來電,並且可能需要另外付費。
+
+# screenshots
+screenshotSaved = 已將畫面擷圖儲存至圖庫
+screenshotFailed = 無法儲存畫面擷圖
+screenshotNoSDCard = 找不到記憶卡
+screenshotSDCardInUse = 記憶卡使用中
+screenshotSDCardLow = 記憶卡的空間不足
+
+# lock screen
+# Note: the space between "security" and "code" is non-break space in Unicode
+enter-security-code=輸入安全代碼
+emergency-call-button=緊急撥號
+unlock-a11y-button.ariaLabel=解鎖
+camera-a11y-button.ariaLabel=相機
+# see http://pubs.opengroup.org/onlinepubs/007908799/xsh/strftime.html
+longDateFormat=%B %e 日 %A
+
+# system dialogs
+login=登入
+username=使用者名稱
+password=密碼
+http-authentication-message={{host}} 要求您輸入使用者名稱與密碼。網站說:「{{realm}}」
+error-message=無法正確載入 {{name}}
+error-title=發生問題 {{name}}
+airplane-is-on=已開啟飛航模式
+airplane-is-turned-on={{name}} 需要網路連線。請於手機設定中關閉飛航模式,並連接至 Wi-Fi 或行動數據網路。
+network-connection-unavailable=無法使用網路連線
+network-error={{name}} 需要網路連線。請連接至 Wi-Fi 或行動數據網路。
+try-again=重試
+dialog-closed = 對話框被使用者關閉
+
+# value selector, time/date picker
+select-time=選擇時間
+choose-option=選取
+choose-options=選取
+select-day=選擇日子
+
+# system overlay
+battery-almost-empty=電力快要耗盡
+battery-low=電力不足
+battery-full=電池已充滿
+
+# wrapper
+add-to-home-screen=新增至裝置主畫面
+
+# crash report dialog
+crash-dialog2-os={{brandShortName}} 剛自錯誤中恢復
+crash-dialog-app={{name}} 剛發生錯誤
+crash-dialog-message=您願意傳送關於此錯誤的報告至 Mozilla,以幫助我們修正這個問題嗎?(報告將只透過 Wi-Fi 傳送。)
+crash-info-link=錯誤報告當中有哪些東西?
+crash-always-report=總是在發生程式錯誤時傳送報告給 Mozilla。
+crash-dont-send=不傳送
+crash-end=傳送報告
+
+# "Crash Reports" information page
+crashReports=錯誤資訊報表
+done=完成
+# Localization note (crash-reports-description-*): These strings are also included in settings.properties
+crash-reports-description-1=錯誤報告包含了該次錯誤與您的裝置的一些詳細資訊,以及錯誤發生時您的裝置的使用狀態。
+crash-reports-description-2=可能包含諸如開啟的頁面與應用程式、輸入至表單中的文字、開啟訊息的內容、最近的瀏覽紀錄、與開放應用程式的地理位置資訊。
+# Localization note (crash-reports-description-3-*): These strings are a paragraph, with a "privacy policy"
+# link in the middle. Include trailing spaces as needed.
+crash-reports-description-3-start=我們使用錯誤報告來試著修復問題並改進產品。我們將依照此文件上的方式處理您的資訊:
+crash-reports-description-3-privacy=隱私權保護政策
+crash-reports-description-3-end=所描述的方式處理您的資訊。
+
+# crash notification banner
+crash-banner-os2={{brandShortName}} 剛發生錯誤。
+crash-banner-app={{name}} 剛發生錯誤。
+crash-banner-report=報告
+
+# bluetooth transfer
+bluetooth-sendingTitle=正在送出藍牙傳輸資料..
+bluetooth-receivingTitle=正在接收藍牙傳輸資料..
+bluetooth-sending-progress=正在送出藍牙傳輸資料..
+bluetooth-receiving-progress=正在接收藍牙傳輸資料..
+acceptFileTransfer=允許藍牙檔案傳輸嗎?
+wantToReceiveFile={{deviceName}} 想要傳送 {{fileName}} ({{fileSize}})。
+byteUnit-B = 位元組
+byteUnit-KB = KB
+byteUnit-MB = MB
+byteUnit-GB = GB
+byteUnit-TB = TB
+byteUnit-PB = PB
+byteUnit-EB = EB
+byteUnit-ZB = ZB
+byteUnit-YB = YB
+# Localization note (fileSize*): The string is a float.
+fileSize = {{size}} {{unit}}
+transfer=傳輸
+cancelFileTransfer=取消藍牙檔案傳輸?
+bluetooth-file-transfer-result=藍牙檔案傳輸 {{status}}。
+complete=完成
+failed=失敗
+transferFinished-sendingCompletedTitle=藍牙檔案傳送完成
+transferFinished-receivedCompletedTitle=已可開啟透過藍牙接收的檔案
+transferFinished-sendingFailedTitle=藍牙檔案傳送失敗
+transferFinished-receivedFailedTitle=藍牙已接收檔案但無法開啟
+transferFinished-completedBody=傳輸完成
+transferFinished-failedBody=傳輸失敗
+unknown-device=未知的裝置
+cannotOpenFile=無法開啟接收的檔案
+unknownMediaTypeToOpen=無法開啟未知的媒體類型:
+cannotReceiveFile=無法接收檔案
+cannotGetStorageState=無法取得裝置儲存空間狀態
+sdcard-not-exist=找不到記憶卡 (請插入記憶卡)
+sdcard-in-use=當手機連接至電腦時無法使用藍牙。請拔除手機以接收檔案。
+sdcard-no-space2=無法完成傳輸:記憶卡已滿。
+unknown-error=未知錯誤
+confirm=確認
+
+# Security :: SIM PIN lock
+noSimCard=未插入 SIM 卡
+simPin=SIM PIN
+pukCode=PUK 碼
+nckCode=NCK 碼
+whatIsSimPin=SIM PIN 是什麼?
+simPinIntro1=SIM PIN 阻止了手機存取行動數據網路。開啟 PIN 時,任何插入了 SIM 卡的裝置將會在重開機時向您詢問 PIN 碼。
+simPinIntro2=SIM PIN 與用來解鎖裝置的解鎖碼不同。
+changeSimPin=修改 PIN
+pinTitle=輸入 SIM PIN
+pukTitle=輸入 PUK 碼
+nckTitle=輸入 NCK 碼
+newpinTitle=設定 PIN
+pinErrorMsg=PIN 不正確。
+pinAttemptMsg={[ plural(n) ]}
+pinAttemptMsg[one]=最後一次機會。
+pinAttemptMsg[two]=還有 {{n}} 次機會。
+pinAttemptMsg[few]=還有 {{n}} 次機會。
+pinAttemptMsg[many]=還有 {{n}} 次機會。
+pinAttemptMsg[other]=還有 {{n}} 次機會。
+pinLastChanceMsg=這是您最後一次能輸入正確 PIN 碼的機會。否則您必須輸入 PUK 碼才能使用此 SIM 卡。
+simCardLockedMsg=SIM 卡已鎖定。
+enterPukMsg=您必須輸入 SIM 卡的 PUK 碼。若需更多資訊,請參考您的 SIM 卡文件或洽詢您的電信業者。
+pukErrorMsg=PUK 碼不正確。
+pukAttemptMsg=您還有 {{n}} 次機會可以輸入正確的 PUK 解鎖碼,否則此 SIM 卡將永久失效。若需更多資訊,請參考您的 SIM 卡文件或洽詢您的電信業者。
+pukLastChanceMsg=這是您最後一次能輸入正確 PUK 解鎖碼的機會。若您這次還是輸入錯誤,您的 SIM 將會永久失效。若需更多資訊,請參考您的 SIM 卡文件或洽詢您的電信業者。
+newSimPinMsg=設定 PIN(須包含 4 ~ 8 碼數字)
+confirmNewSimPinMsg=確認新 PIN 碼
+newPinErrorMsg=兩次輸入的 PIN 不相同。
+nckErrorMsg=NCK 碼不正確。
+nckAttemptMsg=請求網路解鎖失敗。您還剩下 {{n}} 次機會可以輸入正確的 NCK 碼。請參考您的裝置說明書或連絡電信商以取得更多資訊。
+nckLastChanceMsg=請求網路解鎖失敗。您還剩下最後一次機會可以輸入正確的 NCK 碼。請參考您的裝置說明書或連絡電信商以取得更多資訊。
+
+# Activity selection menu titles
+activity-pick= 自下列項目選擇:
+activity-view= 使用下列項目檢視:
+activity-test= 使用下列項目測試:
+activity-share= 使用下列項目分享:
+activity-new= 使用下列項目建立:
+activity-open= 使用下列項目開啟:
+activity-save-bookmark=加入書籤到:
+activity-record=使用下列項目錄製:
+activity-browse=使用下列項目瀏覽:
+activity-configure=使用下列項目設定:
+activity-dial=使用下列項目撥號:
+
+# Persona dialog and Identity
+persona-signin=登入
+
+# Payment dialog
+payment-flow=購買
+
+# Remote Debugger Connection Dialog
+remoteDebuggerMessage=偵測到遠端除錯連線的請求,要允許連線嗎?
+
+# fullscreen permission
+# LOCALIZATION NOTE {{origin}} is the origin of the site which has requested fullscreen, e.g. http://example.com is now fullscreen. It includes protocol + hostname.
+fullscreen-request={{origin}} 已進入全螢幕模式。
+
diff --git a/apps/system/manifest.webapp b/apps/system/manifest.webapp
new file mode 100644
index 0000000..8155791
--- /dev/null
+++ b/apps/system/manifest.webapp
@@ -0,0 +1,68 @@
+{
+ "name": "System",
+ "type": "certified",
+ "description": "Main System",
+ "launch_path": "/index.html",
+ "developer": {
+ "name": "The Gaia Team",
+ "url": "https://github.com/mozilla-b2g/gaia"
+ },
+ "permissions": {
+ "alarms": {},
+ "browser":{},
+ "power":{},
+ "fmradio":{},
+ "webapps-manage":{},
+ "mobileconnection":{},
+ "bluetooth":{},
+ "telephony":{},
+ "voicemail":{},
+ "device-storage:sdcard":{ "access": "readonly" },
+ "device-storage:pictures":{ "access": "readwrite" },
+ "device-storage:videos":{ "access": "readwrite" },
+ "device-storage:music":{ "access": "readcreate" },
+ "settings":{ "access": "readwrite" },
+ "storage":{},
+ "camera":{},
+ "geolocation":{},
+ "wifi-manage":{},
+ "desktop-notification":{},
+ "idle":{},
+ "network-events":{},
+ "embed-apps":{},
+ "background-sensors":{},
+ "permissions":{},
+ "audio-channel-publicnotification":{},
+ "audio-channel-notification":{},
+ "audio-channel-content":{},
+ "cellbroadcast":{}
+ },
+ "locales": {
+ "ar": {
+ "name": "System",
+ "description": "Main System"
+ },
+ "en-US": {
+ "name": "System",
+ "description": "Main System"
+ },
+ "fr": {
+ "name": "System",
+ "description": "Main System"
+ },
+ "zh-TW": {
+ "name": "System",
+ "description": "Main System"
+ }
+ },
+ "default_locale": "en-US",
+ "messages": [
+ { "alarm": "/index.html" },
+ { "bluetooth-opp-transfer-complete": "/index.html" },
+ { "bluetooth-opp-update-progress": "/index.html" },
+ { "bluetooth-opp-receiving-file-confirmation": "/index.html" },
+ { "bluetooth-opp-transfer-start": "/index.html" },
+ { "icc-stkcommand": "/index.html" },
+ { "bluetooth-hfp-status-changed": "/index.html" }
+ ]
+}
diff --git a/apps/system/payment.html b/apps/system/payment.html
new file mode 100644
index 0000000..db9590d
--- /dev/null
+++ b/apps/system/payment.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="pragma" content="no-cache">
+
+ <!-- Payment -->
+ <script defer src="js/payment.js"></script>
+ </head>
+ <body>
+ <div id="requests">
+ <ul>
+ <!-- Placeholder for payment requests -->
+ </ul>
+ </div>
+ </body>
+</html>
diff --git a/apps/system/resources/images/backgrounds/default.png b/apps/system/resources/images/backgrounds/default.png
new file mode 100644
index 0000000..825d42e
--- /dev/null
+++ b/apps/system/resources/images/backgrounds/default.png
Binary files differ
diff --git a/apps/system/resources/sounds/unlock.ogg b/apps/system/resources/sounds/unlock.ogg
new file mode 100644
index 0000000..4ff3304
--- /dev/null
+++ b/apps/system/resources/sounds/unlock.ogg
Binary files differ
diff --git a/apps/system/style/accessibility/accessibility.css b/apps/system/style/accessibility/accessibility.css
new file mode 100644
index 0000000..abb145a
--- /dev/null
+++ b/apps/system/style/accessibility/accessibility.css
@@ -0,0 +1,3 @@
+.accessibility-invert {
+ filter: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxmaWx0ZXIKICAgICAgaWQ9ImludmVydEZpbHRlciIKICAgICAgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KICAgIDxmZUNvbG9yTWF0cml4CiAgICAgICAgdmFsdWVzPSItMSAwIDAgMCAxIDAgLTEgMCAwIDEgMCAwIC0xIDAgMSAwIDAgMCAxIDAgIiAvPgogIDwvZmlsdGVyPgo8L3N2Zz4K#invertFilter);
+}
diff --git a/apps/system/style/app_install_manager/app_install_manager.css b/apps/system/style/app_install_manager/app_install_manager.css
new file mode 100644
index 0000000..d657216
--- /dev/null
+++ b/apps/system/style/app_install_manager/app_install_manager.css
@@ -0,0 +1,98 @@
+/* dialog */
+/* FIXME we have to see why we need such specific style here and why
+ * the general style in confirm.css isn't sufficient */
+#app-install-dialog,
+#app-install-cancel-dialog,
+#app-download-cancel-dialog {
+ display: none;
+ position: absolute;
+ top: 20px;
+ left: 0;
+ width: 100%;
+ bottom: 0;
+ pointer-events: none;
+}
+
+#app-install-dialog.visible,
+#app-install-cancel-dialog.visible,
+#app-download-cancel-dialog.visible {
+ display: inline-block;
+ pointer-events: auto;
+}
+
+#app-install-dialog section,
+#app-install-cancel-dialog section,
+#app-download-cancel-dialog section {
+ height: auto;
+}
+
+#app-install-dialog h1,
+#app-install-cancel-dialog h1,
+#app-download-cancel-dialog h1 {
+ border-bottom: none;
+ background: none;
+}
+
+#app-install-dialog table {
+ border-top: 0.1rem solid #686868;
+ margin: 1rem 0 0;
+ overflow: hidden;
+ padding-top: 1rem;
+ font-size: 1.4rem;
+ width: 100%;
+}
+
+#app-install-dialog table th {
+ text-align: left;
+ margin-right: 0.5rem;
+}
+
+#app-install-dialog table td {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+#app-install-cancel-dialog small:first-child {
+ margin-bottom: 1rem;
+}
+/* ... end of dialog */
+
+/* notification */
+#install-manager-notification-container > .fake-notification {
+ padding: 10px 10px 10px 60px;
+ -moz-box-sizing: border-box;
+
+ background-repeat: no-repeat;
+ background-position: 15px center;
+ background-image: url(images/downloads.png);
+}
+
+#install-manager-notification-container > .fake-notification > * {
+ pointer-events: none;
+}
+
+#install-manager-notification-container progress {
+ width: 100%;
+ height: 4px;
+
+ border: none;
+}
+
+#install-manager-notification-container progress::-moz-progress-bar {
+ background: #ec860f;
+ border-bottom: 1px solid #b96100;
+ border-top: 2px solid #e67200;
+}
+
+#install-manager-notification-container progress:indeterminate::-moz-progress-bar {
+ background: url(../shared/progress.gif) #f2aa56;
+ border: none;
+}
+
+#install-manager-notification-container .message {
+ line-height: 20px;
+ font-weight: 700;
+ font-size: 1.6rem;
+}
+
+/* ... end of notification */
diff --git a/apps/system/style/app_install_manager/images/downloads.png b/apps/system/style/app_install_manager/images/downloads.png
new file mode 100644
index 0000000..8a5dcb7
--- /dev/null
+++ b/apps/system/style/app_install_manager/images/downloads.png
Binary files differ
diff --git a/apps/system/style/attention_screen.css b/apps/system/style/attention_screen.css
new file mode 100644
index 0000000..126d52e
--- /dev/null
+++ b/apps/system/style/attention_screen.css
@@ -0,0 +1,57 @@
+#attention-screen {
+ position: absolute;
+ top: 0px;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ display: none;
+
+ transition: transform 0.5s ease;
+}
+
+#attention-screen.displayed {
+ display: block;
+}
+
+#screen.active-statusbar #attention-screen {
+ transform: translateY(calc(-100% + 40px));
+}
+
+#screen.active-statusbar #attention-screen.status-mode {
+ height: 40px;
+ transform: translateY(0px);
+ transition: none;
+}
+
+#attention-screen.status-mode > iframe {
+ margin-top: 0;
+ height: 100%;
+}
+
+#attention-screen > iframe {
+ border: 0;
+ width: 100%;
+ /* we have the 20px status bar on top */
+ height: calc(100% - 20px);
+ margin-top: 20px;
+
+ background-color: transparent;
+
+ position: absolute;
+ top: 0;
+ left: 0;
+}
+
+#attention-screen > #attention-bar {
+ position: absolute;
+ bottom: 0px;
+ width: 100%;
+ /* Status bar height * 2 */
+ height: 40px;
+ z-index: 2;
+ display: none;
+}
+
+#screen.active-statusbar #attention-screen > #attention-bar {
+ display: block;
+}
diff --git a/apps/system/style/battery_manager/battery_manager.css b/apps/system/style/battery_manager/battery_manager.css
new file mode 100644
index 0000000..5a7a530
--- /dev/null
+++ b/apps/system/style/battery_manager/battery_manager.css
@@ -0,0 +1,55 @@
+#system-overlay.battery {
+ visibility: visible;
+}
+
+#system-overlay.battery #battery {
+ display: block;
+}
+
+#battery {
+ display: none;
+ background-image: url('images/header_bg.png');
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 45px;
+ font-size: 1.5rem;
+ font-weight: 500;
+}
+
+.icon-battery {
+ display: block;
+ float: left;
+
+ width: 42px;
+ height: 18px;
+ margin: 0 1.3rem;
+
+ background-image: url('images/battery_empty_small.png');
+ background-position: center center;
+ background-repeat: no-repeat;
+}
+
+#battery span {
+ vertical-align: middle;
+ height: 45px;
+ line-height: 45px;
+}
+
+.battery-notification {
+ display: inline-block;
+}
+
+#battery.disappearing {
+ animation: notification-disappear 1.5s;
+}
+
+@keyframes notification-disappear {
+ to {
+ transform: translateX(100%);
+ }
+ from {
+ transform: translateX(0);
+ }
+}
diff --git a/apps/system/style/battery_manager/images/battery_empty_small.png b/apps/system/style/battery_manager/images/battery_empty_small.png
new file mode 100644
index 0000000..2075e0c
--- /dev/null
+++ b/apps/system/style/battery_manager/images/battery_empty_small.png
Binary files differ
diff --git a/apps/system/style/battery_manager/images/header_bg.png b/apps/system/style/battery_manager/images/header_bg.png
new file mode 100644
index 0000000..e1ff073
--- /dev/null
+++ b/apps/system/style/battery_manager/images/header_bg.png
Binary files differ
diff --git a/apps/system/style/bb/value_selector.css b/apps/system/style/bb/value_selector.css
new file mode 100644
index 0000000..dd120ef
--- /dev/null
+++ b/apps/system/style/bb/value_selector.css
@@ -0,0 +1,221 @@
+/* ----------------------------------
+ * Value selector (Single & Multiple)
+ * ---------------------------------- */
+
+/* Main dialog setup */
+form[role="dialog"] {
+ background: url(value_selector/images/ui/pattern.png) repeat left top, url(value_selector/images/ui/gradient.png) no-repeat left top;
+ background-size: auto auto, 100% 100%;
+ /* We can't use shortand with background size because is not implemented yet:
+ https://bugzilla.mozilla.org/show_bug.cgi?id=570326; */
+ overflow: hidden;
+ position: absolute;
+ z-index: 100;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ padding: 0 0 7rem;
+ font-family: "MozTT", Sans-serif;
+ font-size: 0;
+ /* Using font-size: 0; we avoid the unwanted visual space (about 3px)
+ created by white-spaces and break lines in the code betewen inline-block elements */
+ color: #fff;
+}
+
+form[role="dialog"]:before {
+ content: "";
+ display: inline-block;
+ vertical-align: top;
+ width: 1px;
+ height: 100%;
+ margin-left: -1px;
+}
+
+form[role="dialog"] > section {
+ padding: 0 1.5rem 0;
+ -moz-box-sizing: padding-box;
+ width: 100%;
+ height: 100%;
+ display: inline-block;
+ vertical-align: top;
+ white-space: normal;
+}
+
+form[role="dialog"] h1 {
+ font: 1.6rem/1em 'MozTT', Sans-serif;
+ color: #fff;
+ border-bottom: 0.1rem solid #616262;
+ background: url(value_selector/images/ui/alpha.png) repeat 0 0;
+ margin: 0 -1.5rem;
+ padding: 2.5rem 3rem 1rem;
+}
+
+/* Menu & buttons setup */
+form[role="dialog"] menu {
+ white-space: nowrap;
+ margin: 0;
+ padding: 1.5rem;
+ border-top: solid 1px rgba(255, 255, 255, 0.1);
+ background: #2d2d2d url(value_selector/images/ui/pattern.png) repeat left top;
+ display: block;
+ overflow: hidden;
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+}
+
+form[role="dialog"] menu button {
+ width: 100%;
+ height: 3.8rem;
+ margin: 0 0 1rem;
+ padding: 0 1.5rem;
+ -moz-box-sizing: border-box;
+ display: inline-block;
+ vertical-align: middle;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ border-radius: 0.3rem;
+ outline: none;
+ border: none;
+ font-size: 1.6rem;
+ font-family: 'MozTT', Sans-serif;
+ font-weight: 600;
+ line-height: 3.8rem;
+ color: #fff;
+ text-shadow: -1px -1px 0 #830b0b;
+ text-align: center;
+ text-decoration: none;
+}
+
+/* Reset & submit defaults */
+form[role="dialog"] menu button[type="reset"],
+form[role="dialog"] menu button[type="submit"] {
+ text-shadow: 1px 1px 0 rgba(255,255,255,0.3);
+ color: #333;
+}
+
+/* Reset */
+form[role="dialog"] menu button[type="reset"] {
+ background-image: url(value_selector/images/ui/button_reset.png);
+ background-color: #fafafa;
+ border: solid 1px #9f9f9f;
+}
+
+/* Submit */
+form[role="dialog"] menu button[type="submit"] {
+ background-image: url(value_selector/images/ui/button_submit.png) ;
+ background-color: #00caf2;
+ border: 1px solid #00acce;
+}
+
+/* Pressed state, reset & submit */
+form[role="dialog"] menu button[type="reset"]:active,
+form[role="dialog"] menu button[type="submit"]:active {
+ border: solid 1px #008aaa;
+ background: #008aaa;
+ color: #333;
+}
+
+/* Disabled, reset & submit) */
+form[role="dialog"] menu button[type="reset"][disabled],
+form[role="dialog"] menu button[type="submit"][disabled] {
+ background-image: url(value_selector/images/ui/button_special_disabled.png);
+ background-color: transparent;
+ border: none;
+ color: #4a4a4a;
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2);
+}
+
+form[role="dialog"] menu button:last-child {
+ margin-left: 1rem;
+}
+
+form[role="dialog"] menu button,
+form[role="dialog"] menu button:first-child {
+ margin: 0;
+}
+
+form[role="dialog"] menu button {
+ width: -moz-calc((100% - 1rem) / 2);
+}
+
+form[role="dialog"] menu button.full {
+ width: 100%;
+}
+
+/* Specific component code */
+form[role="dialog"] [role="listbox"] {
+ position: relative;
+ padding: 0 1.5rem;
+ margin: 0 -1.5rem;
+ max-height: calc(100% - 5rem);
+ overflow: auto;
+ border-top: solid 0.1rem #222323;
+}
+
+form[role="dialog"] .scrollable:before {
+ content: "";
+ display: block;
+ position: absolute;
+ pointer-events: none;
+ top: 5.2rem;
+ left: 0;
+ right: 0;
+ bottom: 6.9rem;
+ background: url(value_selector/images/ui/shadow.png) repeat-x left top, url(value_selector/images/ui/shadow-invert.png) repeat-x left bottom;
+}
+
+form[role="dialog"] [role="listbox"] li {
+ margin: 0;
+ padding-bottom: 1px;
+ height: 5.9rem;
+ list-style: none;
+ position: relative;
+ border-bottom: 1px solid #666;
+ font-weight: lighter;
+ font-size: 2.2rem;
+ line-height: 5.9rem;
+ color: #fff;
+ transition: background 0.2s ease;
+}
+
+form[role="dialog"] [role="listbox"] li span {
+ padding: 0 1.5rem;
+}
+
+form[role="dialog"] [role="listbox"] li:active,
+form[role="dialog"] [role="listbox"] li:active label span {
+ background: #00ABCC;
+ color: #fff!important;
+}
+
+form[role="dialog"] [role="listbox"] li input {
+ position: absolute;
+ left: 0;
+ top: 0;
+ opacity: 0;
+ pointer-events: none;
+}
+
+form[role="dialog"] [role="listbox"] li input:checked + span,
+form[role="dialog"] [role="listbox"] li[aria-checked="true"] span {
+ color: #00abcd;
+ background: url(value_selector/images/icons/checked.png) content-box right center no-repeat;
+}
+
+form[role="dialog"] [role="listbox"] li a,
+form[role="dialog"] [role="listbox"] li label {
+ border-bottom: 1px solid #000;
+ outline: none;
+}
+
+form[role="dialog"] [role="listbox"] li a,
+form[role="dialog"] [role="listbox"] li label,
+form[role="dialog"] [role="listbox"] li label span {
+ text-decoration: none;
+ display: block;
+ color: #fff;
+}
diff --git a/apps/system/style/bb/value_selector/images/icons/checked.png b/apps/system/style/bb/value_selector/images/icons/checked.png
new file mode 100644
index 0000000..69d5c3d
--- /dev/null
+++ b/apps/system/style/bb/value_selector/images/icons/checked.png
Binary files differ
diff --git a/apps/system/style/bb/value_selector/images/ui/alpha.png b/apps/system/style/bb/value_selector/images/ui/alpha.png
new file mode 100644
index 0000000..d7d89aa
--- /dev/null
+++ b/apps/system/style/bb/value_selector/images/ui/alpha.png
Binary files differ
diff --git a/apps/system/style/bb/value_selector/images/ui/button_reset.png b/apps/system/style/bb/value_selector/images/ui/button_reset.png
new file mode 100644
index 0000000..1080862
--- /dev/null
+++ b/apps/system/style/bb/value_selector/images/ui/button_reset.png
Binary files differ
diff --git a/apps/system/style/bb/value_selector/images/ui/button_special_disabled.png b/apps/system/style/bb/value_selector/images/ui/button_special_disabled.png
new file mode 100644
index 0000000..8a93c42
--- /dev/null
+++ b/apps/system/style/bb/value_selector/images/ui/button_special_disabled.png
Binary files differ
diff --git a/apps/system/style/bb/value_selector/images/ui/button_submit.png b/apps/system/style/bb/value_selector/images/ui/button_submit.png
new file mode 100644
index 0000000..a42b1e8
--- /dev/null
+++ b/apps/system/style/bb/value_selector/images/ui/button_submit.png
Binary files differ
diff --git a/apps/system/style/bb/value_selector/images/ui/gradient.png b/apps/system/style/bb/value_selector/images/ui/gradient.png
new file mode 100644
index 0000000..b288545
--- /dev/null
+++ b/apps/system/style/bb/value_selector/images/ui/gradient.png
Binary files differ
diff --git a/apps/system/style/bb/value_selector/images/ui/pattern.png b/apps/system/style/bb/value_selector/images/ui/pattern.png
new file mode 100644
index 0000000..af03f56
--- /dev/null
+++ b/apps/system/style/bb/value_selector/images/ui/pattern.png
Binary files differ
diff --git a/apps/system/style/bb/value_selector/images/ui/shadow-invert.png b/apps/system/style/bb/value_selector/images/ui/shadow-invert.png
new file mode 100644
index 0000000..604e6ae
--- /dev/null
+++ b/apps/system/style/bb/value_selector/images/ui/shadow-invert.png
Binary files differ
diff --git a/apps/system/style/bb/value_selector/images/ui/shadow.png b/apps/system/style/bb/value_selector/images/ui/shadow.png
new file mode 100644
index 0000000..9bc7bd5
--- /dev/null
+++ b/apps/system/style/bb/value_selector/images/ui/shadow.png
Binary files differ
diff --git a/apps/system/style/bluetooth_transfer/bluetooth_transfer.css b/apps/system/style/bluetooth_transfer/bluetooth_transfer.css
new file mode 100644
index 0000000..1ae60d5
--- /dev/null
+++ b/apps/system/style/bluetooth_transfer/bluetooth_transfer.css
@@ -0,0 +1,53 @@
+#bluetooth-transfer-status {
+ position: relative;
+ border-top-color: black;
+
+/* display: none;*/
+}
+
+#bluetooth-transfer-status.displayed {
+ display: block;
+}
+
+#bluetooth-transfer-status.applying .bluetooth-transfer-progress {
+/* display: none;*/
+}
+
+#bluetooth-transfer-status .bluetooth-transfer-progress {
+ display: block;
+ width: auto;
+ margin-right: 1.5rem;
+}
+
+#bluetooth-transfer-status progress {
+ position: absolute;
+ top: 35px;
+ left: 50px;
+ width: -moz-calc(100% - 105px);
+ height: 10px;
+ padding: 0;
+
+ border: 0;
+ border-radius: 10px;
+}
+
+#bluetooth-transfer-status.applying progress {
+/* display: none;*/
+}
+
+#bluetooth-transfer-status progress::-moz-progress-bar {
+ border-radius: 10px;
+ background-color: #52b6cc;
+}
+
+#bluetooth-transfer-status.applying .icon {
+ background-image: url('images/spinner.png');
+
+ animation: spin 1.75s infinite linear;
+ transform-origin : center center;
+}
+
+@-moz-keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
diff --git a/apps/system/style/bluetooth_transfer/images/icon_bluetooth.png b/apps/system/style/bluetooth_transfer/images/icon_bluetooth.png
new file mode 100644
index 0000000..90b56bf
--- /dev/null
+++ b/apps/system/style/bluetooth_transfer/images/icon_bluetooth.png
Binary files differ
diff --git a/apps/system/style/bluetooth_transfer/images/transfer.png b/apps/system/style/bluetooth_transfer/images/transfer.png
new file mode 100644
index 0000000..904f6ad
--- /dev/null
+++ b/apps/system/style/bluetooth_transfer/images/transfer.png
Binary files differ
diff --git a/apps/system/style/cards_view/cards_view.css b/apps/system/style/cards_view/cards_view.css
new file mode 100644
index 0000000..fe90264
--- /dev/null
+++ b/apps/system/style/cards_view/cards_view.css
@@ -0,0 +1,115 @@
+#cards-view {
+ visibility: hidden;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ transition: opacity 0.5s ease, visibility 0.5s ease;
+ opacity: 0;
+ -moz-user-select: none;
+ overflow: scroll;
+ background-color: rgba(0, 0, 0, 0.8);
+}
+
+#cards-view.active {
+ visibility: inherit;
+ opacity: 1;
+}
+
+#cards-view ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+ white-space: nowrap;
+ text-align: center;
+}
+
+#cards-view .card {
+ display: inline-block;
+ width: 100%;
+ height: -moz-calc(100% - 20px);
+ margin: 0;
+ margin-top: 20px;
+ margin-left: -25%;
+ position: relative;
+ background-size: contain;
+ background-position: center center;
+ background-repeat: no-repeat;
+ transform: scale(0.6);
+}
+
+#cards-view .card h1 {
+ position: absolute;
+ top: 1.5rem;
+ left: 1rem;
+ z-index: 99999;
+ text-align: left;
+ font-size: 3.5rem;
+ font-weight: 300;
+ text-overflow: ellipsis;
+ overflow-x: hidden;
+}
+
+#cards-view .card header {
+ top: 1.5rem;
+}
+
+#cards-view .card[data-edit='true'] {
+ position: relative;
+ opacity: 0.8;
+ z-index: 99999;
+}
+
+#cards-view .card:first-child {
+ position: absolute;
+ margin-left: 0;
+}
+
+#cards-view .card:only-child {
+ margin-left: -50%;
+}
+
+#cards-view .card:nth-child(2) {
+ margin-left: 75%;
+}
+
+#cards-view .card > *:not(.close-card) {
+ pointer-events: none;
+}
+
+#cards-view .card > h1 {
+ position: absolute;
+ top: 100%;
+ width: 100%;
+ line-height: 4rem;
+}
+
+#cards-view .card > p {
+ position: absolute;
+ top: -moz-calc(100% + 4rem);
+ width: 100%;
+ font-size: 2rem;
+}
+
+#cards-view .card > img.appIcon {
+ position: relative;
+ height: 8rem;
+ width: 8rem;
+ border-radius: .6rem;
+ top: 50%;
+ margin-top: -4rem;
+}
+
+#cards-view .close-card {
+ position: absolute;
+ top: -1rem;
+ left: -1rem;
+ z-index: 99999;
+ height: 2.6rem;
+ width: 2.6rem;
+ background: url(close.png) no-repeat center center;
+ transform: scale(1.7);
+}
diff --git a/apps/system/style/cards_view/close.png b/apps/system/style/cards_view/close.png
new file mode 100644
index 0000000..de1f82d
--- /dev/null
+++ b/apps/system/style/cards_view/close.png
Binary files differ
diff --git a/apps/system/style/cost_control/cost_control.css b/apps/system/style/cost_control/cost_control.css
new file mode 100644
index 0000000..e39e6f9
--- /dev/null
+++ b/apps/system/style/cost_control/cost_control.css
@@ -0,0 +1,14 @@
+/* Resetting */
+#cost-control-widget,
+#cost-control-widget iframe {
+ width: 100%;
+ height: 4.5rem;
+ border: none;
+ margin: 0;
+ padding: 0;
+}
+
+#cost-control-widget iframe {
+ position: absolute;
+ top: 0;
+}
diff --git a/apps/system/style/crash_reporter/crash_reporter.css b/apps/system/style/crash_reporter/crash_reporter.css
new file mode 100644
index 0000000..1508708
--- /dev/null
+++ b/apps/system/style/crash_reporter/crash_reporter.css
@@ -0,0 +1,43 @@
+#crash-dialog h1 {
+ border-bottom: none;
+}
+
+#crash-dialog p {
+ overflow: visible;
+}
+
+#crash-dialog p:not(:first-of-type) {
+ border-top: none;
+}
+
+#crash-dialog a {
+ text-decoration: underline;
+ margin-top: 2rem;
+}
+
+#crash-dialog.learn-more > form {
+ display: none;
+}
+
+#crash-dialog:not(.learn-more) > section {
+ display: none;
+}
+
+/* "Crash Reports" information page */
+#crash-dialog > section {
+ font-family: "MozTT", Sans-serif;
+ background-color: #fff;
+ color: #000;
+ position: absolute;
+ z-index: 100;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+}
+
+#crash-dialog > section > p {
+ padding: 1rem 3rem;
+ font-size: 1.5rem;
+ white-space: normal;
+}
diff --git a/apps/system/style/fake-notification.css b/apps/system/style/fake-notification.css
new file mode 100644
index 0000000..1250945
--- /dev/null
+++ b/apps/system/style/fake-notification.css
@@ -0,0 +1,9 @@
+.fake-notification {
+ position: relative;
+ height: 60px;
+
+ background-color: rgba(0, 0, 0, 0.5);
+ border-top: 1px black solid;
+ border-bottom: 1px black solid;
+}
+
diff --git a/apps/system/style/gridview/gridview.css b/apps/system/style/gridview/gridview.css
new file mode 100644
index 0000000..e94dd88
--- /dev/null
+++ b/apps/system/style/gridview/gridview.css
@@ -0,0 +1,11 @@
+#debug-grid {
+ position: absolute;
+ top: 0;
+ left: 0;
+ display: block;
+ width: 100%;
+ height: 100%;
+ background: url('./images/grid.png');
+ opacity: 0.2;
+ pointer-events: none;
+}
diff --git a/apps/system/style/gridview/images/grid.png b/apps/system/style/gridview/images/grid.png
new file mode 100644
index 0000000..d0aaefc
--- /dev/null
+++ b/apps/system/style/gridview/images/grid.png
Binary files differ
diff --git a/apps/system/style/icons/voicemail.png b/apps/system/style/icons/voicemail.png
new file mode 100644
index 0000000..e132699
--- /dev/null
+++ b/apps/system/style/icons/voicemail.png
Binary files differ
diff --git a/apps/system/style/list_menu/images/header-left-arrow.png b/apps/system/style/list_menu/images/header-left-arrow.png
new file mode 100644
index 0000000..03ec792
--- /dev/null
+++ b/apps/system/style/list_menu/images/header-left-arrow.png
Binary files differ
diff --git a/apps/system/style/list_menu/images/header-right-arrow.png b/apps/system/style/list_menu/images/header-right-arrow.png
new file mode 100644
index 0000000..5cd5ee6
--- /dev/null
+++ b/apps/system/style/list_menu/images/header-right-arrow.png
Binary files differ
diff --git a/apps/system/style/list_menu/list_menu.css b/apps/system/style/list_menu/list_menu.css
new file mode 100644
index 0000000..63a2ced
--- /dev/null
+++ b/apps/system/style/list_menu/list_menu.css
@@ -0,0 +1,35 @@
+#listmenu {
+ visibility: hidden;
+}
+
+#listmenu.visible {
+ visibility: visible;
+}
+
+#listmenu menu,
+#listmenu.visible menu.slidedown {
+ transition: transform 0.3s ease;
+ transform: translateY(100%);
+}
+
+#listmenu.visible menu {
+ transform: translateY(0);
+}
+
+#listmenu menu button.icon,
+#listmenu menu a[role="button"].icon {
+ background-repeat: no-repeat;
+ background-position: 10px center;
+ padding-left: 45px;
+ background-size: 30px 30px;
+}
+
+/* 320x480 phones */
+@media (orientation: portrait) and (width: 320px),
+ (orientation: landscape) and (width: 480px) {
+ #listmenu menu button.icon,
+ #listmenu menu a[role="button"].icon {
+ background-position: 5px center;
+ padding-left: 40px;
+ }
+}
diff --git a/apps/system/style/lockscreen/images/handle.png b/apps/system/style/lockscreen/images/handle.png
new file mode 100644
index 0000000..503f86f
--- /dev/null
+++ b/apps/system/style/lockscreen/images/handle.png
Binary files differ
diff --git a/apps/system/style/lockscreen/images/icon-camera.png b/apps/system/style/lockscreen/images/icon-camera.png
new file mode 100644
index 0000000..6346a09
--- /dev/null
+++ b/apps/system/style/lockscreen/images/icon-camera.png
Binary files differ
diff --git a/apps/system/style/lockscreen/images/icon-clear.png b/apps/system/style/lockscreen/images/icon-clear.png
new file mode 100644
index 0000000..0e54b75
--- /dev/null
+++ b/apps/system/style/lockscreen/images/icon-clear.png
Binary files differ
diff --git a/apps/system/style/lockscreen/images/icon-unlock.png b/apps/system/style/lockscreen/images/icon-unlock.png
new file mode 100644
index 0000000..4bb87a9
--- /dev/null
+++ b/apps/system/style/lockscreen/images/icon-unlock.png
Binary files differ
diff --git a/apps/system/style/lockscreen/images/mask.png b/apps/system/style/lockscreen/images/mask.png
new file mode 100644
index 0000000..e1b8cf5
--- /dev/null
+++ b/apps/system/style/lockscreen/images/mask.png
Binary files differ
diff --git a/apps/system/style/lockscreen/images/mute.png b/apps/system/style/lockscreen/images/mute.png
new file mode 100644
index 0000000..46e1019
--- /dev/null
+++ b/apps/system/style/lockscreen/images/mute.png
Binary files differ
diff --git a/apps/system/style/lockscreen/lockscreen.css b/apps/system/style/lockscreen/lockscreen.css
new file mode 100644
index 0000000..aadd881
--- /dev/null
+++ b/apps/system/style/lockscreen/lockscreen.css
@@ -0,0 +1,623 @@
+#lockscreen {
+ position: absolute;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ background-color: #000;
+
+ -moz-user-select: none;
+
+ opacity: 1;
+
+ transition:
+ transform 0.5s ease,
+ opacity 0.5s ease,
+ visibility 0.5s ease;
+}
+
+#screen.active-statusbar > #lockscreen {
+ top: 40px;
+ height: calc(100% - 40px);
+}
+
+#screen:not(.locked) > #lockscreen,
+#screen.lockscreen-camera > #lockscreen {
+ transform: scale(2);
+ opacity: 0;
+ visibility: hidden;
+ pointer-events: none;
+
+ transition-delay: 0.3s;
+}
+
+#screen.screenoff > #lockscreen,
+#screen.screenoff > #lockscreen * {
+ transition: none;
+}
+
+#screen.lockscreen-camera > #lockscreen-camera {
+ visibility: visible;
+}
+
+/* When switching lock-camera off,
+ * transition time should be delayed to prevent homescreen appears.
+ * (Let lockscreen appears first) */
+#screen:not(.lockscreen-camera) > #lockscreen-camera {
+ transition-delay: 0.3s;
+}
+
+#lockscreen-camera {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: #000;
+
+ visibility: hidden;
+}
+
+#lockscreen-camera > iframe {
+ width: 100%;
+ height: 100%;
+ border: none;
+}
+
+#lockscreen.uninit > * {
+ display: none;
+}
+
+#lockscreen.no-transition {
+ transition: none;
+}
+
+#lockscreen-container {
+ width: 100%;
+ height: 100%;
+
+ /**
+ * Workaround bug 823418 by trigger a repaint as soon as .screenoff class
+ * is removed, remove me when the bug is fixed.
+ */
+ transition: opacity 0.1s ease;
+}
+
+/**
+ * Workaround bug 823418 by trigger a repaint as soon as .screenoff class
+ * is removed, remove me when the bug is fixed.
+ */
+.screenoff #lockscreen-container {
+ opacity: 0.99;
+}
+
+.lockscreen-wallpaper {
+ position: absolute;
+}
+
+.lockscreen-panel {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+
+ visibility: hidden;
+ transition: visibility 0.5s ease, transform 0.5s ease;
+}
+
+[data-panel="main"] #lockscreen-panel-main,
+[data-panel="passcode"] #lockscreen-panel-passcode,
+[data-panel="emergency-call"] #lockscreen-panel-emergency-call {
+ visibility: inherit;
+}
+
+[data-panel="emergency-call"] #lockscreen-panel-main {
+ transform: translateX(-100%);
+}
+
+#lockscreen h2 {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 10rem;
+ margin: 0;
+ padding: 2.5rem 2.8rem;
+ -moz-box-sizing: border-box;
+ font-size: 3.2rem;
+ font-weight: normal;
+ line-height: 3.2rem;
+ text-shadow: 0 0 3px #333;
+
+ transform: translateY(-10rem);
+ transition: transform 0.3s ease;
+}
+
+#lockscreen-header {
+ z-index: 9999;
+ position: absolute;
+ top: 2rem; /* 2rem = height of status bar */
+ left: 0;
+ width: 100%;
+ -moz-box-sizing: border-box;
+ padding: 1.8rem 2.5rem 1.2rem 2.5rem;
+ color: #fff;
+ text-shadow: 0 0 3px #333;
+ transform: translateY(-10rem);
+ transition: transform 0.2s ease, opacity 0.2s ease;
+ opacity: 1;
+}
+
+[data-panel="passcode"] #lockscreen-header {
+ transform: translateY(0);
+ opacity: 0;
+}
+
+[data-panel="main"] #lockscreen-header {
+ transform: translateY(0);
+}
+
+[data-panel="camera"] #lockscreen-header,
+[data-panel="emergency-call"] #lockscreen-header {
+ transform: translateY(0);
+ transition: none;
+}
+
+#lockscreen-connstate {
+ width: 100%;
+ display: inline-block;
+ font-family: 'MozTT', sans-serif;
+ font-weight: 400;
+ font-size: calc(6 * 0.226rem);
+ text-shadow: 1px 1px 3px #000000;
+
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+#lockscreen-connstate > span {
+ display: block;
+ padding-bottom: 1rem;
+ border-bottom: solid 1px rgba(256,256,256,.4);
+}
+
+#lockscreen-connstate > span:empty {
+ display: none;
+}
+
+#lockscreen-connstate.twolines > span:first-child {
+ border-bottom: none;
+ padding-bottom: 0;
+}
+
+/* For some reason display: inline-block disregards hidden attribute */
+#lockscreen-connstate[hidden] {
+ display: none;
+}
+
+#lockscreen-mute {
+ float: right;
+ width: 4rem;
+ height: 4rem;
+ margin-top: 3.5rem;
+
+ background: transparent url('./images/mute.png') center center no-repeat;
+}
+
+#lockscreen-mute.vibration {
+ background: transparent url('./images/vibration.png') center center no-repeat;
+}
+
+.lockscreen-clock {
+ margin: -0.8rem 0 -1rem -0.4rem;
+ font-weight: 300;
+}
+
+#lockscreen-clock-numbers {
+ font-size: calc(28 * 0.226rem);
+}
+
+#lockscreen-clock-meridiem {
+ font-size: calc(17 * 0.226rem);
+}
+
+#lockscreen-date {
+ font-weight: 400;
+ font-size: calc(7 * 0.226rem);
+}
+
+.lockscreen-clock, #lockscreen-date {
+ font-family: 'MozTT', sans-serif;
+ text-shadow: 1px 1px 3px #000000;
+ color: #fff;
+}
+
+#lockscreen-area {
+ position: absolute;
+ height: 11.2rem;
+ left: 0;
+ right: 0;
+ bottom: 0;
+}
+
+.lockscreen-icon-area {
+ width: calc(50%);
+ height: 12rem;
+ position: absolute;
+ border-style: none;
+ opacity: 0.1;
+ transition: opacity 0.5s ease;
+}
+
+.lockscreen-icon {
+ margin-top: 2rem;
+ border-radius: 3rem;
+ width: 6rem;
+ height: 6rem;
+ background-color: rgba(0, 0, 0, 0.3);
+ -moz-box-sizing: border-box;
+ border: 2px solid rgba(255, 255, 255, 0.8);
+ pointer-events: none;
+}
+
+.lockscreen-icon-area:active > .lockscreen-icon {
+ background-color: rgb(0, 138, 170);
+}
+
+.touched .lockscreen-icon {
+ transition: none;
+}
+
+.lockscreen-icon-a11y-button {
+ width: 100%;
+ height: 100%;
+ border-width: inherit;
+ border-radius: inherit;
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: -1;
+}
+
+.lockscreen-icon-a11y-button * {
+ display: none;
+}
+
+.lockscreen-icon.triggered {
+ background-color: #52b6cc !important;
+ border-color: #52b6cc;
+}
+
+.lockscreen-icon-right {
+ right: 0;
+}
+
+.lockscreen-icon-left {
+ left: 0;
+}
+
+button::-moz-focus-inner {
+ border: 0;
+}
+
+:-moz-any(.touched, .triggered, #screen:not(.locked), #screen.attention, #lockscreen:not([data-panel="main"]))
+:-moz-any(#lockscreen-left-arrow, #lockscreen-right-arrow) {
+ display: none;
+ animation: none;
+}
+
+.lockscreen-icon-right > .lockscreen-icon {
+ margin-right: 4rem;
+ margin-left: auto;
+}
+
+.lockscreen-icon-left > .lockscreen-icon {
+ margin-left: 4rem;
+ margin-right: auto;
+}
+
+#lockscreen-area-unlock > div {
+ background-image: url('./images/icon-unlock.png');
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+#lockscreen-area-camera > div {
+ background-image: url('./images/icon-camera.png');
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+[data-panel="emergency-call"] #lockscreen-panel-passcode {
+ transform: translateX(-100%);
+}
+
+[data-panel="passcode"] h2#lockscreen-passcode-status {
+ visibility: inherit;
+ transform: none;
+}
+
+[data-panel="emergency-call"] h2#lockscreen-passcode-status {
+ transition-delay: 0.5s;
+}
+
+#lockscreen-passcode-code {
+ position: absolute;
+ bottom: 21.4rem;
+ height: 7rem;
+ width: 100%;
+ background-color: rgba(49, 60, 70, 0.9);
+ margin: 0;
+ padding: 0 1rem;
+ -moz-box-sizing: border-box;
+ border-top: 1px solid #525050;
+ visibility: hidden;
+ transform: translateY(calc(21.4rem + 7.2rem));
+ transition: visibility 0.3s ease, transform 0.3s ease;
+}
+
+[data-passcode-status="success"] #lockscreen-passcode-pad,
+[data-passcode-status="success"] #lockscreen-passcode-code {
+ transform: translateY(calc(21.4rem + 7.2rem)) !important;
+}
+
+[data-passcode-status="success"] #lockscreen-passcode-status {
+ transform: translateY(-10rem) !important;
+}
+
+[data-panel="passcode"] #lockscreen-passcode-code {
+ visibility: inherit;
+ transform: translateY(0);
+}
+
+[data-panel="emergency-call"] #lockscreen-passcode-code {
+ transition-delay: 0.5s;
+}
+
+#lockscreen-passcode-code > span {
+ -moz-box-sizing: border-box;
+ display: block;
+ float: left;
+ width: calc(25% - 1rem);
+ margin: 1.5rem 0.5rem;
+ height: calc(100% - 3rem);
+ text-align: center;
+ background-color: #fff;
+ border-radius: 0.5rem;
+
+ position: relative;
+}
+
+[data-passcode-status="error"] #lockscreen-passcode-code > span {
+ border: 0.1rem #B70404 solid;
+}
+
+[data-passcode-status="error"] #lockscreen-passcode-code > span[data-dot]::before {
+ background-color: #B70404;
+}
+
+#lockscreen-passcode-code > span[data-dot]::before {
+ content: '';
+ display: block;
+ position: absolute;
+
+ width: 1.5rem;
+ height: 1.5rem;
+ background-color: #3e3b39;
+ border-radius: 0.75rem;
+ top: 50%;
+ left: 50%;
+ margin-left: -0.75rem;
+ margin-top: -0.75rem;
+}
+
+#lockscreen-passcode-pad {
+ -moz-box-sizing: border-box;
+ position: absolute;
+ bottom: 0;
+ height: 21.4rem;
+ width: 100%;
+ background-color: rgba(0, 0, 0, 0.8);
+ visibility: hidden;
+ transform: translateY(calc(21.4rem + 7em));
+ transition: visibility 0.3s ease, transform 0.3s ease;
+}
+
+[data-panel="passcode"] #lockscreen-passcode-pad {
+ visibility: inherit;
+ transform: translateY(0);
+}
+
+[data-panel="emergency-call"] #lockscreen-passcode-pad {
+ transition-delay: 0.5s;
+}
+
+#lockscreen-passcode-pad > a {
+ -moz-box-sizing: border-box;
+ display: block;
+ float: left;
+ width: 33.333%;
+ height: 5rem;
+ border-top: 1px solid #525050;
+ border-bottom: 1px solid #000000;
+ border-left: 1px solid #525050;
+ border-right: 1px solid #000000;
+ outline: none;
+ padding: 0 2rem;
+
+ font-size: 3.2rem;
+ font-weight: 500;
+ line-height: 5rem;
+
+ color: #fff;
+ text-decoration: none;
+ text-shadow: 0 0 3px #000;
+}
+
+#lockscreen-passcode-pad > a:nth-child(3n+1):not([data-key="b"]) {
+ border-left: none;
+}
+
+#lockscreen-passcode-pad > a.last-row {
+ height: 6.4rem;
+}
+
+#lockscreen-passcode-pad > a > span {
+ pointer-events: none;
+ font-size: 1.2rem;
+ padding: 1.2rem;
+ color: #9aaabc;
+}
+
+.passcode-entered #lockscreen-passcode-pad > a[data-key="c"] {
+ display: none;
+}
+
+#lockscreen-passcode-pad > a[data-key="b"] {
+ display: none;
+ background: url(images/icon-clear.png) no-repeat center center;
+}
+
+.passcode-entered #lockscreen-passcode-pad > a[data-key="b"] {
+ display: block;
+ text-align: left;
+ text-indent: -9999px;
+}
+
+#lockscreen-passcode-pad > a:active {
+ background-color: #00aacd;
+ color: #fff;
+ text-shadow: none;
+}
+
+#lockscreen-passcode-pad > a:active > span {
+ color: #fff;
+ text-shadow: none;
+}
+
+#lockscreen-passcode-pad > a.lockscreen-passcode-pad-func {
+ font-size: 1.2rem;
+ font-weight: 500;
+ line-height: 1.6rem;
+ padding: 1rem;
+ text-align: center;
+ text-transform: uppercase;
+}
+
+#lockscreen-panel-emergency-call {
+ transform: translateX(100%);
+}
+
+#lockscreen-panel-emergency-call::before {
+ content: none;
+}
+
+#lockscreen-panel-emergency-call > iframe {
+ width: 100%;
+ height: 100%;
+ border: none;
+}
+
+[data-panel="emergency-call"] #lockscreen-panel-emergency-call {
+ transform: translateX(0);
+}
+
+#lockscreen-area-handle {
+ position: absolute;
+ background-image: url('./images/handle.png');
+ background-repeat: no-repeat;
+ background-position: center;
+ border-top: 1px solid transparent;
+ top: -2rem;
+ height: 0.9rem;
+ width: 100%;
+ transition:
+ transform 0.5s ease,
+ opacity 0.5s ease;
+}
+
+#lockscreen-icon-container {
+ position: absolute;
+ width: 100%;
+ bottom: -8rem;
+ height: 10rem;
+ pointer-events: none;
+ border-top: 1px solid #989898;
+ background-image: linear-gradient(to bottom, rgba(0,0,0,0.8), rgba(0,0,0,0.2) 20%);
+ transition:
+ transform 0.5s ease,
+ opacity 0.5s ease;
+}
+
+.touched #lockscreen-icon-container {
+ transition: none;
+}
+
+.touched #lockscreen-area-handle {
+ transition: none;
+}
+
+.touched .lockscreen-icon-area {
+ transition: none;
+}
+
+.triggered #lockscreen-icon-container {
+ transform: translateY(-8rem);
+ opacity: 1;
+ pointer-events: auto;
+}
+
+.triggered #lockscreen-area-handle {
+ transform: translateY(-1rem);
+ opacity: 0;
+}
+
+.triggered .lockscreen-icon-area {
+ opacity: 1;
+}
+
+@keyframes lockscreen-elastic {
+ 0%, 40%, 75%, 97% {
+ transform: translateY(0);
+ animation-timing-function: ease-out;
+ }
+ 20% {
+ transform: translateY(-5rem);
+ animation-timing-function: ease-in;
+ }
+ 55% {
+ transform: translateY(-2.3rem);
+ animation-timing-function: ease-in;
+ }
+ 85% {
+ transform: translateY(-0.5rem);
+ animation-timing-function: ease-in;
+ }
+}
+
+@keyframes lockscreen-elastic-icon {
+ 0%, 40%, 75%, 97% {
+ opacity: 0.1;
+ animation-timing-function: ease-out;
+ }
+ 20% {
+ opacity: 0.5;
+ animation-timing-function: ease-in;
+ }
+ 55% {
+ opacity: 0.4;
+ animation-timing-function: ease-in;
+ }
+ 85% {
+ opacity: 0.3;
+ animation-timing-function: ease-in;
+ }
+}
+
+.elastic .lockscreen-icon-area {
+ animation: lockscreen-elastic-icon 2.5s 1;
+}
+
+.elastic #lockscreen-icon-container {
+ animation: lockscreen-elastic 2.5s 1;
+}
diff --git a/apps/system/style/modal_dialog/images/error_bk.png b/apps/system/style/modal_dialog/images/error_bk.png
new file mode 100644
index 0000000..9f71ff8
--- /dev/null
+++ b/apps/system/style/modal_dialog/images/error_bk.png
Binary files differ
diff --git a/apps/system/style/modal_dialog/modal_dialog.css b/apps/system/style/modal_dialog/modal_dialog.css
new file mode 100644
index 0000000..d6e97f8
--- /dev/null
+++ b/apps/system/style/modal_dialog/modal_dialog.css
@@ -0,0 +1,112 @@
+#modal-dialog-alert,
+#modal-dialog-confirm,
+#modal-dialog-prompt,
+#authentication-dialog,
+#authentication-dialog-http,
+#modal-dialog-error,
+#modal-dialog-select-one {
+ display: none;
+}
+
+#authentication-dialog input,
+#modal-dialog input {
+ border-radius: 2px 2px 2px 2px;
+ height: 40px;
+ font-size: 20px;
+ width: 100%;
+ color: white;
+ background: none;
+ -moz-box-sizing: border-box;
+}
+
+/* 480x800 phones */
+@media (orientation: portrait) and (width: 480px),
+ (orientation: landscape) and (width: 800px) {
+ #authentication-dialog input,
+ #modal-dialog input {
+ border-radius: 3px 3px 3px 3px;
+ height: 60px;
+ font-size: 30px;
+ width: 100%;
+ color: white;
+ -moz-box-sizing: border-box;
+ }
+}
+
+#screen.modal-dialog #modal-dialog,
+#screen.authentication-dialog #authentication-dialog,
+#authentication-dialog-http.visible,
+#modal-dialog-alert.visible,
+#modal-dialog-confirm.visible,
+#modal-dialog-prompt.visible,
+#modal-dialog-error.visible,
+#modal-dialog-select-one.visible {
+ display: block;
+}
+
+#modal-dialog-error {
+ background-color: black;
+ background-image: url(images/error_bk.png);
+}
+
+#modal-dialog-sad-face {
+ float: left;
+ font-size: 72px;
+ margin-right: 2px;
+}
+
+/* Confirm dialog */
+.modal-dialog-message-container p {
+ display:inline !important;
+ word-wrap: break-word;
+}
+
+/* Select one dialog */
+
+#modal-dialog-select-one .modal-dialog-message-container {
+ display: block;
+ padding: 0 1.5rem 0 1.5rem;
+ position: absolute;
+ left: 0;
+ bottom: -moz-calc(3.8rem + 3rem);
+}
+
+#modal-dialog-select-one h3 {
+ font-size: 1.8rem;
+}
+
+#modal-dialog-select-one ul {
+ width: 100%;
+ height: auto;
+ list-style: none;
+ padding: 0;
+ margin: 0.5rem 0 2rem 0;
+ display: block;
+ overflow: hidden;
+}
+
+#modal-dialog-select-one ul > li {
+ width: 100%;
+ padding-top: 1rem;
+ margin: 0;
+ display: block;
+ overflow: hidden;
+ border: none;
+ height: auto;
+ line-height: normal;
+}
+
+#modal-dialog-select-one ul > li > button {
+ color: #fff;
+ text-shadow: none;
+ text-align: left;
+ margin: 0;
+ background: #4E4E4E padding-box;
+ border: solid 1px rgba(0, 0, 0, 0.25);
+}
+
+#modal-dialog-select-one ul > li > button:active {
+ background-color: #006f86;
+ color: #333;
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);
+}
diff --git a/apps/system/style/modal_dialog/prompt.css b/apps/system/style/modal_dialog/prompt.css
new file mode 100644
index 0000000..69d726d
--- /dev/null
+++ b/apps/system/style/modal_dialog/prompt.css
@@ -0,0 +1,50 @@
+/* ----------------------------------
+ * STYLES FOR PROMPT
+ * Requires:
+ menu-dialoges/core.css
+ other/buttons/style.css
+ * ---------------------------------- */
+
+[role="dialog"] p {
+ font-family: 'MozTT', Sans-serif;
+ font-weight: lighter;
+ font-size: 2.2rem;
+ color: #FAFAFA;
+ margin: 0;
+}
+
+[role="dialog"] p:last-child {
+ overflow-x: hidden;
+ overflow-y: auto;
+ word-wrap: break-word;
+ /*screen height - menu data-items - dialog padding - title height*/
+ /*almost 60% screen height*/
+ max-height: 28rem;
+}
+
+[role="dialog"] .content {
+ font-family: 'MozTT', Sans-serif;
+ font-weight: lighter;
+ font-size: 2.2rem;
+ color: #fff;
+ border-bottom: 0.1rem solid #777;
+ margin: 0;
+ padding: 2.5rem 0 2rem 0;
+}
+
+[role="dialog"] .content img {
+ float: left;
+ margin-right: 2rem;
+}
+
+[role="dialog"] .content strong {
+ font-weight: lighter;
+}
+
+[role="dialog"] .content small {
+ font-size: 1.4rem;
+ font-family: 'MozTT', Sans-serif;
+ font-weight: normal;
+ color: #cbcbcb;
+ display: block;
+}
diff --git a/apps/system/style/notifications/images/grey-noise-bg.png b/apps/system/style/notifications/images/grey-noise-bg.png
new file mode 100644
index 0000000..0f83b8f
--- /dev/null
+++ b/apps/system/style/notifications/images/grey-noise-bg.png
Binary files differ
diff --git a/apps/system/style/notifications/notifications.css b/apps/system/style/notifications/notifications.css
new file mode 100644
index 0000000..5dbfe5e
--- /dev/null
+++ b/apps/system/style/notifications/notifications.css
@@ -0,0 +1,200 @@
+html, body {
+ font-family: 'MozTT', sans-serif;
+ font-size: 10px;
+}
+
+#notification-toaster {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 50px;
+ overflow: hidden;
+ background-image: url('images/grey-noise-bg.png');
+ background-repeat: repeat-x;
+ -moz-box-sizing: border-box;
+ border-bottom: 1px #2c2c2c solid;
+ -moz-transform: translateY(-50px);
+ -moz-transition: -moz-transform .3s ease-in-out;
+}
+
+#toaster-icon {
+ position: absolute;
+ width: 30px;
+ height: 30px;
+ top: 7px;
+ left: 10px;
+ pointer-events: none;
+}
+
+#notification-toaster > div {
+ left: 50px;
+ width: -moz-calc(100% - 55px);
+ height: 19px;
+ padding: 0;
+
+ line-height: 1.9rem;
+
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+#toaster-title {
+ position: absolute;
+ top: 3px;
+ color: #52b8cc;
+ font-size: 1.5rem;
+ font-weight: 500;
+}
+
+#toaster-detail {
+ position: absolute;
+ top: 22px;
+ color: white;
+ font-size: 1.4rem;
+ font-weight: 400;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+#notification-toaster.displayed {
+ -moz-transform: translateY(0);
+}
+
+#notification-bar {
+ height: 30px;
+ background-color: #52b6cc;
+ color: #1b3f46;
+}
+
+#notification-bar span {
+ display: inline-block;
+ margin-left: 15px;
+ font-size: 1.4rem;
+ font-weight: 500;
+ line-height: 30px;
+}
+
+#notification-bar button {
+ float: right;
+ margin-right: 15px;
+ width: 30%;
+ height: 30px;
+ padding: 0;
+ border: 0;
+ background: none;
+ text-align: right;
+ font-size: 1.4rem;
+ font-weight: 500;
+}
+
+/* remove ugly dotted outline when focus */
+#notification-bar button::-moz-focus-inner {
+ border: 0;
+}
+
+#notifications-container {
+ width: 100%;
+
+ /* minus cost control, quick settings, bar and grippy */
+ height: -moz-calc(100% - 4.5rem - 60px - 30px - 20px);
+ overflow-y: scroll;
+ overflow-x: hidden;
+}
+
+
+.notification {
+ position: relative;
+ height: 60px;
+ font-size: 1.4rem;
+ font-weight: 400;
+ line-height: 1.9rem;
+
+ background-color: rgba(0, 0, 0, 0.5);
+
+ border-top: 1px #404547 solid;
+ border-bottom: 1px black solid;
+}
+
+.notification:first-child {
+ border-top-color: black;
+}
+
+.notification div {
+ pointer-events: none;
+}
+
+.notification > div:first-of-type {
+ width: 19.5rem;
+ margin: 1rem 0 0 5rem;
+ font-size: 1.5rem;
+ font-weight: 500;
+ line-height: 1.9rem;
+ color: #FFFFFF;
+}
+
+.notification > div {
+ margin: 0 0 0 50px;
+ padding: 0;
+ width: -moz-calc(100% - 55px);
+ color: #bfbfbf;
+ font-size: 1.4rem;
+ line-height: 1.9rem;
+ min-height: 1.9rem;
+ font-weight: 400;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.notification > .timestamp {
+ position: absolute;
+ right: 0;
+ top: -.2rem;
+ color: #52B6CC;
+ margin: 13px 15px 0 0;
+ padding: 0;
+ display: inline;
+ line-height: 16px;
+}
+
+.notification > img {
+ float: left;
+ display: block;
+ width: 24px;
+ height: 24px;
+ margin: 15px 10px;
+ pointer-events: none;
+}
+
+.notification.disappearing,
+#notification-toaster.disappearing {
+ transition: transform 0.3s linear;
+ transform: translateX(100%);
+}
+
+.notification.disappearing ~ .notification {
+ transition: transform 0.3s linear;
+ transform: translateY(-62px);
+}
+
+#notifications-lockscreen-container {
+ position: absolute;
+ top: 18rem;
+ left: 0;
+ width: 100%;
+ max-height: 186px;
+ overflow-x: hidden;
+ overflow-y: auto;
+
+ background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.9),
+ rgba(0, 0, 0, 0.6) 78%,
+ rgba(0, 0, 0, 0));
+ background-size: 100% 186px;
+}
+
+#notifications-lockscreen-container .notification {
+ background-color: transparent;
+}
diff --git a/apps/system/style/notifications/ringtones/notifier_exclamation.ogg b/apps/system/style/notifications/ringtones/notifier_exclamation.ogg
new file mode 100644
index 0000000..d953a9a
--- /dev/null
+++ b/apps/system/style/notifications/ringtones/notifier_exclamation.ogg
Binary files differ
diff --git a/apps/system/style/permission_manager/images/PermissionsDialogIcons_Camera.png b/apps/system/style/permission_manager/images/PermissionsDialogIcons_Camera.png
new file mode 100755
index 0000000..3be8cb5
--- /dev/null
+++ b/apps/system/style/permission_manager/images/PermissionsDialogIcons_Camera.png
Binary files differ
diff --git a/apps/system/style/permission_manager/images/PermissionsDialogIcons_Contacts.png b/apps/system/style/permission_manager/images/PermissionsDialogIcons_Contacts.png
new file mode 100755
index 0000000..f11d9d9
--- /dev/null
+++ b/apps/system/style/permission_manager/images/PermissionsDialogIcons_Contacts.png
Binary files differ
diff --git a/apps/system/style/permission_manager/images/PermissionsDialogIcons_DeviceStorage.png b/apps/system/style/permission_manager/images/PermissionsDialogIcons_DeviceStorage.png
new file mode 100755
index 0000000..069eb48
--- /dev/null
+++ b/apps/system/style/permission_manager/images/PermissionsDialogIcons_DeviceStorage.png
Binary files differ
diff --git a/apps/system/style/permission_manager/images/PermissionsDialogIcons_FMRadio.png b/apps/system/style/permission_manager/images/PermissionsDialogIcons_FMRadio.png
new file mode 100755
index 0000000..fc63d12
--- /dev/null
+++ b/apps/system/style/permission_manager/images/PermissionsDialogIcons_FMRadio.png
Binary files differ
diff --git a/apps/system/style/permission_manager/images/PermissionsDialogIcons_Geolocation.png b/apps/system/style/permission_manager/images/PermissionsDialogIcons_Geolocation.png
new file mode 100755
index 0000000..936a627
--- /dev/null
+++ b/apps/system/style/permission_manager/images/PermissionsDialogIcons_Geolocation.png
Binary files differ
diff --git a/apps/system/style/permission_manager/images/PermissionsDialogIcons_WifiInformation.png b/apps/system/style/permission_manager/images/PermissionsDialogIcons_WifiInformation.png
new file mode 100755
index 0000000..903314c
--- /dev/null
+++ b/apps/system/style/permission_manager/images/PermissionsDialogIcons_WifiInformation.png
Binary files differ
diff --git a/apps/system/style/permission_manager/permission_manager.css b/apps/system/style/permission_manager/permission_manager.css
new file mode 100644
index 0000000..49e34b8
--- /dev/null
+++ b/apps/system/style/permission_manager/permission_manager.css
@@ -0,0 +1,86 @@
+#permission-screen {
+ position: absolute;
+ top: 20px;
+ left: 0;
+ width: 100%;
+ height: -moz-calc(100% - 20px);
+ background-color: rgba(0,0,0,0.4);
+ -moz-transition: opacity 0.5s ease;
+ pointer-events: none;
+ display: none;
+}
+
+#screen:-moz-full-screen-ancestor > #permission-screen,
+#screen.fullscreen-app #permission-screen {
+ top: 0;
+ height: 100%;
+}
+
+#screen.attention #permission-screen {
+ top: 40px;
+ height: -moz-calc(100% - 40px);
+}
+
+#permission-screen.visible {
+ pointer-events: auto;
+ display: inline-block;
+}
+
+#permission-remember-section {
+ height: 6rem;
+ border-top: 0.1rem solid #686868;
+ width: 100%;
+ position: relative;
+}
+
+#permission-remember-section {
+ display: none;
+}
+
+#permission-message {
+ background-image: none;
+ background-repeat: no-repeat no-repeat;
+ min-height: 64px;
+}
+
+#permission-screen[data-type] #permission-remember-section,
+#permission-screen[data-type] #permission-icon {
+ display: inherit;
+}
+
+#permission-screen[data-type] #permission-message {
+ padding-left: 64px;
+}
+
+#permission-remember-section label {
+ position: absolute;
+ right: 1rem;
+}
+
+#permission-remember-label {
+ line-height: 5rem;
+}
+
+#permission-screen[data-type="geolocation"] #permission-message {
+ background-image: url('images/PermissionsDialogIcons_Geolocation.png');
+}
+
+#permission-screen[data-type="fmradio"] #permission-message {
+ background-image: url('images/PermissionsDialogIcons_FMRadio.png');
+}
+
+#permission-screen[data-type*="camera"] #permission-message {
+ background-image: url('images/PermissionsDialogIcons_Camera.png');
+}
+
+#permission-screen[data-type*="wifi"] #permission-message {
+ background-image: url('images/PermissionsDialogIcons_WifiInformation.png');
+}
+
+#permission-screen[data-type="contacts"] #permission-message {
+ background-image: url('images/PermissionsDialogIcons_Contacts.png');
+}
+
+#permission-screen[data-type*="device-storage"] #permission-message {
+ background-image: url('images/PermissionsDialogIcons_DeviceStorage.png');
+}
diff --git a/apps/system/style/pinlock/pinlock.css b/apps/system/style/pinlock/pinlock.css
new file mode 100644
index 0000000..307763f
--- /dev/null
+++ b/apps/system/style/pinlock/pinlock.css
@@ -0,0 +1,92 @@
+#pinkeypadscreen {
+ position: absolute;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ background-color: #fff;
+ -moz-user-select: none;
+ transition: transform 0.3s ease;
+}
+
+#pinkeypadscreen > #pinkeypadscreen-container {
+ width: 100%;
+ margin-top: 20px;
+}
+
+#pinkeypadscreen-desc {
+ width: 100%;
+ height: 50px;
+ line-height: 50px;
+ background-color: rgba(0, 0, 0, 0.85);
+ margin: 0;
+ text-align: center;
+ vertical-align: middle;
+ border: 0px;
+ font-size: 32px;
+}
+
+#pinkeypadscreen-code {
+ bottom: 500px;
+ height: 60px;
+ width: 100%;
+ background-color: rgba(255, 255, 255, 0.75);
+ margin: 0;
+}
+
+#pinkeypadscreen-code > span {
+ -moz-box-sizing: border-box;
+ display: block;
+ width: 100%;
+ height: 100%;
+ line-height: 40px;
+ text-align: center;
+ border: 10px solid #888;
+ box-shadow: 0 0 10px #fff inset;
+}
+
+#pinkeypadscreen-display {
+ font-size: 50px;
+ color: #000;
+}
+
+#pinkeypadscreen-pad {
+ -moz-box-sizing: border-box;
+ border: 1px solid #919191;
+ bottom: 0;
+ height: 350px;
+ width: 100%;
+ padding: 4px;
+}
+
+#pinkeypadscreen-pad > a {
+ -moz-box-sizing: border-box;
+ display: block;
+ float: left;
+ width: calc(33.333% - 8px);
+ height: 60px;
+ line-height: 60px;
+ font-size: 48px;
+ margin: 4px;
+ text-align: center;
+ border: 1px solid #919191;
+ border-radius: 8px;
+ outline: none;
+ color: #919191;
+ text-decoration: none;
+}
+
+#pinkeypadscreen-pad > a:active {
+ background-color: rgb(0, 0, 0);
+ color: #ccc;
+}
+
+#pinkeypadscreen-pad > a.pinkeypadscreen-pad-func {
+ font-size: 20px;
+}
+
+#pinkeypadscreen-pad > a.pinkeypadscreen-pad-func-full {
+ width: calc(100% - 8px);
+ font-size: 20px;
+}
+
diff --git a/apps/system/style/popup_manager/popup_manager.css b/apps/system/style/popup_manager/popup_manager.css
new file mode 100644
index 0000000..a33f9a1
--- /dev/null
+++ b/apps/system/style/popup_manager/popup_manager.css
@@ -0,0 +1,112 @@
+#popup-container {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ visibility: hidden;
+ opacity: 1;
+ background: rgba(0, 0, 0, 0.5);
+}
+
+#popup-container[data-trusty="true"] {
+ width: 85%;
+ left: 7%;
+ top: 7%;
+ height: 85%;
+}
+
+#screen.popup #popup-container {
+ visibility: visible;
+}
+
+#popup-throbber {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 3px;
+ z-index: 1;
+}
+
+#screen.popup #popup-container.disappearing {
+ pointer-events: none;
+ transition: visibility ease .6s, opacity ease .6s;
+ visibility: hidden;
+ opacity: 0;
+}
+
+#popup-throbber.loading {
+ height: 4px;
+ background-image: url('../shared/progress.gif');
+}
+
+.popup #popup-container > #frame-container,
+.popup #popup-container > .title-container {
+ transform: translateY(0);
+}
+
+#popup-container[data-trusty="true"] > #frame-container,
+#popup-container[data-trusty="true"] > .title-container {
+ transition: none;
+}
+
+#popup-container > .title-container {
+ transition: transform .6s ease;
+ transform: translateY(-100%);
+ height: 5rem;
+ width: 100%;
+ top: 0;
+ left: 0;
+ position: absolute;
+}
+
+#popup-container > #frame-container {
+ position: absolute;
+ top: 5rem;
+ left: 0;
+ width: 100%;
+ height: -moz-calc(100% - 5rem);
+
+ background-color: #fff;
+ transform: translateY(100%);
+ transition: transform .6s ease;
+}
+
+#popup-container[data-trusty="true"] > #frame-container {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: #fff;
+}
+
+#popup-error-dialog {
+ -moz-box-sizing: border-box;
+ height: 100%;
+ width: 100%;
+ z-index: 1024;
+ display: none;
+ background-color: black;
+ background-image: url(../modal_dialog/images/error_bk.png);
+}
+
+#frame-container > iframe {
+ z-index: 1;
+}
+
+#frame-container.error > #popup-error-dialog {
+ display: block;
+}
+
+#popup-container[data-trusty="true"] > .title-container {
+ visibility: hidden;
+}
+
+#frame-container > * {
+ width: 100%;
+ height: 100%;
+ border: none;
+}
+
diff --git a/apps/system/style/quick_settings/images/airplane-mode-off.png b/apps/system/style/quick_settings/images/airplane-mode-off.png
new file mode 100644
index 0000000..b037976
--- /dev/null
+++ b/apps/system/style/quick_settings/images/airplane-mode-off.png
Binary files differ
diff --git a/apps/system/style/quick_settings/images/airplane-mode-on.png b/apps/system/style/quick_settings/images/airplane-mode-on.png
new file mode 100644
index 0000000..a63b125
--- /dev/null
+++ b/apps/system/style/quick_settings/images/airplane-mode-on.png
Binary files differ
diff --git a/apps/system/style/quick_settings/images/background.png b/apps/system/style/quick_settings/images/background.png
new file mode 100644
index 0000000..a76cc48
--- /dev/null
+++ b/apps/system/style/quick_settings/images/background.png
Binary files differ
diff --git a/apps/system/style/quick_settings/images/bluetooth-off.png b/apps/system/style/quick_settings/images/bluetooth-off.png
new file mode 100644
index 0000000..4cdab70
--- /dev/null
+++ b/apps/system/style/quick_settings/images/bluetooth-off.png
Binary files differ
diff --git a/apps/system/style/quick_settings/images/bluetooth-on.png b/apps/system/style/quick_settings/images/bluetooth-on.png
new file mode 100644
index 0000000..ab1c5cd
--- /dev/null
+++ b/apps/system/style/quick_settings/images/bluetooth-on.png
Binary files differ
diff --git a/apps/system/style/quick_settings/images/data-2g-off.png b/apps/system/style/quick_settings/images/data-2g-off.png
new file mode 100644
index 0000000..d64d0a8
--- /dev/null
+++ b/apps/system/style/quick_settings/images/data-2g-off.png
Binary files differ
diff --git a/apps/system/style/quick_settings/images/data-2g-on.png b/apps/system/style/quick_settings/images/data-2g-on.png
new file mode 100644
index 0000000..3143453
--- /dev/null
+++ b/apps/system/style/quick_settings/images/data-2g-on.png
Binary files differ
diff --git a/apps/system/style/quick_settings/images/data-3g-off.png b/apps/system/style/quick_settings/images/data-3g-off.png
new file mode 100644
index 0000000..37d6a82
--- /dev/null
+++ b/apps/system/style/quick_settings/images/data-3g-off.png
Binary files differ
diff --git a/apps/system/style/quick_settings/images/data-3g-on.png b/apps/system/style/quick_settings/images/data-3g-on.png
new file mode 100644
index 0000000..47dbda1
--- /dev/null
+++ b/apps/system/style/quick_settings/images/data-3g-on.png
Binary files differ
diff --git a/apps/system/style/quick_settings/images/data-4g-off.png b/apps/system/style/quick_settings/images/data-4g-off.png
new file mode 100644
index 0000000..bbf45a1
--- /dev/null
+++ b/apps/system/style/quick_settings/images/data-4g-off.png
Binary files differ
diff --git a/apps/system/style/quick_settings/images/data-4g-on.png b/apps/system/style/quick_settings/images/data-4g-on.png
new file mode 100644
index 0000000..b77dd96
--- /dev/null
+++ b/apps/system/style/quick_settings/images/data-4g-on.png
Binary files differ
diff --git a/apps/system/style/quick_settings/images/data-e-off.png b/apps/system/style/quick_settings/images/data-e-off.png
new file mode 100644
index 0000000..d8215a8
--- /dev/null
+++ b/apps/system/style/quick_settings/images/data-e-off.png
Binary files differ
diff --git a/apps/system/style/quick_settings/images/data-e-on.png b/apps/system/style/quick_settings/images/data-e-on.png
new file mode 100644
index 0000000..491a07d
--- /dev/null
+++ b/apps/system/style/quick_settings/images/data-e-on.png
Binary files differ
diff --git a/apps/system/style/quick_settings/images/data-h+-off.png b/apps/system/style/quick_settings/images/data-h+-off.png
new file mode 100644
index 0000000..85238ee
--- /dev/null
+++ b/apps/system/style/quick_settings/images/data-h+-off.png
Binary files differ
diff --git a/apps/system/style/quick_settings/images/data-h+-on.png b/apps/system/style/quick_settings/images/data-h+-on.png
new file mode 100644
index 0000000..968abbb
--- /dev/null
+++ b/apps/system/style/quick_settings/images/data-h+-on.png
Binary files differ
diff --git a/apps/system/style/quick_settings/images/data-h-off.png b/apps/system/style/quick_settings/images/data-h-off.png
new file mode 100644
index 0000000..99b2dc9
--- /dev/null
+++ b/apps/system/style/quick_settings/images/data-h-off.png
Binary files differ
diff --git a/apps/system/style/quick_settings/images/data-h-on.png b/apps/system/style/quick_settings/images/data-h-on.png
new file mode 100644
index 0000000..9ba3649
--- /dev/null
+++ b/apps/system/style/quick_settings/images/data-h-on.png
Binary files differ
diff --git a/apps/system/style/quick_settings/images/data-o-off.png b/apps/system/style/quick_settings/images/data-o-off.png
new file mode 100644
index 0000000..75dbf17
--- /dev/null
+++ b/apps/system/style/quick_settings/images/data-o-off.png
Binary files differ
diff --git a/apps/system/style/quick_settings/images/data-o-on.png b/apps/system/style/quick_settings/images/data-o-on.png
new file mode 100644
index 0000000..ae8791b
--- /dev/null
+++ b/apps/system/style/quick_settings/images/data-o-on.png
Binary files differ
diff --git a/apps/system/style/quick_settings/images/data-off.png b/apps/system/style/quick_settings/images/data-off.png
new file mode 100644
index 0000000..dcaf9f4
--- /dev/null
+++ b/apps/system/style/quick_settings/images/data-off.png
Binary files differ
diff --git a/apps/system/style/quick_settings/images/power-save-off.png b/apps/system/style/quick_settings/images/power-save-off.png
new file mode 100644
index 0000000..1d740ef
--- /dev/null
+++ b/apps/system/style/quick_settings/images/power-save-off.png
Binary files differ
diff --git a/apps/system/style/quick_settings/images/power-save-on.png b/apps/system/style/quick_settings/images/power-save-on.png
new file mode 100644
index 0000000..c0e3ca4
--- /dev/null
+++ b/apps/system/style/quick_settings/images/power-save-on.png
Binary files differ
diff --git a/apps/system/style/quick_settings/images/settings-off.png b/apps/system/style/quick_settings/images/settings-off.png
new file mode 100644
index 0000000..68fe921
--- /dev/null
+++ b/apps/system/style/quick_settings/images/settings-off.png
Binary files differ
diff --git a/apps/system/style/quick_settings/images/settings-on.png b/apps/system/style/quick_settings/images/settings-on.png
new file mode 100644
index 0000000..d38c3c1
--- /dev/null
+++ b/apps/system/style/quick_settings/images/settings-on.png
Binary files differ
diff --git a/apps/system/style/quick_settings/images/wifi-off.png b/apps/system/style/quick_settings/images/wifi-off.png
new file mode 100644
index 0000000..a7af54e
--- /dev/null
+++ b/apps/system/style/quick_settings/images/wifi-off.png
Binary files differ
diff --git a/apps/system/style/quick_settings/images/wifi-on.png b/apps/system/style/quick_settings/images/wifi-on.png
new file mode 100644
index 0000000..ad0d1f6
--- /dev/null
+++ b/apps/system/style/quick_settings/images/wifi-on.png
Binary files differ
diff --git a/apps/system/style/quick_settings/quick_settings.css b/apps/system/style/quick_settings/quick_settings.css
new file mode 100644
index 0000000..80a49e9
--- /dev/null
+++ b/apps/system/style/quick_settings/quick_settings.css
@@ -0,0 +1,129 @@
+html, body {
+ font-family: 'MozTT', sans-serif;
+ font-size: 10px;
+}
+
+#quick-settings {
+ display: block;
+ width: 100%;
+ height: 60px;
+ padding: 0;
+
+ background-image: url(images/background.png);
+ background-size: 100% 60px;
+
+ border-top:1px black solid;
+}
+
+#quick-settings > a {
+ display: block;
+ float: left;
+ width: -moz-calc(20% - 2px);
+ height: 30px;
+ margin: 15px 0;
+ padding: 0;
+ color: #fff;
+ text-align: center;
+ text-decoration: none;
+ outline: none;
+ font-size: 1.4rem;
+ line-height: 1.8rem;
+}
+
+#quick-settings > .separator {
+ border-right: 1px black solid;
+ border-left: 1px gray solid;
+ margin: 15px 0;
+ height: 30px;
+ float: left;
+}
+
+#quick-settings > a:first-child {
+ width: -moz-calc(20% - 1px);
+ border-left: 0;
+}
+
+#quick-settings > a:last-child {
+ width: -moz-calc(20% - 1px);
+ border-right: 0;
+}
+
+#quick-settings > a {
+ background-position: center;
+ background-repeat: no-repeat;
+}
+#quick-settings > *[data-initializing] {
+ opacity: 0.5;
+}
+#quick-settings-wifi {
+ background-image: url(images/wifi-off.png);
+}
+#quick-settings-wifi[data-enabled] {
+ background-image: url(images/wifi-on.png);
+}
+#quick-settings-data {
+ background-image: url(images/data-off.png);
+}
+#quick-settings-data[data-network="2G"] {
+ background-image: url(images/data-2g-off.png);
+}
+#quick-settings-data[data-enabled][data-network="2G"] {
+ background-image: url(images/data-2g-on.png);
+}
+#quick-settings-data[data-network="3G"] {
+ background-image: url(images/data-3g-off.png);
+}
+#quick-settings-data[data-enabled][data-network="3G"] {
+ background-image: url(images/data-3g-on.png);
+}
+#quick-settings-data[data-network="4G"] {
+ background-image: url(images/data-4g-off.png);
+}
+#quick-settings-data[data-enabled][data-network="4G"] {
+ background-image: url(images/data-4g-on.png);
+}
+#quick-settings-data[data-network="E"] {
+ background-image: url(images/data-e-off.png);
+}
+#quick-settings-data[data-enabled][data-network="E"] {
+ background-image: url(images/data-e-on.png);
+}
+#quick-settings-data[data-network="H"] {
+ background-image: url(images/data-h-off.png);
+}
+#quick-settings-data[data-enabled][data-network="H"] {
+ background-image: url(images/data-h-on.png);
+}
+#quick-settings-data[data-network="H+"] {
+ background-image: url(images/data-h+-off.png);
+}
+#quick-settings-data[data-enabled][data-network="H+"] {
+ background-image: url(images/data-h+-on.png);
+}
+#quick-settings-data[data-network="O"] {
+ background-image: url(images/data-o-off.png);
+}
+#quick-settings-data[data-enabled][data-network="O"] {
+ background-image: url(images/data-o-on.png);
+}
+#quick-settings-bluetooth {
+ background-image: url(images/bluetooth-off.png);
+}
+#quick-settings-bluetooth[data-enabled] {
+ background-image: url(images/bluetooth-on.png);
+}
+#quick-settings-airplane-mode {
+ background-image: url(images/airplane-mode-off.png);
+}
+#quick-settings-airplane-mode[data-enabled] {
+ background-image: url(images/airplane-mode-on.png);
+}
+#quick-settings-full-app {
+ background-image: url(images/settings-off.png);
+}
+#quick-settings-full-app:active {
+ background-image: url(images/settings-on.png);
+}
+.quick-settings-airplane-mode {
+ opacity: 0.5;
+}
diff --git a/apps/system/style/shared/progress.gif b/apps/system/style/shared/progress.gif
new file mode 100644
index 0000000..654a2fd
--- /dev/null
+++ b/apps/system/style/shared/progress.gif
Binary files differ
diff --git a/apps/system/style/simcard.css b/apps/system/style/simcard.css
new file mode 100644
index 0000000..d583dc0
--- /dev/null
+++ b/apps/system/style/simcard.css
@@ -0,0 +1,52 @@
+#simpin-dialog {
+ background: white;
+ color: #000;
+}
+
+#simpin-dialog section[role="region"] {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+}
+#simpin-dialog .container {
+ padding-left: 20px;
+ font-size: 1.5em;
+}
+
+#simpin-dialog .container > div {
+ padding-top: 10px;
+}
+
+#errorMsg{
+ padding: 1rem;
+}
+
+#simpin-dialog .error #messageHeader {
+ color: red;
+}
+
+/* for simpin error message,
+ * to override [role="dialog"] rules in core.css*/
+#messageBody {
+ color: #000;
+ white-space: normal;
+}
+
+#simpin-dialog .input-wrapper {
+ position: relative;
+}
+
+#simpin-dialog .input-wrapper input {
+ font-size: 2.0em;
+ display: block;
+}
+
+#simpin-dialog .input-wrapper input[type="text"] {
+ width: auto;
+ pointer-events: none;
+ position: absolute;
+ top: 0;
+}
+
diff --git a/apps/system/style/sleep_menu/images/airplane.png b/apps/system/style/sleep_menu/images/airplane.png
new file mode 100644
index 0000000..1c03a6b
--- /dev/null
+++ b/apps/system/style/sleep_menu/images/airplane.png
Binary files differ
diff --git a/apps/system/style/sleep_menu/images/power-off.png b/apps/system/style/sleep_menu/images/power-off.png
new file mode 100644
index 0000000..5e6d2f7
--- /dev/null
+++ b/apps/system/style/sleep_menu/images/power-off.png
Binary files differ
diff --git a/apps/system/style/sleep_menu/images/restart.png b/apps/system/style/sleep_menu/images/restart.png
new file mode 100644
index 0000000..3ba3b87
--- /dev/null
+++ b/apps/system/style/sleep_menu/images/restart.png
Binary files differ
diff --git a/apps/system/style/sleep_menu/images/vibration.png b/apps/system/style/sleep_menu/images/vibration.png
new file mode 100644
index 0000000..3fa1f92
--- /dev/null
+++ b/apps/system/style/sleep_menu/images/vibration.png
Binary files differ
diff --git a/apps/system/style/sleep_menu/sleep_menu.css b/apps/system/style/sleep_menu/sleep_menu.css
new file mode 100644
index 0000000..78adc86
--- /dev/null
+++ b/apps/system/style/sleep_menu/sleep_menu.css
@@ -0,0 +1,125 @@
+#sleep-menu {
+ background-attachment: scroll, scroll;
+ background-clip: border-box, border-box;
+ background-color: transparent;
+ background-image: url("../themes/default/images/images/ui/pattern.png"), url("../themes/default/images/ui/gradient.png");
+ background-origin: padding-box, padding-box;
+ background-position: left top, left top;
+ background-repeat: repeat, no-repeat;
+ background-size: auto auto, 100% 100%;
+ bottom: 0;
+ color: #FFFFFF;
+ font-family: "MozTT",Sans-serif;
+ left: 0;
+ overflow: hidden;
+ padding: 1.5rem 0 7rem;
+ position: absolute;
+ right: 0;
+ top: 0;
+ white-space: nowrap;
+ display: none;
+}
+
+#sleep-menu-container {
+ -moz-box-sizing: padding-box;
+ display: inline-block;
+ padding: 0 2.5rem 0 2rem;
+ vertical-align: middle;
+ white-space: normal;
+ width: 100%;
+}
+
+#sleep-menu.visible {
+ display: block;
+}
+
+#sleep-menu-container h3 {
+ border-bottom: 0.1rem solid #686868;
+ color: #FFFFFF;
+ font-family: 'MozTT',Sans-serif;
+ font-size: 1.6rem;
+ font-weight: normal;
+ line-height: 1em;
+ margin: 0 0 1rem;
+ padding-bottom: 1rem;
+}
+
+#sleep-menu-container ul {
+ margin: -1rem -4rem 0;
+ max-height: 37rem;
+ overflow: auto;
+ padding: 0 4rem;
+ position: relative;
+}
+
+#sleep-menu-container li {
+ -moz-transition: background 0.2s ease 0s;
+ border-bottom: 1px solid #666666;
+ color: #FFFFFF;
+ font-family: 'MozTT',Sans-serif;
+ font-size: 2.2rem;
+ font-weight: lighter;
+ height: 5.9rem;
+ line-height: 5.9rem;
+ list-style: none outside none;
+ margin: 0;
+ padding-bottom: 1px;
+ position: relative;
+ display: block;
+ text-decoration: none;
+}
+
+#sleep-menu-container li:active {
+ background: #00ABCC;
+ color: #fff!important;
+}
+
+#sleep-menu menu {
+ background: url("../themes/default/images/ui/pattern.png") repeat scroll left top #2D2D2D;
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
+ bottom: 0;
+ display: block;
+ left: 0;
+ margin: 0;
+ overflow: hidden;
+ padding: 1.5rem;
+ position: absolute;
+ right: 0;
+ white-space: nowrap;
+}
+
+#sleep-menu menu button::-moz-focus-inner {
+ border: medium none;
+ outline: medium none;
+}
+
+#sleep-menu menu button {
+ -moz-box-sizing: border-box;
+ background: url("../themes/default/images/ui/default.png") repeat-x scroll left bottom #FAFAFA;
+ border: 1px solid #9F9F9F;
+ border-radius: 0.3rem 0.3rem 0.3rem 0.3rem;
+ color: #333333;
+ display: inline-block;
+ font-family: 'MozTT',Sans-serif;
+ font-size: 1.6rem;
+ font-weight: 600;
+ height: 3.8rem;
+ line-height: 3.8rem;
+ margin: 0 0 1rem;
+ outline: medium none;
+ overflow: hidden;
+ padding: 0 1.5rem;
+ text-align: center;
+ text-decoration: none;
+ text-overflow: ellipsis;
+ text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.3);
+ vertical-align: middle;
+ white-space: nowrap;
+ width: 100%;
+}
+
+#sleep-menu menu button:active {
+ background: none repeat scroll 0 0 #008AAA;
+ border-color: #008AAA;
+ color: #333333;
+}
diff --git a/apps/system/style/sound_manager/images/VolumeOverlay_Landscape_SegmentOff.png b/apps/system/style/sound_manager/images/VolumeOverlay_Landscape_SegmentOff.png
new file mode 100644
index 0000000..f3aba14
--- /dev/null
+++ b/apps/system/style/sound_manager/images/VolumeOverlay_Landscape_SegmentOff.png
Binary files differ
diff --git a/apps/system/style/sound_manager/images/VolumeOverlay_Landscape_SegmentOff_left.png b/apps/system/style/sound_manager/images/VolumeOverlay_Landscape_SegmentOff_left.png
new file mode 100644
index 0000000..278368f
--- /dev/null
+++ b/apps/system/style/sound_manager/images/VolumeOverlay_Landscape_SegmentOff_left.png
Binary files differ
diff --git a/apps/system/style/sound_manager/images/VolumeOverlay_Landscape_SegmentOff_right.png b/apps/system/style/sound_manager/images/VolumeOverlay_Landscape_SegmentOff_right.png
new file mode 100644
index 0000000..588a176
--- /dev/null
+++ b/apps/system/style/sound_manager/images/VolumeOverlay_Landscape_SegmentOff_right.png
Binary files differ
diff --git a/apps/system/style/sound_manager/images/VolumeOverlay_Landscape_SegmentOn.png b/apps/system/style/sound_manager/images/VolumeOverlay_Landscape_SegmentOn.png
new file mode 100644
index 0000000..abb099f
--- /dev/null
+++ b/apps/system/style/sound_manager/images/VolumeOverlay_Landscape_SegmentOn.png
Binary files differ
diff --git a/apps/system/style/sound_manager/images/VolumeOverlay_Landscape_SegmentOn_left.png b/apps/system/style/sound_manager/images/VolumeOverlay_Landscape_SegmentOn_left.png
new file mode 100644
index 0000000..a8d2ef5
--- /dev/null
+++ b/apps/system/style/sound_manager/images/VolumeOverlay_Landscape_SegmentOn_left.png
Binary files differ
diff --git a/apps/system/style/sound_manager/images/VolumeOverlay_Landscape_SegmentOn_right.png b/apps/system/style/sound_manager/images/VolumeOverlay_Landscape_SegmentOn_right.png
new file mode 100644
index 0000000..c287a9f
--- /dev/null
+++ b/apps/system/style/sound_manager/images/VolumeOverlay_Landscape_SegmentOn_right.png
Binary files differ
diff --git a/apps/system/style/sound_manager/images/header_bg.png b/apps/system/style/sound_manager/images/header_bg.png
new file mode 100644
index 0000000..e1ff073
--- /dev/null
+++ b/apps/system/style/sound_manager/images/header_bg.png
Binary files differ
diff --git a/apps/system/style/sound_manager/images/speaker_loud_icon.png b/apps/system/style/sound_manager/images/speaker_loud_icon.png
new file mode 100644
index 0000000..8a49d8f
--- /dev/null
+++ b/apps/system/style/sound_manager/images/speaker_loud_icon.png
Binary files differ
diff --git a/apps/system/style/sound_manager/images/speaker_mute_icon.png b/apps/system/style/sound_manager/images/speaker_mute_icon.png
new file mode 100644
index 0000000..7b3a397
--- /dev/null
+++ b/apps/system/style/sound_manager/images/speaker_mute_icon.png
Binary files differ
diff --git a/apps/system/style/sound_manager/images/speaker_regular_icon.png b/apps/system/style/sound_manager/images/speaker_regular_icon.png
new file mode 100644
index 0000000..e762611
--- /dev/null
+++ b/apps/system/style/sound_manager/images/speaker_regular_icon.png
Binary files differ
diff --git a/apps/system/style/sound_manager/images/vibration.png b/apps/system/style/sound_manager/images/vibration.png
new file mode 100644
index 0000000..3fa1f92
--- /dev/null
+++ b/apps/system/style/sound_manager/images/vibration.png
Binary files differ
diff --git a/apps/system/style/sound_manager/images/vibration_disabled_icon.png b/apps/system/style/sound_manager/images/vibration_disabled_icon.png
new file mode 100644
index 0000000..b529231
--- /dev/null
+++ b/apps/system/style/sound_manager/images/vibration_disabled_icon.png
Binary files differ
diff --git a/apps/system/style/sound_manager/images/vibration_enabled_icon.png b/apps/system/style/sound_manager/images/vibration_enabled_icon.png
new file mode 100644
index 0000000..e28b3b7
--- /dev/null
+++ b/apps/system/style/sound_manager/images/vibration_enabled_icon.png
Binary files differ
diff --git a/apps/system/style/sound_manager/images/volume-off.png b/apps/system/style/sound_manager/images/volume-off.png
new file mode 100644
index 0000000..edee64e
--- /dev/null
+++ b/apps/system/style/sound_manager/images/volume-off.png
Binary files differ
diff --git a/apps/system/style/sound_manager/images/volume-on.png b/apps/system/style/sound_manager/images/volume-on.png
new file mode 100644
index 0000000..d7ef462
--- /dev/null
+++ b/apps/system/style/sound_manager/images/volume-on.png
Binary files differ
diff --git a/apps/system/style/sound_manager/images/volume_center_active.png b/apps/system/style/sound_manager/images/volume_center_active.png
new file mode 100644
index 0000000..6c80874
--- /dev/null
+++ b/apps/system/style/sound_manager/images/volume_center_active.png
Binary files differ
diff --git a/apps/system/style/sound_manager/images/volume_center_disabled.png b/apps/system/style/sound_manager/images/volume_center_disabled.png
new file mode 100644
index 0000000..76ad232
--- /dev/null
+++ b/apps/system/style/sound_manager/images/volume_center_disabled.png
Binary files differ
diff --git a/apps/system/style/sound_manager/images/volume_left_active.png b/apps/system/style/sound_manager/images/volume_left_active.png
new file mode 100644
index 0000000..cb1f105
--- /dev/null
+++ b/apps/system/style/sound_manager/images/volume_left_active.png
Binary files differ
diff --git a/apps/system/style/sound_manager/images/volume_left_disabled.png b/apps/system/style/sound_manager/images/volume_left_disabled.png
new file mode 100644
index 0000000..1e78c6f
--- /dev/null
+++ b/apps/system/style/sound_manager/images/volume_left_disabled.png
Binary files differ
diff --git a/apps/system/style/sound_manager/images/volume_right_active.png b/apps/system/style/sound_manager/images/volume_right_active.png
new file mode 100644
index 0000000..dcdee3a
--- /dev/null
+++ b/apps/system/style/sound_manager/images/volume_right_active.png
Binary files differ
diff --git a/apps/system/style/sound_manager/images/volume_right_disabled.png b/apps/system/style/sound_manager/images/volume_right_disabled.png
new file mode 100644
index 0000000..d02eb4e
--- /dev/null
+++ b/apps/system/style/sound_manager/images/volume_right_disabled.png
Binary files differ
diff --git a/apps/system/style/sound_manager/sound_manager.css b/apps/system/style/sound_manager/sound_manager.css
new file mode 100644
index 0000000..cdfaab3
--- /dev/null
+++ b/apps/system/style/sound_manager/sound_manager.css
@@ -0,0 +1,184 @@
+#volume {
+ text-align: center;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 40px;
+ -moz-transition: opacity 0.5s ease;
+ pointer-events: none;
+ display: none;
+ background-image: url("images/header_bg.png");
+}
+
+#volume > * {
+ line-height: 40px;
+ height: 40px;
+ display: inline-block;
+ float: left;
+ background-repeat: no-repeat, no-repeat;
+ background-position: center, center;
+}
+
+#volume span {
+ display: inline-block;
+ width: 25px;
+ float: left;
+}
+
+#volume span.vibration {
+ visibility: hidden;
+ margin-left: 5px;
+ background-image: url('images/vibration_disabled_icon.png');
+}
+
+#volume.vibration span.vibration {
+ background-image: url('images/vibration_enabled_icon.png');
+}
+
+#volume.mute span.mute-state {
+ background-image: url('images/speaker_mute_icon.png');
+}
+
+#volume span.mute-state {
+ background-image: url('images/speaker_regular_icon.png');
+}
+
+#volume span.volume {
+ margin-left: 5px;
+ background-image: url('images/speaker_loud_icon.png');
+}
+
+#volume.visible {
+ display: block;
+}
+
+#volume[data-channel="notification"] span.vibration {
+ visibility: visible;
+}
+
+#volume[data-channel="bt_sco"] div:nth-child(n+4),
+#volume[data-channel="content"] div:nth-child(n+4),
+#volume[data-channel="alarm"] div:nth-child(n+4),
+#volume[data-channel="notification"] div:nth-child(n+4) {
+ width: 12px;
+ margin-left: 2px;
+ background-image: url('images/volume_center_disabled.png');
+}
+
+#volume[data-channel="telephony"] div:nth-child(n+4) {
+ width: 42px;
+ margin-left: 2px;
+ background-image: url('images/volume_center_disabled.png');
+}
+
+#volume[data-channel="bt_sco"] div.active:nth-child(n+4),
+#volume[data-channel="content"] div.active:nth-child(n+4),
+#volume[data-channel="alarm"] div.active:nth-child(n+4),
+#volume[data-channel="notification"] div.active:nth-child(n+4),
+#volume[data-channel="telephony"] div.active:nth-child(n+4) {
+ background-image: url('images/volume_center_active.png');
+}
+
+#volume div:nth-child(3) {
+ background-image: url('images/volume_left_disabled.png');
+}
+
+#volume div.active:nth-child(3) {
+ background-image: url('images/volume_left_active.png');
+}
+
+#volume[data-channel="bt_sco"] div:nth-child(17),
+#volume[data-channel="alarm"] div:nth-child(17),
+#volume[data-channel="notification"] div:nth-child(17),
+#volume[data-channel="content"] div:nth-child(17),
+#volume[data-channel="telephony"] div:nth-child(7) {
+ background-image: url('images/volume_right_disabled.png');
+}
+
+#volume[data-channel="bt_sco"] div.active:nth-child(17),
+#volume[data-channel="alarm"] div.active:nth-child(17),
+#volume[data-channel="notification"] div.active:nth-child(17),
+#volume[data-channel="content"] div.active:nth-child(17),
+#volume[data-channel="telephony"] div.active:nth-child(7) {
+ background-image: url('images/volume_right_active.png');
+}
+
+#volume[data-channel="telephony"] div:nth-child(n+8) {
+ display: none;
+}
+
+#volume[data-channel="notification"] > div:nth-child(n+3),
+#volume[data-channel="alarm"] > div:nth-child(n+3),
+#volume[data-channel="bt_sco"] > div:nth-child(n+3),
+#volume[data-channel="content"] > div:nth-child(n+3) {
+ width: 12px;
+ background-size: 12px 10px;
+}
+
+#volume[data-channel="telephony"] > div:nth-child(n+3) {
+ width: 42px;
+ background-size: 42px 10px;
+}
+
+@media (orientation: landscape) and (width: 480px) {
+ #volume[data-channel="bt_sco"] div:nth-child(n+4),
+ #volume[data-channel="content"] div:nth-child(n+4),
+ #volume[data-channel="alarm"] div:nth-child(n+4),
+ #volume[data-channel="notification"] div:nth-child(n+4),
+ #volume[data-channel="telephony"] div:nth-child(n+4) {
+ width: 36px;
+ margin-left: 2px;
+ background-size: 36px 10px;
+ background-image: url('images/VolumeOverlay_Landscape_SegmentOff.png');
+ }
+
+ #volume[data-channel="telephony"] div:nth-child(n+8) {
+ display: none;
+ }
+
+ #volume[data-channel="bt_sco"] div.active:nth-child(n+4),
+ #volume[data-channel="content"] div.active:nth-child(n+4),
+ #volume[data-channel="alarm"] div.active:nth-child(n+4),
+ #volume[data-channel="notification"] div.active:nth-child(n+4),
+ #volume[data-channel="telephony"] div.active:nth-child(n+4) {
+ background-image: url('images/VolumeOverlay_Landscape_SegmentOn.png');
+ }
+
+ #volume div:nth-child(3) {
+ background-image: url('images/VolumeOverlay_Landscape_SegmentOff_left.png');
+ }
+
+ #volume div.active:nth-child(3) {
+ background-image: url('images/VolumeOverlay_Landscape_SegmentOn_left.png');
+ }
+
+ #volume[data-channel="bt_sco"] div:nth-child(17),
+ #volume[data-channel="content"] div:nth-child(17),
+ #volume[data-channel="alarm"] div:nth-child(17),
+ #volume[data-channel="notification"] div:nth-child(17),
+ #volume[data-channel="telephony"] div:nth-child(7) {
+ background-image: url('images/VolumeOverlay_Landscape_SegmentOff_right.png');
+ }
+
+ #volume[data-channel="bt_sco"] div.active:nth-child(17),
+ #volume[data-channel="content"] div.active:nth-child(17),
+ #volume[data-channel="alarm"] div.active:nth-child(17),
+ #volume[data-channel="notification"] div.active:nth-child(17),
+ #volume[data-channel="telephony"] div.active:nth-child(7) {
+ background-image: url('images/VolumeOverlay_Landscape_SegmentOn_right.png');
+ }
+
+ #volume[data-channel="notification"] > div:nth-child(n+3),
+ #volume[data-channel="alarm"] > div:nth-child(n+3),
+ #volume[data-channel="bt_sco"] > div:nth-child(n+3),
+ #volume[data-channel="content"] > div:nth-child(n+3) {
+ width: 22px;
+ background-size: 22px 10px;
+ }
+
+ #volume[data-channel="telephony"] > div:nth-child(n+3) {
+ width: 74px;
+ background-size: 74px 10px;
+ }
+}
diff --git a/apps/system/style/statusbar/images/call-forwarding.png b/apps/system/style/statusbar/images/call-forwarding.png
new file mode 100644
index 0000000..52d5a86
--- /dev/null
+++ b/apps/system/style/statusbar/images/call-forwarding.png
Binary files differ
diff --git a/apps/system/style/statusbar/images/icons.png b/apps/system/style/statusbar/images/icons.png
new file mode 100644
index 0000000..81f4f4c
--- /dev/null
+++ b/apps/system/style/statusbar/images/icons.png
Binary files differ
diff --git a/apps/system/style/statusbar/images/network-activity.gif b/apps/system/style/statusbar/images/network-activity.gif
new file mode 100644
index 0000000..5809833
--- /dev/null
+++ b/apps/system/style/statusbar/images/network-activity.gif
Binary files differ
diff --git a/apps/system/style/statusbar/images/signal-searching.gif b/apps/system/style/statusbar/images/signal-searching.gif
new file mode 100644
index 0000000..f9b1da0
--- /dev/null
+++ b/apps/system/style/statusbar/images/signal-searching.gif
Binary files differ
diff --git a/apps/system/style/statusbar/images/system-downloads.gif b/apps/system/style/statusbar/images/system-downloads.gif
new file mode 100644
index 0000000..bf24971
--- /dev/null
+++ b/apps/system/style/statusbar/images/system-downloads.gif
Binary files differ
diff --git a/apps/system/style/statusbar/images/wifi-connecting.gif b/apps/system/style/statusbar/images/wifi-connecting.gif
new file mode 100644
index 0000000..6cf37dd
--- /dev/null
+++ b/apps/system/style/statusbar/images/wifi-connecting.gif
Binary files differ
diff --git a/apps/system/style/statusbar/statusbar.css b/apps/system/style/statusbar/statusbar.css
new file mode 100644
index 0000000..e4530ad
--- /dev/null
+++ b/apps/system/style/statusbar/statusbar.css
@@ -0,0 +1,412 @@
+/* The status bar disregard the rem rule because we have only 16px icons for now */
+
+body, html {
+ font-family: MozTT, sans-serif;
+ font-weight: 500;
+ font-size: 10px;
+}
+
+#screen.lockscreen-camera > #statusbar {
+ display: none;
+}
+
+#statusbar {
+ overflow: hidden;
+ position: absolute;
+
+ width: 100%;
+ height: 20px;
+ top: 0;
+ left: 0;
+ background-color: #000;
+
+ padding: 2px 0;
+ -moz-box-sizing: border-box;
+}
+
+#statusbar > * {
+ pointer-events: none;
+ float: right;
+ margin: 0 2px;
+}
+
+#statusbar > .sb-start {
+ float: left;
+ -moz-transition: visibility 0.5s ease, -moz-transform 0.5s ease;
+}
+
+*[dir=rtl] #statusbar > * {
+ float: left;
+}
+
+*[dir=rtl] #statusbar > .sb-start {
+ float: right;
+}
+
+#statusbar > .sb-start-upper {
+ visibility: hidden;
+ position: absolute;
+ top: -20px;
+ left: 0;
+
+ -moz-transition: visibility 0.5s ease, -moz-transform 0.5s ease;
+}
+
+*[dir=rtl] #statusbar > .sb-start-upper {
+ left: auto;
+ right: 0;
+}
+
+.utility-tray #statusbar > .sb-start-upper {
+ visibility: visible;
+ -moz-transform: translateY(20px);
+}
+
+.utility-tray #statusbar > .sb-start {
+ visibility: hidden;
+ -moz-transform: translateY(20px);
+}
+
+#statusbar-label {
+ color: #919899;
+ font-size: 1.49rem; /* 6.5pt */
+ position: relative;
+ bottom: 1px;
+}
+
+#statusbar-time {
+ color: #fff;
+ font-size: 1.49rem; /* 6.5pt */
+ position: relative;
+ bottom: 2px;
+}
+
+#statusbar-time span {
+ margin: 0 0.3rem;
+ font-size: 1rem;
+}
+
+#screen.locked #statusbar-time {
+ display: none;
+}
+
+.sb-icon {
+ width: 16px;
+ height: 16px;
+
+ background: url('images/icons.png') no-repeat;
+}
+
+.sb-icon-battery {
+ width: 21px;
+}
+.sb-icon-battery:not([data-level]) {
+ background-position: -286px 0;
+}
+.sb-icon-battery[data-level="0"] {
+ background-position: 0 0;
+}
+.sb-icon-battery[data-level="10"] {
+ background-position: -26px 0;
+}
+.sb-icon-battery[data-level="20"] {
+ background-position: -52px 0;
+}
+.sb-icon-battery[data-level="30"] {
+ background-position: -78px 0;
+}
+.sb-icon-battery[data-level="40"] {
+ background-position: -104px 0;
+}
+.sb-icon-battery[data-level="50"] {
+ background-position: -130px 0;
+}
+.sb-icon-battery[data-level="60"] {
+ background-position: -156px 0;
+}
+.sb-icon-battery[data-level="70"] {
+ background-position: -182px 0;
+}
+.sb-icon-battery[data-level="80"] {
+ background-position: -208px 0;
+}
+.sb-icon-battery[data-level="90"] {
+ background-position: -234px 0;
+}
+.sb-icon-battery[data-level="100"] {
+ background-position: -260px 0;
+}
+
+.sb-icon-battery:not([data-level])[data-charging="true"],
+.sb-icon-battery[data-level="0"][data-charging="true"] {
+ background-position: 0 -20px;
+}
+.sb-icon-battery[data-level="10"][data-charging="true"] {
+ background-position: -26px -20px;
+}
+.sb-icon-battery[data-level="20"][data-charging="true"] {
+ background-position: -52px -20px;
+}
+.sb-icon-battery[data-level="30"][data-charging="true"] {
+ background-position: -78px -20px;
+}
+.sb-icon-battery[data-level="40"][data-charging="true"] {
+ background-position: -104px -20px;
+}
+.sb-icon-battery[data-level="50"][data-charging="true"] {
+ background-position: -130px -20px;
+}
+.sb-icon-battery[data-level="60"][data-charging="true"] {
+ background-position: -156px -20px;
+}
+.sb-icon-battery[data-level="70"][data-charging="true"] {
+ background-position: -182px -20px;
+}
+.sb-icon-battery[data-level="80"][data-charging="true"] {
+ background-position: -208px -20px;
+}
+.sb-icon-battery[data-level="90"][data-charging="true"] {
+ background-position: -234px -20px;
+}
+.sb-icon-battery[data-level="100"][data-charging="true"] {
+ background-position: -260px -20px;
+}
+
+.sb-icon-wifi[data-level="0"][data-connecting="true"] {
+ background: url('images/wifi-connecting.gif') no-repeat;
+ background-position: 0 0;
+}
+
+.sb-icon-wifi:not([data-level]),
+.sb-icon-wifi[data-level="0"] {
+ background-position: 0 -60px;
+}
+.sb-icon-wifi[data-level="1"] {
+ background-position: -21px -60px;
+}
+.sb-icon-wifi[data-level="2"] {
+ background-position: -42px -60px;
+}
+.sb-icon-wifi[data-level="3"] {
+ background-position: -63px -60px;
+}
+.sb-icon-wifi[data-level="4"] {
+ background-position: -84px -60px;
+}
+
+.sb-icon-data[data-type="H+"] {
+ background-position: 0 -80px;
+}
+
+.sb-icon-data[data-type="H"] {
+ background-position: -21px -80px;
+}
+
+.sb-icon-data[data-type="4G"] {
+ background-position: -42px -80px;
+}
+
+.sb-icon-data[data-type="3G"] {
+ background-position: -63px -80px;
+}
+
+.sb-icon-data[data-type="E"] {
+ background-position: -84px -80px;
+}
+
+.sb-icon-data[data-type="2G"] {
+ background-position: -105px -80px;
+}
+
+.sb-icon-data[data-type="circle"] {
+ background-position: -126px -80px;
+}
+
+.sb-icon-flight-mode {
+ background-position: 0 -40px;
+}
+
+.sb-icon-signal[data-level="-1"][data-searching="true"] {
+ background: url('images/signal-searching.gif') no-repeat;
+ background-position: 0 0;
+}
+
+.sb-icon-signal[data-level="-1"] {
+ background-position: -21px -40px;
+}
+
+.sb-icon-signal[data-emergency="true"],
+.sb-icon-signal[data-level="0"] {
+ background-position: -42px -40px;
+}
+
+.sb-icon-signal[data-level="1"] {
+ background-position: -63px -40px;
+}
+
+.sb-icon-signal[data-level="2"] {
+ background-position: -84px -40px;
+}
+
+.sb-icon-signal[data-level="3"] {
+ background-position: -105px -40px;
+}
+
+.sb-icon-signal[data-level="4"] {
+ background-position: -126px -40px;
+}
+
+.sb-icon-signal[data-level="5"] {
+ background-position: -147px -40px;
+}
+
+.sb-icon-signal {
+ /* default icon is the "no sim" icon */
+ background-position: -147px -80px;
+ position: relative;
+}
+
+.sb-icon-signal[data-roaming="true"]::after {
+ content: '';
+ position: absolute;
+ width: 8px;
+ height: 5px;
+ top: 0;
+ left: 0;
+ background: url('images/icons.png') no-repeat;
+ background-position: -168px -40px;
+}
+
+.sb-icon-network-activity {
+ background: url('images/network-activity.gif') no-repeat;
+ background-position: 0 0;
+}
+
+.sb-icon-headphones {
+ background-position: -189px -40px;
+}
+
+.sb-icon-geolocation {
+ background-position: -231px -40px;
+}
+
+.sb-icon-geolocation[data-active="true"] {
+ background-position: -210px -40px;
+}
+
+.sb-icon-recording {
+ background-position: -273px -40px;
+}
+
+.sb-icon-recording[data-active="true"] {
+ background-position: -252px -40px;
+}
+
+.sb-icon-alarm {
+ background-position: -294px -40px;
+}
+
+.sb-icon-tethering {
+ background-position: -126px -60px;
+}
+
+.sb-icon-tethering[data-active="true"] {
+ background-position: -105px -60px;
+}
+
+.sb-icon-bluetooth {
+ background-position: -252px -60px;
+}
+
+.sb-icon-bluetooth[data-active="true"] {
+ background-position: -231px -60px;
+}
+
+.sb-icon-mute {
+ background-position: -273px -60px;
+}
+
+.sb-icon-mute.vibration {
+ background-position: -210px -60px;
+}
+
+.sb-icon-usb {
+ background-position: -294px -60px;
+}
+
+.sb-icon-notification {
+ background: #000;
+ width: 32px;
+ position: relative;
+}
+
+.sb-icon-notification::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 16px;
+ height: 16px;
+
+ background: url('images/icons.png') no-repeat;
+ background-position: -189px -60px;
+}
+
+*[dir=rtl] .sb-icon-notification::before {
+ right: 0;
+ left: auto;
+}
+
+.sb-icon-notification[data-unread="true"]::before {
+ background-position: -168px -60px;
+}
+
+.sb-icon-notification::after {
+ content: attr(data-num);
+ position: absolute;
+ color: #666;
+ width: 16px;
+ height: 16px;
+ top: -2px;
+ right: 0;
+ padding: 0 1px;
+ font-size: 1.49rem; /* 6.5pt */
+}
+
+*[dir=rtl] .sb-icon-notification::after {
+ right: auto;
+ left: 0;
+}
+.sb-icon-notification[data-unread="true"]::after {
+ color: #27b9cf;
+}
+
+.sb-icon-sms {
+ background-position: -147px -60px;
+ position: relative;
+}
+
+.sb-icon-sms::before {
+ content: attr(data-num);
+ position: absolute;
+ color: #000;
+ top: 4px;
+ left: 0;
+ width: 100%;
+ text-align: center;
+ font-size: 1.15rem; /*5pt was 8px */
+}
+
+/* JW: should we show how many current download we have
+ * as in sb-icon-ms or sb-icon-notification ? */
+.sb-icon-system-downloads {
+ /* JW: waiting for the correct icon; in the mean time I made this one */
+ background: url('images/system-downloads.gif') no-repeat;
+ background-position: 0 0;
+}
+
+.sb-icon-call-forwarding {
+ /* Waiting for the correct icon. In the mean time we use this one. */
+ background: url('images/call-forwarding.png') no-repeat;
+ background-position: 0 0;
+}
diff --git a/apps/system/style/system/keyboard.css b/apps/system/style/system/keyboard.css
new file mode 100644
index 0000000..dab5939
--- /dev/null
+++ b/apps/system/style/system/keyboard.css
@@ -0,0 +1,28 @@
+#keyboard-frame {
+ /* See the new mozpasspointerevents attribute added in bug 796452 */
+ pointer-events: none;
+
+ position: absolute;
+ bottom: 0;
+
+ width: 100%;
+ height: 100%;
+
+ transform: translateY(0);
+ -moz-transition: visibility 0.2s ease, -moz-transform 0.2s ease;
+}
+
+#keyboard-frame.hide {
+ visibility: hidden;
+
+ transform: translateY(100%);
+}
+
+#keyboard-frame iframe {
+ position: absolute;
+ bottom: 0;
+
+ width: 100%;
+ height: 100%;
+ border: 0;
+}
diff --git a/apps/system/style/system/system.css b/apps/system/style/system/system.css
new file mode 100644
index 0000000..af2a739
--- /dev/null
+++ b/apps/system/style/system/system.css
@@ -0,0 +1,579 @@
+html {
+ font-size: 10px;
+}
+
+@media screen and (min-width: 480px) {
+ html {
+ font-size: 13.3px;
+ }
+}
+
+body {
+ width: 100%;
+ height: 100%;
+ margin: 0px;
+ padding: 0px;
+
+ overflow: hidden;
+
+ font-family: 'MozTT', sans-serif;
+ color: #fff;
+ font-size: 12px;
+}
+
+#screen {
+ position: absolute;
+ overflow: hidden;
+ width: 100%;
+ height: 100%;
+
+ background-color: #000;
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-position: 50% 50%;
+}
+
+#screen.screenoff {
+ background: #000 !important;
+}
+
+#screen.screenoff * {
+ visibility: hidden !important;
+}
+
+/*
+ * Poweroff animation
+ */
+#screen > div#poweroff-splash {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: #000;
+}
+
+#screen > div#poweroff-splash.step1 {
+ animation: poweroff-splash-fade-in 0.5s;
+}
+
+@keyframes poweroff-splash-fade-in {
+ 0% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
+.poweroff-ring {
+ display: block;
+ position: absolute;
+ border-radius: 50%;
+ width: 60px;
+ height: 60px;
+ margin-left: -30px;
+ margin-top: -30px;
+ left: 50%;
+ opacity: 0;
+}
+
+#poweroff-ring-1 {
+ top: 120px;
+ background-color: #e66600;
+}
+
+#poweroff-ring-2 {
+ top: 240px;
+ background-color: #dc4e00;
+}
+
+#poweroff-ring-3 {
+ top: 360px;
+ background-color: #d24500;
+}
+
+.poweroff-ring > span {
+ display: block;
+ position: absolute;
+ border-radius: 50%;
+ top: 50%;
+ left: 50%;
+ margin: auto;
+ background-color: black;
+ width: 40px;
+ height: 40px;
+ margin-top: -20px;
+ margin-left: -20px;
+}
+
+#poweroff-ring-2 > span {
+ transform: scale(1.125);
+}
+
+#poweroff-ring-3 > span {
+ transform: scale(1.25);
+}
+
+/* Ring 1 : inner diameter scales from 40 pixel to 58 pixel */
+@keyframes ring1-scale {
+ 0% {
+ transform: scale(1);
+ }
+ 100% {
+ transform: scale(1.45);
+ }
+}
+
+/* Ring 2 : inner diameter scales from 45 pixel to 59.5 pixel */
+@keyframes ring2-scale {
+ 0% {
+ transform: scale(1.125);
+ }
+ 100% {
+ transform: scale(1.4875);
+ }
+}
+
+/* Ring 3 : inner diameter scales from 50 pixel to 59 pixel */
+@keyframes ring3-scale {
+ 0% {
+ transform: scale(1.25);
+ }
+ 100% {
+ transform: scale(1.475);
+ }
+}
+
+@keyframes ring-fade-in {
+ 0% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
+@keyframes ring-fade-out {
+ 0% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0;
+ }
+}
+
+#screen > div#poweroff-splash.step2 > #poweroff-ring-1 {
+ animation: ring-fade-in .4s ease-out 0s,
+ ring-fade-out .5s linear .4s;
+}
+
+#screen > div#poweroff-splash.step2 > #poweroff-ring-2 {
+ animation: ring-fade-in .4s ease-out .25s,
+ ring-fade-out .5s linear .65s;
+}
+
+#screen > div#poweroff-splash.step2 > #poweroff-ring-3 {
+ animation: ring-fade-in .4s ease-out .5s,
+ ring-fade-out .5s linear .9s;
+}
+
+#screen > div#poweroff-splash.step2 > #poweroff-ring-1 > span {
+ animation: ring1-scale .5s linear .4s;
+}
+
+#screen > div#poweroff-splash.step2 > #poweroff-ring-2 > span {
+ animation: ring2-scale .5s linear .65s;
+}
+
+#screen > div#poweroff-splash.step2 > #poweroff-ring-3 > span {
+ animation: ring3-scale .5s linear .9s;
+}
+
+
+
+#system-overlay {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ visibility: hidden;
+
+ pointer-events: none;
+}
+
+#system-overlay.volume {
+ visibility: visible;
+}
+
+#windows {
+ position: absolute;
+ left: 0px;
+ width: 100%;
+ top: -100%;
+ height: 0;
+ max-height: 0;
+ border: 0px;
+ overflow: hidden;
+}
+
+#windows.active {
+ top: 0;
+ height: 100%;
+ max-height: 100%;
+}
+
+#windows > .appWindow {
+ position: fixed;
+ border: 0;
+ margin: 0;
+ padding: 0;
+ overflow: hidden;
+ left: 0;
+ top: 20px;
+
+ /* Need to leave this in here to prevent flickering. */
+ transform: scale(1);
+
+/*
+ * Do not specify height/width here!
+ * They should be handle executively in window_manager.js
+ *
+ */
+
+/*
+ * Disable this for now because it forces an expensive fallback path in
+ * Gecko. The performance issue should be fixed by
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=697645 or a related bug.
+ *
+ border-radius: 8px;
+ */
+}
+
+#windows > .appWindow > iframe {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border: none;
+}
+
+#screen.active-statusbar #windows > .appWindow {
+ top: 40px;
+}
+
+#windows > .appWindow:not(.homescreen) {
+ background-color: #fff;
+}
+
+#windows > .appWindow:not(.homescreen).default-background {
+ background: url('/shared/resources/branding/splash_screen_generic.png') center center no-repeat #fff;
+}
+
+#windows > .appWindow:not(.active):not(.homescreen):not(.opening):not(.closing) {
+ visibility: hidden;
+}
+
+#windows > .appWindow:not(.homescreen):not(.active):not(.inlineActivity):not(.opening) {
+ opacity: 0;
+ transform: scale(0.6);
+}
+
+#windows > .appWindow.opening {
+ transition: transform 0.25s ease, opacity 0.15s ease;
+}
+
+#windows.slow-transition > .appWindow.opening {
+ transition: transform 5s ease, opacity 3s ease;
+}
+
+#screen.cards-view > #windows > .appWindow.opening,
+#screen.cards-view > #windows > .appWindow.closing,
+#screen.switch-app > #windows > .appWindow.opening,
+#screen.switch-app > #windows > .appWindow.closing {
+ transition: transform 0.25s ease;
+ visibility: inherit;
+ opacity: 1;
+}
+
+#screen.switch-app > #windows.slow-transition > .appWindow.opening,
+#screen.switch-app > #windows.slow-transition > .appWindow.closing {
+ transition: transform 5s ease;
+}
+
+#screen.switch-app > #windows > .appWindow.opening-card {
+ transform: translateX(70%) scale(0.6);
+ visibility: inherit;
+ opacity: 1;
+}
+
+#screen.switch-app > #windows > .appWindow.opening-switching {
+ transform: scale(0.6);
+ transition: transform 0.25s ease;
+ visibility: inherit;
+ opacity: 1;
+}
+
+#screen.switch-app > #windows.slow-transition > .appWindow.opening-switching {
+ transition: transform 5s ease;
+}
+
+#screen.switch-app > #windows > .appWindow.closing-card {
+ transform: translateX(-70%) scale(0.6);
+ transition: transform 0.25s ease;
+ visibility: inherit;
+ opacity: 1;
+}
+
+#screen.switch-app > #windows.slow-transition > .appWindow.closing-card {
+ transition: transform 5s ease;
+}
+
+#windows > .appWindow.closing {
+ transition: transform 0.25s ease, opacity 0.15s ease 0.1s;
+}
+
+#windows.slow-transition > .appWindow.closing {
+ transition: transform 5s ease, opacity 3s ease 2s;
+}
+
+#windows > .appWindow.inlineActivity,
+#windows > .appWindow.hideBottom {
+ transform: translateY(100%);
+ transition: transform 0.25s ease, visibility 0.25s ease;
+}
+
+#windows.slow-transition > .appWindow.inlineActivity,
+#windows.slow-transition > .appWindow.hideBottom {
+ transition: transform 5s ease, visibility 5s ease;
+}
+
+#windows > .appWindow.back {
+ transition: transform 0.25s ease, visibility 0.25s ease;
+ transform: scale(0.86);
+}
+
+#windows > .appWindow.restored {
+ transform: none;
+ transition: transform 0.25s ease;
+}
+
+#windows > .appWindow.inlineActivity.active {
+ transform: none;
+}
+
+#screen > #windows > .appWindow.fullscreen-app {
+ top: 0;
+}
+
+#screen.attention > #windows > .appWindow.fullscreen-app,
+#screen.attention > #windowSprite,
+#screen.fullscreen-app.attention > #windowSprite {
+ height: calc(100% - 40px);
+ top: 40px;
+}
+
+#windowSprite.before-inline-activity {
+ transform: translateY(calc(100% - 1px));
+ opacity: 1;
+ visibility: visible;
+}
+
+#windowSprite.inline-activity-opening {
+ transition: transform 0.25s ease;
+ transform: translateY(0);
+ opacity: 1;
+ visibility: visible;
+}
+
+#windowSprite.inline-activity-opened {
+ transition: opacity 0.15s ease;
+
+ transform: translateY(0);
+ opacity: 0;
+ visibility: visible;
+}
+
+#windowSprite.before-open {
+ transform: scale(0.6);
+ /* 0.01 opacity is needed to make sure Gecko paints and fire
+ MozAfterPaint event for us. */
+ opacity: 0.01;
+ visibility: visible;
+}
+
+#windowSprite.opening {
+ transition: transform 0.25s ease, opacity 0.15s ease;
+
+ transform: scale(1);
+ opacity: 1;
+ visibility: visible;
+}
+
+#screen.trustedui #windowSprite.opening {
+ opacity: 0;
+}
+
+#windowSprite.opened {
+ transition: opacity 0.15s ease;
+
+ transform: scale(1);
+ opacity: 0;
+ visibility: visible;
+}
+
+#windowSprite.before-close {
+ transform: scale(1);
+ /* 0.01 opacity is needed to make sure Gecko paints and fire
+ MozAfterPaint event for us. */
+ opacity: 0.01;
+ visibility: visible;
+}
+
+#windowSprite.closing {
+ transition: opacity 0.05s ease;
+
+ transform: scale(1);
+ opacity: 1;
+ visibility: visible;
+}
+
+#windowSprite.closed {
+ transition: transform 0.25s ease, opacity 0.15s ease;
+
+ transform: scale(0.6);
+ opacity: 0;
+ visibility: visible;
+}
+
+.accessibility-invert {
+ filter: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxmaWx0ZXIKICAgICAgaWQ9ImludmVydEZpbHRlciIKICAgICAgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KICAgIDxmZUNvbG9yTWF0cml4CiAgICAgICAgdmFsdWVzPSItMSAwIDAgMCAxIDAgLTEgMCAwIDEgMCAwIC0xIDAgMSAwIDAgMCAxIDAgIiAvPgogIDwvZmlsdGVyPgo8L3N2Zz4K#invertFilter);
+}
+
+iframe.backgroundWindow {
+ position: absolute;
+ top: -1px;
+ left: -1px;
+ width: 1px;
+ height: 1px;
+ visibility: hidden;
+}
+
+#dialog-overlay {
+ position: absolute;
+ top: 20px;
+ left: 0;
+ width: 100%;
+ height: calc(100% - 20px);
+ visibility: hidden;
+ pointer-events: none;
+}
+
+#screen.active-statusbar #dialog-overlay {
+ top: 40px;
+ height: calc(100% - 40px);
+}
+
+#screen:-moz-full-screen-ancestor #dialog-overlay,
+#screen.fullscreen-app #dialog-overlay {
+ top: 0;
+ height: 100%;
+}
+
+#screen:not(.crash-dialog) #crash-dialog {
+ visibility: hidden;
+}
+
+/* `.dialog` is set by system_dialog.js when a dialog is shown */
+#screen.dialog #dialog-overlay {
+ visibility: visible;
+ pointer-events: auto;
+}
+
+#screen.authentication-dialog #dialog-overlay,
+#screen.authentication-dialog #authentication-dialog,
+#screen.crash-dialog #dialog-overlay,
+#screen.crash-dialog #crash-dialog
+#screen.modal-dialog #dialog-overlay,
+#screen.modal-dialog #modal-dialog,
+#screen.popup #popup-container,
+#screen.trustedui #trustedui-container,
+#screen.trustedui #dialog-overlay {
+ visibility: visible;
+ pointer-events: auto;
+}
+
+@keyframes banner-bounce {
+ from, to {
+ transform: translateY(100%);
+ opacity: 0;
+ }
+ 12.5%, 87.5% {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+#system-banner {
+ z-index: 0;
+ visibility: hidden;
+ opacity: 0;
+ transform: translateY(100%);
+}
+
+#system-banner.visible {
+ animation: banner-bounce 4s;
+ visibility: visible;
+}
+
+#system-banner button {
+ width: auto;
+}
+
+#system-banner[data-button="false"] button {
+ visibility: hidden;
+}
+
+#screen iframe.communication-frame {
+ visibility: hidden;
+}
+
+/*
+ Styles for FTU starting
+*/
+
+#screen.ftu > [data-z-index-level="app"] > .appWindow.homescreen,
+#screen.ftu > [data-z-index-level="app"] > .appWindow.homescreen.active {
+ opacity: 0;
+}
+
+#screen.ftuStarting #lockscreen {
+ display: none;
+}
+
+#screen.ftuStarting #statusbar {
+ opacity: 0;
+}
+
+/* For app error */
+
+.appWindow > iframe {
+ z-index: 1;
+}
+
+.appError {
+ display: none;
+ background-color: black;
+ z-index: 0;
+}
+
+.appWindow > .appError {
+ background-color: black;
+}
+
+.appError.visible {
+ z-index: 2;
+ display: block;
+}
diff --git a/apps/system/style/themes/default/banner.css b/apps/system/style/themes/default/banner.css
new file mode 100644
index 0000000..1a23fa7
--- /dev/null
+++ b/apps/system/style/themes/default/banner.css
@@ -0,0 +1,36 @@
+/* ----------------------------------
+ * BANNER
+ * Requires:
+ menus-dialogs/core.css
+ * ---------------------------------- */
+
+body[role="application"] section[role="dialog"].banner {
+ top: auto;
+ height: 8rem;
+ padding: 0;
+ text-align: center;
+}
+
+body[role="application"] section[role="dialog"].banner:after {
+ content: "";
+ display: inline-block;
+ vertical-align: middle;
+ width: 1px;
+ height: 100%;
+}
+
+body[role="application"] section[role="dialog"].banner p {
+ display: inline-block;
+ vertical-align: middle;
+ white-space: normal;
+ font-size: 1.6rem;
+ line-height: 1.4em;
+ max-width: 75%;
+ margin: 0;
+}
+
+body[role="application"] section[role="dialog"].banner p strong {
+ text-transform: uppercase;
+ color: #0995b0;
+ font-weight: normal;
+}
diff --git a/apps/system/style/themes/default/buttons.css b/apps/system/style/themes/default/buttons.css
new file mode 100644
index 0000000..f285c25
--- /dev/null
+++ b/apps/system/style/themes/default/buttons.css
@@ -0,0 +1,232 @@
+/* ----------------------------------
+
+ * BUTTONS: DEFAULT
+ * ---------------------------------- */
+[role="dialog"] button::-moz-focus-inner {
+ border: none;
+ outline: none;
+}
+
+[role="dialog"] button,
+[role="dialog"] a[role="button"] {
+ width: 100%;
+ height: 3.8rem;
+ margin: 0 0 1rem;
+ padding: 0 1.5rem;
+ -moz-box-sizing: border-box;
+ display: inline-block;
+ vertical-align: middle;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ background: #fafafa url(images/ui/default.png) repeat-x left bottom;
+ border: 1px solid #9f9f9f;
+ border-radius: 0.3rem;
+ font-size: 1.6rem;
+ font-family: 'MozTT', Sans-serif;
+ font-weight: 600;
+ line-height: 3.8rem;
+ color: #333;
+ text-align: center;
+ text-shadow: 1px 1px 0 rgba(255,255,255,0.3);
+ text-decoration: none;
+ outline: none;
+}
+
+/* Press (default & affirmative) */
+[role="dialog"] button:active,
+[role="dialog"] a[role="button"]:active,
+[role="dialog"] button.affirmative:active,
+[role="dialog"] a.affirmative[role="button"]:active {
+ border-color: #008aaa;
+ background: #008aaa;
+ color: #333;
+}
+
+/* Affirmative */
+[role="dialog"] button.affirmative,
+[role="dialog"] a[role="button"].affirmative {
+ background-image: url(images/ui/affirmative.png);
+ background-color: #00caf2;
+ border-color: #00acce;
+}
+
+/* Negative */
+[role="dialog"] button.negative,
+[role="dialog"] a.negative[role="button"] {
+ background-image: url(images/ui/negative.png);
+ background-color: #b70404;
+ color: #fff;
+ text-shadow: -1px -1px 0 #830b0b;
+ border: none;
+}
+
+/* Negative Press */
+[role="dialog"] button.negative:active,
+[role="dialog"] a[role="button"].negative:active {
+ background-image: url(images/ui/negative-press.png);
+ background-color: #890707;
+}
+
+/* Disabled (default & affirmative) */
+[role="dialog"] button[disabled="disabled"],
+[role="dialog"] a[role="button"][aria-disabled="true"],
+[role="dialog"] button[disabled="disabled"].affirmative,
+[role="dialog"] a[role="button"][aria-disabled="true"].affirmative {
+ background-image: url(images/ui/disabled-bright.png);
+ background-color: transparent;
+ border-color: #dadada;
+ color: #bcbcbc;
+}
+
+/* Disabled dark (default & affirmative) */
+[role="dialog"] menu button[disabled="disabled"],
+[role="dialog"] menu a[role="button"][aria-disabled="true"],
+[role="dialog"] menu button[disabled="disabled"].affirmative,
+[role="dialog"] menu a[role="button"][aria-disabled="true"].affirmative {
+ background-image: url(images/ui/disabled-dark.png);
+ background-color: transparent;
+ border: none;
+ color: #4a4a4a;
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2);
+}
+
+/* Negative disabled */
+[role="dialog"] button[disabled="disabled"].negative,
+[role="dialog"] a[role="button"][aria-disabled="true"].negative {
+ background-image: url(images/ui/negative-disabled.png);
+ color: #fff;
+ text-shadow: none;
+}
+
+/* Icons in buttons */
+[role="dialog"] button span,
+[role="dialog"] a[role="button"] span {
+ float: left;
+ width: 2.5rem;
+ height: 2.5rem;
+ margin: 0 0.5rem 0 -1rem;
+ background: transparent no-repeat center center;
+}
+
+[role="dialog"] button span.end,
+[role="dialog"] a[role="button"] span.end {
+ float: right;
+ margin: 0.3rem -1.5rem 0 1rem;
+}
+
+/* Icon base types */
+[role="dialog"] button span.goto,
+[role="dialog"] a[role="button"] span.goto {
+ background-image: url(images/icons/goto.png);
+}
+
+[role="dialog"] button span.launch,
+[role="dialog"] a[role="button"] span.launch {
+ background-image: url(images/icons/launch.png);
+ background-position: 1rem bottom;
+}
+
+[role="dialog"] button span.favorite,
+[role="dialog"] a[role="button"] span.favorite {
+ background-image: url(images/icons/favorite.png);
+}
+
+[role="dialog"] button span.call,
+[role="dialog"] a[role="button"] span.call {
+ background-image: url(images/icons/call.png);
+ background-position: center bottom;
+}
+
+[role="dialog"] button span.tick,
+[role="dialog"] a[role="button"] span.tick {
+ background-image: url(images/icons/tick.png);
+}
+
+
+/* ----------------------------------
+ * BUTTONS: LIST
+ * ---------------------------------- */
+[role="dialog"] .buttons-list {
+ padding: 0;
+ margin: 0;
+ list-style: none;
+}
+
+[role="dialog"] .buttons-list label {
+ font: 1.4rem/1em "MozTT", Sans-serif;
+ text-transform: uppercase;
+ display: block;
+ padding-left: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+/* Default */
+[role="dialog"] .buttons-list button,
+[role="dialog"] .buttons-list a[role="button"] {
+ background: #e7e7e7;
+ border-color: #b6b6b6;
+ text-align: left;
+ font-size: 1.4rem;
+ line-height: 2.9rem;
+ position: relative;
+ overflow: visible;
+}
+
+[role="dialog"] .buttons-list a[role="button"]:after,
+[role="dialog"] .buttons-list button:after {
+ content: "";
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 100%;
+ background: url(images/ui/shadow.png) repeat-x left bottom;
+ height: 2px;
+ margin-top: 1px;
+ pointer-events: none;
+}
+
+/* Press */
+[role="dialog"] .buttons-list button:active,
+[role="dialog"] .buttons-list a[role="button"]:active {
+ border-color: #008aaa;
+ background: #008aaa;
+ color: #333;
+}
+
+[role="dialog"] .buttons-list a[role="button"]:active:after,
+[role="dialog"] .buttons-list button:active:after {
+ opacity: 0;
+}
+
+/* Disabled */
+[role="dialog"] .buttons-list button[disabled="disabled"],
+[role="dialog"] .buttons-list a[role="button"][aria-disabled="true"] {
+ background: #ededed;
+ border-color: #d3d3d3;
+ color: #a6a6a6;
+}
+
+[role="dialog"] .buttons-list button[disabled="disabled"]:after,
+[role="dialog"] .buttons-list a[role="button"][aria-disabled="true"]:after {
+ opacity: 1;
+}
+
+
+/* Compact mode */
+[role="dialog"] .buttons-list[data-mode="compact"] { margin-bottom: 1rem; }
+[role="dialog"] .buttons-list[data-mode="compact"] button,
+[role="dialog"] .buttons-list[data-mode="compact"] a[role="button"] {
+ margin: -1px 0;
+ border-radius: 0;
+}
+
+[role="dialog"] .buttons-list[data-mode="compact"] li:first-child button,
+[role="dialog"] .buttons-list[data-mode="compact"] li:first-child a[role="button"] {
+ border-radius: 0.3rem 0.3rem 0 0;
+}
+
+[role="dialog"] .buttons-list[data-mode="compact"] li:last-child button,
+[role="dialog"] .buttons-list[data-mode="compact"] li:last-child a[role="button"] {
+ border-radius:0 0 0.3rem 0.3rem;
+}
diff --git a/apps/system/style/themes/default/core.css b/apps/system/style/themes/default/core.css
new file mode 100644
index 0000000..c3a3170
--- /dev/null
+++ b/apps/system/style/themes/default/core.css
@@ -0,0 +1,101 @@
+/* ----------------------------------
+ * CORE STYLES FOR DIALOGS AND MENUS
+ * Is required for all the subcomponents (except banner)
+ * ---------------------------------- */
+
+[role="dialog"] {
+ background: url(images/ui/pattern.png) repeat left top, url(images/ui/gradient.png) no-repeat left top;
+ background-size: auto auto, 100% 100%;
+ overflow: hidden;
+ position: absolute;
+ z-index: 100;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ white-space: nowrap;
+ padding: 1.5rem 0 7rem;
+ font-family: "MozTT", Sans-serif;
+ color: #fff;
+}
+
+[role="dialog"]:before {
+ content: "";
+ display: inline-block;
+ vertical-align: middle;
+ width: 1px;
+ height: 100%;
+}
+
+[role="dialog"] .inner {
+ padding: 0 2.5rem 0 2rem;
+ -moz-box-sizing: padding-box;
+ width: 100%;
+ display: inline-block;
+ vertical-align: middle;
+ white-space: normal;
+}
+
+[role="dialog"] h3 {
+ font-family: 'MozTT', Sans-serif;
+ font-weight: normal;
+ font-size: 1.6rem;
+ line-height: 1em;
+ color: #fff;
+ border-bottom: 0.1rem solid #686868;
+ margin: 0 0 1rem;
+ padding-bottom: 1rem;
+}
+
+[role="dialog"] menu:not([type="toolbar"]) {
+ white-space: nowrap;
+ margin: 0;
+ padding: 1.5rem;
+ border-top: solid 1px rgba(255, 255, 255, 0.1);
+ background: #2d2d2d url(images/ui/pattern.png) repeat left top;
+ display: block;
+ overflow: hidden;
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+}
+
+[role="dialog"] menu:not([type="toolbar"]) button:last-child,
+[role="dialog"] menu:not([type="toolbar"]) a[role="button"]:last-child {
+ margin-left: 1rem;
+}
+
+[role="dialog"] menu:not([type="toolbar"]) button,
+[role="dialog"] menu:not([type="toolbar"]) a[role="button"],
+[role="dialog"] menu:not([type="toolbar"]) button:first-child,
+[role="dialog"] menu:not([type="toolbar"]) a[role="button"]:first-child {
+ margin: 0;
+}
+
+[role="dialog"] menu:not([type="toolbar"])[data-items="2"] button,
+[role="dialog"] menu:not([type="toolbar"])[data-items="2"] a[role="button"] {
+ width: -moz-calc((100% - 1rem) / 2);
+}
+
+
+/* ----------------------------------
+ * INLINE BANNER
+ * ---------------------------------- */
+
+[role="dialog"].inline {
+ position: relative;
+ margin: -0.4rem 0 0 0;
+ padding: 1rem 0 0 0;
+ background: rgba(0,0,0,0.83);
+}
+
+[role="dialog"].inline p {
+ border: 0;
+ padding: 1rem 1.5rem;
+}
+
+[role="dialog"].inline menu {
+ position: relative;
+ background: none;
+}
diff --git a/apps/system/style/themes/default/images/noise.png b/apps/system/style/themes/default/images/noise.png
new file mode 100644
index 0000000..5f5428f
--- /dev/null
+++ b/apps/system/style/themes/default/images/noise.png
Binary files differ
diff --git a/apps/system/style/themes/default/images/notifications.png b/apps/system/style/themes/default/images/notifications.png
new file mode 100644
index 0000000..f5e966d
--- /dev/null
+++ b/apps/system/style/themes/default/images/notifications.png
Binary files differ
diff --git a/apps/system/style/themes/default/images/notifications/close.png b/apps/system/style/themes/default/images/notifications/close.png
new file mode 100644
index 0000000..9604f1b
--- /dev/null
+++ b/apps/system/style/themes/default/images/notifications/close.png
Binary files differ
diff --git a/apps/system/style/themes/default/images/tasks/close-active.png b/apps/system/style/themes/default/images/tasks/close-active.png
new file mode 100644
index 0000000..b2666b8
--- /dev/null
+++ b/apps/system/style/themes/default/images/tasks/close-active.png
Binary files differ
diff --git a/apps/system/style/themes/default/images/tasks/close.png b/apps/system/style/themes/default/images/tasks/close.png
new file mode 100644
index 0000000..9604f1b
--- /dev/null
+++ b/apps/system/style/themes/default/images/tasks/close.png
Binary files differ
diff --git a/apps/system/style/themes/default/images/ui/affirmative.png b/apps/system/style/themes/default/images/ui/affirmative.png
new file mode 100644
index 0000000..42aed39
--- /dev/null
+++ b/apps/system/style/themes/default/images/ui/affirmative.png
Binary files differ
diff --git a/apps/system/style/themes/default/images/ui/default.png b/apps/system/style/themes/default/images/ui/default.png
new file mode 100644
index 0000000..2ff298a
--- /dev/null
+++ b/apps/system/style/themes/default/images/ui/default.png
Binary files differ
diff --git a/apps/system/style/themes/default/images/ui/gradient.png b/apps/system/style/themes/default/images/ui/gradient.png
new file mode 100644
index 0000000..9097844
--- /dev/null
+++ b/apps/system/style/themes/default/images/ui/gradient.png
Binary files differ
diff --git a/apps/system/style/themes/default/images/ui/pattern.png b/apps/system/style/themes/default/images/ui/pattern.png
new file mode 100644
index 0000000..4f7bc8b
--- /dev/null
+++ b/apps/system/style/themes/default/images/ui/pattern.png
Binary files differ
diff --git a/apps/system/style/themes/default/images/ui/time_pattern.png b/apps/system/style/themes/default/images/ui/time_pattern.png
new file mode 100644
index 0000000..c86eba0
--- /dev/null
+++ b/apps/system/style/themes/default/images/ui/time_pattern.png
Binary files differ
diff --git a/apps/system/style/themes/default/menus.css b/apps/system/style/themes/default/menus.css
new file mode 100644
index 0000000..5b36c71
--- /dev/null
+++ b/apps/system/style/themes/default/menus.css
@@ -0,0 +1,70 @@
+/* ----------------------------------
+ * ACTION / OBJECT MENU
+ * Requires:
+ menu-dialoges/core.css
+ * ---------------------------------- */
+
+[role="dialog"] menu.actions {
+ margin: 0;
+ padding: 0;
+ border: none;
+ background: none;
+ display: block;
+ overflow: hidden;
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+}
+
+[role="dialog"] menu.actions h3 {
+ display: block;
+ margin: 0 1.5rem;
+}
+
+[role="dialog"] menu.actions > ul {
+ list-style: none;
+ padding: 0;
+ margin: 0.5rem 0 0 0;
+ display: block;
+ overflow: hidden;
+}
+
+[role="dialog"] menu.actions > ul > li {
+ padding: 1rem 1.5rem 0 1.5rem;
+ margin: 0;
+ display: block;
+ overflow: hidden;
+ border: none;
+ height: auto;
+ line-height: normal;
+}
+
+[role="dialog"] menu.actions > ul > li:last-child {
+ border-top: solid 1px rgba(255, 255, 255, 0.1);
+ background: #2d2d2d url(images/ui/pattern.png) repeat left top;
+ margin-top: 1.5rem;
+}
+
+[role="dialog"] menu.actions > ul > li:not(:last-child) > button,
+[role="dialog"] menu.actions > ul > li:not(:last-child) > a[role="button"] {
+ font-size: 1.4rem;
+ color: #fff;
+ text-shadow: none;
+ text-align: left;
+ padding: 0 1rem;
+ background: #4E4E4E padding-box;
+ border: solid 1px rgba(0, 0, 0, 0.25);
+}
+
+[role="dialog"] menu.actions > ul > li:not(:last-child) > button:active,
+[role="dialog"] menu.actions > ul > li:not(:last-child) > a[role="button"]:active {
+ background-color: #006f86;
+ color: #333;
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);
+}
+
+[role="dialog"] menu.actions > ul > li:last-child > button,
+[role="dialog"] menu.actions > ul > li:last-child > a[role="button"] {
+ margin: 0.5rem 0 1rem 0;
+}
diff --git a/apps/system/style/themes/default/system.css b/apps/system/style/themes/default/system.css
new file mode 100644
index 0000000..27e59cf
--- /dev/null
+++ b/apps/system/style/themes/default/system.css
@@ -0,0 +1,27 @@
+/* Windows */
+
+div.windowSprite {
+ background: url("images/noise.png") repeat scroll 50% 50%, #373a3d;
+}
+
+/* Tasks Manager */
+
+#cardsView li {
+ background-color: #00f;
+}
+
+#cardsView li > a {
+ background: url('images/tasks/close.png') no-repeat;
+}
+
+#cardsView li > a:active {
+ background: url('images/tasks/close-active.png') no-repeat;
+}
+
+#cardsView li > h1 {
+ text-align: center;
+ color: #fff;
+ font-size: 2em;
+ text-shadow: #000 0 2px 1px;
+}
+
diff --git a/apps/system/style/trusted_ui/trusted_ui.css b/apps/system/style/trusted_ui/trusted_ui.css
new file mode 100644
index 0000000..5367fcc
--- /dev/null
+++ b/apps/system/style/trusted_ui/trusted_ui.css
@@ -0,0 +1,82 @@
+#trustedui-container {
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ position: absolute;
+ transition: transform ease 0.5s, opacity 0.5s;
+}
+
+#screen #trustedui-container.up {
+ opacity: 0;
+ transform: translateY(-100%);
+}
+
+#trustedui-inner {
+ position: absolute;
+ width: 86%;
+ height: 86%;
+ top: 7%;
+ left: 7%;
+ transition: transform ease 0.5s, opacity 0.5s;
+}
+
+#screen:not(.trustedui) #trustedui-inner {
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+}
+
+#screen.trustedui #trustedui-container {
+ opacity: 1;
+ visibility: visible;
+ transform: scale(1);
+}
+
+#screen.trustedui #trustedui-container.closing,
+#screen:not(.trustedui) #trustedui-container.closing {
+ opacity: 0;
+ transform: scale(0.6);
+}
+
+#trustedui-throbber {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 3px;
+ z-index: 1;
+}
+
+#trustedui-throbber.loading {
+ height: 4px;
+ background-image: url('../shared/progress.gif');
+}
+
+#trustedui-inner > .title-container {
+ height: 5rem;
+ width: 100%;
+ top: 0;
+ left: 0;
+ position: absolute;
+}
+
+#trustedui-inner > #trustedui-frame-container {
+ position: absolute;
+ top: 5rem;
+ left: 0;
+ width: 100%;
+ height: calc(100% - 5rem);
+ border: none;
+ background-color: #fff;
+}
+
+#trustedui-frame-container > iframe {
+ width: 100%;
+ height: 100%;
+ border: none;
+}
+
+#trustedui-inner iframe:not(.selected) {
+ display: none;
+}
diff --git a/apps/system/style/ttlview/ttlview.css b/apps/system/style/ttlview/ttlview.css
new file mode 100644
index 0000000..b10bde7
--- /dev/null
+++ b/apps/system/style/ttlview/ttlview.css
@@ -0,0 +1,16 @@
+#debug-ttl {
+ position: absolute;
+ top: 0;
+ left: 0;
+ display: block;
+ height: 20px;
+ background-color: darkgreen;
+ pointer-events: none;
+ font-size: 14px;
+ line-height: 20px;
+ text-align: right;
+ font-weight: bold;
+ overflow: hidden;
+ padding-left: 3px;
+ padding-right: 3px;
+}
diff --git a/apps/system/style/update_manager/images/grey-noise-bg.png b/apps/system/style/update_manager/images/grey-noise-bg.png
new file mode 100644
index 0000000..0f83b8f
--- /dev/null
+++ b/apps/system/style/update_manager/images/grey-noise-bg.png
Binary files differ
diff --git a/apps/system/style/update_manager/images/iconindicator_download_24x24.png b/apps/system/style/update_manager/images/iconindicator_download_24x24.png
new file mode 100644
index 0000000..536a43b
--- /dev/null
+++ b/apps/system/style/update_manager/images/iconindicator_download_24x24.png
Binary files differ
diff --git a/apps/system/style/update_manager/images/loader.png b/apps/system/style/update_manager/images/loader.png
new file mode 100644
index 0000000..0146695
--- /dev/null
+++ b/apps/system/style/update_manager/images/loader.png
Binary files differ
diff --git a/apps/system/style/update_manager/update_manager.css b/apps/system/style/update_manager/update_manager.css
new file mode 100644
index 0000000..39f37d6
--- /dev/null
+++ b/apps/system/style/update_manager/update_manager.css
@@ -0,0 +1,253 @@
+html, body {
+ font-family: 'MozTT', sans-serif;
+ font-size: 10px;
+}
+
+#update-manager-container {
+ display: none;
+}
+
+#update-manager-container.displayed {
+ display: block;
+}
+
+#update-manager-container > .activity {
+ display: none;
+ float: left;
+ width: 32px;
+ height: 30px;
+ margin: 14px 10px;
+ background-image: url(images/loader.png);
+ background-position: center center;
+ background-repeat: no-repeat;
+ animation: 0.9s fn-rotate infinite linear;
+}
+
+@keyframes fn-rotate {
+ from {
+ transform: rotate(1deg);
+ }
+
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+#update-manager-container.downloading > .activity {
+ display: block;
+}
+
+#update-manager-container > .icon {
+ float: left;
+ display: block;
+ width: 24px;
+ height: 24px;
+ margin: 18px 10px;
+}
+
+#update-manager-container > .icon,
+#update-manager-toaster > .icon {
+ background-image: url('images/iconindicator_download_24x24.png');
+}
+
+#update-manager-container.downloading > .icon {
+ display: none;
+}
+
+#update-manager-toaster > .icon {
+ float: left;
+ display: block;
+ width: 2.4rem;
+ height: 2.4rem;
+ margin: 1.3rem;
+}
+
+#update-manager-toaster > .message {
+ position: absolute;
+ left: 50px;
+ top: 10px;
+ width: -moz-calc(100% - 55px);
+ color: #52b8cc;
+ font-size: 1.5rem;
+ font-weight: 500;
+ line-height: 2.8rem;
+}
+
+#update-manager-container > .message {
+ margin: 10px 0 0 50px;
+ width: -moz-calc(100% - 55px);
+ font-size: 1.5rem;
+ font-weight: 500;
+ line-height: 1.9rem;
+ color: #FFFFFF;
+}
+
+#update-manager-container > .message > span {
+ display: block;
+ color: #bfbfbf;
+ font-size: 1.4rem;
+ line-height: 1.9rem;
+ font-weight: 400;
+ text-overflow: ellipsis;
+}
+
+#update-manager-toaster > .message > span {
+ display: none;
+}
+
+#update-manager-toaster {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 50px;
+ overflow: hidden;
+ background-image: url('images/grey-noise-bg.png');
+ background-repeat: repeat-x;
+ -moz-box-sizing: border-box;
+ border-bottom: 1px #2c2c2c solid;
+ -moz-transform: translateY(-50px);
+ -moz-transition: -moz-transform .3s ease-in-out;
+}
+
+#update-manager-toaster.displayed {
+ -moz-transform: translateY(0);
+}
+
+#updates-viaDataConnection-dialog,
+#updates-download-dialog {
+ padding-bottom: 4.9rem;
+ padding-top: 0;
+ display: none;
+}
+
+#updates-viaDataConnection-dialog.visible,
+#updates-download-dialog.visible {
+ display: inline-block;
+ pointer-events: auto;
+}
+
+#updates-viaDataConnection-dialog h1,
+#updates-download-dialog h1 {
+ font-size: 1.9rem;
+ color: #fff;
+ padding: 1.5rem 1rem 1.1rem 1rem;
+ background: rgba(255, 255, 255, 0.0);
+}
+
+#updates-viaDataConnection-dialog h1 {
+ padding-left: 2.8rem;
+ height: 2.4rem;
+ line-height: 2.4rem;
+ background: url('images/iconindicator_download_24x24.png') no-repeat scroll 0 50% transparent;
+ border: none;
+}
+
+#updates-viaDataConnection-dialog section,
+#updates-download-dialog section {
+ height: auto;
+ line-height: 3.5rem;
+ overflow-y: scroll;
+ max-height: -moz-calc(100% - 3.8rem);
+ color: #ebebeb;
+ font-size: 2.5rem;
+}
+
+#updates-download-dialog ul {
+ list-style: none;
+ margin-top: 0.4rem;
+ padding-left: 0;
+}
+
+#updates-download-dialog li {
+ height: 5rem;
+ margin: 0 1rem;
+ border-top: solid 1px rgba(255, 255, 255, 0.05);
+}
+
+#updates-download-dialog li:first-child {
+ margin-top: 0;
+ border-top: 0;
+}
+
+#updates-download-dialog div {
+ display: block;
+ font-weight: 500;
+ color: #ebebeb;
+ font-size: 1.4rem;
+ line-height: 0.6rem;
+}
+
+#updates-download-dialog li .name {
+ font-size: 1.5rem;
+ line-height: 3rem;
+ height: 3rem;
+ color: white;
+ font-weight: normal;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+#updates-download-dialog li.nosize .name {
+ line-height: 5rem;
+ height: 5rem;
+}
+
+#updates-download-dialog label {
+ display: inline-block;
+ height: 5rem;
+ float: right;
+ color: #ebebeb;
+ line-height: 5rem;
+ font-weight: 300;
+ font-size: 1.4rem;
+}
+
+#updates-download-dialog label.required {
+ width: 10rem;
+ right: 0.5rem;
+ text-align: right;
+ text-transform: uppercase;
+ height: 3rem;
+ line-height: 2.9rem;
+}
+
+#updates-download-dialog p {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 6.8rem;
+ height: 6.6rem;
+ padding: 0.5rem 1.8rem;
+ margin: 0;
+
+ background-color: rgba(0, 0, 0, 0.4);
+ border-top: solid 1px rgba(255, 255, 255, 0.2);
+ overflow-y: scroll;
+
+ font-size: 1.6rem;
+ white-space: normal;
+}
+
+#updates-download-dialog[data-nowifi="true"] section {
+ /* Fixing at the top of the screen to properly make room for the warning */
+ position: absolute;
+ top: 20px;
+ max-height: calc(100% - 15rem - 20px);
+}
+
+#updates-download-dialog #updates-download-button[data-online="false"] {
+ visibility: collapse;
+}
+
+#updates-download-dialog #updates-offline-warning,
+#updates-download-dialog #updates-data-connection-warning,
+#updates-download-dialog[data-online="false"] #updates-download-button {
+ visibility: collapse;
+}
+
+#updates-download-dialog[data-online="false"] #updates-offline-warning,
+#updates-download-dialog[data-online="true"][data-nowifi="true"][data-data-connection-inline-warning="true"] #updates-data-connection-warning {
+ visibility: visible;
+}
diff --git a/apps/system/style/utility_tray/images/grippy.png b/apps/system/style/utility_tray/images/grippy.png
new file mode 100644
index 0000000..c8db064
--- /dev/null
+++ b/apps/system/style/utility_tray/images/grippy.png
Binary files differ
diff --git a/apps/system/style/utility_tray/utility_tray.css b/apps/system/style/utility_tray/utility_tray.css
new file mode 100644
index 0000000..8d9754e
--- /dev/null
+++ b/apps/system/style/utility_tray/utility_tray.css
@@ -0,0 +1,29 @@
+#screen.lockscreen-camera > #utility-tray {
+ display: none;
+}
+
+#screen.utility-tray > #utility-tray {
+ visibility: visible;
+}
+
+#utility-tray {
+ position: absolute;
+ top: -moz-calc(-100% + 40px);
+ width: -moz-calc(100%);
+ height: -moz-calc(100% - 20px);
+ background-color: rgba(0, 0, 0, 0.7);
+ visibility: hidden;
+}
+
+#utility-tray-grippy {
+ pointer-events: none;
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ height: 20px;
+
+ border-bottom: 1px solid #111;
+ border-top: 1px solid #111;
+
+ background: #111 url('images/grippy.png') no-repeat 50% 50%;
+}
diff --git a/apps/system/style/value_selector/date_picker.css b/apps/system/style/value_selector/date_picker.css
new file mode 100644
index 0000000..b0bc654
--- /dev/null
+++ b/apps/system/style/value_selector/date_picker.css
@@ -0,0 +1,152 @@
+/* override section[role="dialog"] h3 */
+#date-picker h3 {
+ font-family: 'MozTT', Sans-serif;
+ font-weight: normal;
+ font-size: 1.6rem;
+ line-height: 1em;
+ color: #fff;
+ border-bottom: 0.1rem solid #686868;
+ margin: 0 2.5rem 1rem 2rem;
+ padding-bottom: 1rem;
+}
+
+/* specific component code for date picker */
+#date-picker {
+ height: 87.5%;
+ width: calc(100% + 5px);
+ padding: 0;
+ font-size: 2.2rem;
+ line-height: 1em;
+ margin-left: -5px;
+ display: inline-block;
+ vertical-align: middle;
+ white-space: normal;
+}
+
+#date-picker ol {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+#date-picker-controls > button {
+ float: none;
+ width: auto;
+ text-overflow: clip;
+ font: 0/0 a;
+ position: absolute;
+ top: 0.5rem;
+ z-index: 100;
+ width: 7.5rem;
+ height: 4.5rem;
+ border: none;
+ background: transparent no-repeat center center;
+}
+
+#date-picker-controls {
+ position: relative;
+ overflow: hidden;
+}
+
+#date-picker-controls h1 {
+ font-weight: 200;
+ font-size: 2.2rem;
+ line-height: 1em;
+ text-align: center;
+ margin: 1.5rem 0px 2rem;
+}
+
+#date-picker-controls button.next {
+ background-image: url(images/icons/next.png);
+ right: 0;
+}
+
+#date-picker-controls button.previous {
+ background-image: url(images/icons/prev.png);
+ left: 0;
+}
+
+#date-picker:after {
+ content: "";
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 13.4rem;
+ bottom: 0;
+ background: url(images/ui/shadow.png) repeat-y left top, url(images/ui/shadow-invert.png) repeat-y right top;
+ pointer-events: none;
+}
+
+#date-picker ol {
+ width: 100%;
+ overflow: hidden;
+ clear: both;
+}
+
+#date-picker li {
+ width: 14.285%;
+ text-align: center;
+ float: left;
+}
+
+#date-picker-weekdays li {
+ width: 14.285%;
+ font-size: 1.1rem;
+ line-height: 1em;
+ font-weight: normal;
+ text-transform: uppercase;
+ background: #333;
+ padding: 0.5rem 0.2rem;
+ border-top: solid 1px #000;
+}
+
+#date-picker-container {
+ border-bottom: 1px solid #000;
+}
+
+#date-picker-container li {
+ border: solid 1px #000;
+ border-bottom: none;
+ line-height: 4.5rem;
+ height: 4.5rem;
+}
+
+#date-picker-container .weeks-4 li {
+ line-height: 5.625rem;
+ height: 5.625rem;
+}
+
+#date-picker-container .weeks-6 li {
+ line-height: 3.75rem;
+ height: 3.75rem;
+}
+
+#date-picker-container li > span {
+ display: block;
+ font-weight: 200;
+ text-align: center;
+ transition: background 0.2s ease;
+ border: solid 1px #555;
+ border-left: none;
+ border-bottom: none;
+}
+
+#date-picker-container li.past span {
+ color: #717171;
+}
+
+#date-picker-container li.present {
+ background: url(images/ui/pattern-current.png) repeat left top;
+}
+
+#date-picker-container span.selected {
+ background: #00A5C5;
+}
+
+#date-picker-container ol li:last-child {
+ border-right: none;
+}
+
+#date-picker * {
+ -moz-box-sizing: border-box;
+}
diff --git a/apps/system/style/value_selector/images/icons/next.png b/apps/system/style/value_selector/images/icons/next.png
new file mode 100644
index 0000000..82e1515
--- /dev/null
+++ b/apps/system/style/value_selector/images/icons/next.png
Binary files differ
diff --git a/apps/system/style/value_selector/images/icons/prev.png b/apps/system/style/value_selector/images/icons/prev.png
new file mode 100644
index 0000000..387fce9
--- /dev/null
+++ b/apps/system/style/value_selector/images/icons/prev.png
Binary files differ
diff --git a/apps/system/style/value_selector/images/ui/pattern-current.png b/apps/system/style/value_selector/images/ui/pattern-current.png
new file mode 100644
index 0000000..426d7b2
--- /dev/null
+++ b/apps/system/style/value_selector/images/ui/pattern-current.png
Binary files differ
diff --git a/apps/system/style/value_selector/images/ui/shadow-invert.png b/apps/system/style/value_selector/images/ui/shadow-invert.png
new file mode 100644
index 0000000..68b2d5b
--- /dev/null
+++ b/apps/system/style/value_selector/images/ui/shadow-invert.png
Binary files differ
diff --git a/apps/system/style/value_selector/images/ui/shadow.png b/apps/system/style/value_selector/images/ui/shadow.png
new file mode 100644
index 0000000..41f9209
--- /dev/null
+++ b/apps/system/style/value_selector/images/ui/shadow.png
Binary files differ
diff --git a/apps/system/style/value_selector/spin_date_picker.css b/apps/system/style/value_selector/spin_date_picker.css
new file mode 100644
index 0000000..722d5fc
--- /dev/null
+++ b/apps/system/style/value_selector/spin_date_picker.css
@@ -0,0 +1,249 @@
+/* ----------- SpinDateSelectorView ---------- */
+#spin-date-picker .picker-bar {
+ position: relative;
+ margin-left: auto;
+ margin-right: auto;
+ overflow: hidden;
+ width: 85%;
+ font-size: 8px;
+}
+
+#spin-date-picker .picker-bar-gradient {
+ -moz-border-top-colors:
+ rgba(0, 0, 0, 0.5) rgba(0, 0, 0, 0.4) rgba(0, 0, 0, 0.3)
+ rgba(0, 0, 0, 0.2) rgba(0, 0, 0, 0.15) rgba(0, 0, 0, 0.1)
+ rgba(0, 0, 0, 0.05);
+
+ -moz-border-bottom-colors:
+ rgba(0, 0, 0, 0.5) rgba(0, 0, 0, 0.4) rgba(0, 0, 0, 0.3)
+ rgba(0, 0, 0, 0.2) rgba(0, 0, 0, 0.15) rgba(0, 0, 0, 0.1)
+ rgba(0, 0, 0, 0.05);
+
+ border-top: solid 0.7rem;
+ border-bottom: solid 0.7rem;
+ border-left: 1px solid #111;
+ border-right: 1px solid #111;
+
+ position: absolute;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ -moz-box-sizing: border-box;
+}
+
+#spin-date-picker .picker-container {
+ width: 100%;
+ position: relative;
+ padding-top: 8.5rem;
+ height: 16rem;
+ overflow: hidden;
+}
+
+#spin-date-picker .value-indicator-wrapper {
+ -moz-border-top-colors:
+ rgba(0, 0, 0, 0.05) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.15)
+ rgba(0, 0, 0, 0.2) rgba(0, 0, 0, 0.3) rgba(0, 0, 0, 0.4)
+ rgba(0, 0, 0, 0.5);
+
+ -moz-border-bottom-colors:
+ rgba(0, 0, 0, 0.05) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.15)
+ rgba(0, 0, 0, 0.2) rgba(0, 0, 0, 0.3) rgba(0, 0, 0, 0.4)
+ rgba(0, 0, 0, 0.5);
+
+ border-top: solid 0.7rem;
+ border-bottom: solid 0.7rem;
+ position: absolute;
+ height: 6rem;
+ width: 100%;
+ pointer-events: none;
+}
+
+#spin-date-picker .value-indicator-hover {
+ pointer-events: none;
+ background-color: #00A5C5;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ right:0;
+}
+
+div.animation-on {
+ -moz-transition-duration: 0.5s;
+ -moz-transition-property: top, left;
+}
+
+.picker-unit {
+ position: relative;
+ font: 2.5rem/8rem 'MozTT',sans-serif;
+ color: #FFF;
+ text-align: center;
+ vertical-align: middle;
+ width: 100%;
+ height: 6rem;
+ pointer-events: none;
+}
+
+#spin-date-picker .picker-bar-background {
+ background: url("../themes/default/images/ui/time_pattern.png") repeat scroll left top transparent;
+ position: absolute;
+ width: 73px;
+ height: 100%;
+ right: 0;
+ top: 0;
+}
+
+#spin-date-picker .left-picker-separator {
+ position: absolute;
+ width: 0.2rem;
+ height: 100%;
+ top: 0;
+ left: 54px;
+ background: -moz-linear-gradient(center left, #555 50%, #000 50%);
+ pointer-events: none;
+}
+
+#spin-date-picker .right-picker-separator {
+ position: absolute;
+ width: 0.2rem;
+ height: 100%;
+ top: 0;
+ right: 73px;
+ background: -moz-linear-gradient(center left, #555 50%, #000 50%);
+ pointer-events: none;
+}
+
+#spin-date-picker .value-picker-date-wrapper {
+ position: absolute;
+ width: 54px;
+ height: 100%;
+ top: 10em;
+ left: 0;
+}
+
+#spin-date-picker .value-picker-month-wrapper {
+ position: absolute;
+ width: -moz-calc(100% - 127px);
+ height: 100%;
+ top: 10em;
+ left: 54px;
+}
+
+#spin-date-picker .value-picker-year-wrapper {
+ position: absolute;
+ width: 73px;
+ height: 100%;
+ top: 10em;
+ right: 0px;
+}
+
+#spin-date-picker .value-picker-date {
+ position: relative;
+ -moz-user-select: none;
+ text-align: center;
+ width: 100%;
+}
+
+#spin-date-picker .value-picker-month {
+ position: relative;
+ -moz-user-select: none;
+ width: 100%;
+}
+
+#spin-date-picker .value-picker-year {
+ position: relative;
+ -moz-user-select: none;
+ width: 100%;
+}
+
+#spin-date-picker .picker-container.YMD .value-picker-year-wrapper {
+ right: auto;
+ left: 0;
+}
+
+#spin-date-picker .picker-container.YMD .value-picker-month-wrapper {
+ right: auto;
+ left: 73px;
+}
+
+#spin-date-picker .picker-container.YMD .value-picker-date-wrapper {
+ right: 0;
+ left: auto;
+}
+
+#spin-date-picker .picker-container.YMD .left-picker-separator {
+ right: auto;
+ left: 73px;
+}
+
+#spin-date-picker .picker-container.YMD .right-picker-separator {
+ right: 54px;
+ left: auto;
+}
+
+#spin-date-picker .picker-container.YMD .picker-bar-background {
+ right: auto;
+ left: 0px;
+}
+
+
+#spin-date-picker .picker-container.DMY .value-picker-year-wrapper {
+ right: 0;
+ left: auto;
+}
+
+#spin-date-picker .picker-container.DMY .value-picker-month-wrapper {
+ right: auto;
+ left: 54px;
+}
+
+#spin-date-picker .picker-container.DMY .value-picker-date-wrapper {
+ right: auto;
+ left: 0;
+}
+
+#spin-date-picker .picker-container.DMY .left-picker-separator {
+ right: auto;
+ left: 54px;
+}
+
+#spin-date-picker .picker-container.DMY .right-picker-separator {
+ right: 73px;
+ left: auto;
+}
+
+#spin-date-picker .picker-container.DMY .picker-bar-background {
+ right: 0px;
+ left: auto;
+}
+
+
+#spin-date-picker .picker-container.MDY .value-picker-year-wrapper {
+ right: 0;
+ left: auto;
+}
+
+#spin-date-picker .picker-container.MDY .value-picker-month-wrapper {
+ right: auto;
+ left: 0px;
+}
+
+#spin-date-picker .picker-container.MDY .value-picker-date-wrapper {
+ right: auto;
+ left: -moz-calc(100% - 127px);
+}
+
+#spin-date-picker .picker-container.MDY .left-picker-separator {
+ right: auto;
+ left: -moz-calc(100% - 127px);
+}
+
+#spin-date-picker .picker-container.MDY .right-picker-separator {
+ right: 73px;
+ left: auto;
+}
+
+#spin-date-picker .picker-container.MDY .picker-bar-background {
+ right: 0px;
+ left: auto;
+} \ No newline at end of file
diff --git a/apps/system/style/value_selector/time_picker.css b/apps/system/style/value_selector/time_picker.css
new file mode 100644
index 0000000..ea612ba
--- /dev/null
+++ b/apps/system/style/value_selector/time_picker.css
@@ -0,0 +1,212 @@
+/* ----------- TimeSelectorView ---------- */
+#picker-bar {
+ position: relative;
+ margin-left: auto;
+ margin-right: auto;
+ overflow: hidden;
+}
+
+#picker-bar.format12h {
+ width: 85%;
+}
+
+#picker-bar.format24h {
+ width: 60%;
+}
+
+#picker-bar-gradient {
+ -moz-border-top-colors:
+ rgba(0, 0, 0, 0.5) rgba(0, 0, 0, 0.4) rgba(0, 0, 0, 0.3)
+ rgba(0, 0, 0, 0.2) rgba(0, 0, 0, 0.15) rgba(0, 0, 0, 0.1)
+ rgba(0, 0, 0, 0.05);
+
+ -moz-border-bottom-colors:
+ rgba(0, 0, 0, 0.5) rgba(0, 0, 0, 0.4) rgba(0, 0, 0, 0.3)
+ rgba(0, 0, 0, 0.2) rgba(0, 0, 0, 0.15) rgba(0, 0, 0, 0.1)
+ rgba(0, 0, 0, 0.05);
+
+ border-top: solid 0.7rem;
+ border-bottom: solid 0.7rem;
+ border-left: 1px solid #111;
+ border-right: 1px solid #111;
+
+ position: absolute;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ -moz-box-sizing: border-box;
+}
+
+#picker-container {
+ width: 100%;
+ position: relative;
+ padding-top: 8.5rem;
+ height: 16rem;
+ overflow: hidden;
+}
+
+#value-indicator-wrapper {
+ -moz-border-top-colors:
+ rgba(0, 0, 0, 0.05) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.15)
+ rgba(0, 0, 0, 0.2) rgba(0, 0, 0, 0.3) rgba(0, 0, 0, 0.4)
+ rgba(0, 0, 0, 0.5);
+
+ -moz-border-bottom-colors:
+ rgba(0, 0, 0, 0.05) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.15)
+ rgba(0, 0, 0, 0.2) rgba(0, 0, 0, 0.3) rgba(0, 0, 0, 0.4)
+ rgba(0, 0, 0, 0.5);
+
+ border-top: solid 0.7rem;
+ border-bottom: solid 0.7rem;
+ position: absolute;
+ height: 6rem;
+ width: 100%;
+ pointer-events: none;
+}
+
+#value-indicator-hover-time {
+ pointer-events: none;
+ background-color: #00A5C5;
+ position: absolute;
+ height: 100%;
+ font: 3rem/6rem 'MozTT',sans-serif;
+ text-align: center;
+}
+
+#picker-bar.format12h #value-indicator-hover-time {
+ width: 66%;
+}
+
+#picker-bar.format24h #value-indicator-hover-time {
+ width: 100%;
+}
+
+#value-indicator-hover {
+ pointer-events: none;
+ background-color: #00A5C5;
+ position: absolute;
+ width: 33%;
+ height: 100%;
+ right:0;
+}
+
+#picker-bar.format12h #value-indicator-hover {
+ display: block;
+}
+
+#picker-bar.format24h #value-indicator-hover {
+ display: none;
+}
+
+div.animation-on {
+ -moz-transition-duration: 0.5s;
+ -moz-transition-property: top, left;
+}
+
+#value-picker-hours {
+ position: relative;
+ -moz-user-select: none;
+ float: left;
+ text-align: center;
+}
+
+#picker-bar.format12h #value-picker-hours {
+ width: 33%;
+}
+
+#picker-bar.format24h #value-picker-hours {
+ width: 50%;
+}
+
+#value-picker-minutes {
+ position: relative;
+ -moz-user-select: none;
+ float: left;
+}
+
+#picker-bar.format12h #value-picker-minutes {
+ width: 34%;
+}
+
+#picker-bar.format24h #value-picker-minutes {
+ width: 50%;
+}
+
+#value-picker-hour24-state {
+ position: relative;
+ -moz-user-select: none;
+ width: 33%;
+ float: left;
+}
+
+#picker-bar.format12h #value-picker-hour24-state {
+ display: block;
+}
+
+#picker-bar.format24h #value-picker-hour24-state {
+ display: none;
+}
+
+.picker-unit {
+ position: relative;
+ font: 3rem/8rem 'MozTT',sans-serif;
+ color: #FFF;
+ text-align: center;
+ vertical-align: middle;
+ width: 100%;
+ height: 6rem;
+ pointer-events: none;
+}
+
+#picker-bar-background {
+ background: url("../themes/default/images/ui/time_pattern.png") repeat scroll left top transparent;
+ position: absolute;
+ width: 33%;
+ height: 100%;
+ right: 0;
+ top: 0;
+}
+
+#picker-bar.format12h #picker-bar-background {
+ display: block;
+}
+
+#picker-bar.format24h #picker-bar-background {
+ display: none;
+}
+
+#left-picker-separator {
+ position: absolute;
+ width: 0.2rem;
+ height: 100%;
+ top: 0;
+ background: -moz-linear-gradient(center left, #555 50%, #000 50%);
+ pointer-events: none;
+}
+
+#picker-bar.format12h #left-picker-separator {
+ left: 33%;
+}
+
+#picker-bar.format24h #left-picker-separator {
+ left: 49.5%;
+}
+
+#right-picker-separator {
+ position: absolute;
+ width: 0.2rem;
+ height: 100%;
+ top: 0;
+ left: 66%;
+ background: -moz-linear-gradient(center left, #555 50%, #000 50%);
+ pointer-events: none;
+}
+
+#picker-bar.format12h #right-picker-separator {
+ display: block;
+}
+
+#picker-bar.format24h #right-picker-separator {
+ display: none;
+}
diff --git a/apps/system/style/value_selector/value_selector.css b/apps/system/style/value_selector/value_selector.css
new file mode 100644
index 0000000..7184046
--- /dev/null
+++ b/apps/system/style/value_selector/value_selector.css
@@ -0,0 +1,170 @@
+#value-selector {
+ width: 100%;
+ height: -moz-calc(100% - 20px);
+ top: 20px;
+ left: 0;
+ position: absolute;
+}
+
+#screen.active-statusbar #value-selector {
+ height: -moz-calc(100% - 40px);
+ top: 40px;
+}
+
+#screen:-moz-full-screen-ancestor #value-seletcor,
+#screen.fullscreen-app #value-selector {
+ height: 100%;
+ top: 0;
+}
+
+#select-options-buttons,
+#time-picker-buttons,
+#spin-date-picker-buttons {
+ /* from building blocks, core.css - menu:not([type="toolbar"]*/
+ white-space: nowrap;
+ margin: 0;
+ padding: 0;
+ border-top: solid 1px rgba(255, 255, 255, 0.1);
+ background: #2d2d2d url(../themes/default/images/ui/pattern.png) repeat left top;
+ display: block;
+ overflow: hidden;
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ text-align: center;
+ font-size: 0; /* to get rid of the spacing between 2 buttons */
+}
+
+#value-selector button:first-child {
+ margin-right: 0rem;
+}
+
+#select-options-buttons button,
+#time-picker-buttons button,
+#spin-date-picker-buttons button{
+ /* from building block - other/buttons/style.css*/
+ width: calc((100% - 4.5rem) / 2); /*overridden for 2 buttons*/
+
+ height: 3.8rem;
+ margin: 1.5rem;
+ padding: 0 1.5rem;
+ display: inline-block;
+ vertical-align: middle;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ background: #fafafa url(../themes/default/images/ui/default.png) repeat-x left bottom;
+ border: 1px solid #9f9f9f;
+ border-radius: 0.3rem;
+ font-size: 1.6rem;
+ font-family: 'MozTT', Sans-serif;
+ font-weight: 600;
+ line-height: 3.8rem;
+ color: #333;
+ text-align: center;
+ text-shadow: 1px 1px 0 rgba(255,255,255,0.3);
+ text-decoration: none;
+ outline: none;
+}
+
+/* Affirmative */
+#value-selector button.affirmative {
+ background-image: url(../themes/default/images/ui/affirmative.png);
+ background-color: #00caf2;
+ border-color: #00acce;
+}
+
+#select-options-buttons button.full {
+ width: calc(100% - 4.5rem);
+ margin: 1.5rem;
+}
+
+/*active class used to complement the "return false" in onmousedown effect */
+#value-selector button.active,
+button.affirmative.active {
+ border-color: #008aaa;
+ background: #008aaa;
+ color: #333;
+}
+
+#value-selector-container li > label {
+ pointer-events: none;
+}
+
+#value-selector-container li.active label span {
+ background: none repeat scroll 0 0 #00ABCC;
+ color: #FFFFFF !important;
+}
+
+#time-picker-popup,
+#spin-date-picker-popup {
+ top: 0px;
+ padding: 0 0 7rem; /*remove the top padding*/
+
+ /* from building blocks, core.css - [role=dialog]*/
+ background: url(../themes/default/images/ui/pattern.png) repeat left top, url(../themes/default/images/ui/gradient.png) no-repeat left top;
+ background-size: auto auto, 100% 100%;
+ overflow: hidden;
+ position: absolute;
+ z-index: 100;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ white-space: nowrap;
+ padding: 0 0 7rem; /*remove the top padding*/
+ font-family: "MozTT", Sans-serif;
+ color: #fff;
+}
+
+#time-picker-popup h3,
+#spin-date-picker-popup h3 {
+ font-family: 'MozTT', Sans-serif;
+ font-weight: normal;
+ font-size: 1.6rem;
+ line-height: 1em;
+ color: #fff;
+ border-bottom: 0.1rem solid #686868;
+ margin: 0 0 1.5rem;
+ padding-bottom: 1.5rem;
+}
+
+.table-wrapper {
+ display: table;
+ width: 100%;
+ height: 100%;
+}
+
+.table-cell {
+ display: table-cell;
+ vertical-align: middle;
+ width: 100%;
+ height: 100%;
+}
+
+/* The following rules is to override the styles defined in building blocks of value selector */
+#value-selector li {
+ height: auto;
+ padding-bottom: 0px;
+ line-height: 3.9rem;
+}
+
+#value-selector li span {
+ padding: 1rem 1.5rem;
+ line-height: 4rem;
+ word-wrap: break-word;
+}
+
+#value-selector li input:checked + span,
+#value-selector li[aria-checked="true"] span {
+ padding-right: 2.6rem;
+ margin-right: 1.2rem;
+ background: transparent url("../bb/value_selector/images/icons/checked.png") no-repeat 100% 50%;
+}
+
+html[dir="rtl"] #value-selector li input:checked + span,
+html[dir="rtl"] #value-selector li[aria-checked="true"] span {
+ padding-left: 2.6rem;
+ margin-left: 1.2rem;
+}
+
diff --git a/apps/system/style/wrapper/images/back.png b/apps/system/style/wrapper/images/back.png
new file mode 100644
index 0000000..74a12ac
--- /dev/null
+++ b/apps/system/style/wrapper/images/back.png
Binary files differ
diff --git a/apps/system/style/wrapper/images/bookmark.png b/apps/system/style/wrapper/images/bookmark.png
new file mode 100644
index 0000000..77b18a4
--- /dev/null
+++ b/apps/system/style/wrapper/images/bookmark.png
Binary files differ
diff --git a/apps/system/style/wrapper/images/close.png b/apps/system/style/wrapper/images/close.png
new file mode 100644
index 0000000..3dd988e
--- /dev/null
+++ b/apps/system/style/wrapper/images/close.png
Binary files differ
diff --git a/apps/system/style/wrapper/images/forward.png b/apps/system/style/wrapper/images/forward.png
new file mode 100644
index 0000000..6aac3b4
--- /dev/null
+++ b/apps/system/style/wrapper/images/forward.png
Binary files differ
diff --git a/apps/system/style/wrapper/images/open.png b/apps/system/style/wrapper/images/open.png
new file mode 100644
index 0000000..bf2c1c5
--- /dev/null
+++ b/apps/system/style/wrapper/images/open.png
Binary files differ
diff --git a/apps/system/style/wrapper/images/reload.png b/apps/system/style/wrapper/images/reload.png
new file mode 100644
index 0000000..0b72b3d
--- /dev/null
+++ b/apps/system/style/wrapper/images/reload.png
Binary files differ
diff --git a/apps/system/style/wrapper/wrapper.css b/apps/system/style/wrapper/wrapper.css
new file mode 100644
index 0000000..8fe0c87
--- /dev/null
+++ b/apps/system/style/wrapper/wrapper.css
@@ -0,0 +1,113 @@
+
+#wrapper-activity-indicator {
+ position: fixed;
+ top: 20px; /* This is the status bar height */
+ left: 0;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 4px;
+ background-image: url('../shared/progress.gif');
+
+ pointer-events: none;
+ visibility: hidden;
+}
+
+#wrapper-activity-indicator.visible {
+ -moz-transition-delay: .5s; /* Delay opening a wrapper */
+ -moz-transition-property: visibility;
+ visibility: visible;
+}
+
+#wrapper-footer {
+ position: fixed;
+ bottom: -4rem;
+ left: 0;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 5rem;
+
+ pointer-events: none;
+ visibility: hidden;
+}
+
+#wrapper-footer.visible {
+ pointer-events: auto;
+ visibility: visible;
+}
+
+#handler {
+ width: 100%;
+ height: 1rem;
+ background-color: #000;
+ background-repeat: no-repeat;
+ background-position: right;
+}
+
+#wrapper-footer.closed #handler {
+ background-image: url("images/open.png");
+}
+
+#wrapper-footer menu[type="buttonbar"] {
+ width: 100%;
+ height: 4rem;
+ margin: 0;
+ padding: 0;
+ background-color: rgba(0,0,0,.8);
+ -moz-transform: translateY(-4rem);
+ -moz-transition: -moz-transform .4s ease;
+}
+
+#wrapper-footer.closed menu {
+ -moz-transform: translateY(0);
+}
+
+#wrapper-footer menu button {
+ background-color: transparent;
+ -moz-appearance: none;
+ border: none;
+ display: block;
+ margin: 0;
+ padding: 0;
+ width: 20%;
+ height: 100%;
+ float: left;
+ background-position: center;
+ background-repeat: no-repeat;
+ opacity: 1;
+}
+
+#back-button {
+ background-image: url("images/back.png");
+}
+
+#forward-button {
+ background-image: url("images/forward.png");
+}
+
+#reload-button {
+ background-image: url("images/reload.png");
+}
+
+#bookmark-button {
+ background-image: url("images/bookmark.png");
+}
+
+#bookmark-button[data-disabled] {
+ background-image: none;
+}
+
+#close-button {
+ background-image: url("images/close.png");
+}
+
+#wrapper-footer menu button[data-disabled] {
+ opacity: .2;
+ pointer-events: none;
+}
+
+#wrapper-footer input::-moz-focus-inner, input:active {
+ border: 0;
+ background: transparent !important;
+}
diff --git a/apps/system/style/zindex.css b/apps/system/style/zindex.css
new file mode 100644
index 0000000..6abc373
--- /dev/null
+++ b/apps/system/style/zindex.css
@@ -0,0 +1,215 @@
+/* zIndex of important system app parts
+ * is gathered here to clearly specify all system-app hierarchy */
+
+/* Reset zIndex */
+#screen > *:not([data-z-index-level]) {
+ z-index: 0;
+}
+
+/* Level 1 */
+/**
+ * Topest layer, covers all of the screen no matter what's the height
+ * of the status bar.
+ * System overlay > Sleep menu > Card view
+ */
+
+/* Find the same code in system/index.html
+#screen > [data-z-index-level="initlogo"] {
+ z-index: 65536;
+}
+*/
+
+#screen > *[data-z-index-level="poweroff-splash"] {
+ z-index: 65536;
+}
+
+#screen > [data-z-index-level="debug-grid"],
+#screen > [data-z-index-level="debug-ttl"] {
+ z-index: 65536;
+}
+
+#screen > [data-z-index-level="system-notification-banner"] {
+ z-index: 65536;
+}
+
+#screen > [data-z-index-level="system-overlay"] {
+ z-index: 65536;
+}
+
+#screen > [data-z-index-level="sleep-menu"] {
+ z-index: 65536;
+}
+
+/* Promote the transitioning appWindow to this level as the entry and exiting transition
+ of the cards view. */
+#screen.cards-view > [data-z-index-level="app"] > .appWindow.active,
+#screen.cards-view > [data-z-index-level="app"] > .appWindow.opening,
+#screen.cards-view > [data-z-index-level="app"] > .appWindow.closing {
+ z-index: 65536;
+}
+#screen.cards-view > [data-z-index-level="cards-view"] {
+ z-index: 65535;
+}
+
+#screen > [data-z-index-level="keyboard-frame"] {
+ z-index: 65536;
+}
+
+/* Level 2: Notification toaster */
+#screen > [data-z-index-level="notification-toaster"] {
+ z-index: 32768;
+}
+
+/* Level 3: Attention screen */
+#screen > [data-z-index-level="attention-screen"] {
+ z-index: 16384;
+}
+
+/* Level 4: Activity menu, context menu and value selector */
+#screen > [data-z-index-level="list-menu"] {
+ z-index: 8192;
+}
+
+/* Level 5: Statusbar, Utility-Tray */
+#screen > [data-z-index-level="statusbar"] {
+ z-index: 4096;
+}
+
+#screen > [data-z-index-level="utility-tray"] {
+ z-index: 4095;
+}
+
+/* Demote level 5 elements to homescreen level if there is an active
+ full screen app frame */
+#screen.fullscreen-app:not(.locked):not(.attention) > [data-z-index-level="statusbar"],
+#screen.fullscreen-app:not(.locked):not(.attention) > [data-z-index-level="utility-tray"] {
+ z-index: 0;
+}
+
+/* Prompote level 5 elements to attention screen level when
+ * there is an active attention screen not minimized in
+ * the status bar */
+#screen.attention:not(.active-statusbar) > [data-z-index-level="statusbar"] {
+ z-index: 16386;
+}
+#screen.attention:not(.active-statusbar) > [data-z-index-level="utility-tray"] {
+ z-index: 16385;
+}
+
+/* Level 6: Lock Screen */
+#screen > [data-z-index-level="lockscreen"] {
+ z-index: 2048;
+}
+
+#screen > [data-z-index-level="lockscreen-camera"] {
+ z-index: 2047;
+}
+
+#screen > [data-z-index-level="simpin-dialog"] {
+ z-index: 2046;
+}
+
+/* Keep keyboard under lockscreen when locked */
+#screen.locked > [data-z-index-level="keyboard-frame"] {
+ z-index: 2045;
+}
+
+/* Level 7: Dialog Module */
+#screen > [data-z-index-level="dialog-overlay"],
+#screen > [data-z-index-level="value-selector"],
+#screen > [data-z-index-level="permission-screen"],
+#screen > [data-z-index-level="app-install-dialog"],
+#screen > [data-z-index-level="updates-download-dialog"],
+#screen > [data-z-index-level="updates-viaDataConnection-dialog"],
+#screen > [data-z-index-level="app"] > .appWindow.inlineActivity {
+ z-index: 1024;
+}
+
+/* Level 8 */
+#screen.switch-app [data-z-index-level="app"] > .appWindow.opening,
+#screen.switch-app [data-z-index-level="app"] > .appWindow.closing,
+#screen.switch-app [data-z-index-level="app"] > .appWindow:not(.homescreen).active {
+ z-index: 6;
+}
+
+#screen > [data-z-index-level="app"] {
+ /**
+ * Should not specify z-index here
+ * Keyboard should be kept upon #windows
+ * and beneath #windows > .appWindow
+ */
+}
+
+/*
+ Styles during FTU. In this case lockscreen & homescreen should be behind FTU.
+ Once FTU is done, we will remove this styles in order to get the structure
+ of z-index as usual.
+*/
+
+#screen.ftu > [data-z-index-level="statusbar"]{
+ z-index: 1;
+}
+
+#screen.ftu > [data-z-index-level="lockscreen"]{
+ z-index: 1;
+}
+
+#screen.ftu > [data-z-index-level="app"] > .appWindow.homescreen,
+#screen.ftu > [data-z-index-level="app"] > .appWindow.homescreen.active {
+ z-index: 0;
+}
+
+/*
+ * The below z-index numbers is used to met the following conditions:
+ * - active ones must be on top of the inactive ones
+ * - keyboard frame must be below the active ones
+ * - keyboard frame must be above the inactive ones
+ * - app frame must be on top of homescreen frame, inactive or active.
+ * - finally, everything else need to be on top of them (hence the lowest nums)
+ *
+ */
+#screen > [data-z-index-level="app"] > #wrapper-footer,
+#screen > [data-z-index-level="app"] > #wrapper-activity-indicator {
+ z-index: 5;
+}
+
+#screen > [data-z-index-level="app"] > .appWindow.active,
+#screen > [data-z-index-level="app"] > .appWindow.opening,
+#screen > [data-z-index-level="app"] > .appWindow.closing {
+ z-index: 4;
+}
+
+#screen > [data-z-index-level="app"] > .appWindow.homescreen.active {
+ z-index: 3;
+}
+
+#screen > [data-z-index-level="app"] > .appWindow {
+ z-index: 1;
+}
+
+#screen > [data-z-index-level="app"] > .appWindow.homescreen {
+ z-index: 0;
+}
+
+/* We promotes the following overlays on top of the fullscreen web content. */
+
+#screen:-moz-full-screen-ancestor > [data-z-index-level="app"] > .appWindow.inlineActivity,
+#screen:-moz-full-screen-ancestor > [data-z-index-level="sleep-menu"],
+#screen:-moz-full-screen-ancestor > [data-z-index-level="list-menu"],
+#screen.locked:-moz-full-screen-ancestor > [data-z-index-level="statusbar"],
+#screen:-moz-full-screen-ancestor > [data-z-index-level="lockscreen"],
+#screen:-moz-full-screen-ancestor > [data-z-index-level="lockscreen-camera"],
+
+#screen:-moz-full-screen-ancestor > [data-z-index-level="value-selector"],
+#screen:-moz-full-screen-ancestor > [data-z-index-level="system-overlay"],
+#screen:-moz-full-screen-ancestor > [data-z-index-level="dialog-overlay"],
+
+#screen:-moz-full-screen-ancestor > [data-z-index-level="keyboard-frame"],
+
+#screen:-moz-full-screen-ancestor > [data-z-index-level="notification-toaster"],
+#screen:-moz-full-screen-ancestor > [data-z-index-level="cards-view"],
+#screen:-moz-full-screen-ancestor > [data-z-index-level="attention-screen"],
+#screen:-moz-full-screen-ancestor > [data-z-index-level="permission-screen"],
+#screen:-moz-full-screen-ancestor > [data-z-index-level="app-install-dialog"] {
+ z-index: 2147483647;
+}
diff --git a/apps/system/test/integration/atoms/notification.js b/apps/system/test/integration/atoms/notification.js
new file mode 100644
index 0000000..cf3e44f
--- /dev/null
+++ b/apps/system/test/integration/atoms/notification.js
@@ -0,0 +1,15 @@
+(function notification(text, desc) {
+ window.addEventListener('mozChromeEvent', function(e) {
+ var detail = e.detail;
+ if (detail.type === 'desktop-notification') {
+ marionetteScriptFinished(JSON.stringify(detail));
+ }
+ });
+
+ var notify = window.navigator.mozNotification;
+ var notification = notify.createNotification(
+ text, desc
+ );
+
+ notification.show();
+}.apply(this, arguments));
diff --git a/apps/system/test/integration/notification_test.js b/apps/system/test/integration/notification_test.js
new file mode 100644
index 0000000..aa532e4
--- /dev/null
+++ b/apps/system/test/integration/notification_test.js
@@ -0,0 +1,41 @@
+require('/apps/system/test/integration/system_integration.js');
+
+suite('notifications', function() {
+
+ var device;
+ var app;
+
+ MarionetteHelper.start(function(client) {
+ app = new SystemIntegration(client);
+ device = app.device;
+ });
+
+ setup(function() {
+ yield app.launch();
+ });
+
+ test('text/description notification', function() {
+
+ var title = 'uniq--integration--uniq';
+ var description = 'q--desc--q';
+
+ yield device.setContext('chrome');
+
+ yield IntegrationHelper.sendAtom(
+ device,
+ '/apps/system/test/integration/atoms/notification',
+ true,
+ [title, description]
+ );
+
+ yield device.setContext('content');
+ var container = yield app.element('notificationsContainer');
+
+ var text = yield container.getAttribute('innerHTML');
+ assert.ok(text, 'container should have notifications');
+
+ assert.include(text, title, 'should include title');
+ assert.include(text, description, 'should include description');
+ });
+});
+
diff --git a/apps/system/test/integration/system_integration.js b/apps/system/test/integration/system_integration.js
new file mode 100644
index 0000000..1116e17
--- /dev/null
+++ b/apps/system/test/integration/system_integration.js
@@ -0,0 +1,49 @@
+require('/tests/js/app_integration.js');
+require('/tests/js/integration_helper.js');
+
+function SystemIntegration() {
+ AppIntegration.apply(this, arguments);
+}
+
+SystemIntegration.prototype = {
+ __proto__: AppIntegration.prototype,
+
+ appName: 'System',
+
+ selectors: {
+ /** notifications */
+ notificationsContainer: '#notifications-container'
+ },
+
+ /**
+ * Override base launch method.
+ * The system app is launched by directly
+ * going to its url we we determine by getting
+ * all the apps and finding the 'System' apps origin.
+ */
+ launch: function(callback) {
+ var self = this;
+ this.task(function(app, next, done) {
+ var device = app.device;
+ yield device.setScriptTimeout(5000);
+
+ yield IntegrationHelper.importScript(
+ device,
+ '/tests/atoms/gaia_apps.js',
+ MochaTask.nodeNext
+ );
+
+ var result = yield device.executeAsyncScript(
+ 'GaiaApps.locateWithName("' + self.appName + '");'
+ );
+
+ // locate the origin of the system app.
+ // We must append the /index.html because of the app:// protocol.
+ yield device.goUrl(result.origin + '/index.html');
+
+ // complete the task
+ done();
+ }, callback);
+ }
+
+};
diff --git a/apps/system/test/unit/_proxy.html b/apps/system/test/unit/_proxy.html
new file mode 100644
index 0000000..2102451
--- /dev/null
+++ b/apps/system/test/unit/_proxy.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <title>Serve the tests</title>
+
+ <script type="text/javascript" charset="utf-8">
+ (function(window){
+ var Loader = window.CommonResourceLoader = {},
+ host = document.location.host,
+ domain = host.replace(/(^[\w\d-]+\.)?([\w\d]+\.[a-z]+)/, 'test-agent.$2');
+
+ Loader.domain = document.location.protocol + '//' + domain;
+ Loader.url = function(url){
+ return this.domain + url;
+ }
+
+ Loader.script = function(url, doc){
+ doc = doc || document;
+ doc.write('<script type="application/javascript;version=1.8" src="' + this.url(url) + '"><\/script>');
+ return this;
+ };
+ }(this));
+ </script>
+
+ <style type="text/css" media="all">
+ html,body,iframe {
+ height: 100%;
+ width: 100%;
+ }
+ </style>
+
+</head>
+<body>
+
+<!-- Test Agent UI will be loaded in here -->
+<div id="test-agent-ui">
+</div>
+
+<script type="text/javascript" charset="utf-8">
+CommonResourceLoader.
+ script('/common/test/test_url_resolver.js').
+ script('/common/vendor/test-agent/test-agent.js').
+ script('/common/test/agent_proxy.js');
+</script>
+</body>
+</html>
+
+
diff --git a/apps/system/test/unit/_sandbox.html b/apps/system/test/unit/_sandbox.html
new file mode 100644
index 0000000..70d0efa
--- /dev/null
+++ b/apps/system/test/unit/_sandbox.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <title>Tests</title>
+ <link rel="stylesheet" type="text/css" href="/vendor/mocha/mocha.css" />
+ <style type="text/css" media="all">
+ iframe {
+ border: none;
+ padding: 0px;
+ }
+ </style>
+ <script type="text/javascript" charset="utf-8">
+ </script>
+</head>
+
+<body>
+
+<div id="mocha">
+</div>
+
+<div id="test">
+</div>
+
+</body>
+</html>
+
+
diff --git a/apps/system/test/unit/app_install_manager_test.js b/apps/system/test/unit/app_install_manager_test.js
new file mode 100644
index 0000000..7e235e5
--- /dev/null
+++ b/apps/system/test/unit/app_install_manager_test.js
@@ -0,0 +1,1036 @@
+'use strict';
+
+requireApp('system/test/unit/mock_app.js');
+requireApp('system/test/unit/mock_chrome_event.js');
+requireApp('system/test/unit/mock_statusbar.js');
+requireApp('system/test/unit/mock_manifest_helper.js');
+requireApp('system/test/unit/mock_app.js');
+requireApp('system/test/unit/mock_system_banner.js');
+requireApp('system/test/unit/mock_notification_screen.js');
+requireApp('system/test/unit/mock_applications.js');
+requireApp('system/test/unit/mock_utility_tray.js');
+requireApp('system/test/unit/mock_modal_dialog.js');
+requireApp('system/test/unit/mock_navigator_wake_lock.js');
+requireApp('system/test/unit/mocks_helper.js');
+
+requireApp('system/js/app_install_manager.js');
+
+var mocksForAppInstallManager = [
+ 'StatusBar',
+ 'SystemBanner',
+ 'NotificationScreen',
+ 'Applications',
+ 'UtilityTray',
+ 'ModalDialog',
+ 'ManifestHelper'
+];
+
+mocksForAppInstallManager.forEach(function(mockName) {
+ if (! window[mockName]) {
+ window[mockName] = null;
+ }
+});
+
+suite('system/AppInstallManager >', function() {
+ var realL10n;
+ var realDispatchResponse;
+ var realRequestWakeLock;
+
+ var fakeDialog, fakeNotif;
+ var fakeInstallCancelDialog, fakeDownloadCancelDialog;
+
+ var lastL10nParams = null;
+ var lastDispatchedResponse = null;
+
+ var mocksHelper;
+
+ suiteSetup(function() {
+ realL10n = navigator.mozL10n;
+ navigator.mozL10n = {
+ get: function get(key, params) {
+ lastL10nParams = params;
+ if (params) {
+ return key + JSON.stringify(params);
+ }
+
+ return key;
+ }
+ };
+
+ realDispatchResponse = AppInstallManager.dispatchResponse;
+ AppInstallManager.dispatchResponse = function fakeDispatch(id, type) {
+ lastDispatchedResponse = {
+ id: id,
+ type: type
+ };
+ };
+
+ realRequestWakeLock = navigator.requestWakeLock;
+ navigator.requestWakeLock = MockNavigatorWakeLock.requestWakeLock;
+
+ mocksHelper = new MocksHelper(mocksForAppInstallManager);
+ mocksHelper.suiteSetup();
+ });
+
+ suiteTeardown(function() {
+ AppInstallManager.dialog = null;
+ AppInstallManager.msg = null;
+ AppInstallManager.size = null;
+ AppInstallManager.authorName = null;
+ AppInstallManager.authorUrl = null;
+ AppInstallManager.installButton = null;
+ AppInstallManager.cancelButton = null;
+ AppInstallManager.installCallback = null;
+ AppInstallManager.cancelCallback = null;
+
+ navigator.mozL10n = realL10n;
+ AppInstallManager.dispatchResponse = realDispatchResponse;
+
+ navigator.requestWakeLock = realRequestWakeLock;
+ realRequestWakeLock = null;
+
+ mocksHelper.suiteTeardown();
+ });
+
+ setup(function() {
+ fakeDialog = document.createElement('form');
+ fakeDialog.id = 'app-install-dialog';
+ fakeDialog.innerHTML = [
+ '<section>',
+ '<h1 id="app-install-message"></h1>',
+ '<table>',
+ '<tr>',
+ '<th data-l10n-id="size">Size</th>',
+ '<td id="app-install-size"></td>',
+ '</tr>',
+ '<tr>',
+ '<th data-l10n-id="author">Author</th>',
+ '<td>',
+ '<span id="app-install-author-name"></span>',
+ '<br /><span id="app-install-author-url"></span>',
+ '</td>',
+ '</tr>',
+ '</table>',
+ '<menu>',
+ '<button id="app-install-cancel-button" type="reset"' +
+ ' data-l10n-id="cancel">Cancel</button>',
+ '<button id="app-install-install-button" type="submit"' +
+ ' data-l10n-id="install">Install</button>',
+ '</menu>',
+ '</section>'
+ ].join('');
+
+ fakeInstallCancelDialog = document.createElement('form');
+ fakeInstallCancelDialog.id = 'app-install-cancel-dialog';
+ fakeInstallCancelDialog.innerHTML = [
+ '<section>',
+ '<h1 data-l10n-id="cancel-install">Cancel Install</h1>',
+ '<p>',
+ '<small data-l10n-id="cancelling-will-not-refund">Cancelling ' +
+ 'will not refund a purchase. Refunds for paid content are ' +
+ 'provided by the original seller.</small>',
+ '<small data-l10n-id="apps-can-be-installed-later">Apps can be ' +
+ 'installed later from the original installation source.</small>',
+ '</p>',
+ '<p data-l10n-id="are-you-sure-you-want-to-cancel">' +
+ 'Are you sure you want to cancel this install?</p>',
+ '<menu>',
+ '<button id="app-install-confirm-cancel-button" type="reset" ' +
+ 'data-l10n-id="cancel-install">Cancel Install</button>',
+ '<button id="app-install-resume-button" type="submit" ' +
+ 'data-l10n-id="resume">Resume</button>',
+ '</menu>',
+ '</section>'
+ ].join('');
+
+ fakeDownloadCancelDialog = document.createElement('form');
+ fakeDownloadCancelDialog.id = 'app-download-cancel-dialog';
+ fakeDownloadCancelDialog.innerHTML = [
+ '<section>',
+ '<h1></h1>',
+ '<p data-l10n-id="app-download-can-be-restarted">' +
+ 'The download can be restarted later.</p>',
+ '<menu>',
+ '<button id="app-download-stop-button" class="danger confirm" ',
+ 'data-l10n-id="app-download-stop-button">Stop Download</button>',
+ '<button id="app-download-continue-button" class="cancel" ',
+ 'type="reset" data-l10n-id="continue">Continue</button>',
+ '</menu>',
+ '</section>'
+ ].join('');
+
+ fakeNotif = document.createElement('div');
+ fakeNotif.id = 'install-manager-notification-container';
+
+ document.body.appendChild(fakeDialog);
+ document.body.appendChild(fakeInstallCancelDialog);
+ document.body.appendChild(fakeDownloadCancelDialog);
+ document.body.appendChild(fakeNotif);
+
+ mocksHelper.setup();
+
+ AppInstallManager.init();
+ });
+
+ teardown(function() {
+ fakeDialog.parentNode.removeChild(fakeDialog);
+ fakeInstallCancelDialog.parentNode.removeChild(fakeInstallCancelDialog);
+ fakeDownloadCancelDialog.parentNode.removeChild(fakeDownloadCancelDialog);
+ fakeNotif.parentNode.removeChild(fakeNotif);
+ lastDispatchedResponse = null;
+ lastL10nParams = null;
+
+ mocksHelper.teardown();
+ MockNavigatorWakeLock.mTeardown();
+ });
+
+ suite('init >', function() {
+ test('should bind dom elements', function() {
+ assert.equal('app-install-dialog', AppInstallManager.dialog.id);
+ assert.equal('app-install-message', AppInstallManager.msg.id);
+ assert.equal('app-install-size', AppInstallManager.size.id);
+ assert.equal('app-install-author-name', AppInstallManager.authorName.id);
+ assert.equal('app-install-author-url', AppInstallManager.authorUrl.id);
+ assert.equal('app-install-install-button',
+ AppInstallManager.installButton.id);
+ assert.equal('app-install-cancel-button',
+ AppInstallManager.cancelButton.id);
+ assert.equal('app-install-cancel-dialog',
+ AppInstallManager.installCancelDialog.id);
+ assert.equal('app-install-confirm-cancel-button',
+ AppInstallManager.confirmCancelButton.id);
+ assert.equal('app-install-resume-button',
+ AppInstallManager.resumeButton.id);
+ });
+
+ test('should bind to the click event', function() {
+ assert.equal(AppInstallManager.handleInstall.name,
+ AppInstallManager.installButton.onclick.name);
+ assert.equal(AppInstallManager.showInstallCancelDialog.name,
+ AppInstallManager.cancelButton.onclick.name);
+ assert.equal(AppInstallManager.handleInstallCancel.name,
+ AppInstallManager.confirmCancelButton.onclick.name);
+ assert.equal(AppInstallManager.hideInstallCancelDialog.name,
+ AppInstallManager.resumeButton.onclick.name);
+ });
+ });
+
+ suite('events >', function() {
+ suite('webapps-ask-install >', function() {
+ setup(function() {
+ var evt = new MockChromeEvent({
+ type: 'webapps-ask-install',
+ id: 42,
+ app: {
+ manifest: {
+ name: 'Fake app',
+ size: 5245678,
+ developer: {
+ name: 'Fake dev',
+ url: 'http://fakesoftware.com'
+ }
+ }
+ }
+ });
+
+ AppInstallManager.handleAppInstallPrompt(evt.detail);
+ });
+
+ test('should display the dialog', function() {
+ assert.equal('visible', AppInstallManager.dialog.className);
+ });
+
+ test('should fill the message with app name', function() {
+ assert.equal(AppInstallManager.msg.textContent,
+ 'install-app{"name":"Fake app"}');
+ });
+
+ test('should use the mini manifest if no manifest', function() {
+ var evt = new MockChromeEvent({
+ type: 'webapps-ask-install',
+ id: 42,
+ app: {
+ updateManifest: {
+ name: 'Fake app',
+ size: 5245678,
+ developer: {
+ name: 'Fake dev',
+ url: 'http://fakesoftware.com'
+ }
+ }
+ }
+ });
+
+ AppInstallManager.handleAppInstallPrompt(evt.detail);
+
+ assert.equal(AppInstallManager.msg.textContent,
+ 'install-app{"name":"Fake app"}');
+ });
+
+ suite('developer infos >', function() {
+ test('should fill the developer infos', function() {
+ assert.equal('Fake dev', AppInstallManager.authorName.textContent);
+ assert.equal('http://fakesoftware.com',
+ AppInstallManager.authorUrl.textContent);
+ });
+
+ test('should tell if the developer is unknown', function() {
+ var evt = new MockChromeEvent({
+ type: 'webapps-ask-install',
+ id: 42,
+ app: {
+ updateManifest: {
+ name: 'Fake app',
+ size: 5245678
+ }
+ }
+ });
+
+ AppInstallManager.handleAppInstallPrompt(evt.detail);
+ assert.equal('unknown', AppInstallManager.authorName.textContent);
+ assert.equal('', AppInstallManager.authorUrl.textContent);
+ });
+
+ test('should handle empty developer object properly', function() {
+ var evt = new MockChromeEvent({
+ type: 'webapps-ask-install',
+ id: 42,
+ app: {
+ updateManifest: {
+ name: 'Fake app',
+ size: 5245678,
+ developer: {}
+ }
+ }
+ });
+
+ AppInstallManager.handleAppInstallPrompt(evt.detail);
+ assert.equal('unknown', AppInstallManager.authorName.textContent);
+ assert.equal('', AppInstallManager.authorUrl.textContent);
+ });
+
+ test('should tell if the developer name is unknown', function() {
+ var evt = new MockChromeEvent({
+ type: 'webapps-ask-install',
+ id: 42,
+ app: {
+ updateManifest: {
+ name: 'Fake app',
+ size: 5245678,
+ developer: {
+ url: 'http://example.com'
+ }
+ }
+ }
+ });
+
+ AppInstallManager.handleAppInstallPrompt(evt.detail);
+ assert.equal('unknown', AppInstallManager.authorName.textContent);
+ assert.equal('http://example.com',
+ AppInstallManager.authorUrl.textContent);
+ });
+
+ test('the developer url should default to blank', function() {
+ var evt = new MockChromeEvent({
+ type: 'webapps-ask-install',
+ id: 42,
+ app: {
+ updateManifest: {
+ name: 'Fake app',
+ size: 5245678,
+ developer: {
+ name: 'Fake dev'
+ }
+ }
+ }
+ });
+
+ AppInstallManager.handleAppInstallPrompt(evt.detail);
+ assert.equal('Fake dev', AppInstallManager.authorName.textContent);
+ assert.equal('', AppInstallManager.authorUrl.textContent);
+ });
+ });
+
+ suite('install size >', function() {
+ test('should display the package size', function() {
+ assert.equal('5.00 MB', AppInstallManager.size.textContent);
+ });
+
+ test('should tell if the size is unknown', function() {
+ var evt = new MockChromeEvent({
+ type: 'webapps-ask-install',
+ id: 42,
+ app: {
+ manifest: {
+ name: 'Fake app',
+ developer: {
+ name: 'Fake dev',
+ url: 'http://fakesoftware.com'
+ }
+ }
+ }
+ });
+
+ AppInstallManager.handleAppInstallPrompt(evt.detail);
+ assert.equal('unknown', AppInstallManager.size.textContent);
+ });
+ });
+
+ suite('callbacks >', function() {
+ suite('install >', function() {
+ var defaultPrevented = false;
+ setup(function() {
+ AppInstallManager.handleInstall({preventDefault: function() {
+ defaultPrevented = true;
+ }});
+ });
+
+ test('should dispatch a webapps-install-granted with the right id',
+ function() {
+ assert.equal(42, lastDispatchedResponse.id);
+ assert.equal('webapps-install-granted',
+ lastDispatchedResponse.type);
+ });
+
+ test('should prevent the default to avoid form submission',
+ function() {
+ assert.isTrue(defaultPrevented);
+ });
+
+ test('should hide the dialog', function() {
+ assert.equal('', AppInstallManager.dialog.className);
+ });
+
+ test('should remove the callback', function() {
+ assert.equal(null, AppInstallManager.installCallback);
+ });
+ });
+
+ suite('show cancel dialog >', function() {
+ setup(function() {
+ AppInstallManager.showInstallCancelDialog();
+ });
+
+ test('should show cancel dialog and hide dialog', function() {
+ assert.equal('visible',
+ AppInstallManager.installCancelDialog.className);
+ assert.equal('', AppInstallManager.dialog.className);
+ });
+ });
+
+ suite('hide cancel dialog >', function() {
+ setup(function() {
+ AppInstallManager.hideInstallCancelDialog();
+ });
+
+ test('should hide cancel dialog and show dialog', function() {
+ assert.equal('', AppInstallManager.installCancelDialog.className);
+ assert.equal('visible', AppInstallManager.dialog.className);
+ });
+ });
+
+ suite('confirm cancel >', function() {
+ setup(function() {
+ AppInstallManager.handleInstallCancel();
+ });
+
+ test('should dispatch a webapps-install-denied', function() {
+ assert.equal(42, lastDispatchedResponse.id);
+ assert.equal('webapps-install-denied', lastDispatchedResponse.type);
+ });
+
+ test('should hide the dialog', function() {
+ assert.equal('', AppInstallManager.installCancelDialog.className);
+ });
+
+ test('should remove the callback', function() {
+ assert.equal(null, AppInstallManager.installCancelCallback);
+ });
+ });
+ });
+ });
+ });
+
+ suite('duringInstall >', function() {
+ var mockApp, mockAppName;
+
+ function dispatchEvent() {
+ var e = new CustomEvent('applicationinstall', {
+ detail: { application: mockApp }
+ });
+ window.dispatchEvent(e);
+ }
+
+ suite('hosted app without cache >', function() {
+ setup(function() {
+ mockAppName = 'Fake hosted app';
+ mockApp = new MockApp({
+ manifest: {
+ name: mockAppName,
+ developer: {
+ name: 'Fake dev',
+ url: 'http://fakesoftware.com'
+ }
+ },
+ updateManifest: null,
+ installState: 'installed'
+ });
+ MockSystemBanner.mTeardown();
+ dispatchEvent();
+ });
+
+ test('should not show the icon', function() {
+ assert.isUndefined(MockStatusBar.wasMethodCalled['incSystemDownloads']);
+ });
+
+ test('should not add a notification', function() {
+ assert.equal(fakeNotif.childElementCount, 0);
+ });
+
+ test('should display a confirmation', function() {
+ assert.equal(MockSystemBanner.mMessage,
+ 'app-install-success{"appName":"' + mockAppName + '"}');
+ });
+
+ });
+
+ function beforeFirstProgressSuite() {
+ suite('before first progress >', function() {
+ test('should not show the icon', function() {
+ var method = 'incSystemDownloads';
+ assert.isUndefined(MockStatusBar.wasMethodCalled[method]);
+ });
+
+ test('should not add a notification', function() {
+ assert.equal(fakeNotif.childElementCount, 0);
+ });
+
+ suite('on downloadsuccess >', function() {
+ setup(function() {
+ // reseting these mocks as we want to test only one call
+ MockNotificationScreen.mTeardown();
+ MockStatusBar.mTeardown();
+
+ mockApp.mTriggerDownloadSuccess();
+ });
+
+ test('should not remove a notification', function() {
+ var method = 'decExternalNotifications';
+ assert.isUndefined(MockNotificationScreen.wasMethodCalled[method]);
+ });
+
+ test('should not remove the download icon', function() {
+ var method = 'decSystemDownloads';
+ assert.isUndefined(MockStatusBar.wasMethodCalled[method]);
+ });
+
+ test('should display a confirmation', function() {
+ assert.equal(MockSystemBanner.mMessage,
+ 'app-install-success{"appName":"' + mockAppName + '"}');
+ });
+
+ });
+
+ suite('on downloaderror >', function() {
+ setup(function() {
+ // reseting these mocks as we want to test only one call
+ MockNotificationScreen.mTeardown();
+ MockStatusBar.mTeardown();
+
+ mockApp.mTriggerDownloadError();
+ });
+
+ test('should not remove a notification', function() {
+ var method = 'decExternalNotifications';
+ assert.isUndefined(MockNotificationScreen.wasMethodCalled[method]);
+ });
+
+ test('should not remove the download icon', function() {
+ var method = 'decSystemDownloads';
+ assert.isUndefined(MockStatusBar.wasMethodCalled[method]);
+ });
+ });
+ });
+ }
+
+ function downloadErrorSuite(downloadEventsSuite) {
+ suite('on downloadError >', function() {
+ setup(function() {
+ // reseting these mocks as we only want to test the
+ // following call
+ MockStatusBar.mTeardown();
+ MockSystemBanner.mTeardown();
+ MockModalDialog.mTeardown();
+ });
+
+ function downloadErrorTests(errorName) {
+ test('should display an error', function() {
+ var expectedErrorMsg = knownErrors[errorName] +
+ '{"appName":"' + mockAppName + '"}';
+
+ assert.equal(MockSystemBanner.mMessage, expectedErrorMsg);
+ });
+
+ test('should not display the error dialog', function() {
+ assert.isFalse(MockModalDialog.alert.mWasCalled);
+ });
+
+ }
+
+ function specificDownloadErrorSuite(errorName) {
+ suite(errorName + ' >', function() {
+ setup(function() {
+ mockApp.mTriggerDownloadError(errorName);
+ });
+
+ downloadErrorTests(errorName);
+ });
+ }
+
+ var knownErrors = {
+ 'FALLBACK_ERROR': 'app-install-generic-error',
+ 'NETWORK_ERROR': 'app-install-download-failed',
+ 'DOWNLOAD_ERROR': 'app-install-download-failed',
+ 'MISSING_MANIFEST': 'app-install-install-failed',
+ 'INVALID_MANIFEST': 'app-install-install-failed',
+ 'INSTALL_FROM_DENIED': 'app-install-install-failed',
+ 'INVALID_SECURITY_LEVEL': 'app-install-install-failed',
+ 'INVALID_PACKAGE': 'app-install-install-failed',
+ 'APP_CACHE_DOWNLOAD_ERROR': 'app-install-download-failed'
+ };
+
+ Object.keys(knownErrors).forEach(specificDownloadErrorSuite);
+
+ suite('GENERIC_ERROR >', function() {
+ setup(function() {
+ mockApp.mTriggerDownloadError('GENERIC_ERROR');
+ });
+
+ test('should remove the notif', function() {
+ assert.equal(fakeNotif.childElementCount, 0);
+ });
+
+ test('should remove the icon', function() {
+ var method = 'decSystemDownloads';
+ assert.ok(MockStatusBar.wasMethodCalled[method]);
+ });
+
+ beforeFirstProgressSuite();
+ downloadEventsSuite(/*afterError*/ true);
+ });
+
+ });
+ }
+
+ suite('hosted app with cache >', function() {
+ setup(function() {
+ mockAppName = 'Fake hosted app with cache';
+ mockApp = new MockApp({
+ manifest: {
+ name: mockAppName,
+ developer: {
+ name: 'Fake dev',
+ url: 'http://fakesoftware.com'
+ }
+ },
+ updateManifest: null,
+ installState: 'pending'
+ });
+ MockSystemBanner.mTeardown();
+ dispatchEvent();
+ });
+
+ function downloadEventsSuite(afterError) {
+ var suiteName = 'on first progress';
+ if (afterError) {
+ suiteName += ' after error';
+ }
+ suiteName += ' >';
+
+ suite(suiteName, function() {
+ setup(function() {
+ // reseting these mocks as we want to test only the following
+ // calls
+ MockNotificationScreen.mTeardown();
+ MockStatusBar.mTeardown();
+
+ mockApp.mTriggerDownloadProgress(NaN);
+ });
+
+ test('should add a notification', function() {
+ var method = 'incExternalNotifications';
+ assert.equal(fakeNotif.childElementCount, 1);
+ assert.ok(MockNotificationScreen.wasMethodCalled[method]);
+ });
+
+ test('notification should have a message', function() {
+ assert.equal(fakeNotif.querySelector('.message').textContent,
+ 'downloadingAppMessage{"appName":"Fake hosted app with cache"}');
+ assert.equal(fakeNotif.querySelector('progress').textContent,
+ 'downloadingAppProgressIndeterminate');
+ });
+
+ test('notification progress should be indeterminate', function() {
+ assert.equal(fakeNotif.querySelector('progress').position, -1);
+ });
+
+ test('should request wifi wake lock', function() {
+ assert.equal('wifi', MockNavigatorWakeLock.mLastWakeLock.topic);
+ assert.isFalse(MockNavigatorWakeLock.mLastWakeLock.released);
+ });
+
+ suite('on downloadsuccess >', function() {
+ setup(function() {
+ mockApp.mTriggerDownloadSuccess();
+ });
+
+ test('should remove the notif', function() {
+ var method = 'decExternalNotifications';
+ assert.equal(fakeNotif.childElementCount, 0);
+ assert.ok(MockNotificationScreen.wasMethodCalled[method]);
+ });
+
+ test('should release the wifi wake lock', function() {
+ assert.equal('wifi', MockNavigatorWakeLock.mLastWakeLock.topic);
+ assert.isTrue(MockNavigatorWakeLock.mLastWakeLock.released);
+ });
+ });
+
+ test('on downloadsuccess > should remove only its progress handler',
+ function() {
+
+ var onprogressCalled = false;
+ mockApp.onprogress = function() {
+ onprogressCalled = true;
+ };
+ mockApp.mTriggerDownloadSuccess();
+ mockApp.mTriggerDownloadProgress(10);
+ assert.isTrue(onprogressCalled);
+ });
+
+ test('on downloadsuccess > should display a confirmation',
+ function() {
+ mockApp.mTriggerDownloadSuccess();
+ assert.equal(MockSystemBanner.mMessage,
+ 'app-install-success{"appName":"' + mockAppName + '"}');
+ });
+
+ suite('on indeterminate progress >', function() {
+ setup(function() {
+ mockApp.mTriggerDownloadProgress(NaN);
+ });
+
+ test('should keep the progress indeterminate', function() {
+ var progressNode = fakeNotif.querySelector('progress');
+ assert.equal(progressNode.position, -1);
+ assert.equal(progressNode.textContent,
+ 'downloadingAppProgressIndeterminate');
+ });
+ });
+
+ suite('on quantified progress >', function() {
+ setup(function() {
+ mockApp.mTriggerDownloadProgress(10);
+ });
+
+ test('should have a quantified progress', function() {
+ var progressNode = fakeNotif.querySelector('progress');
+ assert.equal(progressNode.position, -1);
+ assert.equal(progressNode.textContent,
+ 'downloadingAppProgressNoMax{"progress":"10.00 bytes"}');
+ });
+ });
+
+ if (!afterError) {
+ downloadErrorSuite(downloadEventsSuite);
+ }
+ });
+ }
+
+ beforeFirstProgressSuite();
+ downloadEventsSuite(/*afterError*/ false);
+ });
+
+ suite('packaged app >', function() {
+ setup(function() {
+ mockAppName = 'Fake packaged app';
+ mockApp = new MockApp({
+ manifest: null,
+ updateManifest: {
+ name: mockAppName,
+ size: 5245678,
+ developer: {
+ name: 'Fake dev',
+ url: 'http://fakesoftware.com'
+ }
+ },
+ installState: 'pending'
+ });
+
+ dispatchEvent();
+ });
+
+
+ function downloadEventsSuite(afterError) {
+ var suiteName = 'on first progress';
+ if (afterError) {
+ suiteName += ' after error';
+ }
+ suiteName += ' >';
+
+ suite(suiteName, function() {
+ var newprogress = 5;
+
+ setup(function() {
+ // resetting this mock because we want to test only the
+ // following call
+ MockNotificationScreen.mTeardown();
+ MockSystemBanner.mTeardown();
+ mockApp.mTriggerDownloadProgress(newprogress);
+ });
+
+ test('should add a notification', function() {
+ var method = 'incExternalNotifications';
+ assert.equal(fakeNotif.childElementCount, 1);
+ assert.ok(MockNotificationScreen.wasMethodCalled[method]);
+ });
+
+ test('notification should have a message', function() {
+ var expectedText = 'downloadingAppMessage{"appName":"' +
+ mockAppName + '"}';
+ assert.equal(fakeNotif.querySelector('.message').textContent,
+ expectedText);
+ });
+
+ test('notification progress should have a max and a value',
+ function() {
+ assert.equal(fakeNotif.querySelector('progress').max,
+ mockApp.updateManifest.size);
+ assert.equal(fakeNotif.querySelector('progress').value,
+ newprogress);
+ });
+
+ test('notification progress should not be indeterminate',
+ function() {
+ assert.notEqual(fakeNotif.querySelector('progress').position, -1);
+ });
+
+ test('should request wifi wake lock', function() {
+ assert.equal('wifi', MockNavigatorWakeLock.mLastWakeLock.topic);
+ assert.isFalse(MockNavigatorWakeLock.mLastWakeLock.released);
+ });
+
+ suite('on downloadsuccess >', function() {
+ setup(function() {
+ mockApp.mTriggerDownloadSuccess();
+ });
+
+ test('should remove the notif', function() {
+ var method = 'decExternalNotifications';
+ assert.equal(fakeNotif.childElementCount, 0);
+ assert.ok(MockNotificationScreen.wasMethodCalled[method]);
+ });
+
+ test('should release the wifi wake lock', function() {
+ assert.equal('wifi', MockNavigatorWakeLock.mLastWakeLock.topic);
+ assert.isTrue(MockNavigatorWakeLock.mLastWakeLock.released);
+ });
+
+ });
+
+ test('on downloadsuccess > ' +
+ 'should not break if wifi unlock throws an exception',
+ function() {
+ MockNavigatorWakeLock.mThrowAtNextUnlock();
+ mockApp.mTriggerDownloadSuccess();
+ assert.ok(true);
+ });
+
+ test('on downloadsuccess > should display a confirmation',
+ function() {
+ mockApp.mTriggerDownloadSuccess();
+ assert.equal(MockSystemBanner.mMessage,
+ 'app-install-success{"appName":"' + mockAppName + '"}');
+ });
+
+ test('on indeterminate progress > ' +
+ 'should update the progress text content',
+ function() {
+ mockApp.mTriggerDownloadProgress(NaN);
+
+ var progressNode = fakeNotif.querySelector('progress');
+ assert.equal(progressNode.textContent,
+ 'downloadingAppProgressIndeterminate');
+ });
+
+ suite('on progress >', function() {
+ var size, ratio;
+ var newprogress = 10;
+
+ setup(function() {
+ size = mockApp.updateManifest.size;
+ ratio = newprogress / size;
+ mockApp.mTriggerDownloadProgress(newprogress);
+ });
+
+ test('should update the progress notification', function() {
+ var progressNode = fakeNotif.querySelector('progress');
+ assert.equal(progressNode.position, ratio);
+ assert.equal(progressNode.textContent,
+ 'downloadingAppProgress{"progress":"10.00 bytes",' +
+ '"max":"5.00 MB"}');
+ });
+ });
+
+ if (!afterError) {
+ downloadErrorSuite(downloadEventsSuite);
+ }
+ });
+ }
+
+ beforeFirstProgressSuite();
+ downloadEventsSuite(/*afterError*/ false);
+
+ suite('on INSUFFICIENT_STORAGE downloaderror >', function() {
+ test('should display an alert', function() {
+ mockApp.mTriggerDownloadError('INSUFFICIENT_STORAGE');
+ assert.isNull(MockSystemBanner.mMessage);
+ assert.isTrue(MockModalDialog.alert.mWasCalled);
+ var args = MockModalDialog.alert.mArgs;
+ assert.equal(args[0], 'not-enough-space');
+ assert.equal(args[1], 'not-enough-space-message');
+ assert.deepEqual(args[2], { title: 'ok' });
+ });
+
+ beforeFirstProgressSuite();
+ downloadEventsSuite(/*afterError*/ true);
+ });
+
+
+ });
+
+ suite('cancelling a download >', function() {
+ setup(function() {
+ mockApp = new MockApp({ installState: 'pending' });
+ MockApplications.mRegisterMockApp(mockApp);
+ dispatchEvent();
+ mockApp.mTriggerDownloadProgress(10);
+ });
+
+ test('tapping the notification should display the dialog', function() {
+ fakeNotif.querySelector('.fake-notification').click();
+ assert.isTrue(fakeDownloadCancelDialog.classList.contains('visible'));
+ });
+
+ test('tapping the container should not display the dialog', function() {
+ fakeNotif.click();
+ assert.isFalse(fakeDownloadCancelDialog.classList.contains('visible'));
+ });
+
+ test('should set the title with the app name', function() {
+ fakeNotif.querySelector('.fake-notification').click();
+ var title = fakeDownloadCancelDialog.querySelector('h1');
+ assert.equal(title.textContent, 'stopDownloading{"app":"Mock app"}');
+ });
+
+ test('should add the manifest url in data set', function() {
+ fakeNotif.querySelector('.fake-notification').click();
+ assert.equal(fakeDownloadCancelDialog.dataset.manifest,
+ mockApp.manifestURL);
+ });
+
+ test('should hide the notification tray', function() {
+ fakeNotif.querySelector('.fake-notification').click();
+ assert.isFalse(MockUtilityTray.mShown);
+ });
+
+ test('cancelling should hide the dialog only', function() {
+ fakeNotif.querySelector('.fake-notification').click();
+ fakeDownloadCancelDialog.querySelector('.cancel').click();
+ assert.isFalse(fakeDownloadCancelDialog.classList.contains('visible'));
+ assert.isFalse(mockApp.mCancelCalled);
+ });
+
+ test('accepting should hide the dialog and call cancelDownload on app',
+ function() {
+ fakeNotif.querySelector('.fake-notification').click();
+ fakeDownloadCancelDialog.querySelector('.confirm').click();
+ assert.isFalse(fakeDownloadCancelDialog.classList.contains('visible'));
+ assert.ok(mockApp.mCancelCalled);
+ });
+
+ test('accepting should hide the dialog but not call cancelDownload ' +
+ 'if app is uninstalled',
+ function() {
+ fakeNotif.querySelector('.fake-notification').click();
+ MockApplications.mUnregisterMockApp(mockApp);
+ fakeDownloadCancelDialog.querySelector('.confirm').click();
+ assert.isFalse(fakeDownloadCancelDialog.classList.contains('visible'));
+ assert.isFalse(mockApp.mCancelCalled);
+ });
+ });
+
+ });
+
+ suite('restarting after reboot >', function() {
+ var mockApp, installedMockApp;
+
+ setup(function() {
+ mockApp = new MockApp({
+ updateManifest: null,
+ installState: 'pending'
+ });
+
+ installedMockApp = new MockApp({
+ updateManifest: null,
+ installState: 'installed'
+ });
+
+ var e = new CustomEvent('applicationready', {
+ detail: { applications: {} }
+ });
+ e.detail.applications[mockApp.manifestURL] = mockApp;
+ e.detail.applications[installedMockApp.manifestURL] = installedMockApp;
+ window.dispatchEvent(e);
+
+ });
+
+ test('should add a notification for the pending app', function() {
+ mockApp.mTriggerDownloadProgress(50);
+
+ var method = 'incExternalNotifications';
+ assert.equal(fakeNotif.childElementCount, 1);
+ assert.ok(MockNotificationScreen.wasMethodCalled[method]);
+ });
+
+ test('should not add a notification for the installed app', function() {
+ installedMockApp.mTriggerDownloadProgress(50);
+
+ var method = 'incExternalNotifications';
+ assert.equal(fakeNotif.childElementCount, 0);
+ assert.isUndefined(MockNotificationScreen.wasMethodCalled[method]);
+ });
+ });
+
+ suite('humanizeSize >', function() {
+ test('should handle bytes size', function() {
+ assert.equal('42.00 bytes', AppInstallManager.humanizeSize(42));
+ });
+
+ test('should handle kilobytes size', function() {
+ assert.equal('1.00 kB', AppInstallManager.humanizeSize(1024));
+ });
+
+ test('should handle megabytes size', function() {
+ assert.equal('4.67 MB', AppInstallManager.humanizeSize(4901024));
+ });
+
+ test('should handle gigabytes size', function() {
+ assert.equal('3.73 GB', AppInstallManager.humanizeSize(4000901024));
+ });
+
+ test('should handle 0', function() {
+ assert.equal('0.00 bytes', AppInstallManager.humanizeSize(0));
+ });
+ });
+});
diff --git a/apps/system/test/unit/battery_manager_test.js b/apps/system/test/unit/battery_manager_test.js
new file mode 100644
index 0000000..ee85f86
--- /dev/null
+++ b/apps/system/test/unit/battery_manager_test.js
@@ -0,0 +1,204 @@
+'use strict';
+
+requireApp('system/test/unit/mock_navigator_battery.js');
+requireApp('system/test/unit/mock_settings_listener.js');
+requireApp('system/test/unit/mock_sleep_menu.js');
+requireApp('system/test/unit/mock_gesture_detector.js');
+requireApp('system/test/unit/mocks_helper.js');
+requireApp('system/js/battery_manager.js');
+
+var mocksForBatteryManager = [
+ 'SettingsListener',
+ 'SleepMenu',
+ 'GestureDetector'
+];
+
+mocksForBatteryManager.forEach(function(mockName) {
+ if (! window[mockName]) {
+ window[mockName] = null;
+ }
+});
+
+
+suite('battery manager >', function() {
+ var realBattery;
+ var screenNode, notifNode, overlayNode;
+ var mocksHelper;
+ var tinyTimeout = 10;
+
+ suiteSetup(function() {
+ mocksHelper = new MocksHelper(mocksForBatteryManager);
+ mocksHelper.suiteSetup();
+
+ realBattery = BatteryManager._battery;
+ BatteryManager._battery = MockNavigatorBattery;
+
+ // must be big enough, otherwise the BatteryManager timeout occurs
+ // before the different suites execute.
+ BatteryManager.TOASTER_TIMEOUT = tinyTimeout;
+ });
+
+ suiteTeardown(function() {
+ mocksHelper.suiteTeardown();
+
+ BatteryManager._battery = realBattery;
+ realBattery = null;
+ });
+
+ setup(function() {
+ mocksHelper.setup();
+
+ var batteryNotificationMarkup =
+ '<div id="system-overlay" data-z-index-level="system-overlay">' +
+ '<div id="battery">' +
+ '<span class="icon-battery"></span>' +
+ '<span class="battery-notification" '+
+ 'data-l10n-id="battery-almost-empty">Battery almost empty' +
+ '</span>' +
+ '</div>' +
+ '</div>';
+
+ screenNode = document.createElement('div');
+ screenNode.id = 'screen';
+ screenNode.innerHTML = batteryNotificationMarkup;
+ document.body.appendChild(screenNode);
+
+ overlayNode = document.getElementById('system-overlay');
+ notifNode = document.getElementById('battery');
+
+ MockNavigatorBattery.level = 1;
+ PowerSaveHandler.init();
+ BatteryManager.init();
+ });
+
+ teardown(function() {
+ mocksHelper.teardown();
+
+ screenNode.parentNode.removeChild(screenNode);
+ });
+
+ function sendScreenChange(val) {
+ var detail = { screenEnabled: val};
+ var evt = new CustomEvent('screenchange', { detail: detail });
+ window.dispatchEvent(evt);
+ }
+
+ function sendLevelChange(level) {
+ MockNavigatorBattery.level = level;
+
+ var evt = new CustomEvent('levelchange');
+ MockNavigatorBattery.mTriggerEvent(evt);
+ }
+
+ function sendChargingChange(val) {
+ MockNavigatorBattery.charging = val;
+
+ var evt = new CustomEvent('chargingchange');
+ MockNavigatorBattery.mTriggerEvent(evt);
+ }
+
+ suite('"level is near empty" notification >', function() {
+ function assertDisplayed() {
+ assert.ok(overlayNode.classList.contains('battery'));
+ }
+
+ function assertNotDisplayed() {
+ assert.isFalse(overlayNode.classList.contains('battery'));
+ }
+
+ teardown(function(done) {
+ // wait for the notification timeout
+ setTimeout(done, tinyTimeout * 2);
+ });
+
+ suite('init >', function() {
+ setup(function() {
+ MockNavigatorBattery.level = 0.02;
+ BatteryManager.init();
+ });
+
+ test('display notification', function() {
+ assertDisplayed();
+ });
+ });
+
+ suite('battery goes empty >', function() {
+ setup(function() {
+ sendLevelChange(0.05);
+ });
+
+ test('display notification', function() {
+ assertDisplayed();
+ });
+
+ test('do not display twice', function(done) {
+ setTimeout(function() {
+ sendLevelChange(0.02);
+
+ assertNotDisplayed();
+ done();
+ }, tinyTimeout * 2);
+ });
+
+ suite('charging >', function() {
+ setup(function() {
+ sendChargingChange(true);
+ });
+
+ test('hide notification', function() {
+ assertNotDisplayed();
+ });
+
+ test('not charging > show notification', function() {
+ sendChargingChange(false);
+ assertDisplayed();
+ });
+
+ suite('goes up >', function() {
+ setup(function() {
+ sendLevelChange(0.2);
+ });
+
+ test('hide notification', function() {
+ assertNotDisplayed();
+ });
+
+ suite('not charging >', function() {
+ setup(function() {
+ sendChargingChange(false);
+ });
+
+ test('should not display', function() {
+ assertNotDisplayed();
+ });
+
+ test('goes empty again > display notification', function() {
+ sendLevelChange(0.02);
+
+ assertDisplayed();
+ });
+ });
+
+ });
+
+ });
+ });
+
+ suite('screen goes off > battery goes empty >', function() {
+ setup(function() {
+ sendScreenChange(false);
+ sendLevelChange(0.05);
+ });
+
+ test('no notification', function() {
+ assertNotDisplayed();
+ });
+
+ test('screen goes on > display notification', function() {
+ sendScreenChange(true);
+
+ assertDisplayed();
+ });
+ });
+ });
+});
diff --git a/apps/system/test/unit/date_picker_test.js b/apps/system/test/unit/date_picker_test.js
new file mode 100644
index 0000000..51d8867
--- /dev/null
+++ b/apps/system/test/unit/date_picker_test.js
@@ -0,0 +1,483 @@
+requireApp('system/js/value_selector/date_picker.js');
+
+suite('date picker', function() {
+ var subject;
+ var Calc;
+ var triggerEvent;
+
+ teardown(function() {
+ var el = document.getElementById('test');
+ el.parentNode.removeChild(el);
+ });
+
+ setup(function() {
+ var div = document.createElement('div');
+ div.id = 'test';
+ document.body.appendChild(div);
+ subject = new DatePicker(div);
+ Calc = DatePicker.Calc;
+ subject._position = new Date(2012, 1, 1);
+ });
+
+ suite('Calc', function() {
+ var mocked = {};
+
+ function mock(fn, value) {
+ if (!(fn in mocked)) {
+ mocked[fn] = subject[fn];
+ }
+ subject[fn] = function() {
+ return value;
+ };
+ }
+
+ teardown(function() {
+ var key;
+ for (key in mocked) {
+ if (mocked.hasOwnProperty(key)) {
+ subject[key] = mocked[key];
+ }
+ }
+ });
+
+ setup(function() {
+ subject = DatePicker.Calc;
+ });
+
+ suite('#isSameDate', function() {
+
+ test('same day', function() {
+ assert.isTrue(subject.isSameDate(
+ new Date(2012, 1, 1, 8),
+ new Date(2012, 1, 1, 23)
+ ));
+ });
+
+ test('same day different month', function() {
+ assert.isFalse(subject.isSameDate(
+ new Date(2012, 2, 1, 8),
+ new Date(2012, 1, 1, 8)
+ ));
+ });
+ });
+
+ suite('#isToday', function() {
+ test('when given is today', function() {
+ var result = subject.isToday(new Date());
+
+ assert.isTrue(result, 'should be true when given today');
+ });
+
+ test('when given is not today', function() {
+ var now = new Date();
+ now.setDate(now.getDate() - 1);
+ var result = subject.isToday(now);
+
+ assert.isFalse(result, 'should be false when given is not today');
+ });
+ });
+
+ suite('#isPast', function() {
+ test('when date is passed', function() {
+ var date = new Date();
+ date.setTime(Date.now() - 1000);
+ var result = subject.isPast(date);
+
+ assert.isTrue(result, 'should be true when given is in the past');
+ });
+
+ test('when given is in the future', function() {
+ var date = new Date();
+ date.setTime(Date.now() + 100);
+ var result = subject.isPast(date);
+
+ assert.isFalse(result,
+ 'should return false when date is in the future');
+ });
+
+ });
+
+ suite('#isFuture', function() {
+ test('when date is passed', function() {
+ var date = new Date();
+ date.setTime(Date.now() - 100);
+ var result = subject.isFuture(date);
+
+ assert.isFalse(result);
+ });
+
+ test('when given is in the future', function() {
+ var date = new Date(Date.now() + 100);
+ var result = subject.isFuture(date);
+
+ assert.isTrue(result);
+ });
+
+ });
+
+ suite('#dateFromId', function() {
+ var id;
+ var result;
+ var date = new Date(2012, 7, 3);
+
+ setup(function() {
+ id = subject.getDayId(date);
+ });
+
+ test('id to date', function() {
+ assert.deepEqual(
+ subject.dateFromId(id),
+ date
+ );
+ });
+
+ });
+
+ test('#getDayId', function() {
+ var result = subject.getDayId(
+ new Date(2012, 3, 7)
+ );
+
+ assert.equal(result, '2012-3-7');
+ });
+
+ suite('#relativeState', function() {
+
+ setup(function() {
+ mock('isToday', false);
+ });
+
+ test('when in the past', function() {
+ mock('isPast', true);
+ var state = subject.relativeState(
+ new Date(1991, 1, 1),
+ new Date(1991, 1, 1)
+ );
+
+ assert.equal(state, subject.PAST);
+ });
+
+ test('when in the future', function() {
+ mock('isPast', false);
+ var state = subject.relativeState(
+ new Date(1991, subject.today.getMonth(), 1),
+ new Date(1991, subject.today.getMonth(), 1)
+ );
+ assert.equal(state, subject.FUTURE);
+ });
+
+ test('when is in a different month in the past', function() {
+ mock('isPast', true);
+
+ var state = subject.relativeState(
+ new Date(1991, subject.today.getMonth() - 1, 1),
+ new Date(1991, subject.today.getMonth(), 1)
+ );
+
+ assert.include(state, subject.PAST);
+ assert.include(state, subject.OTHER_MONTH);
+ });
+
+ test('when is in a different month in the future', function() {
+ mock('isPast', false);
+
+ var state = subject.relativeState(
+ new Date(1991, subject.today.getMonth() + 1, 1),
+ new Date(1991, subject.today.getMonth(), 1)
+ );
+
+ assert.include(state, subject.FUTURE);
+ assert.include(state, subject.OTHER_MONTH);
+ });
+
+
+ test('when is today', function() {
+ mock('isToday', true);
+ var state = subject.relativeState(new Date(1991, 1, 1));
+
+ assert.equal(state, subject.PRESENT);
+ });
+
+ });
+
+ });
+
+ suite('#_daysIn', function() {
+ test('leap year', function() {
+ var result = subject._daysInMonth(2012, 1);
+ assert.equal(result, 29);
+ });
+
+ test('normal', function() {
+ var result = subject._daysInMonth(2012, 0);
+ assert.equal(result, 31);
+ });
+ });
+
+ suite('#_renderDay', function() {
+
+ test('simple', function() {
+ var date = new Date(2012, 1, 27);
+ var result = subject._renderDay(date).firstChild;
+ var html = result.outerHTML;
+
+ assert.ok(html);
+ assert.equal(result.dataset.date, Calc.getDayId(date));
+ assert.include(html, '27');
+ });
+
+ test('today', function() {
+ var date = new Date();
+ var result = subject._renderDay(date);
+ assert.ok(result.classList.contains('present'));
+ });
+
+ test('past', function() {
+ var date = new Date(2009, 1, 1);
+ var result = subject._renderDay(date);
+ assert.ok(result.classList.contains('past'));
+ });
+
+ test('future', function() {
+ var date = new Date();
+ date.setDate(date.getDate() + 2);
+
+ var result = subject._renderDay(date);
+ assert.ok(result.classList.contains('future'));
+ });
+ });
+
+ suite('#_renderWeek', function() {
+ var days = [
+ new Date(2012, 0, 29),
+ new Date(2012, 0, 30),
+ new Date(2012, 0, 31),
+ new Date(2012, 1, 1),
+ new Date(2012, 1, 2),
+ new Date(2012, 1, 3),
+ new Date(2012, 1, 4)
+ ];
+
+ var result;
+
+ setup(function() {
+ result = subject._renderWeek(days);
+ });
+
+ test('container', function() {
+ assert.equal(result.tagName.toLowerCase(), 'ol');
+ assert.ok(result.outerHTML);
+ });
+
+ days.forEach(function(day) {
+ test('week day ' + day, function() {
+ var expected = subject._renderDay(day);
+ assert.include(result.outerHTML, expected.outerHTML);
+ });
+ });
+
+ });
+
+ suite('#_renderMonth', function() {
+
+ function weekHtml(start, end) {
+ var range = Calc.daysBetween(start, end);
+ return subject._renderWeek(range).outerHTML;
+ }
+
+ test('Feb 2012', function() {
+ var month = 1;
+ var year = 2012;
+
+ var result = subject._renderMonth(year, month);
+ var html = result.outerHTML;
+
+ assert.ok(html, 'has contents');
+
+ assert.include(
+ html,
+ weekHtml(new Date(2012, 0, 29), new Date(2012, 1, 4)),
+ 'has first week'
+ );
+
+ assert.include(
+ html,
+ weekHtml(new Date(2012, 1, 5), new Date(2012, 1, 11)),
+ 'has second week'
+ );
+
+ assert.include(
+ html,
+ weekHtml(new Date(2012, 1, 12), new Date(2012, 1, 18)),
+ 'has third week'
+ );
+
+ assert.include(
+ html,
+ weekHtml(new Date(2012, 1, 19), new Date(2012, 1, 25)),
+ 'has fourth week'
+ );
+
+
+ assert.include(
+ html,
+ weekHtml(new Date(2012, 1, 26), new Date(2012, 2, 3)),
+ 'has fifth week'
+ );
+ });
+ });
+
+ suite('prev/next', function() {
+ var calledWith;
+
+ setup(function() {
+ subject.display(2012, 0, 1);
+ });
+
+ test('#next', function() {
+ subject.next();
+ assert.equal(subject.year, 2012);
+ assert.equal(subject.month, 1);
+ });
+
+ test('#previous', function() {
+ subject.previous();
+ assert.equal(subject.year, 2011);
+ assert.equal(subject.month, 11);
+ });
+
+ });
+
+ suite('prev/next with last day of a month', function() {
+ var calledWith;
+
+ setup(function() {
+ // init as 2012/3/31
+ subject.display(2012, 2, 31);
+ });
+
+ test('#next', function() {
+ subject.next();
+
+ // should be 2012/4/30
+ assert.equal(subject.year, 2012);
+ assert.equal(subject.month, 3);
+ assert.equal(subject.date, 30);
+ });
+
+ test('#previous', function() {
+ subject.previous();
+
+ // should be 2012/2/29
+ assert.equal(subject.year, 2012);
+ assert.equal(subject.month, 1);
+ assert.equal(subject.date, 29);
+ });
+
+ });
+
+ suite('#display', function() {
+ var year = 2012;
+ var month = 11;
+ var date = 1;
+ var calledWith;
+
+ setup(function() {
+ calledWith = null;
+ subject.onmonthchange = function() {
+ calledWith = arguments;
+ };
+
+ subject.display(year, month, date);
+ });
+
+ test('initial render', function() {
+ assert.deepEqual(
+ calledWith[0],
+ new Date(year, month),
+ 'should fire onmonthchange'
+ );
+
+ assert.equal(subject.year, 2012);
+ assert.equal(subject.month, 11);
+ assert.equal(subject.date, 1);
+ assert.ok(subject.monthDisplay);
+ });
+
+ test('second rendering', function() {
+ subject.display(2011, 2, 1);
+
+ assert.deepEqual(
+ calledWith[0],
+ new Date(2011, 2),
+ 'should fire onmonthchange again'
+ );
+
+ assert.equal(subject.year, 2011);
+ assert.equal(subject.month, 2);
+ assert.equal(subject.date, 1);
+ assert.ok(subject.monthDisplay);
+ });
+ });
+
+ suite('setters', function() {
+ test('#value', function() {
+ var calledWith;
+ var date;
+
+ subject.onvaluechange = function() {
+ calledWith = arguments;
+ }
+
+ subject.value = date = new Date(2012, 1, 1);
+
+ assert.deepEqual(subject.value, date);
+ assert.deepEqual(calledWith, [date, null]);
+ });
+ });
+
+ suite('dom events', function() {
+ function triggerEvent(element, eventName) {
+ var event = document.createEvent('HTMLEvents');
+ event.initEvent(eventName, true, true);
+ element.dispatchEvent(event);
+ }
+
+ setup(function() {
+ subject.display(2012, 1, 1);
+ });
+
+ test('select', function() {
+ var calledWith;
+ subject.onvaluechange = function() {
+ calledWith = arguments;
+ }
+
+ var target = subject.element.querySelector('[data-date="2012-1-1"]');
+ triggerEvent(target, 'click');
+
+ assert.ok(target.classList.contains('selected'), 'adds selected class');
+
+ assert.deepEqual(
+ subject.value,
+ new Date(2012, 1, 1),
+ 'changes value'
+ );
+
+ assert.deepEqual(
+ calledWith,
+ [new Date(2012, 1, 1), new Date(2012, 1, 1)],
+ 'calls onvaluechange'
+ );
+
+ triggerEvent(
+ subject.element.querySelector('[data-date="2012-1-2"]'),
+ 'click'
+ );
+
+ assert.ok(!target.classList.contains('selected'), 'clears past selected');
+ });
+ });
+
+});
+
diff --git a/apps/system/test/unit/identity_test.js b/apps/system/test/unit/identity_test.js
new file mode 100644
index 0000000..150944c
--- /dev/null
+++ b/apps/system/test/unit/identity_test.js
@@ -0,0 +1,92 @@
+'use strict';
+
+requireApp('system/js/identity.js');
+requireApp('system/test/unit/mock_chrome_event.js');
+requireApp('system/test/unit/mock_trusted_ui_manager.js');
+requireApp('system/test/unit/mock_l10n.js');
+
+// ensure its defined as a global so mocha will not complain about us
+// leaking new global variables during the test
+if (!window.TrustedUIManager) {
+ window.TrustedUIManager = true;
+}
+
+suite('identity', function() {
+ var subject;
+ var realL10n;
+ var realTrustedUIManager;
+ var realDispatchEvent;
+
+ var lastDispatchedEvent = null;
+
+ suiteSetup(function() {
+ subject = Identity;
+ realTrustedUIManager = window.TrustedUIManager;
+ window.TrustedUIManager = MockTrustedUIManager;
+
+ realL10n = navigator.mozL10n;
+ navigator.mozL10n = MockL10n;
+
+ realDispatchEvent = subject._dispatchEvent;
+ subject._dispatchEvent = function (obj) {
+ lastDispatchedEvent = obj;
+ };
+ });
+
+ suiteTeardown(function() {
+ window.TrustedUIManager = realTrustedUIManager;
+ subject._dispatchEvent = realDispatchEvent;
+
+ navigator.mozL10n = realL10n;
+ });
+
+ setup(function() {});
+
+ teardown(function() {
+ MockTrustedUIManager.mTeardown();
+ });
+
+ suite('open popup', function() {
+ setup(function() {
+ var event = new MockChromeEvent({
+ type: 'open-id-dialog',
+ id: 'test-open-event-id',
+ showUI: true
+ });
+ subject.handleEvent(event);
+ });
+
+ test('popup parameters', function() {
+ assert.equal(MockTrustedUIManager.mOpened, true);
+ assert.equal(MockTrustedUIManager.mName, 'persona-signin');
+ assert.equal(MockTrustedUIManager.mChromeEventId, 'test-open-event-id');
+ });
+
+ test('frame event listener', function() {
+ var frame = MockTrustedUIManager.mFrame;
+ var event = document.createEvent('CustomEvent');
+ event.initCustomEvent('mozbrowserloadstart', true, true, {target: frame});
+ frame.dispatchEvent(event);
+
+ assert.equal(frame, lastDispatchedEvent.frame);
+ assert.equal('test-open-event-id', lastDispatchedEvent.id);
+ });
+ });
+
+ suite('close popup', function() {
+ setup(function() {
+ var event = new MockChromeEvent({
+ type: 'received-id-assertion',
+ id: 'test-close-event-id',
+ showUI: true
+ });
+ subject.handleEvent(event);
+ });
+
+ test('close', function() {
+ assert.equal(false, MockTrustedUIManager.mOpened);
+ assert.equal('test-close-event-id', lastDispatchedEvent.id);
+ });
+ });
+});
+
diff --git a/apps/system/test/unit/lockscreen_test.js b/apps/system/test/unit/lockscreen_test.js
new file mode 100644
index 0000000..7258670
--- /dev/null
+++ b/apps/system/test/unit/lockscreen_test.js
@@ -0,0 +1,122 @@
+'use strict';
+
+requireApp('system/test/unit/mock_settings_listener.js');
+requireApp('system/test/unit/mocks_helper.js');
+requireApp('system/test/unit/mock_l10n.js');
+
+requireApp('system/js/lockscreen.js');
+
+var mocksForStatusBar = ['SettingsListener'];
+
+mocksForStatusBar.forEach(function(mockName) {
+ if (! window[mockName]) {
+ window[mockName] = null;
+ }
+});
+
+suite('lockscreen', function() {
+ var mocksHelper;
+
+ var realSettingsListener, realMozL10n;
+
+ var fakeLockscreenPanel;
+
+ var red_png, green_png;
+
+ suiteSetup(function() {
+ mocksHelper = new MocksHelper(mocksForStatusBar);
+ mocksHelper.suiteSetup();
+ realMozL10n = navigator.mozL10n;
+ navigator.mozL10n = MockL10n;
+
+ red_png =
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAHgAQMAAADuQiYHAAAAA1BMVEX/AAAZ4gk3AAAAKUlEQVR4Xu3AMQEAAADCIPuntsROWAQAAAAAAAAAAAAAAAAAAAAAAADgTOAAAZXle7kAAAAASUVORK5CYII=';
+ green_png =
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAHgAQMAAAAlmcL5AAAAA1BMVEUAgACc+aWRAAAAKUlEQVR4Xu3BMQEAAADCoPVPbQ0PoAAAAAAAAAAAAAAAAAAAAAAAAL4MSSAAAZTTyRkAAAAASUVORK5CYII=';
+ });
+
+ suiteTeardown(function() {
+ mocksHelper.suiteTeardown();
+ navigator.mozL10n = realMozL10n;
+ });
+
+ setup(function() {
+ fakeLockscreenPanel = document.createElement('div');
+ fakeLockscreenPanel.classList.add('lockscreen-panel');
+ fakeLockscreenPanel.setAttribute('data-wallpaper', '');
+ document.body.appendChild(fakeLockscreenPanel);
+
+ mocksHelper.setup();
+ });
+
+ teardown(function() {
+ fakeLockscreenPanel.parentNode.removeChild(fakeLockscreenPanel);
+ mocksHelper.teardown();
+ });
+
+ test('wallpaper has vignette effect', function(done) {
+ LockScreen.updateBackground(red_png);
+
+ (function checkCanvas() {
+ var canvas = fakeLockscreenPanel.getElementsByTagName('canvas')[0];
+ if (!canvas) {
+ setTimeout(checkCanvas, 10);
+ return;
+ }
+ var ctx = canvas.getContext('2d');
+ var top_pixel = ctx.getImageData(0, 0, 1, 1).data;
+
+ assert.equal(top_pixel[0], 77);
+ assert.equal(top_pixel[1], 0);
+ assert.equal(top_pixel[2], 0);
+
+ var center_width = Math.floor(canvas.width / 2);
+ var center_height = Math.floor(canvas.height / 2);
+ var center_pixel = ctx.getImageData(center_width, center_height,
+ 1 , 1).data;
+ assert.equal(center_pixel[0], 251);
+ assert.equal(center_pixel[1], 0);
+ assert.equal(center_pixel[2], 0);
+
+ done();
+ })();
+
+ });
+
+ test('multiple wallpaper updates only keep one canvas', function(done) {
+ function waitFirstUpdate(callback) {
+ var first_canvas = fakeLockscreenPanel.getElementsByTagName('canvas')[0];
+ if (!first_canvas) {
+ setTimeout(waitFirstUpdate, 10, callback);
+ return;
+ }
+
+ setTimeout(callback, 10);
+ }
+
+ function waitSecondUpdate(callback) {
+ var second_canvas = fakeLockscreenPanel.getElementsByTagName('canvas')[0];
+
+ var ctx = second_canvas.getContext('2d');
+ var top_pixel = ctx.getImageData(0, 0, 1, 1).data;
+ // Canvas is not green yet
+ if (top_pixel[1] == 0) {
+ setTimeout(waitSecondUpdate, 10, callback);
+ return;
+ }
+
+ setTimeout(callback, 10);
+ }
+
+ LockScreen.updateBackground(red_png);
+ setTimeout(waitFirstUpdate, 10, function then() {
+ LockScreen.updateBackground(green_png);
+
+ setTimeout(waitSecondUpdate, 10, function then2() {
+ assert.equal(fakeLockscreenPanel.getElementsByTagName('canvas').length, 1);
+ done();
+ });
+ });
+
+ });
+});
diff --git a/apps/system/test/unit/mobile_operator_test.js b/apps/system/test/unit/mobile_operator_test.js
new file mode 100644
index 0000000..6204217
--- /dev/null
+++ b/apps/system/test/unit/mobile_operator_test.js
@@ -0,0 +1,96 @@
+/* This should live in the shared directory */
+
+'use strict';
+
+requireApp('system/shared/js/mobile_operator.js');
+
+suite('shared/MobileOperator', function() {
+ var MockMobileConnection;
+ var BRAZIL_MCC = 724;
+
+
+ setup(function() {
+ MockMobileConnection = {
+ voice: {
+ network: {
+ shortName: 'Fake short',
+ longName: 'Fake long',
+ mnc: '6'
+ },
+ cell: { gsmLocationAreaCode: 71 }
+ },
+ iccInfo: { spn: 'Fake SPN' }
+ };
+ });
+
+ suite('Worldwide connection', function() {
+ test('Connection with short name', function() {
+ var infos = MobileOperator.userFacingInfo(MockMobileConnection);
+ assert.equal(infos.operator, 'Fake short');
+ assert.isUndefined(infos.carrier);
+ assert.isUndefined(infos.region);
+ });
+ test('Connection with long name', function() {
+ MockMobileConnection.voice.network.shortName = '';
+ var infos = MobileOperator.userFacingInfo(MockMobileConnection);
+ assert.equal(infos.operator, 'Fake long');
+ assert.isUndefined(infos.carrier);
+ assert.isUndefined(infos.region);
+ });
+ test('Connection with SPN display', function() {
+ MockMobileConnection.iccInfo.isDisplaySpnRequired = true;
+ var infos = MobileOperator.userFacingInfo(MockMobileConnection);
+ assert.equal(infos.operator, 'Fake SPN');
+ assert.isUndefined(infos.carrier);
+ assert.isUndefined(infos.region);
+ });
+ test('Connection with SPN display and network display', function() {
+ MockMobileConnection.iccInfo.isDisplaySpnRequired = true;
+ MockMobileConnection.iccInfo.isDisplayNetworkNameRequired = true;
+ var infos = MobileOperator.userFacingInfo(MockMobileConnection);
+ assert.equal(infos.operator, 'Fake short Fake SPN');
+ assert.isUndefined(infos.carrier);
+ assert.isUndefined(infos.region);
+ });
+ test('Connection with roaming', function() {
+ MockMobileConnection.voice.roaming = true;
+ var infos = MobileOperator.userFacingInfo(MockMobileConnection);
+ assert.equal(infos.operator, 'Fake short');
+ assert.isUndefined(infos.carrier);
+ assert.isUndefined(infos.region);
+ });
+ test('Connection with roaming and SPN display', function() {
+ MockMobileConnection.voice.roaming = true;
+ MockMobileConnection.iccInfo.isDisplaySpnRequired = true;
+ var infos = MobileOperator.userFacingInfo(MockMobileConnection);
+ assert.equal(infos.operator, 'Fake short');
+ assert.isUndefined(infos.carrier);
+ assert.isUndefined(infos.region);
+ });
+ });
+ suite('Brazilian connection', function() {
+ test('Connection ', function() {
+ MockMobileConnection.voice.network.mcc = BRAZIL_MCC;
+ var infos = MobileOperator.userFacingInfo(MockMobileConnection);
+ assert.equal(infos.operator, 'Fake short');
+ assert.equal(infos.carrier, 'VIVO');
+ assert.equal(infos.region, 'BA 71');
+ });
+ test('Connection with unknown mnc', function() {
+ MockMobileConnection.voice.network.mcc = BRAZIL_MCC;
+ MockMobileConnection.voice.network.mnc = 42;
+ var infos = MobileOperator.userFacingInfo(MockMobileConnection);
+ assert.equal(infos.operator, 'Fake short');
+ assert.equal(infos.carrier, '72442');
+ assert.equal(infos.region, 'BA 71');
+ });
+ test('Connection with unknown gsmLocationAreaCode', function() {
+ MockMobileConnection.voice.network.mcc = BRAZIL_MCC;
+ MockMobileConnection.voice.cell.gsmLocationAreaCode = 2;
+ var infos = MobileOperator.userFacingInfo(MockMobileConnection);
+ assert.equal(infos.operator, 'Fake short');
+ assert.equal(infos.carrier, 'VIVO');
+ assert.equal(infos.region, '');
+ });
+ });
+});
diff --git a/apps/system/test/unit/mock_app.js b/apps/system/test/unit/mock_app.js
new file mode 100644
index 0000000..2d34051
--- /dev/null
+++ b/apps/system/test/unit/mock_app.js
@@ -0,0 +1,93 @@
+'use strict';
+
+var idGen = 0;
+
+function MockApp(opts) {
+ /* default values */
+ this.origin = 'https://testapp.gaiamobile.org';
+ this.manifestURL = 'https://testapp.gaiamobile.org/manifest' +
+ idGen + '.webapp';
+ this.manifest = {
+ name: 'Mock app'
+ };
+
+ this.removable = true;
+ this.installState = 'installed';
+ this.downloadAvailable = false;
+ this.downloadError = null;
+ this.downloadSize = null;
+
+ this.mId = idGen++;
+ this.mDownloadCalled = false;
+ this.mCancelCalled = false;
+
+ /* overwrite default values with whatever comes in "opts" from the test */
+ if (opts) {
+ for (var key in opts) {
+ this[key] = opts[key];
+ }
+ }
+}
+
+MockApp.prototype.download = function() {
+ this.mDownloadCalled = true;
+};
+
+MockApp.prototype.cancelDownload = function() {
+ this.mCancelCalled = true;
+};
+
+MockApp.prototype.mTriggerDownloadAvailable = function(size) {
+ this.downloadAvailable = true;
+ this.downloadSize = size;
+ if (this.ondownloadavailable) {
+ this.ondownloadavailable({
+ application: this
+ });
+ }
+};
+
+MockApp.prototype.mTriggerDownloadSuccess = function() {
+ this.downloadAvailable = false;
+ this.downloadSize = null;
+ if (this.ondownloadsuccess) {
+ this.ondownloadsuccess({
+ application: this
+ });
+ }
+};
+
+MockApp.prototype.mTriggerDownloadError = function(error) {
+ this.downloadAvailable = true;
+ this.downloadSize = null;
+
+ this.downloadError = {
+ name: error || 'UNKNOWN_ERROR'
+ };
+
+ if (this.ondownloaderror) {
+ this.ondownloaderror({
+ application: this
+ });
+ }
+};
+
+MockApp.prototype.mTriggerDownloadProgress = function(progress) {
+ this.progress = progress;
+
+ if (this.onprogress) {
+ this.onprogress({
+ application: this
+ });
+ }
+};
+
+MockApp.prototype.mTriggerDownloadApplied = function() {
+ this.downloadAvailable = false;
+ this.downloadSize = null;
+ if (this.ondownloadapplied) {
+ this.ondownloadapplied({
+ application: this
+ });
+ }
+};
diff --git a/apps/system/test/unit/mock_applications.js b/apps/system/test/unit/mock_applications.js
new file mode 100644
index 0000000..8b3e765
--- /dev/null
+++ b/apps/system/test/unit/mock_applications.js
@@ -0,0 +1,26 @@
+var MockApplications = (function() {
+ var mockApps = {};
+
+ function getByManifestURL(url) {
+ return mockApps[url];
+ }
+
+ function mRegisterMockApp(mockApp) {
+ mockApps[mockApp.manifestURL] = mockApp;
+ }
+
+ function mUnregisterMockApp(mockApp) {
+ mockApps[mockApp.manifestURL] = null;
+ }
+
+ function mTeardown() {
+ mockApps = {};
+ }
+
+ return {
+ getByManifestURL: getByManifestURL,
+ mRegisterMockApp: mRegisterMockApp,
+ mUnregisterMockApp: mUnregisterMockApp,
+ mTeardown: mTeardown
+ };
+})();
diff --git a/apps/system/test/unit/mock_apps_mgmt.js b/apps/system/test/unit/mock_apps_mgmt.js
new file mode 100644
index 0000000..948ef02
--- /dev/null
+++ b/apps/system/test/unit/mock_apps_mgmt.js
@@ -0,0 +1,64 @@
+var MockAppsMgmt = {
+ getAll: function mam_getAll() {
+ var request = {};
+
+ setTimeout((function nextTick() {
+ if (request.onsuccess) {
+ var evt = {
+ target: {
+ result: this.mApps
+ }
+ };
+ request.onsuccess(evt);
+ if (this.mNext) {
+ this.mNext();
+ }
+ }
+ }).bind(this));
+
+ return request;
+ },
+
+ applyDownload: function mam_applyDownload(app) {
+ this.mLastAppApplied = app;
+ },
+
+ mApps: [],
+ mLastAppApplied: null,
+ mNext: null,
+ mTeardown: function mam_mTeardown() {
+ this.mLastAppApplied = null;
+ this.mApps = [];
+ this.mNext = null;
+ },
+
+ mTriggerOninstall: function mam_mTriggerOninstall(app) {
+ if (this.oninstall) {
+ var evt = {
+ application: app
+ };
+ this.oninstall(evt);
+ }
+
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('applicationinstall',
+ true, false,
+ { application: app });
+ window.dispatchEvent(evt);
+ },
+
+ mTriggerOnuninstall: function mam_mTriggerOnuninstall(app) {
+ if (this.onuninstall) {
+ var evt = {
+ application: app
+ };
+ this.onuninstall(evt);
+ }
+
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('applicationuninstall',
+ true, false,
+ { application: app });
+ window.dispatchEvent(evt);
+ }
+};
diff --git a/apps/system/test/unit/mock_asyncStorage.js b/apps/system/test/unit/mock_asyncStorage.js
new file mode 100644
index 0000000..a657e8b
--- /dev/null
+++ b/apps/system/test/unit/mock_asyncStorage.js
@@ -0,0 +1,36 @@
+'use strict';
+
+var MockasyncStorage = {
+ mItems: {},
+
+ setItem: function(key, value, callback) {
+ this.mItems[key] = value;
+ if (typeof callback === 'function') {
+ callback();
+ }
+ },
+
+ getItem: function(key, callback) {
+ var value = this.mItems[key];
+ // use '|| null' will turn a 'false' to null
+ if (value === undefined)
+ value = null;
+ if (typeof callback === 'function') {
+ callback(value);
+ }
+ },
+
+ removeItem: function(key, callback) {
+ if (key in this.mItems) {
+ delete this.mItems[key];
+ }
+
+ if (typeof callback === 'function') {
+ callback();
+ }
+ },
+
+ mTeardown: function() {
+ this.mItems = {};
+ }
+};
diff --git a/apps/system/test/unit/mock_chrome_event.js b/apps/system/test/unit/mock_chrome_event.js
new file mode 100644
index 0000000..40555f9
--- /dev/null
+++ b/apps/system/test/unit/mock_chrome_event.js
@@ -0,0 +1,4 @@
+function MockChromeEvent(detail) {
+ this.type = 'mozChromeEvent';
+ this.detail = detail;
+}
diff --git a/apps/system/test/unit/mock_custom_dialog.js b/apps/system/test/unit/mock_custom_dialog.js
new file mode 100644
index 0000000..ed4b927
--- /dev/null
+++ b/apps/system/test/unit/mock_custom_dialog.js
@@ -0,0 +1,26 @@
+var MockCustomDialog = {
+ show: function(title, msg, cancel, confirm) {
+ this.mShown = true;
+ this.mShowedTitle = title;
+ this.mShowedMsg = msg;
+ this.mShowedCancel = cancel;
+ this.mShowedConfirm = confirm;
+ },
+
+ hide: function() {
+ this.mShown = false;
+ },
+
+ mShown: false,
+ mShowedTitle: null,
+ mShowedMsg: null,
+ mShowedCancel: null,
+ mShowedConfirm: null,
+ mTeardown: function teardown() {
+ this.mShown = false;
+ this.mShowedTitle = null;
+ this.mShowedMsg = null;
+ this.mShowedCancel = null;
+ this.mShowedConfirm = null;
+ }
+};
diff --git a/apps/system/test/unit/mock_gesture_detector.js b/apps/system/test/unit/mock_gesture_detector.js
new file mode 100644
index 0000000..b66e51c
--- /dev/null
+++ b/apps/system/test/unit/mock_gesture_detector.js
@@ -0,0 +1,9 @@
+'use strict';
+
+var MockGestureDetector = function() {};
+
+MockGestureDetector.prototype = {
+ startDetecting: function() {},
+ stopDetecting: function() {}
+};
+
diff --git a/apps/system/test/unit/mock_l10n.js b/apps/system/test/unit/mock_l10n.js
new file mode 100644
index 0000000..35b01d8
--- /dev/null
+++ b/apps/system/test/unit/mock_l10n.js
@@ -0,0 +1,17 @@
+'use strict';
+
+var MockL10n = {
+ get: function get(key, params) {
+ if (params) {
+ return key + JSON.stringify(params);
+ }
+ return key;
+ },
+ DateTimeFormat: function() {}
+};
+
+MockL10n.DateTimeFormat.prototype = {
+ localeFormat: function mockLocaleFormat(time, strFormat) {
+ return '' + time;
+ }
+};
diff --git a/apps/system/test/unit/mock_manifest_helper.js b/apps/system/test/unit/mock_manifest_helper.js
new file mode 100644
index 0000000..34f9a13
--- /dev/null
+++ b/apps/system/test/unit/mock_manifest_helper.js
@@ -0,0 +1,5 @@
+MockManifestHelper = function(manifest) {
+ for (var prop in manifest) {
+ this[prop] = manifest[prop];
+ }
+};
diff --git a/apps/system/test/unit/mock_mobile_operator.js b/apps/system/test/unit/mock_mobile_operator.js
new file mode 100644
index 0000000..430a613
--- /dev/null
+++ b/apps/system/test/unit/mock_mobile_operator.js
@@ -0,0 +1,15 @@
+'use strict';
+
+var MockMobileOperator = {
+ userFacingInfo: function mmo_userFacingInfo(mobileConnection) {
+ return {
+ 'operator': this.mOperator,
+ 'carrier': this.mCarrier,
+ 'region': this.mRegion
+ }
+ },
+
+ mOperator: '',
+ mCarrier: '',
+ mRegion: ''
+};
diff --git a/apps/system/test/unit/mock_modal_dialog.js b/apps/system/test/unit/mock_modal_dialog.js
new file mode 100644
index 0000000..beb452e
--- /dev/null
+++ b/apps/system/test/unit/mock_modal_dialog.js
@@ -0,0 +1,34 @@
+var MockModalDialog = {
+
+ mMethods: [
+ 'alert'
+ ],
+
+ mPopulate: function mmd_mPopulate() {
+ this.mMethods.forEach(function(methodName) {
+ this[methodName] = function mmd_method() {
+ this.mMethodCalled(methodName, Array.slice(arguments));
+ };
+ }, this);
+ },
+
+ init: function mmd_init() {
+ this.mMethods.forEach(function(methodName) {
+ this[methodName].mWasCalled = false;
+ this[methodName].mArgs = null;
+ }, this);
+ },
+
+ mMethodCalled: function mmd_mMethodCalled(name, args) {
+ this[name].mWasCalled = true;
+ this[name].mArgs = args;
+ },
+
+ mTeardown: function mmd_mTeardown() {
+ this.init();
+ }
+};
+
+MockModalDialog.mPopulate();
+
+
diff --git a/apps/system/test/unit/mock_navigator_battery.js b/apps/system/test/unit/mock_navigator_battery.js
new file mode 100644
index 0000000..5145de1
--- /dev/null
+++ b/apps/system/test/unit/mock_navigator_battery.js
@@ -0,0 +1,55 @@
+'use strict';
+
+(function() {
+
+ var props = ['level', 'charging'];
+
+ var listeners;
+
+ function mnb_init() {
+ props.forEach(function(prop) {
+ Mock[prop] = null;
+ });
+
+ listeners = {};
+ }
+
+ function mnb_addEventListener(evtName, func) {
+ listeners[evtName] = listeners[evtName] || [];
+ listeners[evtName].push(func);
+ }
+
+ function mnb_removeEventListener(evtName, func) {
+ if (listeners[evtName]) {
+ var listenerArray = listeners[evtName];
+ var index = listenerArray.indexOf(func);
+ if (index !== -1) {
+ listenerArray.splice(index, 1);
+ }
+ }
+ }
+
+ function mnb_mTriggerEvent(evt) {
+ var evtName = evt.type;
+ if (listeners[evtName]) {
+ listeners[evtName].forEach(function(listener) {
+ if (listener.handleEvent) {
+ listener.handleEvent(evt);
+ } else {
+ listener.call(Mock, evt);
+ }
+ });
+ }
+ }
+
+ var Mock = {
+ addEventListener: mnb_addEventListener,
+ removeEventListener: mnb_removeEventListener,
+ mTeardown: mnb_init,
+ mTriggerEvent: mnb_mTriggerEvent
+ };
+
+ mnb_init();
+
+ window.MockNavigatorBattery = Mock;
+})();
diff --git a/apps/system/test/unit/mock_navigator_moz_mobile_connection.js b/apps/system/test/unit/mock_navigator_moz_mobile_connection.js
new file mode 100644
index 0000000..fb05aaa
--- /dev/null
+++ b/apps/system/test/unit/mock_navigator_moz_mobile_connection.js
@@ -0,0 +1,21 @@
+'use strict';
+
+(function() {
+
+ var props = ['voice', 'cardState', 'iccInfo', 'data'];
+
+ function mnmmc_init() {
+ props.forEach(function(prop) {
+ Mock[prop] = null;
+ });
+ }
+
+ var Mock = {
+ addEventListener: function() {},
+ mTeardown: mnmmc_init
+ };
+
+ mnmmc_init();
+
+ window.MockNavigatorMozMobileConnection = Mock;
+})();
diff --git a/apps/system/test/unit/mock_navigator_moz_telephony.js b/apps/system/test/unit/mock_navigator_moz_telephony.js
new file mode 100644
index 0000000..21f461c
--- /dev/null
+++ b/apps/system/test/unit/mock_navigator_moz_telephony.js
@@ -0,0 +1,55 @@
+'use strict';
+
+(function() {
+
+ var props = ['active', 'calls'];
+
+ var listeners;
+
+ function mnmt_init() {
+ props.forEach(function(prop) {
+ Mock[prop] = null;
+ });
+
+ listeners = {};
+ }
+
+ function mnmt_addEventListener(evtName, func) {
+ listeners[evtName] = listeners[evtName] || [];
+ listeners[evtName].push(func);
+ }
+
+ function mnmt_removeEventListener(evtName, func) {
+ if (listeners[evtName]) {
+ var listenerArray = listeners[evtName];
+ var index = listenerArray.indexOf(func);
+ if (index !== -1) {
+ listenerArray.splice(index, 1);
+ }
+ }
+ }
+
+ function mnmt_mTriggerEvent(evt) {
+ var evtName = evt.type;
+ if (listeners[evtName]) {
+ listeners[evtName].forEach(function(listener) {
+ if (listener.handleEvent) {
+ listener.handleEvent(evt);
+ } else {
+ listener.call(Mock, evt);
+ }
+ });
+ }
+ }
+
+ var Mock= {
+ addEventListener: mnmt_addEventListener,
+ removeEventListener: mnmt_removeEventListener,
+ mTeardown: mnmt_init,
+ mTriggerEvent: mnmt_mTriggerEvent
+ };
+
+ mnmt_init();
+
+ window.MockNavigatorMozTelephony = Mock;
+})();
diff --git a/apps/system/test/unit/mock_navigator_settings.js b/apps/system/test/unit/mock_navigator_settings.js
new file mode 100644
index 0000000..8b0de8a
--- /dev/null
+++ b/apps/system/test/unit/mock_navigator_settings.js
@@ -0,0 +1,64 @@
+(function(window) {
+ var observers = {},
+ settings = {},
+ removedObservers = {};
+
+ function mns_mLockSet(obj) {
+ for (var key in obj) {
+ settings[key] = obj[key];
+ }
+ }
+
+ function mns_addObserver(name, cb) {
+ observers[name] = observers[name] || [];
+ observers[name].push(cb);
+ }
+
+ function mns_removeObserver(name, cb) {
+ removedObservers[name] = removedObservers[name] || [];
+ removedObservers[name].push(cb);
+ }
+
+ function mns_createLock() {
+ return {
+ set: mns_mLockSet
+ };
+ }
+
+ function mns_mTriggerObservers(name, args) {
+ var theseObservers = observers[name];
+
+ if (! theseObservers) {
+ return;
+ }
+
+ theseObservers.forEach(function(func) {
+ func(args);
+ });
+ }
+
+ function mns_teardown() {
+ observers = {};
+ settings = {};
+ removedObservers = {};
+ }
+
+ window.MockNavigatorSettings = {
+ addObserver: mns_addObserver,
+ removeObserver: mns_removeObserver,
+ createLock: mns_createLock,
+
+ mTriggerObservers: mns_mTriggerObservers,
+ mTeardown: mns_teardown,
+ get mObservers() {
+ return observers;
+ },
+ get mSettings() {
+ return settings;
+ },
+ get mRemovedObservers() {
+ return removedObservers;
+ }
+ };
+
+})(this);
diff --git a/apps/system/test/unit/mock_navigator_wake_lock.js b/apps/system/test/unit/mock_navigator_wake_lock.js
new file mode 100644
index 0000000..c9ff845
--- /dev/null
+++ b/apps/system/test/unit/mock_navigator_wake_lock.js
@@ -0,0 +1,41 @@
+'use strict';
+
+(function() {
+ var lastWakeLock,
+ throwAtNextUnlock;
+
+ function mnwl_requestWakeLock(lock) {
+ lastWakeLock = {
+ released: false,
+ topic: lock,
+ unlock: function() {
+ if (throwAtNextUnlock) {
+ throwAtNextUnlock = false;
+ throw "NS_ERROR_DOM_INVALID_STATE_ERR";
+ }
+
+ this.released = true;
+ }
+ };
+ return lastWakeLock;
+ }
+
+ function mnwl_teardown() {
+ lastWakeLock = undefined;
+ throwAtNextUnlock = undefined;
+ }
+
+ function mnwl_throwAtNextUnlock() {
+ throwAtNextUnlock = true;
+ }
+
+ window.MockNavigatorWakeLock = {
+ requestWakeLock: mnwl_requestWakeLock,
+ mTeardown: mnwl_teardown,
+ mThrowAtNextUnlock: mnwl_throwAtNextUnlock,
+ get mLastWakeLock() {
+ return lastWakeLock;
+ }
+ };
+
+})();
diff --git a/apps/system/test/unit/mock_notification_helper.js b/apps/system/test/unit/mock_notification_helper.js
new file mode 100644
index 0000000..adb80f9
--- /dev/null
+++ b/apps/system/test/unit/mock_notification_helper.js
@@ -0,0 +1,22 @@
+var MockNotificationHelper = {
+ send: function(title, body, icon, clickCB, closeCB) {
+ this.mTitle = title;
+ this.mBody = body;
+ this.mIcon = icon;
+ this.mClickCB = clickCB;
+ this.mCloseCB = closeCB;
+ },
+
+ mTitle: null,
+ mBody: null,
+ mIcon: null,
+ mClickCB: null,
+ mCloseCB: null,
+ mTeardown: function teardown() {
+ this.mTitle = null;
+ this.mBody = null;
+ this.mIcon = null;
+ this.mClickCB = null;
+ this.mCloseCB = null;
+ }
+};
diff --git a/apps/system/test/unit/mock_notification_screen.js b/apps/system/test/unit/mock_notification_screen.js
new file mode 100644
index 0000000..2c88a90
--- /dev/null
+++ b/apps/system/test/unit/mock_notification_screen.js
@@ -0,0 +1,38 @@
+var MockNotificationScreen = {
+ wasMethodCalled: {},
+
+ mockMethods: [
+ 'incExternalNotifications',
+ 'decExternalNotifications',
+ 'updateStatusBarIcon'
+ ],
+
+ mockPopulate: function mockPopulate() {
+ this.mockMethods.forEach(function(methodName) {
+ // we could probably put this method outside if we had a closure
+ this[methodName] = function mns_method() {
+ this.methodCalled(methodName);
+ };
+ }, this);
+ },
+
+ init: function mns_init() {
+ this.wasMethodCalled = {};
+ this.mockMethods.forEach(function(methodName) {
+ this[methodName].wasCalled = false;
+ }, this);
+ },
+
+ methodCalled: function mns_methodCalled(name) {
+ this.wasMethodCalled[name] =
+ this.wasMethodCalled[name] ? this.wasMethodCalled[name]++ : 1;
+ this[name].wasCalled = true;
+ },
+
+ mTeardown: function mns_mTeardown() {
+ this.init();
+ }
+};
+
+MockNotificationScreen.mockPopulate();
+
diff --git a/apps/system/test/unit/mock_settings_listener.js b/apps/system/test/unit/mock_settings_listener.js
new file mode 100644
index 0000000..6a4da7f
--- /dev/null
+++ b/apps/system/test/unit/mock_settings_listener.js
@@ -0,0 +1,16 @@
+var MockSettingsListener = {
+ observe: function msl_observe(name, defaultValue, cb) {
+ this.mName = name;
+ this.mDefaultValue = defaultValue;
+ this.mCallback = cb;
+ },
+
+ mName: null,
+ mDefaultValue: null,
+ mCallback: null,
+ mTeardown: function teardown() {
+ this.mName = null;
+ this.mDefaultValue = null;
+ this.mDefaultCallback = null;
+ }
+};
diff --git a/apps/system/test/unit/mock_sleep_menu.js b/apps/system/test/unit/mock_sleep_menu.js
new file mode 100644
index 0000000..1b883dc
--- /dev/null
+++ b/apps/system/test/unit/mock_sleep_menu.js
@@ -0,0 +1,5 @@
+'use strict';
+
+var MockSleepMenu = {
+ startPowerOff: function(){}
+};
diff --git a/apps/system/test/unit/mock_statusbar.js b/apps/system/test/unit/mock_statusbar.js
new file mode 100644
index 0000000..8813a3b
--- /dev/null
+++ b/apps/system/test/unit/mock_statusbar.js
@@ -0,0 +1,36 @@
+var MockStatusBar = {
+ notificationsCount: null,
+
+ wasMethodCalled: {},
+
+ methodCalled: function msb_methodCalled(name) {
+ this.wasMethodCalled[name] =
+ this.wasMethodCalled[name] ? this.wasMethodCalled[name]++ : 1;
+ },
+
+ updateNotification: function(count) {
+ var number = new Number(count);
+ this.notificationsCount = number.toString();
+ this.methodCalled('updateNotification');
+ },
+
+ updateNotificationUnread: function(unread) {
+ this.mNotificationUnread = unread;
+ },
+
+ mNotificationUnread: false,
+ mTeardown: function teardown() {
+ this.notificationsCount = null;
+ this.mNotificationsUpdated = false;
+ this.mNotificationUnread = false;
+ this.wasMethodCalled = {};
+ },
+
+ incSystemDownloads: function msb_incSystemDownloads() {
+ this.methodCalled('incSystemDownloads');
+ },
+
+ decSystemDownloads: function msb_decSystemDownloads() {
+ this.methodCalled('decSystemDownloads');
+ }
+};
diff --git a/apps/system/test/unit/mock_system_banner.js b/apps/system/test/unit/mock_system_banner.js
new file mode 100644
index 0000000..e40d26a
--- /dev/null
+++ b/apps/system/test/unit/mock_system_banner.js
@@ -0,0 +1,13 @@
+var MockSystemBanner = {
+ show: function(message) {
+ this.mShowCount++;
+ this.mMessage = message;
+ },
+
+ mShowCount: 0,
+ mMessage: null,
+ mTeardown: function teardown() {
+ this.mShowCount = 0;
+ this.mMessage = null;
+ }
+};
diff --git a/apps/system/test/unit/mock_trusted_ui_manager.js b/apps/system/test/unit/mock_trusted_ui_manager.js
new file mode 100644
index 0000000..672ed47
--- /dev/null
+++ b/apps/system/test/unit/mock_trusted_ui_manager.js
@@ -0,0 +1,25 @@
+'use strict';
+
+var MockTrustedUIManager = {
+ open: function(name, frame, chromeEventId) {
+ this.mOpened = true;
+ this.mName = name;
+ this.mFrame = frame;
+ this.mChromeEventId = chromeEventId;
+ },
+
+ close: function() {
+ this.mOpened = false;
+ },
+
+ mOpened: false,
+ mName: null,
+ mFrame: null,
+ mChromeEventId: null,
+ mTeardown: function teardown() {
+ this.mOpened = false;
+ this.mName = null;
+ this.mFrame = null;
+ this.mChromeEventId = null;
+ }
+};
diff --git a/apps/system/test/unit/mock_updatable.js b/apps/system/test/unit/mock_updatable.js
new file mode 100644
index 0000000..33f5fb8
--- /dev/null
+++ b/apps/system/test/unit/mock_updatable.js
@@ -0,0 +1,72 @@
+'use strict';
+
+function MockAppUpdatable(aApp) {
+ this.app = aApp;
+
+ this.mDownloadCalled = false;
+ this.mCancelCalled = false;
+ this.mUninitCalled = false;
+ MockAppUpdatable.mCount++;
+}
+
+MockAppUpdatable.mTeardown = function() {
+ MockAppUpdatable.mCount = 0;
+};
+
+MockAppUpdatable.mCount = 0;
+
+MockAppUpdatable.prototype.uninit = function() {
+ this.mUninitCalled = true;
+};
+
+MockAppUpdatable.prototype.download = function() {
+ this.mDownloadCalled = true;
+};
+
+MockAppUpdatable.prototype.cancelDownload = function() {
+ this.mCancelCalled = true;
+};
+
+function MockSystemUpdatable() {
+ this.size = null;
+ this.name = 'systemUpdate';
+
+ this.mDownloadCalled = false;
+ this.mCancelCalled = false;
+ this.mUninitCalled = false;
+
+ MockSystemUpdatable.mInstancesCount++;
+}
+
+MockSystemUpdatable.mInstancesCount = 0;
+MockSystemUpdatable.mTeardown = function() {
+ MockSystemUpdatable.mInstancesCount = 0;
+ delete MockSystemUpdatable.mKnownUpdate;
+};
+
+
+MockSystemUpdatable.prototype.uninit = function() {
+ this.mUninitCalled = true;
+};
+
+MockSystemUpdatable.prototype.download = function() {
+ this.mDownloadCalled = true;
+};
+
+MockSystemUpdatable.prototype.cancelDownload = function() {
+ this.mCancelCalled = true;
+};
+
+MockSystemUpdatable.prototype.rememberKnownUpdate = function() {
+ this.mKnownUpdate = true;
+};
+
+MockSystemUpdatable.prototype.forgetKnownUpdate = function() {
+ delete this.mKnownUpdate;
+};
+
+MockSystemUpdatable.prototype.checkKnownUpdate = function(callback) {
+ if (this.mKnownUpdate && typeof callback === 'function') {
+ callback();
+ }
+};
diff --git a/apps/system/test/unit/mock_update_manager.js b/apps/system/test/unit/mock_update_manager.js
new file mode 100644
index 0000000..86cb4b9
--- /dev/null
+++ b/apps/system/test/unit/mock_update_manager.js
@@ -0,0 +1,60 @@
+'use strict';
+
+var MockUpdateManager = {
+ addToUpdatesQueue: function mum_addtoUpdateQueue(updatable) {
+ this.mLastUpdatesAdd = updatable;
+ },
+ addToUpdatableApps: function mum_addToUpdatableApps(updatable) {
+ this.mLastUpdatableAdd = updatable;
+ },
+
+ removeFromUpdatesQueue: function mum_removeFromUpdateQueue(updatable) {
+ this.mLastUpdatesRemoval = updatable;
+ },
+
+ addToDownloadsQueue: function mum_addtoActiveDownloads(updatable) {
+ this.mLastDownloadsAdd = updatable;
+ },
+ removeFromDownloadsQueue:
+ function mum_removeFromActiveDownloads(updatable) {
+
+ this.mLastDownloadsRemoval = updatable;
+ },
+
+ downloadProgressed: function mum_downloadProgressed(bytes) {
+ this.mProgressCalledWith = bytes;
+ },
+
+ startedUncompressing: function mum_startedUncompressing() {
+ this.mStartedUncompressingCalled = true;
+ },
+
+ requestErrorBanner: function mum_requestErrorBanner() {
+ this.mErrorBannerRequested = true;
+ },
+
+ checkForUpdates: function mum_checkForUpdate(forced) {
+ this.mCheckForUpdatesCalledWith = forced;
+ },
+
+ mErrorBannerRequested: false,
+ mLastUpdatesAdd: null,
+ mLastUpdatableAdd: null,
+ mLastUpdatesRemoval: null,
+ mLastDownloadsAdd: null,
+ mLastDownloadsRemoval: null,
+ mProgressCalledWith: null,
+ mCheckForUpdatesCalledWith: null,
+ mStartedUncompressingCalled: false,
+ mTeardown: function mum_mTeardown() {
+ this.mErrorBannerRequested = false;
+ this.mLastUpdatesAdd = null;
+ this.mLastUpdatableAdd = null;
+ this.mLastUpdatesRemoval = null;
+ this.mLastDownloadsAdd = null;
+ this.mLastDownloadsRemoval = null;
+ this.mProgressCalledWith = null;
+ this.mCheckForUpdatesCalledWith = null;
+ this.mStartedUncompressingCalled = false;
+ }
+};
diff --git a/apps/system/test/unit/mock_utility_tray.js b/apps/system/test/unit/mock_utility_tray.js
new file mode 100644
index 0000000..8bf8a8b
--- /dev/null
+++ b/apps/system/test/unit/mock_utility_tray.js
@@ -0,0 +1,14 @@
+var MockUtilityTray = {
+ show: function() {
+ this.mShown = true;
+ },
+
+ hide: function() {
+ this.mShown = false;
+ },
+
+ mShown: false,
+ mTeardown: function teardown() {
+ this.mShown = false;
+ }
+};
diff --git a/apps/system/test/unit/mock_window_manager.js b/apps/system/test/unit/mock_window_manager.js
new file mode 100644
index 0000000..331e254
--- /dev/null
+++ b/apps/system/test/unit/mock_window_manager.js
@@ -0,0 +1,16 @@
+var MockWindowManager = {
+ getDisplayedApp: function mwm_getDisplayedApp() {
+ return this.mDisplayedApp;
+ },
+
+ kill: function mwm_kill(origin) {
+ this.mLastKilledOrigin = origin;
+ },
+
+ mDisplayedApp: '',
+ mLastKilledOrigin: '',
+ mTeardown: function() {
+ this.mDisplayedApp = '';
+ this.mLastKilledOrigin = '';
+ }
+};
diff --git a/apps/system/test/unit/mocks_helper.js b/apps/system/test/unit/mocks_helper.js
new file mode 100644
index 0000000..40c8689
--- /dev/null
+++ b/apps/system/test/unit/mocks_helper.js
@@ -0,0 +1,40 @@
+var MocksHelper = function(mocks) {
+ this.mocks = mocks;
+ this.realWindowObjects = {};
+};
+
+MocksHelper.prototype = {
+
+ setup: function mh_setup() {
+ },
+
+ suiteSetup: function mh_suiteSetup() {
+ this.mocks.forEach(function(objName) {
+ var mockName = 'Mock' + objName;
+ if (!window[mockName]) {
+ throw 'Mock ' + mockName + ' has not been loaded into the test';
+ }
+
+ this.realWindowObjects[objName] = window[objName];
+ window[objName] = window[mockName];
+ }, this);
+ },
+
+ suiteTeardown: function mh_suiteTeardown() {
+ this.mocks.forEach(function(objName) {
+ window[objName] = this.realWindowObjects[objName];
+ }, this);
+ },
+
+ teardown: function mh_teardown() {
+ this.mocks.forEach(function(objName) {
+ var mockName = 'Mock' + objName;
+ var mock = window[mockName];
+
+ if (mock.mTeardown) {
+ mock.mTeardown();
+ }
+ });
+ }
+};
+
diff --git a/apps/system/test/unit/notifications_test.js b/apps/system/test/unit/notifications_test.js
new file mode 100644
index 0000000..e323f07
--- /dev/null
+++ b/apps/system/test/unit/notifications_test.js
@@ -0,0 +1,132 @@
+'use strict';
+
+requireApp('system/test/unit/mock_statusbar.js');
+requireApp('system/test/unit/mock_gesture_detector.js');
+requireApp('system/test/unit/mock_settings_listener.js');
+requireApp('system/test/unit/mocks_helper.js');
+
+requireApp('system/js/notifications.js');
+
+var mocksForNotificationScreen = ['StatusBar', 'GestureDetector',
+ 'SettingsListener'];
+
+mocksForNotificationScreen.forEach(function(mockName) {
+ if (! window[mockName]) {
+ window[mockName] = null;
+ }
+});
+
+
+suite('system/NotificationScreen >', function() {
+ var fakeNotifContainer, fakeLockScreenContainer, fakeToaster,
+ fakeButton, fakeToasterIcon, fakeToasterTitle, fakeToasterDetail;
+
+ var mocksHelper;
+
+ suiteSetup(function() {
+ mocksHelper = new MocksHelper(mocksForNotificationScreen);
+ mocksHelper.suiteSetup();
+ });
+
+ suiteTeardown(function() {
+ mocksHelper.suiteTeardown();
+ });
+
+ setup(function() {
+ fakeNotifContainer = document.createElement('div');
+ fakeNotifContainer.id = 'desktop-notifications-container';
+ // add some children, we don't care what they are
+ fakeNotifContainer.appendChild(document.createElement('div'));
+ fakeNotifContainer.appendChild(document.createElement('div'));
+
+ function createFakeElement(tag, id) {
+ var obj = document.createElement(tag);
+ obj.id = id;
+ return obj;
+ };
+
+ fakeLockScreenContainer = createFakeElement('div',
+ 'notifications-lockscreen-container');
+ fakeToaster = createFakeElement('div', 'notification-toaster');
+ fakeButton = createFakeElement('button', 'notification-clear');
+ fakeToasterIcon = createFakeElement('img', 'toaster-icon');
+ fakeToasterTitle = createFakeElement('div', 'toaster-title');
+ fakeToasterDetail = createFakeElement('div', 'toaster-detail');
+
+ document.body.appendChild(fakeNotifContainer);
+
+ document.body.appendChild(fakeLockScreenContainer);
+ document.body.appendChild(fakeToaster);
+ document.body.appendChild(fakeButton);
+ document.body.appendChild(fakeToasterIcon);
+ document.body.appendChild(fakeToasterTitle);
+ document.body.appendChild(fakeToasterDetail);
+
+ mocksHelper.setup();
+
+ NotificationScreen.init();
+ });
+
+ teardown(function() {
+ fakeNotifContainer.parentNode.removeChild(fakeNotifContainer);
+ fakeLockScreenContainer.parentNode.removeChild(fakeLockScreenContainer);
+ fakeToaster.parentNode.removeChild(fakeToaster);
+ fakeButton.parentNode.removeChild(fakeButton);
+
+ mocksHelper.teardown();
+ });
+
+ suite('updateStatusBarIcon >', function() {
+ setup(function() {
+ NotificationScreen.updateStatusBarIcon();
+ });
+
+ test('should update the icon in the status bar', function() {
+ assert.ok(MockStatusBar.wasMethodCalled['updateNotification']);
+ assert.equal(2, MockStatusBar.notificationsCount);
+ });
+
+ test('external notif should not be able to decrease the global count',
+ function() {
+
+ NotificationScreen.decExternalNotifications();
+ assert.equal(2, MockStatusBar.notificationsCount);
+ });
+
+ test('external notif should increase the global count',
+ function() {
+
+ NotificationScreen.incExternalNotifications();
+ assert.isTrue(MockStatusBar.mNotificationUnread);
+ assert.equal(3, MockStatusBar.notificationsCount);
+ });
+
+ test('external notif should decrease the global count',
+ function() {
+
+ NotificationScreen.incExternalNotifications();
+ MockStatusBar.mNotificationUnread = false;
+ NotificationScreen.decExternalNotifications();
+ assert.isFalse(MockStatusBar.mNotificationUnread);
+ assert.equal(2, MockStatusBar.notificationsCount);
+ });
+
+ test('should change the read status', function() {
+ NotificationScreen.updateStatusBarIcon(true);
+ assert.isTrue(MockStatusBar.mNotificationUnread);
+ });
+
+ test('calling addNotification without icon', function() {
+ var toasterIcon = NotificationScreen.toasterIcon;
+ var imgpath = 'http://example.com/test.png';
+ var detail = {icon: imgpath, title: 'title', detail: 'detail'};
+ NotificationScreen.addNotification(detail);
+ assert.equal(imgpath, toasterIcon.src);
+ assert.isFalse(toasterIcon.hidden);
+ delete detail.icon;
+ NotificationScreen.addNotification(detail);
+ assert.isTrue(toasterIcon.hidden);
+ });
+ });
+
+});
diff --git a/apps/system/test/unit/statusbar_test.js b/apps/system/test/unit/statusbar_test.js
new file mode 100644
index 0000000..fba5c54
--- /dev/null
+++ b/apps/system/test/unit/statusbar_test.js
@@ -0,0 +1,421 @@
+'use strict';
+
+requireApp('system/test/unit/mock_settings_listener.js');
+requireApp('system/test/unit/mock_l10n.js');
+requireApp('system/test/unit/mock_navigator_moz_mobile_connection.js');
+requireApp('system/test/unit/mock_navigator_moz_telephony.js');
+requireApp('system/test/unit/mock_mobile_operator.js');
+requireApp('system/test/unit/mocks_helper.js');
+
+requireApp('system/js/statusbar.js');
+
+var mocksForStatusBar = ['SettingsListener', 'MobileOperator'];
+
+mocksForStatusBar.forEach(function(mockName) {
+ if (! window[mockName]) {
+ window[mockName] = null;
+ }
+});
+
+suite('system/Statusbar', function() {
+ var fakeStatusBarNode;
+ var mocksHelper;
+
+ var realSettingsListener, realMozL10n, realMozMobileConnection,
+ realMozTelephony,
+ fakeIcons = [];
+
+ suiteSetup(function() {
+ mocksHelper = new MocksHelper(mocksForStatusBar);
+ mocksHelper.suiteSetup();
+ realMozL10n = navigator.mozL10n;
+ navigator.mozL10n = MockL10n;
+ realMozMobileConnection = navigator.mozMobileConnection;
+ navigator.mozMobileConnection = MockNavigatorMozMobileConnection;
+ realMozTelephony = navigator.mozTelephony;
+ navigator.mozTelephony = MockNavigatorMozTelephony;
+ });
+
+ suiteTeardown(function() {
+ mocksHelper.suiteTeardown();
+ navigator.mozL10n = realMozL10n;
+ navigator.mozMobileConnection = realMozMobileConnection;
+ navigator.mozTelephony = realMozTelephony;
+ window.SettingsListener = realSettingsListener;
+ });
+
+ setup(function() {
+ mocksHelper.setup();
+ fakeStatusBarNode = document.createElement('div');
+ fakeStatusBarNode.id = 'statusbar';
+ document.body.appendChild(fakeStatusBarNode);
+
+ StatusBar.ELEMENTS.forEach(function testAddElement(elementName) {
+ var elt = document.createElement('div');
+ elt.id = 'statusbar-' + elementName;
+ elt.hidden = true;
+ fakeStatusBarNode.appendChild(elt);
+ fakeIcons[elementName] = elt;
+ });
+
+ // executing init again
+ StatusBar.init();
+ });
+ teardown(function() {
+ mocksHelper.teardown();
+ fakeStatusBarNode.parentNode.removeChild(fakeStatusBarNode);
+ MockNavigatorMozTelephony.mTeardown();
+ MockNavigatorMozMobileConnection.mTeardown();
+ });
+
+ suite('system-downloads', function() {
+ test('incrementing should display the icon', function() {
+ StatusBar.incSystemDownloads();
+ assert.isFalse(fakeIcons['system-downloads'].hidden);
+ });
+ test('incrementing then decrementing should not display the icon',
+ function() {
+ StatusBar.incSystemDownloads();
+ StatusBar.decSystemDownloads();
+ assert.isTrue(fakeIcons['system-downloads'].hidden);
+ });
+ test('incrementing twice then decrementing once should display the icon',
+ function() {
+ StatusBar.incSystemDownloads();
+ StatusBar.incSystemDownloads();
+ StatusBar.decSystemDownloads();
+ assert.isFalse(fakeIcons['system-downloads'].hidden);
+ });
+ test('incrementing then decrementing twice should not display the icon',
+ function() {
+ StatusBar.incSystemDownloads();
+ StatusBar.decSystemDownloads();
+ StatusBar.decSystemDownloads();
+ assert.isTrue(fakeIcons['system-downloads'].hidden);
+ });
+
+ /* JW: testing that we can't have a negative counter */
+ test('incrementing then decrementing twice then incrementing should ' +
+ 'display the icon', function() {
+ StatusBar.incSystemDownloads();
+ StatusBar.decSystemDownloads();
+ StatusBar.decSystemDownloads();
+ StatusBar.incSystemDownloads();
+ assert.isFalse(fakeIcons['system-downloads'].hidden);
+ });
+ });
+
+ suite('signal icon', function() {
+ var dataset;
+ setup(function() {
+ dataset = fakeIcons.signal.dataset;
+ });
+
+ test('no network without sim, not searching', function() {
+ MockNavigatorMozMobileConnection.voice = {
+ connected: false,
+ relSignalStrength: null,
+ emergencyCallsOnly: false,
+ state: 'notSearching',
+ roaming: false,
+ network: {}
+ };
+
+ MockNavigatorMozMobileConnection.cardState = 'absent';
+ MockNavigatorMozMobileConnection.iccInfo = {};
+
+ StatusBar.update.signal.call(StatusBar);
+
+ assert.notEqual(dataset.roaming, 'true');
+ assert.notEqual(dataset.emergency, 'true');
+ assert.isUndefined(dataset.level);
+ assert.notEqual(dataset.searching, 'true');
+ });
+
+ test('no network without sim, searching', function() {
+ MockNavigatorMozMobileConnection.voice = {
+ connected: false,
+ relSignalStrength: null,
+ emergencyCallsOnly: false,
+ state: 'searching',
+ roaming: false,
+ network: {}
+ };
+
+ MockNavigatorMozMobileConnection.cardState = 'absent';
+ MockNavigatorMozMobileConnection.iccInfo = {};
+
+ StatusBar.update.signal.call(StatusBar);
+
+ assert.notEqual(dataset.roaming, 'true');
+ assert.notEqual(dataset.emergency, 'true');
+ assert.isUndefined(dataset.level);
+ assert.notEqual(dataset.searching, 'true');
+ });
+
+ test('no network with sim', function() {
+ MockNavigatorMozMobileConnection.voice = {
+ connected: false,
+ relSignalStrength: null,
+ emergencyCallsOnly: false,
+ state: 'notSearching',
+ roaming: false,
+ network: {}
+ };
+
+ MockNavigatorMozMobileConnection.cardState = 'pinRequired';
+ MockNavigatorMozMobileConnection.iccInfo = {};
+
+ StatusBar.update.signal.call(StatusBar);
+
+ assert.notEqual(dataset.roaming, 'true');
+ assert.notEqual(dataset.emergency, 'true');
+ assert.equal(dataset.level, -1);
+ assert.notEqual(dataset.searching, 'true');
+ });
+
+ test('searching', function() {
+ MockNavigatorMozMobileConnection.voice = {
+ connected: false,
+ relSignalStrength: null,
+ emergencyCallsOnly: false,
+ state: 'searching',
+ roaming: false,
+ network: {}
+ };
+
+ MockNavigatorMozMobileConnection.cardState = 'ready';
+ MockNavigatorMozMobileConnection.iccInfo = {};
+
+ StatusBar.update.signal.call(StatusBar);
+
+ assert.notEqual(dataset.roaming, 'true');
+ assert.notEqual(dataset.emergency, 'true');
+ assert.equal(dataset.level, -1);
+ assert.equal(dataset.searching, 'true');
+ });
+
+ test('emergency calls only, no sim', function() {
+ MockNavigatorMozMobileConnection.voice = {
+ connected: false,
+ relSignalStrength: 80,
+ emergencyCallsOnly: true,
+ state: 'notSearching',
+ roaming: false,
+ network: {}
+ };
+
+ MockNavigatorMozMobileConnection.cardState = 'absent';
+ MockNavigatorMozMobileConnection.iccInfo = {};
+
+ StatusBar.update.signal.call(StatusBar);
+
+ assert.notEqual(dataset.roaming, 'true');
+ assert.notEqual(dataset.emergency, 'true');
+ assert.isUndefined(dataset.level);
+ assert.notEqual(dataset.searching, 'true');
+ });
+
+ test('emergency calls only, with sim', function() {
+ MockNavigatorMozMobileConnection.voice = {
+ connected: false,
+ relSignalStrength: 80,
+ emergencyCallsOnly: true,
+ state: 'notSearching',
+ roaming: false,
+ network: {}
+ };
+
+ MockNavigatorMozMobileConnection.cardState = 'pinRequired';
+ MockNavigatorMozMobileConnection.iccInfo = {};
+
+ StatusBar.update.signal.call(StatusBar);
+
+ assert.notEqual(dataset.roaming, 'true');
+ assert.equal(dataset.emergency, 'true');
+ assert.equal(dataset.level, '-1');
+ assert.notEqual(dataset.searching, 'true');
+ });
+
+ test('emergency calls only, in call', function() {
+ MockNavigatorMozMobileConnection.voice = {
+ connected: false,
+ relSignalStrength: 80,
+ emergencyCallsOnly: true,
+ state: 'notSearching',
+ roaming: false,
+ network: {}
+ };
+
+ MockNavigatorMozMobileConnection.cardState = 'pinRequired';
+ MockNavigatorMozMobileConnection.iccInfo = {};
+
+ MockNavigatorMozTelephony.active = {
+ state: 'connected'
+ };
+
+ StatusBar.update.signal.call(StatusBar);
+
+ assert.notEqual(dataset.roaming, 'true');
+ assert.equal(dataset.level, 4);
+ assert.notEqual(dataset.emergency, 'true');
+ assert.notEqual(dataset.searching, 'true');
+ });
+
+ test('emergency calls only, dialing', function() {
+ MockNavigatorMozMobileConnection.voice = {
+ connected: false,
+ relSignalStrength: 80,
+ emergencyCallsOnly: true,
+ state: 'notSearching',
+ roaming: false,
+ network: {}
+ };
+
+ MockNavigatorMozMobileConnection.cardState = 'pinRequired';
+ MockNavigatorMozMobileConnection.iccInfo = {};
+
+ MockNavigatorMozTelephony.active = {
+ state: 'dialing'
+ };
+
+ StatusBar.update.signal.call(StatusBar);
+
+ assert.notEqual(dataset.roaming, 'true');
+ assert.equal(dataset.level, 4);
+ assert.notEqual(dataset.emergency, 'true');
+ assert.notEqual(dataset.searching, 'true');
+ });
+
+ test('emergency calls, passing a call', function() {
+ MockNavigatorMozMobileConnection.voice = {
+ connected: false,
+ relSignalStrength: 80,
+ emergencyCallsOnly: true,
+ state: 'notSearching',
+ roaming: false,
+ network: {}
+ };
+
+ MockNavigatorMozMobileConnection.cardState = 'pinRequired';
+ MockNavigatorMozMobileConnection.iccInfo = {};
+
+ StatusBar.update.signal.call(StatusBar);
+
+ var activeCall = {
+ state: 'dialing'
+ };
+
+ MockNavigatorMozTelephony.active = activeCall;
+ MockNavigatorMozTelephony.calls = [activeCall];
+
+ var evt = new CustomEvent('callschanged');
+ MockNavigatorMozTelephony.mTriggerEvent(evt);
+
+ assert.notEqual(dataset.roaming, 'true');
+ assert.equal(dataset.level, 4);
+ assert.notEqual(dataset.emergency, 'true');
+ assert.notEqual(dataset.searching, 'true');
+ });
+
+ test('normal carrier', function() {
+ MockNavigatorMozMobileConnection.voice = {
+ connected: true,
+ relSignalStrength: 80,
+ emergencyCallsOnly: false,
+ state: 'notSearching',
+ roaming: false,
+ network: {}
+ };
+
+ MockNavigatorMozMobileConnection.cardState = 'ready';
+ MockNavigatorMozMobileConnection.iccInfo = {};
+
+ StatusBar.update.signal.call(StatusBar);
+
+ assert.notEqual(dataset.roaming, 'true');
+ assert.equal(dataset.level, 4);
+ assert.notEqual(dataset.emergency, 'true');
+ assert.notEqual(dataset.searching, 'true');
+ });
+
+ test('roaming', function() {
+ MockNavigatorMozMobileConnection.voice = {
+ connected: true,
+ relSignalStrength: 80,
+ emergencyCallsOnly: false,
+ state: 'notSearching',
+ roaming: true,
+ network: {}
+ };
+
+ MockNavigatorMozMobileConnection.cardState = 'ready';
+ MockNavigatorMozMobileConnection.iccInfo = {};
+
+ StatusBar.update.signal.call(StatusBar);
+
+ assert.equal(dataset.roaming, 'true');
+ assert.equal(dataset.level, 4);
+ assert.notEqual(dataset.emergency, 'true');
+ assert.notEqual(dataset.searching, 'true');
+ });
+
+ test('emergency calls, roaming', function() {
+ MockNavigatorMozMobileConnection.voice = {
+ connected: false,
+ relSignalStrength: 80,
+ emergencyCallsOnly: true,
+ state: 'notSearching',
+ roaming: true,
+ network: {}
+ };
+
+ MockNavigatorMozMobileConnection.cardState = 'ready';
+ MockNavigatorMozMobileConnection.iccInfo = {};
+
+ StatusBar.update.signal.call(StatusBar);
+
+ assert.notEqual(dataset.roaming, 'true');
+ assert.equal(dataset.level, -1);
+ assert.equal(dataset.emergency, 'true');
+ assert.notEqual(dataset.searching, 'true');
+ });
+ }),
+
+ suite('operator name', function() {
+ setup(function() {
+ MockNavigatorMozMobileConnection.voice = {
+ connected: true,
+ network: {
+ shortName: 'Fake short',
+ longName: 'Fake long',
+ mnc: 10 // VIVO
+ },
+ cell: {
+ gsmLocationAreaCode: 71 // BA
+ }
+ }
+
+ MockNavigatorMozMobileConnection.iccInfo = {
+ isDisplaySpnRequired: false,
+ spn: 'Fake SPN'
+ }
+ });
+
+ test('Connection without region', function() {
+ MobileOperator.mOperator = 'Orange';
+ var evt = new CustomEvent('iccinfochange');
+ StatusBar.handleEvent(evt);
+ assert.include(fakeIcons.label.textContent, 'Orange');
+ });
+ test('Connection with region', function() {
+ MobileOperator.mOperator = 'Orange';
+ MobileOperator.mRegion = 'PR';
+ var evt = new CustomEvent('iccinfochange');
+ StatusBar.handleEvent(evt);
+ var label_content = fakeIcons.label.textContent;
+ assert.include(label_content, 'Orange');
+ assert.include(label_content, 'PR');
+ });
+ });
+});
diff --git a/apps/system/test/unit/style/lockscreen/images/mask.png b/apps/system/test/unit/style/lockscreen/images/mask.png
new file mode 100644
index 0000000..e1b8cf5
--- /dev/null
+++ b/apps/system/test/unit/style/lockscreen/images/mask.png
Binary files differ
diff --git a/apps/system/test/unit/updatable_test.js b/apps/system/test/unit/updatable_test.js
new file mode 100644
index 0000000..7af2abf
--- /dev/null
+++ b/apps/system/test/unit/updatable_test.js
@@ -0,0 +1,630 @@
+'use strict';
+
+requireApp('system/js/updatable.js');
+
+requireApp('system/test/unit/mock_app.js');
+requireApp('system/test/unit/mock_asyncStorage.js');
+requireApp('system/test/unit/mock_update_manager.js');
+requireApp('system/test/unit/mock_window_manager.js');
+requireApp('system/test/unit/mock_apps_mgmt.js');
+requireApp('system/test/unit/mock_chrome_event.js');
+requireApp('system/test/unit/mock_custom_dialog.js');
+requireApp('system/test/unit/mock_utility_tray.js');
+requireApp('system/test/unit/mock_manifest_helper.js');
+requireApp('system/test/unit/mocks_helper.js');
+
+
+var mocksForUpdatable = [
+ 'CustomDialog',
+ 'UpdateManager',
+ 'WindowManager',
+ 'UtilityTray',
+ 'ManifestHelper',
+ 'asyncStorage'
+];
+
+mocksForUpdatable.forEach(function(mockName) {
+ if (!window[mockName]) {
+ window[mockName] = null;
+ }
+});
+
+suite('system/Updatable', function() {
+ var subject;
+ var mockApp;
+
+ var realDispatchEvent;
+ var realL10n;
+
+ var mocksHelper;
+
+ var lastDispatchedEvent = null;
+ var fakeDispatchEvent;
+
+ suiteSetup(function() {
+ realL10n = navigator.mozL10n;
+ navigator.mozL10n = {
+ get: function get(key) {
+ return key;
+ }
+ };
+
+ mocksHelper = new MocksHelper(mocksForUpdatable);
+ mocksHelper.suiteSetup();
+ });
+
+ suiteTeardown(function() {
+ navigator.mozL10n = realL10n;
+ mocksHelper.suiteTeardown();
+ });
+
+ setup(function() {
+ mockApp = new MockApp();
+ subject = new AppUpdatable(mockApp);
+ subject._mgmt = MockAppsMgmt;
+
+ fakeDispatchEvent = function(type, value) {
+ lastDispatchedEvent = {
+ type: type,
+ value: value
+ };
+ };
+ subject._dispatchEvent = fakeDispatchEvent;
+
+ mocksHelper.setup();
+ });
+
+ teardown(function() {
+ MockAppsMgmt.mTeardown();
+ mocksHelper.teardown();
+
+ subject._dispatchEvent = realDispatchEvent;
+ lastDispatchedEvent = null;
+ });
+
+ function downloadAvailableSuite(name, setupFunc) {
+ suite(name, function() {
+ setup(setupFunc);
+
+ test('should add self to the available downloads', function() {
+ assert.isNotNull(MockUpdateManager.mLastUpdatesAdd);
+ assert.equal(MockUpdateManager.mLastUpdatesAdd.app.mId,
+ mockApp.mId);
+ });
+
+ suite('first progress', function() {
+ setup(function() {
+ mockApp.mTriggerDownloadProgress(42);
+ });
+
+ test('should add self to active downloads', function() {
+ assert.isNotNull(MockUpdateManager.mLastDownloadsAdd);
+ assert.equal(MockUpdateManager.mLastDownloadsAdd.app.mId,
+ mockApp.mId);
+ });
+
+ test('should start with first progress value', function() {
+ assert.equal(42, subject.progress);
+ });
+ });
+ });
+ }
+
+ suite('init', function() {
+ test('should keep a reference to the app', function() {
+ assert.equal(mockApp, subject.app);
+ });
+
+ test('should handle fresh app with just an updateManifest', function() {
+ var freshApp = new MockApp();
+ freshApp.manifest = undefined;
+ subject = new AppUpdatable(freshApp);
+ assert.equal(freshApp, subject.app);
+ });
+
+ test('should add itself to updatable apps', function() {
+ assert.equal(MockUpdateManager.mLastUpdatableAdd, subject);
+ });
+
+ test('should remember about the update on startup', function() {
+ asyncStorage.mItems[SystemUpdatable.KNOWN_UPDATE_FLAG] = true;
+ var systemUpdatable = new SystemUpdatable();
+ assert.equal(MockUpdateManager.mCheckForUpdatesCalledWith, true);
+ });
+
+ downloadAvailableSuite('app has a download available', function() {
+ mockApp.downloadAvailable = true;
+ subject = new AppUpdatable(mockApp);
+ });
+
+ test('should apply update if downloaded', function() {
+ mockApp.readyToApplyDownload = true;
+ subject = new AppUpdatable(mockApp);
+ // We cannot test for this._mgmt methods because it's created in
+ // a constructor, so we check if the window is killed because
+ // WindowManager.kill() is also called in applyUpdate() method
+ assert.equal(MockWindowManager.mLastKilledOrigin, subject.app.origin);
+ });
+ });
+
+ suite('infos', function() {
+ suite('name', function() {
+ test('should give a name for system updates', function() {
+ subject = new SystemUpdatable(42);
+ assert.equal('systemUpdate', subject.name);
+ });
+
+ test('should give a name for app updates', function() {
+ assert.equal('Mock app', subject.name);
+ });
+ });
+
+ suite('size', function() {
+ test('should give packaged app update size', function() {
+ assert.equal(null, subject.size);
+ });
+
+ test('should return null for hosted apps', function() {
+ mockApp.updateManifest = null;
+ subject = new AppUpdatable(mockApp);
+ assert.isNull(subject.size);
+ });
+
+ test('should update size on download available', function() {
+ mockApp.updateManifest = null;
+ subject = new AppUpdatable(mockApp);
+ assert.isNull(subject.size);
+
+ mockApp.mTriggerDownloadAvailable(45678);
+ assert.equal(45678, subject.size);
+ });
+ });
+ });
+
+ suite('actions', function() {
+ suite('ask for download', function() {
+ setup(function() {
+ mockApp.mTriggerDownloadAvailable();
+ subject.download();
+ });
+
+ test('should call download on the app', function() {
+ assert.isTrue(mockApp.mDownloadCalled);
+ });
+ });
+
+ suite('download system update', function() {
+ setup(function() {
+ subject = new SystemUpdatable(42);
+ subject._dispatchEvent = fakeDispatchEvent;
+ subject.progress = 42;
+ subject.download();
+ });
+
+ test('should send download message for system updates', function() {
+ assert.equal('update-available-result', lastDispatchedEvent.type);
+ assert.equal('download', lastDispatchedEvent.value);
+ });
+
+ test('should add system updates to active downloads too', function() {
+ assert.isNotNull(MockUpdateManager.mLastDownloadsAdd);
+ assert.equal(subject, MockUpdateManager.mLastDownloadsAdd);
+ });
+
+ test('should start system updates with progress 0 too', function() {
+ assert.equal(subject.progress, 0);
+ });
+
+ test('should do nothing if already downloading', function() {
+ lastDispatchedEvent = null;
+ subject.progress = 42;
+ subject.download();
+
+ assert.equal(subject.progress, 42);
+ assert.isNull(lastDispatchedEvent);
+ });
+ });
+
+ suite('cancel app update download', function() {
+ setup(function() {
+ subject.cancelDownload();
+ });
+
+ test('should call cancelDownload on the app', function() {
+ assert.isTrue(mockApp.mCancelCalled);
+ });
+ });
+
+ suite('cancel system update download', function() {
+ setup(function() {
+ asyncStorage.setItem(SystemUpdatable.KNOWN_UPDATE_FLAG, true);
+ subject = new SystemUpdatable(42);
+ subject.download();
+ subject._dispatchEvent = fakeDispatchEvent;
+ subject.cancelDownload();
+ });
+
+ test('should send cancel message', function() {
+ assert.equal('update-download-cancel', lastDispatchedEvent.type);
+ });
+
+ test('should remove the downloading flag', function() {
+ assert.isFalse(subject.downloading);
+ });
+ });
+ });
+
+ suite('events', function() {
+ suite('apps events', function() {
+ // This function checks that we release the callbacks properly
+ // at the end of a download. Assumes subject.download() was called.
+ function testCleanup() {
+ test('should stop responding to progress', function() {
+ mockApp.mTriggerDownloadProgress(42);
+ assert.notEqual(subject.progress, 42);
+ });
+
+ test('should stop responding to error', function() {
+ MockUpdateManager.mErrorBannerRequested = false;
+ mockApp.mTriggerDownloadError();
+ assert.isFalse(MockUpdateManager.mErrorBannerRequested);
+ });
+
+ test('progress should be reset', function() {
+ assert.isNull(subject.progress);
+ });
+ }
+
+ downloadAvailableSuite('ondownloadavailable', function() {
+ mockApp.mTriggerDownloadAvailable();
+ });
+
+ suite('ondownloadavailable when not installed', function() {
+ setup(function() {
+ mockApp.installState = 'pending';
+ mockApp.mTriggerDownloadAvailable();
+ });
+
+ test('should not add self to the available downloads', function() {
+ assert.isNull(MockUpdateManager.mLastUpdatesAdd);
+ });
+
+ test('should not answer to progress', function() {
+ mockApp.mTriggerDownloadSuccess();
+ assert.isNull(MockUpdateManager.mLastDownloadsRemoval);
+ });
+ });
+
+ suite('downloadavailable at init when not installed', function() {
+ setup(function() {
+ mockApp.installState = 'pending';
+ subject = new AppUpdatable(mockApp);
+ mockApp.mTriggerDownloadAvailable();
+ });
+
+ test('should not add self to the available downloads', function() {
+ assert.isNull(MockUpdateManager.mLastUpdatesAdd);
+ });
+
+ test('should not answer to progress', function() {
+ mockApp.mTriggerDownloadSuccess();
+ assert.isNull(MockUpdateManager.mLastDownloadsRemoval);
+ });
+ });
+
+ suite('ondownloadsuccess', function() {
+ test('should remove self from active downloads', function() {
+ mockApp.mTriggerDownloadAvailable();
+ mockApp.mTriggerDownloadProgress(42);
+ mockApp.mTriggerDownloadSuccess();
+ assert.isNotNull(MockUpdateManager.mLastDownloadsRemoval);
+ assert.equal(MockUpdateManager.mLastDownloadsRemoval.app.mId,
+ mockApp.mId);
+ });
+
+ test('should not remove self if not downloading', function() {
+ mockApp.mTriggerDownloadSuccess();
+ assert.isNull(MockUpdateManager.mLastDownloadsRemoval);
+ });
+
+ test('should remove self from available downloads', function() {
+ mockApp.mTriggerDownloadAvailable();
+ mockApp.mTriggerDownloadProgress(42);
+ mockApp.mTriggerDownloadSuccess();
+ assert.isNotNull(MockUpdateManager.mLastUpdatesRemoval);
+ assert.equal(MockUpdateManager.mLastUpdatesRemoval.app.mId,
+ mockApp.mId);
+ });
+
+ suite('application of the download', function() {
+ test('should apply if the app is not in foreground', function() {
+ mockApp.mTriggerDownloadAvailable();
+ MockWindowManager.mDisplayedApp =
+ 'http://homescreen.gaiamobile.org';
+ mockApp.mTriggerDownloadSuccess();
+ assert.isNotNull(MockAppsMgmt.mLastAppApplied);
+ assert.equal(MockAppsMgmt.mLastAppApplied.mId, mockApp.mId);
+ });
+
+ test('should wait for appwillclose if it is', function() {
+ var origin = 'http://testapp.gaiamobile.org';
+ mockApp.origin = origin;
+ MockWindowManager.mDisplayedApp = origin;
+
+ mockApp.mTriggerDownloadAvailable();
+ mockApp.mTriggerDownloadSuccess();
+ assert.isNull(MockAppsMgmt.mLastAppApplied);
+
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('appwillclose', true, false,
+ { origin: origin });
+ window.dispatchEvent(evt);
+
+ assert.isNotNull(MockAppsMgmt.mLastAppApplied);
+ assert.equal(MockAppsMgmt.mLastAppApplied.mId, mockApp.mId);
+ });
+
+ test('should kill the app before applying the update', function() {
+ mockApp.mTriggerDownloadAvailable();
+ mockApp.mTriggerDownloadSuccess();
+ assert.equal('https://testapp.gaiamobile.org',
+ MockWindowManager.mLastKilledOrigin);
+ });
+ });
+ });
+
+ suite('ondownloaderror', function() {
+ setup(function() {
+ mockApp.mTriggerDownloadAvailable();
+ mockApp.mTriggerDownloadError();
+ });
+
+ test('should request error banner', function() {
+ assert.isTrue(MockUpdateManager.mErrorBannerRequested);
+ });
+
+ test('should remove self from active downloads', function() {
+ assert.isNotNull(MockUpdateManager.mLastDownloadsRemoval);
+ assert.equal(MockUpdateManager.mLastDownloadsRemoval.app.mId,
+ mockApp.mId);
+ });
+
+ test('progress should be reset', function() {
+ assert.isNull(subject.progress);
+ });
+
+ test('should still answer to progress events', function() {
+ mockApp.mTriggerDownloadProgress(42);
+ assert.equal(42, subject.progress);
+ });
+ });
+
+ suite('onprogress', function() {
+ setup(function() {
+ mockApp.mTriggerDownloadAvailable();
+ });
+
+ test('should send progress to update manager', function() {
+ mockApp.mTriggerDownloadProgress(1234);
+ assert.equal(1234, MockUpdateManager.mProgressCalledWith);
+ });
+
+ test('should send progress delta to update manager', function() {
+ mockApp.mTriggerDownloadProgress(1234);
+ mockApp.mTriggerDownloadProgress(2234);
+ assert.equal(1000, MockUpdateManager.mProgressCalledWith);
+ });
+ });
+
+ suite('ondownloadapplied', function() {
+ setup(function() {
+ mockApp.mTriggerDownloadAvailable();
+ mockApp.mTriggerDownloadApplied();
+ });
+
+ testCleanup();
+ });
+ });
+
+ suite('system update events', function() {
+ setup(function() {
+ subject = new SystemUpdatable(42);
+ subject._dispatchEvent = fakeDispatchEvent;
+ subject.download();
+ });
+
+ suite('update-downloaded', function() {
+ setup(function() {
+ asyncStorage.setItem(SystemUpdatable.KNOWN_UPDATE_FLAG, true);
+ var event = new MockChromeEvent({
+ type: 'update-downloaded'
+ });
+ subject.handleEvent(event);
+ });
+
+ test('should reset the downloading flag', function() {
+ assert.isFalse(subject.downloading);
+ });
+
+ test('should reset SystemUpdatable.KNOWN_UPDATE_FLAG', function() {
+ assert.isUndefined(asyncStorage.mItems[SystemUpdatable.KNOWN_UPDATE_FLAG]);
+ });
+
+ testSystemApplyPrompt();
+ });
+
+ suite('update-prompt-apply', function() {
+ setup(function() {
+ asyncStorage.setItem(SystemUpdatable.KNOWN_UPDATE_FLAG, true);
+ MockUtilityTray.show();
+ var event = new MockChromeEvent({
+ type: 'update-prompt-apply'
+ });
+ subject.handleEvent(event);
+ });
+
+ test('should reset SystemUpdatable.KNOWN_UPDATE_FLAG', function() {
+ assert.isUndefined(asyncStorage.mItems[SystemUpdatable.KNOWN_UPDATE_FLAG]);
+ });
+
+ testSystemApplyPrompt();
+ });
+
+ suite('update-error', function() {
+ setup(function() {
+ subject = new SystemUpdatable(42);
+ var event = new MockChromeEvent({
+ type: 'update-error'
+ });
+ subject.handleEvent(event);
+ });
+
+ test('should request error banner', function() {
+ assert.isTrue(MockUpdateManager.mErrorBannerRequested);
+ });
+
+ test('should remove self from active downloads', function() {
+ assert.isNotNull(MockUpdateManager.mLastDownloadsRemoval);
+ assert.equal(subject, MockUpdateManager.mLastDownloadsRemoval);
+ });
+
+ test('should remove the downloading flag', function() {
+ assert.isFalse(subject.downloading);
+ });
+ });
+
+ suite('update download events', function() {
+ var event;
+ setup(function() {
+ subject = new SystemUpdatable(98734);
+ subject.download();
+ });
+
+ suite('when the download starts', function() {
+ setup(function() {
+ event = new MockChromeEvent({
+ type: 'update-download-started',
+ total: 98734
+ });
+ });
+
+ test('should clear paused flag', function() {
+ subject.paused = true;
+ subject.handleEvent(event);
+ assert.isFalse(subject.paused);
+ });
+ });
+
+ suite('when the download receives progress', function() {
+ setup(function() {
+ event = new MockChromeEvent({
+ type: 'update-download-progress',
+ progress: 1234,
+ total: 98734
+ });
+ });
+
+ test('should send progress to update manager', function() {
+ subject.handleEvent(event);
+ assert.equal(1234, MockUpdateManager.mProgressCalledWith);
+ });
+
+ test('should send progress delta to update manager', function() {
+ subject.handleEvent(event);
+ event.detail.progress = 2234;
+ subject.handleEvent(event);
+ assert.equal(1000, MockUpdateManager.mProgressCalledWith);
+ });
+ });
+
+ suite('when the download is paused', function() {
+ setup(function() {
+ asyncStorage.setItem(SystemUpdatable.KNOWN_UPDATE_FLAG, true);
+ event = new MockChromeEvent({
+ type: 'update-download-stopped',
+ paused: true
+ });
+ subject.handleEvent(event);
+ });
+
+ test('should set the paused flag', function() {
+ assert.isTrue(subject.paused);
+ });
+ test('shouldn\'t signal "started uncompressing"', function() {
+ assert.isFalse(MockUpdateManager.mStartedUncompressingCalled);
+ });
+ test('should not reset SystemUpdatable.KNOWN_UPDATE_FLAG', function() {
+ assert.isTrue(asyncStorage.mItems[SystemUpdatable.KNOWN_UPDATE_FLAG]);
+ });
+ });
+
+ suite('when the download is complete', function() {
+ setup(function() {
+ asyncStorage.setItem(SystemUpdatable.KNOWN_UPDATE_FLAG, true);
+ event = new MockChromeEvent({
+ type: 'update-download-stopped',
+ paused: false
+ });
+ subject.handleEvent(event);
+ });
+
+ test('should clear the paused flag', function() {
+ assert.isFalse(subject.paused);
+ });
+
+ test('should signal the UpdateManager', function() {
+ assert.isTrue(MockUpdateManager.mStartedUncompressingCalled);
+ });
+ test('should not reset SystemUpdatable.KNOWN_UPDATE_FLAG', function() {
+ assert.isTrue(asyncStorage.mItems[SystemUpdatable.KNOWN_UPDATE_FLAG]);
+ });
+ });
+ });
+ });
+ });
+
+
+ function testSystemApplyPrompt() {
+ test('apply prompt shown', function() {
+ assert.isTrue(MockCustomDialog.mShown);
+ assert.equal('systemUpdateReady', MockCustomDialog.mShowedTitle);
+ assert.equal('wantToInstall', MockCustomDialog.mShowedMsg);
+
+ assert.equal('later', MockCustomDialog.mShowedCancel.title);
+ assert.equal('installNow', MockCustomDialog.mShowedConfirm.title);
+ });
+
+ test('utility tray hidden', function() {
+ assert.isFalse(MockUtilityTray.mShown);
+ });
+
+ test('apply prompt cancel callback', function() {
+ assert.equal(subject.declineInstall.name,
+ MockCustomDialog.mShowedCancel.callback.name);
+
+ subject.declineInstall();
+ assert.isFalse(MockCustomDialog.mShown);
+
+ assert.equal('update-prompt-apply-result', lastDispatchedEvent.type);
+ assert.equal('wait', lastDispatchedEvent.value);
+ });
+
+ test('canceling should remove from downloads queue', function() {
+ subject.declineInstall();
+
+ assert.isNotNull(MockUpdateManager.mLastDownloadsRemoval);
+ assert.equal(subject, MockUpdateManager.mLastDownloadsRemoval);
+ });
+
+ test('apply prompt confirm callback', function() {
+ assert.equal(subject.acceptInstall.name,
+ MockCustomDialog.mShowedConfirm.callback.name);
+
+ subject.acceptInstall();
+ assert.isFalse(MockCustomDialog.mShown);
+
+ assert.equal('update-prompt-apply-result', lastDispatchedEvent.type);
+ assert.equal('restart', lastDispatchedEvent.value);
+ });
+ }
+});
diff --git a/apps/system/test/unit/update_manager_test.js b/apps/system/test/unit/update_manager_test.js
new file mode 100644
index 0000000..2b2ef4c
--- /dev/null
+++ b/apps/system/test/unit/update_manager_test.js
@@ -0,0 +1,1449 @@
+'use strict';
+
+requireApp('system/js/update_manager.js');
+
+requireApp('system/test/unit/mock_app.js');
+requireApp('system/test/unit/mock_updatable.js');
+requireApp('system/test/unit/mock_apps_mgmt.js');
+requireApp('system/test/unit/mock_custom_dialog.js');
+requireApp('system/test/unit/mock_utility_tray.js');
+requireApp('system/test/unit/mock_system_banner.js');
+requireApp('system/test/unit/mock_chrome_event.js');
+requireApp('system/test/unit/mock_settings_listener.js');
+requireApp('system/test/unit/mock_statusbar.js');
+requireApp('system/test/unit/mock_notification_screen.js');
+requireApp('system/test/unit/mock_navigator_settings.js');
+requireApp('system/test/unit/mock_navigator_wake_lock.js');
+requireApp('system/test/unit/mock_navigator_moz_mobile_connection.js');
+requireApp('system/test/unit/mock_l10n.js');
+requireApp('system/test/unit/mock_asyncStorage.js');
+
+requireApp('system/test/unit/mocks_helper.js');
+
+var mocksForUpdateManager = [
+ 'StatusBar',
+ 'SystemBanner',
+ 'NotificationScreen',
+ 'UtilityTray',
+ 'CustomDialog',
+ 'SystemUpdatable',
+ 'AppUpdatable',
+ 'SettingsListener',
+ 'asyncStorage'
+];
+
+mocksForUpdateManager.forEach(function(mockName) {
+ if (! window[mockName]) {
+ window[mockName] = null;
+ }
+});
+
+suite('system/UpdateManager', function() {
+ var realL10n;
+ var realWifiManager;
+ var realRequestWakeLock;
+ var realNavigatorSettings;
+ var realDispatchEvent;
+
+ var apps;
+ var updatableApps;
+ var uAppWithDownloadAvailable;
+ var appWithDownloadAvailable;
+ var fakeNode;
+ var fakeToaster;
+ var fakeDialog;
+ var fakeWarning;
+
+ var tinyTimeout = 10;
+ var lastDispatchedEvent = null;
+
+ var mocksHelper;
+
+ suiteSetup(function() {
+ realNavigatorSettings = navigator.mozSettings;
+ navigator.mozSettings = MockNavigatorSettings;
+
+ realL10n = navigator.mozL10n;
+ navigator.mozL10n = MockL10n;
+
+ realWifiManager = navigator.mozWifiManager;
+ navigator.mozWifiManager = {
+ connection: {
+ status: 'connected'
+ }
+ };
+
+ realRequestWakeLock = navigator.requestWakeLock;
+ navigator.requestWakeLock = MockNavigatorWakeLock.requestWakeLock;
+
+ realDispatchEvent = UpdateManager._dispatchEvent;
+ UpdateManager._dispatchEvent = function fakeDispatch(type, value) {
+ lastDispatchedEvent = {
+ type: type,
+ value: value
+ };
+ };
+
+ mocksHelper = new MocksHelper(mocksForUpdateManager);
+ mocksHelper.suiteSetup();
+
+ UpdateManager.NOTIFICATION_BUFFERING_TIMEOUT = 0;
+ UpdateManager.TOASTER_TIMEOUT = 0;
+ });
+
+ suiteTeardown(function() {
+ navigator.mozSettings = realNavigatorSettings;
+ realNavigatorSettings = null;
+
+ navigator.mozL10n = realL10n;
+ navigator.mozWifiManager = realWifiManager;
+ navigator.requestWakeLock = realRequestWakeLock;
+ realRequestWakeLock = null;
+
+ UpdateManager._dispatchEvent = realDispatchEvent;
+
+ mocksHelper.suiteTeardown();
+ });
+
+ setup(function() {
+ UpdateManager._mgmt = MockAppsMgmt;
+
+ apps = [new MockApp(), new MockApp(), new MockApp()];
+ updatableApps = apps.map(function(app) {
+ return new AppUpdatable(app);
+ });
+ MockAppsMgmt.mApps = apps;
+
+ uAppWithDownloadAvailable = updatableApps[2];
+ appWithDownloadAvailable = apps[2];
+ appWithDownloadAvailable.downloadAvailable = true;
+
+ fakeNode = document.createElement('div');
+ fakeNode.id = 'update-manager-container';
+ fakeNode.innerHTML = [
+ '<div class="icon">',
+ '</div>',
+ '<div class="activity">',
+ '</div>',
+ '<div class="message">',
+ '</div>'
+ ].join('');
+
+ fakeToaster = document.createElement('div');
+ fakeToaster.id = 'update-manager-toaster';
+ fakeToaster.innerHTML = [
+ '<div class="icon">',
+ '</div>',
+ '<div class="message">',
+ '</div>'
+ ].join('');
+
+ fakeDialog = document.createElement('form');
+ fakeDialog.id = 'updates-download-dialog';
+ fakeDialog.innerHTML = [
+ '<section>',
+ '<h1>',
+ 'Updates',
+ '</h1>',
+ '<ul>',
+ '</ul>',
+ '<menu>',
+ '<button id="updates-later-button" type="reset">',
+ 'Later',
+ '</button>',
+ '<button id="updates-download-button" type="submit">',
+ 'Download',
+ '</button>',
+ '</menu>',
+ '</section>'
+ ].join('');
+
+ fakeWarning = document.createElement('form');
+ fakeWarning.id = 'updates-viaDataConnection-dialog';
+ fakeWarning.innerHTML = [
+ '<section>',
+ '<h1>',
+ 'Updates',
+ '</h1>',
+ '<p>',
+ '</p>',
+ '<menu>',
+ '<button id="updates-viaDataConnection-notnow-button" type="reset">',
+ 'Not Now',
+ '</button>',
+ '<button id="updates-viaDataConnection-download-button" type="submit">',
+ 'Download',
+ '</button>',
+ '</menu>',
+ '</section>'
+ ].join('');
+
+ document.body.appendChild(fakeNode);
+ document.body.appendChild(fakeToaster);
+ document.body.appendChild(fakeDialog);
+ document.body.appendChild(fakeWarning);
+
+ mocksHelper.setup();
+ });
+
+ teardown(function(done) {
+ // We wait for the nextTick in order to let the UpdateManger's
+ // timeouts finish (they are all set to 0)
+ setTimeout(function() {
+ UpdateManager.updatableApps = [];
+ UpdateManager.systemUpdatable = null;
+ UpdateManager.updatesQueue = [];
+ UpdateManager.downloadsQueue = [];
+ UpdateManager._downloading = false;
+ UpdateManager._uncompressing = false;
+ UpdateManager.container = null;
+ UpdateManager.message = null;
+ UpdateManager.toaster = null;
+ UpdateManager.toasterMessage = null;
+ UpdateManager.laterButton = null;
+ UpdateManager.downloadButton = null;
+ UpdateManager.downloadDialog = null;
+ UpdateManager.downloadDialogTitle = null;
+ UpdateManager.downloadDialogList = null;
+ UpdateManager.lastUpdatesAvailable = 0;
+
+ MockAppsMgmt.mTeardown();
+
+ mocksHelper.teardown();
+
+ fakeNode.parentNode.removeChild(fakeNode);
+ fakeToaster.parentNode.removeChild(fakeToaster);
+ fakeDialog.parentNode.removeChild(fakeDialog);
+
+ lastDispatchedEvent = null;
+ MockNavigatorWakeLock.mTeardown();
+ MockNavigatorSettings.mTeardown();
+
+ done();
+ });
+ });
+
+ suite('init', function() {
+ test('should get all applications', function(done) {
+ MockAppsMgmt.mNext = function() {
+ done();
+ };
+ UpdateManager.init();
+ });
+
+ test('should create AppUpdatable on init', function(done) {
+ MockAppUpdatable.mTeardown();
+
+ MockAppsMgmt.mNext = function() {
+ assert.equal(MockAppUpdatable.mCount, apps.length);
+ done();
+ };
+ UpdateManager.init();
+ });
+
+ test('should bind dom elements', function() {
+ UpdateManager.init();
+ assert.equal('update-manager-container', UpdateManager.container.id);
+ assert.equal('message', UpdateManager.message.className);
+
+ assert.equal('update-manager-toaster', UpdateManager.toaster.id);
+ assert.equal('message', UpdateManager.toasterMessage.className);
+
+ assert.equal('updates-later-button', UpdateManager.laterButton.id);
+ assert.equal('updates-download-button', UpdateManager.downloadButton.id);
+ assert.equal('updates-download-dialog', UpdateManager.downloadDialog.id);
+ assert.equal('updates-viaDataConnection-dialog',
+ UpdateManager.downloadViaDataConnectionDialog.id);
+ assert.equal('updates-viaDataConnection-notnow-button',
+ UpdateManager.notnowButton.id);
+ assert.equal('updates-viaDataConnection-download-button',
+ UpdateManager.downloadViaDataConnectionButton.id);
+ assert.equal('H1', UpdateManager.downloadDialogTitle.tagName);
+ assert.equal('UL', UpdateManager.downloadDialogList.tagName);
+ });
+
+ test('should bind to the click event', function() {
+ UpdateManager.init();
+ assert.equal(UpdateManager.containerClicked.name,
+ UpdateManager.container.onclick.name);
+
+ assert.equal(UpdateManager.requestDownloads.name,
+ UpdateManager.downloadButton.onclick.name);
+
+ assert.equal(UpdateManager.cancelPrompt.name,
+ UpdateManager.laterButton.onclick.name);
+
+ assert.equal(UpdateManager.cancelDataConnectionUpdatesPrompt.name,
+ UpdateManager.notnowButton.onclick.name);
+
+ assert.equal(UpdateManager.requestDownloads.name,
+ UpdateManager.downloadViaDataConnectionButton.onclick.name);
+ });
+ });
+
+ suite('events', function() {
+ suite('app install', function() {
+ var installedApp;
+
+ setup(function() {
+ MockAppUpdatable.mTeardown();
+
+ UpdateManager.init();
+
+ installedApp = new MockApp();
+ installedApp.downloadAvailable = true;
+ MockAppsMgmt.mTriggerOninstall(installedApp);
+ });
+
+ test('should instantiate an updatable app', function() {
+ assert.equal(MockAppUpdatable.mCount, 1);
+ });
+ });
+
+ suite('app uninstall', function() {
+ var partialApp;
+
+ setup(function() {
+ UpdateManager.init();
+ UpdateManager.updatableApps = updatableApps;
+ UpdateManager.addToUpdatesQueue(uAppWithDownloadAvailable);
+
+ partialApp = {
+ origin: appWithDownloadAvailable.origin,
+ manifestURL: appWithDownloadAvailable.manifestURL
+ };
+ });
+
+ test('should remove the updatable app', function() {
+ var initialLength = UpdateManager.updatableApps.length;
+ MockAppsMgmt.mTriggerOnuninstall(partialApp);
+ assert.equal(initialLength - 1, UpdateManager.updatableApps.length);
+ });
+
+ test('should remove from the update queue', function() {
+ var initialLength = UpdateManager.updatesQueue.length;
+ MockAppsMgmt.mTriggerOnuninstall(partialApp);
+ assert.equal(initialLength - 1, UpdateManager.updatesQueue.length);
+ });
+
+ test('should remove from the update queue even if no downloadavailable',
+ function() {
+ uAppWithDownloadAvailable.app.downloadAvailable = false;
+ var initialLength = UpdateManager.updatesQueue.length;
+ MockAppsMgmt.mTriggerOnuninstall(partialApp);
+ assert.equal(initialLength - 1, UpdateManager.updatesQueue.length);
+ });
+
+ test('should call uninit on the updatable', function() {
+ var lastIndex = UpdateManager.updatesQueue.length - 1;
+ var updatableApp = UpdateManager.updatesQueue[lastIndex];
+ MockAppsMgmt.mTriggerOnuninstall(partialApp);
+ assert.isTrue(updatableApp.mUninitCalled);
+ });
+ });
+
+ suite('system update available', function() {
+ var event;
+
+ setup(function() {
+ UpdateManager.init();
+ event = new MockChromeEvent({
+ type: 'update-available',
+ size: 42
+ });
+ UpdateManager.handleEvent(event);
+ });
+
+ test('should add a system updatable to the updates', function() {
+ var lastIndex = UpdateManager.updatesQueue.length - 1;
+ assert.equal(undefined, UpdateManager.updatesQueue[lastIndex].app);
+ });
+
+ test('should init the updatable with the download size', function() {
+ var lastIndex = UpdateManager.updatesQueue.length - 1;
+ assert.equal(42, UpdateManager.updatesQueue[lastIndex].size);
+ });
+
+ test('should not add or instanciate a system updatable if there is one',
+ function() {
+ var initialLength = UpdateManager.updatesQueue.length;
+
+ UpdateManager.handleEvent(event);
+
+ assert.equal(UpdateManager.updatesQueue.length, initialLength);
+ assert.equal(MockSystemUpdatable.mInstancesCount, 1);
+ });
+
+ test('should remember that update is available', function() {
+ assert.isTrue(UpdateManager.systemUpdatable.mKnownUpdate);
+ });
+ });
+
+ suite('no system update available', function() {
+ setup(function() {
+ UpdateManager.init();
+ });
+
+ test('should not remember about the update', function() {
+ assert.isUndefined(UpdateManager.systemUpdatable.mKnownUpdate);
+ });
+ });
+
+ });
+
+ suite('UI', function() {
+ setup(function() {
+ MockAppsMgmt.mApps = [];
+ UpdateManager.init();
+ UpdateManager.updatableApps = updatableApps;
+ });
+
+ suite('downloading state', function() {
+ test('should add the css class if downloading', function() {
+ UpdateManager._downloading = true;
+ UpdateManager.render();
+ var css = UpdateManager.container.classList;
+ assert.isTrue(css.contains('downloading'));
+ });
+
+ test('should remove the css class if not downloading', function() {
+ UpdateManager._downloading = true;
+ UpdateManager.render();
+
+ UpdateManager._downloading = false;
+ UpdateManager.render();
+ var css = UpdateManager.container.classList;
+ assert.isFalse(css.contains('downloading'));
+ });
+
+ test('should show the downloading progress if downloading', function() {
+ UpdateManager._downloading = true;
+ UpdateManager.render();
+ assert.equal('downloadingUpdateMessage{"progress":"0.00 bytes"}',
+ UpdateManager.message.textContent);
+ });
+
+ test('should not show the toaster if downloading', function(done) {
+ UpdateManager.NOTIFICATION_BUFFERING_TIMEOUT = tinyTimeout;
+ UpdateManager.TOASTER_TIMEOUT = tinyTimeout;
+ UpdateManager._downloading = true;
+ UpdateManager.addToUpdatesQueue(uAppWithDownloadAvailable);
+
+ setTimeout(function() {
+ var css = UpdateManager.toaster.classList;
+ assert.isFalse(css.contains('displayed'));
+ UpdateManager.NOTIFICATION_BUFFERING_TIMEOUT = 0;
+ UpdateManager.TOASTER_TIMEOUT = 0;
+ done();
+ }, tinyTimeout * 1.5);
+ });
+
+ test('should show the available message if not downloading', function() {
+ UpdateManager.updatesQueue = updatableApps;
+ UpdateManager.render();
+ assert.equal('updateAvailableInfo{"n":3}',
+ UpdateManager.message.textContent);
+ });
+ });
+
+ suite('progress display', function() {
+ setup(function() {
+ UpdateManager.updatesQueue = [uAppWithDownloadAvailable];
+
+ var evt = document.createEvent('MouseEvents');
+ evt.initEvent('click', true, true);
+ UpdateManager.startDownloads(evt);
+
+ UpdateManager.addToDownloadsQueue(uAppWithDownloadAvailable);
+
+ UpdateManager.downloadProgressed(1234);
+ });
+
+ test('downloadedBytes should be reset by startDownloads', function() {
+ var evt = document.createEvent('MouseEvents');
+ evt.initEvent('click', true, true);
+ UpdateManager.startDownloads(evt);
+
+ assert.equal('downloadingUpdateMessage{"progress":"0.00 bytes"}',
+ UpdateManager.message.textContent);
+ });
+
+ test('downloadedBytes should be reset when stopping the download',
+ function() {
+
+ UpdateManager.removeFromDownloadsQueue(uAppWithDownloadAvailable);
+ UpdateManager.addToDownloadsQueue(uAppWithDownloadAvailable);
+
+ assert.equal('downloadingUpdateMessage{"progress":"0.00 bytes"}',
+ UpdateManager.message.textContent);
+ });
+
+ test('should increment the downloadedBytes', function() {
+ UpdateManager.downloadProgressed(100);
+ assert.equal('downloadingUpdateMessage{"progress":"1.30 kB"}',
+ UpdateManager.message.textContent);
+ });
+
+ test('should not update if bytes <= 0', function() {
+ UpdateManager.downloadProgressed(-100);
+ assert.equal('downloadingUpdateMessage{"progress":"1.21 kB"}',
+ UpdateManager.message.textContent);
+ });
+
+ test('should display the notification', function() {
+ assert.isTrue(fakeNode.classList.contains('displayed'));
+ });
+
+ });
+
+ suite('uncompress display', function() {
+ var systemUpdatable;
+
+ setup(function() {
+ systemUpdatable = new MockSystemUpdatable();
+ });
+
+ suite('when we only have the system update', function() {
+ setup(function() {
+ UpdateManager.addToUpdatesQueue(systemUpdatable);
+ UpdateManager.addToDownloadsQueue(systemUpdatable);
+ UpdateManager.startedUncompressing();
+ });
+
+ test('should render in uncompressing mode', function() {
+ assert.equal(UpdateManager.message.textContent,
+ 'uncompressingMessage');
+ });
+ });
+
+ suite('when we have various ongoing updates', function() {
+ setup(function() {
+ UpdateManager.addToUpdatableApps(uAppWithDownloadAvailable);
+ UpdateManager.addToUpdatesQueue(uAppWithDownloadAvailable);
+ UpdateManager.addToDownloadsQueue(uAppWithDownloadAvailable);
+
+ UpdateManager.addToUpdatesQueue(systemUpdatable);
+ UpdateManager.addToDownloadsQueue(systemUpdatable);
+
+ UpdateManager.startedUncompressing();
+ });
+
+ test('should stay in downloading mode', function() {
+ assert.include(UpdateManager.message.textContent,
+ 'downloadingUpdateMessage');
+ });
+
+ suite('once the app updates are done', function() {
+ setup(function() {
+ UpdateManager.removeFromDownloadsQueue(uAppWithDownloadAvailable);
+ UpdateManager.removeFromUpdatesQueue(uAppWithDownloadAvailable);
+ });
+
+ test('should render in uncompressing mode', function() {
+ assert.equal(UpdateManager.message.textContent,
+ 'uncompressingMessage');
+ });
+ });
+ });
+ });
+
+ suite('container visibility', function() {
+ suiteSetup(function() {
+ UpdateManager.NOTIFICATION_BUFFERING_TIMEOUT = tinyTimeout;
+ UpdateManager.TOASTER_TIMEOUT = tinyTimeout;
+ });
+
+ suiteTeardown(function() {
+ UpdateManager.NOTIFICATION_BUFFERING_TIMEOUT = 0;
+ UpdateManager.TOASTER_TIMEOUT = 0;
+ });
+
+ setup(function() {
+ UpdateManager.addToUpdatesQueue(uAppWithDownloadAvailable);
+ });
+
+ teardown(function(done) {
+ // wait for all actions to happen in UpdateManager before reseting
+ setTimeout(function() {
+ done();
+ }, tinyTimeout * 2);
+ });
+
+ suite('notification behavior after addToDownloadsQueue', function() {
+ setup(function() {
+ var css = UpdateManager.container.classList;
+ assert.isFalse(css.contains('displayed'));
+ UpdateManager.addToDownloadsQueue(uAppWithDownloadAvailable);
+ });
+
+ test('should be displayed only once', function() {
+ var css = UpdateManager.container.classList;
+ assert.isTrue(css.contains('displayed'));
+ assert.equal(MockNotificationScreen.wasMethodCalled['incExternalNotifications'], 1);
+ });
+
+ test('should not be displayed after timeout', function(done) {
+ setTimeout(function() {
+ var css = UpdateManager.container.classList;
+ assert.isTrue(css.contains('displayed'));
+ assert.equal(MockNotificationScreen.wasMethodCalled['incExternalNotifications'], 1);
+ done();
+ }, tinyTimeout * 2);
+
+ });
+ });
+
+ suite('notification behavior after addToDownloadsQueue after timeout', function() {
+ setup(function(done) {
+ setTimeout(function() {
+ var css = UpdateManager.container.classList;
+ assert.isFalse(css.contains('displayed'));
+ UpdateManager.addToDownloadsQueue(uAppWithDownloadAvailable);
+ done();
+ });
+ });
+
+ test('should not increment the counter if already displayed', function() {
+ var css = UpdateManager.container.classList;
+ assert.isTrue(css.contains('displayed'));
+ assert.equal(MockNotificationScreen.wasMethodCalled['incExternalNotifications'], 1);
+ });
+ });
+
+ suite('displaying the container after a timeout', function() {
+ setup(function() {
+ var css = UpdateManager.container.classList;
+ assert.isFalse(css.contains('displayed'));
+ });
+
+ test('should display after a timeout', function(done) {
+ setTimeout(function() {
+ var css = UpdateManager.container.classList;
+ assert.isTrue(css.contains('displayed'));
+ assert.equal(MockNotificationScreen.wasMethodCalled['incExternalNotifications'], 1);
+ done();
+ }, tinyTimeout * 2);
+ });
+
+ test('should not display if there are no more updates', function(done) {
+ UpdateManager.updatesQueue.forEach(function(uApp) {
+ UpdateManager.removeFromUpdatesQueue(uApp);
+ });
+
+ setTimeout(function() {
+ var css = UpdateManager.container.classList;
+ assert.isFalse(css.contains('displayed'));
+ done();
+ }, tinyTimeout * 2);
+ });
+
+ test('should display an updated count', function(done) {
+ UpdateManager.addToUpdatesQueue(updatableApps[1]);
+ setTimeout(function() {
+ assert.equal('updateAvailableInfo{"n":2}',
+ UpdateManager.message.textContent);
+ done();
+ }, tinyTimeout * 2);
+ });
+
+ suite('update toaster', function() {
+ test('should display after a timeout', function(done) {
+ var css = UpdateManager.container.classList;
+ assert.isFalse(css.contains('displayed'));
+ setTimeout(function() {
+ var css = UpdateManager.toaster.classList;
+ assert.isTrue(css.contains('displayed'));
+ assert.equal('updateAvailableInfo{"n":1}',
+ UpdateManager.toasterMessage.textContent);
+ done();
+ }, tinyTimeout * 1.5);
+ });
+
+ test('should reset toaster value when notification was activated', function(done) {
+ setTimeout(function() {
+ UpdateManager.addToUpdatesQueue(updatableApps[1]);
+ assert.equal('updateAvailableInfo{"n":1}',
+ UpdateManager.toasterMessage.textContent);
+ done();
+ }, tinyTimeout * 2);
+ });
+
+ test('should show the right message', function(done) {
+ setTimeout(function() {
+ assert.equal('updateAvailableInfo{"n":1}',
+ UpdateManager.toasterMessage.textContent);
+ done();
+ }, tinyTimeout * 2);
+ });
+
+
+ test('should hide after TOASTER_TIMEOUT', function(done) {
+ UpdateManager.addToUpdatesQueue(updatableApps[1]);
+ setTimeout(function() {
+ setTimeout(function() {
+ var css = UpdateManager.toaster.classList;
+ assert.isFalse(css.contains('displayed'));
+ done();
+ }, tinyTimeout * 2);
+ }, tinyTimeout * 2);
+ });
+
+ });
+
+ test('should add a new statusbar notification', function(done) {
+ var method1 = 'incExternalNotifications';
+ setTimeout(function() {
+ assert.ok(MockNotificationScreen.wasMethodCalled[method1]);
+ done();
+ }, tinyTimeout * 2);
+ });
+ });
+
+ suite('no more updates', function() {
+ setup(function() {
+ UpdateManager.container.classList.add('displayed');
+ UpdateManager.updatesQueue = [uAppWithDownloadAvailable];
+ UpdateManager.removeFromUpdatesQueue(uAppWithDownloadAvailable);
+ });
+
+ test('should hide the container', function() {
+ var css = UpdateManager.container.classList;
+ assert.isFalse(css.contains('displayed'));
+ });
+
+ test('should decrease the external notifications count', function() {
+ var method1 = 'decExternalNotifications';
+ assert.ok(MockNotificationScreen.wasMethodCalled[method1]);
+ });
+ });
+ });
+
+ suite('after downloads', function() {
+ test('should check if new updates where found', function() {
+ var uApp = updatableApps[0];
+
+ UpdateManager.updatableApps = updatableApps;
+ UpdateManager.downloadsQueue = [uApp];
+
+ UpdateManager.removeFromDownloadsQueue(uApp);
+ assert.equal(uAppWithDownloadAvailable.app.mId,
+ UpdateManager.updatesQueue[0].app.mId);
+ });
+ });
+
+ suite('error banner requests', function() {
+ suiteSetup(function() {
+ UpdateManager.NOTIFICATION_BUFFERING_TIMEOUT = tinyTimeout;
+ UpdateManager.TOASTER_TIMEOUT = tinyTimeout;
+ });
+
+ suiteTeardown(function() {
+ UpdateManager.NOTIFICATION_BUFFERING_TIMEOUT = 0;
+ UpdateManager.TOASTER_TIMEOUT = 0;
+ });
+
+ setup(function() {
+ UpdateManager.init();
+ UpdateManager.requestErrorBanner();
+ });
+
+ teardown(function(done) {
+ // wait for all actions to happen in UpdateManager before reseting
+ setTimeout(function() {
+ done();
+ }, tinyTimeout * 2);
+ });
+
+ test('should wait before showing the system banner', function(done) {
+ assert.equal(0, MockSystemBanner.mShowCount);
+
+ setTimeout(function() {
+ done();
+ }, tinyTimeout * 2);
+ });
+
+ test('should show after NOTIFICATION_BUFFERING_TIMEOUT', function(done) {
+ setTimeout(function() {
+ assert.equal(1, MockSystemBanner.mShowCount);
+ assert.equal('downloadError', MockSystemBanner.mMessage);
+ done();
+ }, tinyTimeout * 2);
+ });
+
+ test('should show only once if called multiple time', function(done) {
+ UpdateManager.requestErrorBanner();
+ setTimeout(function() {
+ assert.equal(1, MockSystemBanner.mShowCount);
+ done();
+ }, tinyTimeout * 2);
+ });
+ });
+
+ suite('humanizeSize', function() {
+ test('should handle 0', function() {
+ assert.equal('0.00 bytes', UpdateManager._humanizeSize(0));
+ });
+
+ test('should handle bytes size', function() {
+ assert.equal('42.00 bytes', UpdateManager._humanizeSize(42));
+ });
+
+ test('should handle kilobytes size', function() {
+ assert.equal('1.00 kB', UpdateManager._humanizeSize(1024));
+ });
+
+ test('should handle megabytes size', function() {
+ assert.equal('4.67 MB', UpdateManager._humanizeSize(4901024));
+ });
+
+ test('should handle gigabytes size', function() {
+ assert.equal('3.73 GB', UpdateManager._humanizeSize(4000901024));
+ });
+ });
+ });
+
+ suite('actions', function() {
+ setup(function() {
+ UpdateManager.init();
+ });
+
+ suite('start downloads', function() {
+ var systemUpdatable, appUpdatable, evt;
+
+ setup(function() {
+ UpdateManager.init();
+
+ systemUpdatable = new MockSystemUpdatable();
+
+ appUpdatable = new MockAppUpdatable(new MockApp());
+ appUpdatable.name = 'Angry birds';
+ appUpdatable.size = '423459';
+
+ UpdateManager.addToUpdatableApps(appUpdatable);
+ UpdateManager.addToUpdatesQueue(appUpdatable);
+ UpdateManager.addToUpdatesQueue(systemUpdatable);
+
+ UpdateManager.container.click();
+
+ evt = document.createEvent('MouseEvents');
+ evt.initEvent('click', true, true);
+ });
+
+ suite('data connection warning', function() {
+ var downloadDialog;
+ setup(function() {
+ downloadDialog = UpdateManager.downloadDialog;
+ });
+
+ test('should switch the online data attribute when online',
+ function() {
+ downloadDialog.dataset.online = false;
+ window.dispatchEvent(new CustomEvent('online'));
+ assert.equal(downloadDialog.dataset.online, 'true');
+ });
+
+ test('should leave the online data attribute true when online',
+ function() {
+ downloadDialog.dataset.online = true;
+ window.dispatchEvent(new CustomEvent('online'));
+ assert.equal(downloadDialog.dataset.online, 'true');
+ });
+
+ test('should switch the nowifi data attribute when connected',
+ function() {
+ downloadDialog.dataset.nowifi = true;
+ window.dispatchEvent(new CustomEvent('wifi-statuschange'));
+ assert.equal(downloadDialog.dataset.nowifi, 'false');
+ });
+
+ test('should switch the nowifi data attribute when disconnected',
+ function() {
+ downloadDialog.dataset.nowifi = false;
+ navigator.mozWifiManager.connection.status = 'disconnected';
+ window.dispatchEvent(new CustomEvent('wifi-statuschange'));
+ assert.equal(downloadDialog.dataset.nowifi, 'true');
+ });
+ });
+
+ test('should enable the download button', function() {
+ var downloadButton = UpdateManager.downloadButton;
+ assert.isFalse(downloadButton.disabled);
+ });
+
+ suite('with all the checkboxes checked', function() {
+ setup(function() {
+ UpdateManager.startDownloads(evt);
+ });
+
+ test('should download system updates', function() {
+ assert.isTrue(systemUpdatable.mDownloadCalled);
+ });
+
+ test('should call download on checked app updatables', function() {
+ assert.isTrue(appUpdatable.mDownloadCalled);
+ });
+ });
+
+ suite('with no checkbox checked', function() {
+ setup(function() {
+ var dialog = UpdateManager.downloadDialogList;
+ var checkboxes = dialog.querySelectorAll('input[type="checkbox"]');
+ for (var i = 0; i < checkboxes.length; i++) {
+ var checkbox = checkboxes[i];
+ if (checkbox.checked) {
+ checkbox.click();
+ }
+ }
+
+ UpdateManager.startDownloads(evt);
+ });
+
+ test('the download button should be enabled', function() {
+ assert.isFalse(UpdateManager.downloadButton.disabled);
+ });
+
+ test('should still download system updates', function() {
+ assert.isTrue(systemUpdatable.mDownloadCalled);
+ });
+
+ test('should not call download on unchecked app updatables',
+ function() {
+ assert.isFalse(appUpdatable.mDownloadCalled);
+ });
+ });
+
+ suite('with only app updates', function() {
+ setup(function() {
+ UpdateManager.removeFromUpdatesQueue(systemUpdatable);
+ UpdateManager.container.click();
+ });
+
+ suite('unchecking all the checkboxes', function() {
+ var dialog, downloadButton;
+
+ setup(function() {
+ dialog = UpdateManager.downloadDialogList;
+ var checkboxes = dialog.querySelectorAll('input[type="checkbox"]');
+ for (var i = 0; i < checkboxes.length; i++) {
+ var checkbox = checkboxes[i];
+ if (checkbox.checked) {
+ checkboxes[i].click();
+ }
+ }
+
+ downloadButton = UpdateManager.downloadButton;
+ });
+
+ test('should disable the download button', function() {
+ assert.isTrue(downloadButton.disabled);
+ });
+
+ suite('then checking one back', function() {
+ setup(function() {
+ var checkbox = dialog.querySelector('input[type="checkbox"]');
+ checkbox.click();
+ });
+
+ test('should enable the download button back', function() {
+ assert.isFalse(downloadButton.disabled);
+ });
+ });
+ });
+ });
+ });
+
+ suite('cancel all downloads', function() {
+ var systemUpdatable;
+
+ setup(function() {
+ systemUpdatable = new MockSystemUpdatable();
+ UpdateManager.updatableApps = updatableApps;
+ [systemUpdatable, uAppWithDownloadAvailable].forEach(function(updatable) {
+ UpdateManager.addToUpdatesQueue(updatable);
+ UpdateManager.addToDownloadsQueue(updatable);
+ });
+
+ UpdateManager.cancelAllDownloads();
+ });
+
+ test('should call cancelDownload on the app updatables', function() {
+ assert.isTrue(uAppWithDownloadAvailable.mCancelCalled);
+ });
+
+ test('should call cancelDownload on the system updatable', function() {
+ assert.isTrue(systemUpdatable.mCancelCalled);
+ });
+
+ test('should empty the downloads queue', function() {
+ assert.equal(UpdateManager.downloadsQueue.length, 0);
+ });
+
+ test('should leave the updates available', function() {
+ assert.equal(UpdateManager.updatesQueue.length, 2);
+ });
+ });
+
+ suite('download prompt', function() {
+ setup(function() {
+ MockUtilityTray.show();
+ var systemUpdatable = new MockSystemUpdatable();
+ systemUpdatable.size = 5296345;
+ var appUpdatable = new MockAppUpdatable(new MockApp());
+ appUpdatable.name = 'Angry birds';
+ appUpdatable.size = '423459';
+ var hostedAppUpdatable = new MockAppUpdatable(new MockApp());
+ hostedAppUpdatable.name = 'Twitter';
+ UpdateManager.updatesQueue = [hostedAppUpdatable, appUpdatable,
+ systemUpdatable];
+ UpdateManager.containerClicked();
+ UpdateManager._isDataConnectionWarningDialogEnabled = true;
+ UpdateManager.downloadDialog.dataset.nowifi = false;
+ });
+
+ suite('download prompt', function() {
+ test('should hide the utility tray', function() {
+ assert.isFalse(MockUtilityTray.mShown);
+ });
+
+ test('should show the download dialog', function() {
+ var css = UpdateManager.downloadDialog.classList;
+ assert.isTrue(css.contains('visible'));
+ });
+
+ test('should set the title', function() {
+ var title = fakeDialog.querySelector('h1');
+ assert.equal('numberOfUpdates{"n":3}', title.textContent);
+ });
+
+ suite('update list rendering', function() {
+ test('should create an item for each update', function() {
+ assert.equal(3, UpdateManager.downloadDialogList.children.length);
+ });
+
+ test('should render system update item first with required',
+ function() {
+ var item = UpdateManager.downloadDialogList.children[0];
+
+ assert.include(item.textContent, 'systemUpdate');
+ assert.include(item.textContent, '5.05 MB');
+ assert.include(item.textContent, 'required');
+ });
+
+ test('should render packaged app items alphabetically with checkbox',
+ function() {
+ var item = UpdateManager.downloadDialogList.children[1];
+
+ assert.include(item.textContent, 'Angry birds');
+ assert.include(item.textContent, '413.53 kB');
+
+ var checkbox = item.querySelector('input');
+ assert.equal(checkbox.type, 'checkbox');
+ assert.isTrue(checkbox.checked);
+ assert.equal(checkbox.dataset.position, '1');
+ });
+
+ test('should render hosted app items alphabetically with checkbox',
+ function() {
+ var item = UpdateManager.downloadDialogList.children[2];
+
+ assert.include(item.textContent, 'Twitter');
+
+ var checkbox = item.querySelector('input');
+ assert.equal(checkbox.type, 'checkbox');
+ assert.isTrue(checkbox.checked);
+ assert.equal(checkbox.dataset.position, '2');
+ });
+ });
+ });
+
+ test('should handle clicking download in the data connection warning dialog', function() {
+ UpdateManager.downloadDialog.dataset.nowifi = true;
+
+ var evt = {
+ preventDefault: function() {},
+ type: 'click',
+ target: UpdateManager.downloadViaDataConnectionButton
+ };
+
+ UpdateManager.requestDownloads(evt);
+ MockasyncStorage.getItem('gaia.system.isDataConnectionWarningDialogEnabled', function(value) {
+ assert.isFalse(value);
+ });
+ assert.isFalse(UpdateManager._isDataConnectionWarningDialogEnabled);
+ assert.equal(UpdateManager.downloadDialog.dataset.dataConnectionInlineWarning, 'true');
+
+ MockasyncStorage.mTeardown();
+ });
+
+ test('should handle clicking download when using data connection in the first time', function() {
+ UpdateManager.downloadDialog.dataset.nowifi = true;
+
+ var evt = document.createEvent('MouseEvents');
+ evt.initEvent('click', true, true);
+
+ UpdateManager.requestDownloads(evt);
+ var css = UpdateManager.downloadViaDataConnectionDialog.classList;
+ assert.isTrue(css.contains('visible'));
+ });
+
+ test('should handle clicking download when using wifi', function() {
+ UpdateManager._isDataConnectionWarningDialogEnabled = false;
+
+ var calledToMockStartDownloads = false;
+ var realStartDownloadsFunc = UpdateManager.startDownloads;
+ UpdateManager.startDownloads = function() {
+ calledToMockStartDownloads = true;
+ };
+
+ var evt = document.createEvent('MouseEvents');
+ evt.initEvent('click', true, true);
+
+ UpdateManager.requestDownloads(evt);
+ assert.isTrue(calledToMockStartDownloads);
+
+ UpdateManager.startDownloads = realStartDownloadsFunc;
+ });
+
+ test('should handle cancellation on the data connection warning dialog', function() {
+ UpdateManager.cancelDataConnectionUpdatesPrompt();
+
+ var css = UpdateManager.downloadViaDataConnectionDialog.classList;
+ assert.isFalse(css.contains('visible'));
+ css = UpdateManager.downloadDialog.classList;
+ assert.isFalse(css.contains('visible'));
+ });
+
+ test('should handle cancellation', function() {
+ UpdateManager.cancelPrompt();
+
+ var css = UpdateManager.downloadDialog.classList;
+ assert.isFalse(css.contains('visible'));
+ });
+
+ test('should handle confirmation', function() {
+ UpdateManager._isDataConnectionWarningDialogEnabled = false;
+
+ var evt = document.createEvent('MouseEvents');
+ evt.initEvent('click', true, true);
+
+ UpdateManager.requestDownloads(evt);
+ var css = UpdateManager.downloadDialog.classList;
+ assert.isFalse(css.contains('visible'));
+ css = UpdateManager.downloadViaDataConnectionDialog.classList;
+ assert.isFalse(css.contains('visible'));
+ assert.isTrue(MockUtilityTray.mShown);
+ assert.isTrue(evt.defaultPrevented);
+ });
+ });
+
+ suite('cancel prompt', function() {
+ setup(function() {
+ UpdateManager._downloading = true;
+ MockUtilityTray.show();
+ UpdateManager.containerClicked();
+ });
+
+ test('should show the cancel', function() {
+ assert.isTrue(MockCustomDialog.mShown);
+ assert.isFalse(MockUtilityTray.mShown);
+
+ assert.equal('cancelAllDownloads', MockCustomDialog.mShowedTitle);
+ assert.equal('wantToCancelAll', MockCustomDialog.mShowedMsg);
+
+ assert.equal('no', MockCustomDialog.mShowedCancel.title);
+ assert.equal('yes', MockCustomDialog.mShowedConfirm.title);
+ });
+
+ test('should handle cancellation', function() {
+ assert.equal('um_cancelPrompt',
+ MockCustomDialog.mShowedCancel.callback.name);
+
+ UpdateManager.cancelPrompt();
+ assert.isFalse(MockCustomDialog.mShown);
+ });
+
+ test('should handle confirmation', function() {
+ assert.equal('um_cancelAllDownloads',
+ MockCustomDialog.mShowedConfirm.callback.name);
+
+ UpdateManager.cancelAllDownloads();
+ assert.isFalse(MockCustomDialog.mShown);
+ });
+ });
+
+ suite('check for updates', function() {
+ setup(function() {
+ UpdateManager.init();
+ });
+
+ test('should observe the setting', function() {
+ assert.equal('gaia.system.checkForUpdates', MockSettingsListener.mName);
+ assert.equal(false, MockSettingsListener.mDefaultValue);
+ assert.equal(UpdateManager.checkForUpdates.name,
+ MockSettingsListener.mCallback.name);
+ });
+
+ suite('when asked to check', function() {
+ setup(function() {
+ UpdateManager.checkForUpdates(true);
+ });
+
+ test('should dispatch force update event if asked for', function() {
+ assert.equal('force-update-check', lastDispatchedEvent.type);
+ });
+
+ test('should set the setting back to false', function() {
+ var setting = 'gaia.system.checkForUpdates';
+ assert.isFalse(MockNavigatorSettings.mSettings[setting]);
+ });
+ });
+
+ test('should not dispatch force update event if not asked', function() {
+ UpdateManager.checkForUpdates(false);
+ assert.isNull(lastDispatchedEvent);
+ });
+ });
+ });
+
+ suite('queues support', function() {
+ suite('updates queue', function() {
+ suite('addToUpdatesQueue', function() {
+ setup(function() {
+ var installedApp = new MockApp();
+ var updatableApp = new MockAppUpdatable(installedApp);
+
+ var pendingApp = new MockApp({ installState: 'pending' }),
+ uPendingApp = new MockAppUpdatable(pendingApp);
+
+ UpdateManager.updatableApps = [updatableApp, uPendingApp];
+ UpdateManager.init();
+ });
+
+ test('should add the updatable app to the array', function() {
+ var updatableApp = UpdateManager.updatableApps[0];
+
+ var initialLength = UpdateManager.updatesQueue.length;
+ UpdateManager.addToUpdatesQueue(updatableApp);
+ assert.equal(initialLength + 1, UpdateManager.updatesQueue.length);
+ });
+
+ test('should render', function() {
+ var updatableApp = UpdateManager.updatableApps[0];
+
+ UpdateManager.addToUpdatesQueue(updatableApp);
+ assert.equal('updateAvailableInfo{"n":1}',
+ UpdateManager.message.textContent);
+ });
+
+ test('should not add app if not in updatableApps array', function() {
+ var updatableApp = new MockAppUpdatable(new MockApp);
+ var initialLength = UpdateManager.updatesQueue.length;
+ UpdateManager.addToUpdatesQueue(updatableApp);
+ assert.equal(initialLength, UpdateManager.updatesQueue.length);
+ });
+
+ test('should add a system update to the array', function() {
+ var systemUpdate = new MockSystemUpdatable();
+
+ var initialLength = UpdateManager.updatesQueue.length;
+ UpdateManager.addToUpdatesQueue(systemUpdate);
+ assert.equal(initialLength + 1, UpdateManager.updatesQueue.length);
+ });
+
+ test('should not add more than one system update', function() {
+ var systemUpdate = new MockSystemUpdatable();
+
+ UpdateManager.updatesQueue.push(new MockSystemUpdatable());
+ var initialLength = UpdateManager.updatesQueue.length;
+ UpdateManager.addToUpdatesQueue(systemUpdate);
+ assert.equal(initialLength, UpdateManager.updatesQueue.length);
+ });
+
+ test('should not add if app already in the array', function() {
+ var updatableApp = UpdateManager.updatableApps[0];
+ UpdateManager.addToUpdatesQueue(updatableApp);
+
+ var initialLength = UpdateManager.updatesQueue.length;
+ UpdateManager.addToUpdatesQueue(updatableApp);
+ assert.equal(initialLength, UpdateManager.updatesQueue.length);
+ });
+
+ test('should not add if downloading', function() {
+ UpdateManager._downloading = true;
+ var updatableApp = UpdateManager.updatableApps[0];
+
+ var initialLength = UpdateManager.updatesQueue.length;
+ UpdateManager.addToUpdatesQueue(updatableApp);
+ assert.equal(initialLength, UpdateManager.updatesQueue.length);
+ });
+
+ test('should not add a pending app to the array', function() {
+ var updatableApp = UpdateManager.updatableApps[1];
+
+ var initialLength = UpdateManager.updatesQueue.length;
+
+ UpdateManager.addToUpdatesQueue(updatableApp);
+ assert.equal(UpdateManager.updatesQueue.length, initialLength);
+ });
+
+ });
+
+ suite('removeFromUpdatesQueue', function() {
+ var updatableApp;
+
+ setup(function() {
+ var installedApp = new MockApp();
+ updatableApp = new MockAppUpdatable(installedApp);
+ UpdateManager.updatableApps = [updatableApp];
+ UpdateManager.updatesQueue = [updatableApp];
+ UpdateManager.init();
+ });
+
+ test('should remove if in updatesQueue array', function() {
+ var initialLength = UpdateManager.updatesQueue.length;
+ UpdateManager.removeFromUpdatesQueue(updatableApp);
+ assert.equal(initialLength - 1, UpdateManager.updatesQueue.length);
+ });
+
+ test('should render', function() {
+ UpdateManager.removeFromUpdatesQueue(updatableApp);
+ assert.equal('updateAvailableInfo{"n":0}',
+ UpdateManager.message.textContent);
+ });
+
+ test('should remove system updates too', function() {
+ var systemUpdate = new MockSystemUpdatable();
+ UpdateManager.updatesQueue.push(systemUpdate);
+
+ var initialLength = UpdateManager.updatesQueue.length;
+ UpdateManager.removeFromUpdatesQueue(systemUpdate);
+ assert.equal(initialLength - 1, UpdateManager.updatesQueue.length);
+ });
+ });
+ });
+
+ suite('downloads queue', function() {
+ suite('addToDownloadsQueue', function() {
+ var updatableApp;
+
+ setup(function() {
+ var installedApp = new MockApp();
+ updatableApp = new MockAppUpdatable(installedApp);
+ UpdateManager.updatableApps = [updatableApp];
+ UpdateManager.init();
+ });
+
+ test('should add the updatable to the array', function() {
+ var initialLength = UpdateManager.downloadsQueue.length;
+ UpdateManager.addToDownloadsQueue(updatableApp);
+ assert.equal(initialLength + 1, UpdateManager.downloadsQueue.length);
+ });
+
+ test('should add system updates too', function() {
+ var initialLength = UpdateManager.downloadsQueue.length;
+ UpdateManager.addToDownloadsQueue(new MockSystemUpdatable());
+ assert.equal(initialLength + 1, UpdateManager.downloadsQueue.length);
+ });
+
+ test('should not add more than one system updates', function() {
+ var initialLength = UpdateManager.downloadsQueue.length;
+ UpdateManager.addToDownloadsQueue(new MockSystemUpdatable());
+ UpdateManager.addToDownloadsQueue(new MockSystemUpdatable());
+ assert.equal(initialLength + 1, UpdateManager.downloadsQueue.length);
+ });
+
+ suite('switching to downloading mode on first add', function() {
+ setup(function() {
+ UpdateManager.addToDownloadsQueue(updatableApp);
+ });
+
+ test('should add css class', function() {
+ var css = UpdateManager.container.classList;
+ assert.isTrue(css.contains('downloading'));
+ });
+
+ test('should ask for statusbar indicator', function() {
+ var incMethod = 'incSystemDownloads';
+ assert.ok(MockStatusBar.wasMethodCalled[incMethod]);
+ });
+
+ test('should request wifi wake lock', function() {
+ assert.equal('wifi', MockNavigatorWakeLock.mLastWakeLock.topic);
+ assert.isFalse(MockNavigatorWakeLock.mLastWakeLock.released);
+ });
+ });
+
+ test('should not add app if not in updatableApps array', function() {
+ var updatableApp = new MockAppUpdatable(new MockApp);
+ var initialLength = UpdateManager.downloadsQueue.length;
+ UpdateManager.addToDownloadsQueue(updatableApp);
+ assert.equal(initialLength, UpdateManager.downloadsQueue.length);
+ });
+
+ test('should not add if already in the array', function() {
+ UpdateManager.addToDownloadsQueue(updatableApp);
+
+ var initialLength = UpdateManager.downloadsQueue.length;
+ UpdateManager.addToDownloadsQueue(updatableApp);
+ assert.equal(initialLength, UpdateManager.downloadsQueue.length);
+ });
+ });
+
+ suite('removeFromDownloadsQueue', function() {
+ var updatableApp;
+
+ setup(function() {
+ var installedApp = new MockApp();
+ updatableApp = new MockAppUpdatable(installedApp);
+ UpdateManager.init();
+
+ UpdateManager.addToUpdatableApps(updatableApp);
+ UpdateManager.addToDownloadsQueue(updatableApp);
+ });
+
+ test('should remove if in downloadsQueue array', function() {
+ var initialLength = UpdateManager.downloadsQueue.length;
+ UpdateManager.removeFromDownloadsQueue(updatableApp);
+ assert.equal(initialLength - 1, UpdateManager.downloadsQueue.length);
+ });
+
+ suite('should switch off downloading mode on last remove', function() {
+ setup(function() {
+ UpdateManager.removeFromDownloadsQueue(updatableApp);
+ });
+
+ test('should remove css class', function() {
+ var css = UpdateManager.container.classList;
+ assert.isFalse(css.contains('downloading'));
+ });
+
+ test('should remove statusbar indicator', function() {
+ var decMethod = 'decSystemDownloads';
+ assert.ok(MockStatusBar.wasMethodCalled[decMethod]);
+ });
+
+ test('should release the wifi wake lock', function() {
+ assert.equal('wifi', MockNavigatorWakeLock.mLastWakeLock.topic);
+ assert.isTrue(MockNavigatorWakeLock.mLastWakeLock.released);
+ });
+ });
+
+ test('should not break if wifi unlock throws an exception',
+ function() {
+ MockNavigatorWakeLock.mThrowAtNextUnlock();
+ UpdateManager.removeFromDownloadsQueue(updatableApp);
+ assert.ok(true);
+ });
+
+ test('should remove system updates too', function() {
+ var systemUpdate = new MockSystemUpdatable();
+ UpdateManager.downloadsQueue.push(systemUpdate);
+
+ var initialLength = UpdateManager.downloadsQueue.length;
+ UpdateManager.removeFromDownloadsQueue(systemUpdate);
+ assert.equal(initialLength - 1, UpdateManager.downloadsQueue.length);
+ });
+ });
+ });
+ });
+});
diff --git a/build/BUSYBOX_LICENSE b/build/BUSYBOX_LICENSE
new file mode 100644
index 0000000..e5b619c
--- /dev/null
+++ b/build/BUSYBOX_LICENSE
@@ -0,0 +1,16 @@
+About BusyBox
+
+BusyBox is licensed under the GNU General Public License version 2
+
+Check busybox' site for more information:
+ http://busybox.net
+ http://busybox.net/license.html
+
+Distributed busybox-armv6l binary was obtained from:
+ http://www.busybox.net/downloads/binaries/1.19.0/busybox-armv6l
+
+You can check this by comparing the sha1 signature of the binary:
+ b7d3edcd61956f4dfcccad7c5ee4d92bcbbcb172
+
+The sources are available at:
+ http://www.busybox.net/downloads/busybox-1.19.0.tar.bz2
diff --git a/build/applications-data.js b/build/applications-data.js
new file mode 100644
index 0000000..69ef24b
--- /dev/null
+++ b/build/applications-data.js
@@ -0,0 +1,243 @@
+'use strict';
+
+const PREFERRED_ICON_SIZE = 60;
+const GAIA_CORE_APP_SRCDIR = 'apps';
+const GAIA_EXTERNAL_APP_SRCDIR = 'external-apps';
+const INSTALL_TIME = 132333986000; // Match this to value in webapp-manifests.js
+
+// Initial Homescreen icon descriptors.
+
+// c.f. the corresponding implementation in the Homescreen app.
+function bestMatchingIcon(preferred_size, manifest, origin) {
+ var icons = manifest.icons;
+ if (!icons) {
+ return undefined;
+ }
+
+ var preferredSize = Number.MAX_VALUE;
+ var max = 0;
+
+ for (var size in icons) {
+ size = parseInt(size, 10);
+ if (size > max)
+ max = size;
+
+ if (size >= PREFERRED_ICON_SIZE && size < preferredSize)
+ preferredSize = size;
+ }
+ // If there is an icon matching the preferred size, we return the result,
+ // if there isn't, we will return the maximum available size.
+ if (preferredSize === Number.MAX_VALUE)
+ preferredSize = max;
+
+ var url = icons[preferredSize];
+ if (!url) {
+ return undefined;
+ }
+
+ // If the icon path is not an absolute URL, prepend the app's origin.
+ if (url.indexOf('data:') == 0 ||
+ url.indexOf('app://') == 0 ||
+ url.indexOf('http://') == 0 ||
+ url.indexOf('https://') == 0)
+ return url;
+
+ return origin + url;
+}
+
+function iconDescriptor(directory, app_name, entry_point) {
+ let origin = gaiaOriginURL(app_name);
+ let manifestURL = gaiaManifestURL(app_name);
+
+ // For external/3rd party apps that don't use the Gaia domain, we have an
+ // 'origin' file that specifies the URL.
+ let dir = getFile(GAIA_DIR, directory, app_name);
+ let originFile = dir.clone();
+ originFile.append("origin");
+ if (originFile.exists()) {
+ origin = getFileContent(originFile).replace(/^\s+|\s+$/, '');
+ if (origin.slice(-1) == "/") {
+ manifestURL = origin + "manifest.webapp";
+ } else {
+ manifestURL = origin + "/manifest.webapp";
+ }
+ }
+
+ let manifestFile = dir.clone();
+ manifestFile.append("manifest.webapp");
+ let manifest = getJSON(manifestFile);
+
+ if (entry_point &&
+ manifest.entry_points &&
+ manifest.entry_points[entry_point]) {
+ manifest = manifest.entry_points[entry_point];
+ }
+ let icon = bestMatchingIcon(PREFERRED_ICON_SIZE, manifest, origin);
+
+ //TODO set localizedName once we know the default locale
+ return {
+ manifestURL: manifestURL,
+ entry_point: entry_point,
+ updateTime: INSTALL_TIME,
+ name: manifest.name,
+ icon: icon
+ };
+}
+
+// zeroth grid page is the dock
+let customize = {"homescreens": [
+ [
+ ["apps", "communications", "dialer"],
+ ["apps", "sms"],
+ ["apps", "communications", "contacts"],
+ ["apps", "browser"]
+ ], [
+ ["apps", "camera"],
+ ["apps", "gallery"],
+ ["apps", "fm"],
+ ["apps", "settings"],
+ ["external-apps", "marketplace"]
+ ], [
+ ["apps", "calendar"],
+ ["apps", "clock"],
+ ["apps", "costcontrol"],
+ ["apps", "email"],
+ ["apps", "music"],
+ ["apps", "video"]
+ ]
+]};
+
+if (DOGFOOD == 1) {
+ customize.homescreens[0].push(["dogfood_apps", "feedback"]);
+}
+
+let init = getFile(GAIA_DIR, 'customize.json');
+if (init.exists()) {
+ customize = getJSON(init);
+}
+
+let content = {
+ search_page: {
+ provider: 'EverythingME',
+ enabled: true
+ },
+
+ grid: customize.homescreens.map(
+ function map_homescreens(applist) {
+ var output = [];
+ for (var i = 0; i < applist.length; i++) {
+ if (applist[i] !== null) {
+ output.push(iconDescriptor.apply(null, applist[i]));
+ }
+ }
+ return output;
+ }
+ )
+};
+
+init = getFile(GAIA_DIR, GAIA_CORE_APP_SRCDIR, 'homescreen', 'js', 'init.json');
+writeContent(init, JSON.stringify(content));
+
+// Apps that should never appear in settings > app permissions
+// bug 830659: We want homescreen to appear in order to remove e.me geolocation permission
+let hidden_apps = [
+ gaiaManifestURL('keyboard'),
+ gaiaManifestURL('wallpaper'),
+ gaiaManifestURL('bluetooth'),
+ gaiaManifestURL('pdfjs')
+];
+
+init = getFile(GAIA_DIR, GAIA_CORE_APP_SRCDIR, 'settings', 'js', 'hiddenapps.js');
+writeContent(init, "var HIDDEN_APPS = " + JSON.stringify(hidden_apps));
+
+// Apps that should never appear as icons in the homescreen grid or dock
+hidden_apps = hidden_apps.concat([
+ gaiaManifestURL('homescreen'),
+ gaiaManifestURL('system')
+]);
+
+init = getFile(GAIA_DIR, GAIA_CORE_APP_SRCDIR, 'homescreen', 'js', 'hiddenapps.js');
+writeContent(init, "var HIDDEN_APPS = " + JSON.stringify(hidden_apps));
+
+// Cost Control
+init = getFile(GAIA_DIR, 'apps', 'costcontrol', 'js', 'config.json');
+
+content = {
+ provider: 'Vivo',
+ enable_on: { 724: [6, 10, 11, 23] }, // { MCC: [ MNC1, MNC2, ...] }
+ is_free: true,
+ is_roaming_free: true,
+ credit: { currency : 'R$' },
+ balance: {
+ destination: '8000',
+ text: 'SALDO',
+ senders: ['1515'],
+ regexp: 'Saldo Recarga: R\\$\\s*([0-9]+)(?:[,\\.]([0-9]+))?'
+ },
+ topup: {
+ destination: '7000',
+ ussd_destination: '*321#',
+ text: '&code',
+ senders: ['1515', '7000'],
+ confirmation_regexp: 'Voce recarregou R\\$\\s*([0-9]+)(?:[,\\.]([0-9]+))?',
+ incorrect_code_regexp: '(Favor enviar|envie novamente|Verifique) o codigo de recarga'
+ },
+ default_low_limit_threshold: 3
+};
+
+writeContent(init, JSON.stringify(content));
+
+// SMS
+init = getFile(GAIA_DIR, 'apps', 'sms', 'js', 'blacklist.json');
+content = ["1515", "7000"];
+writeContent(init, JSON.stringify(content));
+
+// Browser
+init = getFile(GAIA_DIR, 'apps', 'browser', 'js', 'init.json');
+
+content = {
+ "bookmarks": [
+ { "title": "Vivo Busca",
+ "uri": "http://www.google.com.br/m/search?client=ms-hms-tef-br",
+ "iconUri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAN1wAADdcBQiibeAAAAAd0SU1FB9wMDg8JHqyWLdEAAAbmSURBVFjD1ZdrjF1VFcd/6+xzz7nPzqOlFNpBKqA8Uoi0Cg1BjaARa+SDFDEKamk7My2tMZIYIUZ8pISoCbaEzsyFkqZSBaLhUUKMBLSAYMm0hqaNSFvSKTJMS2fambmP89h7+eFOHyOdYUD7wfVtn7Nz1m899n+vA/8Ptpz1U9zX/YG/LZO9XOb1UHbLj6+XsC4XTstl/YwfIOKpw9k0SaLharSL3srf2AhAu9dNt2v/7wDacz101xrOl+d7mgPfv1iM+bIJ/Ks9412IJyVBaurcGzZOt9nU/SGq13eW6+2HADpyZbpqyz4cQEf+AbqqS/kqPzFnFGbP94PM9/zAvynM5zC+Ac/DZAye7yHGA1Vqw1XiSv25aLR69+GRgRce4Y6os/gg60dv/WAAHWGZrmgZN/ELv6XYujiTDe4xvt+mTonqdU1qsaSJxTOAJ2SKAfmWgpZaW8RzQvXIaK06NHLn4NC7PZu5vdIe9tAdLZ8QwIxLO11023auYaW0lc5bHOSza41nzorjSCtDI+SmGWm7tJXzr5rFeVfMZPbFLRSbA+qjNTm4/5BKVig1N2d8439WrBcV6jNf/r29y63wNvKqPj71EnQWH1gQFnIPe57/sXq1otFoXc67cibzb5zL+VefyayLmvCzBhQGD1To2/Yurz3Vx44/7scv5LW1daZER6qHB/sPrequLPvtZCUwJ0ffyxa+zdqmQnPp9iCb/WJcq2t9pCqXLTqH6++ezyVfmk1heohNlDRypLEjKPjMuqiJCz49i4xn2PPXt8Uap4VCKS9Ky4XDV2/dwTNH2tlAL09MDNDLFgCualk8L8hl7zW+Hwy9M8gFC8+U69fM5+xLm0gjxVkFBBnLnTpFLZjA0Hb5dFzk2PXsAfLTi+KTOSdN4t2vRk9uP5VzAO/kxde4J/BNuDCTCwpRra75pows+Ppczp7XxPCIIbEeiCCiJ2oogqqiTvGzhvk3zOUj81pl9MiwelljsoXCZ5axdvpEJRgHkCNb9DLegiCXJarUmT2vhXM/NYNq5FOt+6iCoO9tpGMQVmmek2fedXOoHKogPgTZsC1FZ04JIBMGgWe8NjFCmiRyxtwSMz5aYrjig4DxGs5V5ZQQAGHB56wLm/GNiE0t4nltmSCcMSUA8Y2HSIgHaiAs+fjFDKkVjKfHUy8T6Kdqoz+CnE+Y93HWgdJiPFOaEoBNU6uqFRGDlzHYxKGJwxNwTjhF9k95sK1V4loKThCRIWft6JQAXJQkmtgBdY6gGDI0UOXoW6Pkcxargh1LvU4AIiKkkWPoQIXoqEUUrLVvJ0l6eEoAEfVhl7oXklpEsaVE/56jvPXaEZqLCYFvcU5Qxp+CE6kH8WDkYI1//rmfsBjirCOpxe/kaRp8X4AOr8xmfpAmSbQtqUTVbDbH0YFYd/7pACMH67QWY4KMbYR/UhOq6vEGdBYO7DjM35/o01wph9YtcbW+9T5u7v9pbs3kAF2ucXXWhitvxtWox9Ys0+fMlN4n9+srm/YQVS2eSMOZHItax60P7DjM43duJxOG4olHEsUvuRH7PMC+2sL3v4xWZrvpSVdGl6Wf7/fEfCFfKrVKKLL7uT51sZOW2QX80OCHBs8IYoQ0dlQOR+x96SC/W/UKR/sjCqUiLnHER2uP3G+XPAzwSRayfUxtJwQ4N72F3TxGr3164BP2ugGQa0vTmrOm4MuuZ/t074sDUj8Sk8aOkUN1BvsqvLF1gL+s/wfPrNmJSz0KhQLqVKPhuqRRkr+cRbu383TfdrawnO7jkj/hbbjaPMha2xgiVmQfvCU/Y9rPCq3TztHAURmpUD00qvFoLJnQI64moEKQCwjzIcYYNHFEIzVsnKhDBeRlYFWZjt5jc2MP7ZNfx7eZDdxnl4zNd+XPhS25FcXmadf4uaCZjOKsJY0smlqcU0gcNnYk9Yi0Equ1VhSngtDoWHleobNMx+sAS7mfB1gx+Uz4XfMbfm2/2cgEG5rJ8BVT9K8IctlLMkGmIMaEqEvS2MZpVN8XDUcFG6XXOywNryI0pGtMweQxRW8r03lwFQ+xju+8/1QMsNrfxNr05hONysaPp0RFhwYCaUg+LjBjbz+vNwWE9wI3CMeFYhyEwo+BX5bprE5pLD/Zvh88yq/iGyfds4yuuQLrgEWcEG5piKeKIO8CV/TQse+USjiZTeZ8OV0AlOl4E/gh8OIEwdUAN6EUf1jroeM4RA8dO0HvAN2l46IHRX+u6L/+5wDvheh8QZEfgQ7SSP1+Rb4FuqlMZ3JaAI5BtHP/mMLZpwQWKnK5wpXA5jIrao1eWc9ps8U8ytKxn9TVrOdWHvqPRj2Nzo/ZXdzFN9g07tlSyqfc+2+KJinaWejipgAAAABJRU5ErkJggg=="
+ },
+ { "title": "Serviços e Downloads",
+ "uri": "http://vds.vivo.com.br",
+ "iconUri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABOWlDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjarZGxSsNQFIa/G0XFoVYI4uBwJ1FQbNXBjElbiiBYq0OSrUlDldIk3NyqfQhHtw4u7j6Bk6PgoPgEvoHi1MEhSHASwW/6zs/hcOAHo2LXnYZRhkGsVbvpSNfz5ewTM0wBQCfMUrvVOgCIkzjiJwI+XxEAz5t23WnwN+bDVGlgAmx3oywEUQH6FzrVIMaAGfRTDeIOMNVJuwbiASj1cn8BSkHub0BJuZ4P4gMwe67ngzEHmEHuK4Cpo0sNUEvSkTrrnWpZtSxL2t0kiOTxKNPRIJP7cZioNFEdHXWB/D8AFvPFdtORa1XL2lvnn3E9X+b2foQAxNJjkRWEQ3X+3YWx8/tc3Bgvw+EtTE+KbPcKbjZg4brIVqtQ3oL78RfCs0/+HAmzJwAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAAAd0SU1FB9wMDg8FJ18m6tUAAAjBSURBVFjDrVd7cFXFGf/tnnPuM++bhBSD8rQq+MCkZVoQaEc7aivRqY8RLVroyB/BUmuSiqMdWxGQURkBR7AkPnCKVUSxVmYqRQTCACUE0YgPCCDP5L5C7r3nsbtnv/5xEyDVDsykO7MzZ3Z/+51vf99zgQscH6Pixl0/qan/fOEDhf8L8+XihqKOeQ/Ut44YeQcA/P0C5PLzAbo6NkZbh47+KlBVsSE0fORyp+3L3g8Bawsqz2Bahw7HRoBnWtsSVklsuVle/mb7vb9wfrxne/H55JvnAxz/w5LrKx+6d0xo1EiQlFCpNGXXdkybjO63+zETTxzG1srq6wqvnWCFho/G0Ecug0zFg+nNH84AsGxQChx5/wM99qe18E4d619iZqxk7cdJ32CwNC8NYFL6GMyS4s1GZRFEsisPYmD7//i8P2gT8HDZpkMvrIHqSUCl4lCpOEpn/wwE/dBkdGNS+hi2hCpvKrzxh1DJ/L5KJ9D5/OvQPtYPWoE6J5lzvum+L7PvK8h0CjKdgi9tWBXlz2wxqsIAYA4pXUchUP++e+wE5TpPPlHnpI6fTz47H+C9aDmm5RJ4v7SKqn85AaQJAKAzLjJv7XqcwYxHb7lyhVHRFxyM4cT6f0PlvCDjTEzLJQbnhP0CVM4bf3pvZ3v0klgfdxxmRckjAGyEGfzTvQAAN5GFyrp1dW5KXEh4s6ZhjQBDMfk0I+2kfeL0t+ZES/I3FbOwKt48ALw+Ejv4vUmjRiJPAnROAkqDFwfPYE62HsxahaHCm7vOsm8fWo3IiF/BO7EupqHv0na3QaTXFYyZc5wBQONFDU71j0YFw2UR1rG5HZlETyPAlrSkXh7gxevDsXCwJGQXX1JKoD7zMQAEgIGceA6ZE70VdU4yee45op2wD31dT35muerZCxgBsoouY9HR9Yw1Dmu8w4pYbw6bNBpaaRAIbsamAzv3M+F4U61QYMvKEyvpHBbeKasquZUbA/2XtEbyZE9HnZ0cd+766c9WTADUDj/zGcjPAoyDAbDKxoEZ4Vnc0qgMWCaE40IKD9L1wE2DXTppLKqvuGSzEvKbWbGZFQCwoGQK6uzkbb3dvTalBem0B532QGlBmXjWr7OT47bfMhEAEN/5dDjZ9uxB2fP5Du/UR9B2CloIaOFBS4Hc4U+R+bI9zJ4e0lQYlKxX15TDL7IAIpAmEJ2d3Z0nqberZ2tzomVKHwsPRmAsDVHehwU0skyuqbOT0wGge9ufN5FMTiX7EGOMAX2TcQaAQbsepfccZEZhYREDgNcwj2uDplghY9OpawvgFDGgX4m8EUl5knUfOOlKV85vTrQ89V6kvAPAFQBAjA7V5ZIjj/+z6UFobxFzD0ZAksAYY2AAZwDnIOkjte8wtOPdTtp/9+rFR3wOANyEvt9f9NGInDYu3nr6ntjODCjjQSoJKQSEEMyHRmzMkGC0vGD+rPKZrdPsxFhiVA6GqrpccuThd+es9bNHl+r03rB2c9BCMi0EtC8hMzbiezpxctv+6a7tGVct6nybmZb/rUT0qjUP98mFAIDFpY2PZGL8MRkzorAIRABR3hdTX8cBILIq3uwAwBev3sOtUNA3nf0A5+inXXk+cl2ZbK47s9CMWEtqF3zttM8bjfELD3x3IuqoUsDR/HcijLSGtknoKBGdMcl35nPLgLJtcCkBzs4ooIUPJRUUAO3IfKL6r9zHAaBpWGNfLIE3XdQ4vWF4A8lKf4UsVhWKJJSQkFJBSUVujw0i2r4q3uzs2nd5+a59lw+5dPpr2ndy64SMQAtBvhDQngDnGqVDowVVY8qeMgtC9s4nxt+djXdxAPji5bqztaBpWCMHYQpx2iQKFXxDD4gGEBH5xNRp6Wpfz29OtDzV2n7ZNgKbCDAw6PjE8V9U7nvuhgcZiUVBIxXhnPJOyPpqM+MgxiFYDEZB8e3gePfS6Wt91jSsoZCAXifqQTO/74cYEIbkaNKe3tKcaJkKABt3jJ3TIyqWZWQMACFsZFEePr7m+gkd0wGg7clJ75iGe2skmMmHYF/45RVh4MEIhcdcx4KFRUU8rNkMAxrCF3l7SQUlJJRQ8G0F3eMf054e0pxomTr//p/ns5sbXHC8N4KM6yDjuujKmdSdLbwTADasrUHN49tuEx6PpJPRTifDQZ4EybPTz/YyQ+VghaMzzLIc7xaccIoLcN13cw0yJGeM2GQwbGuOtxAAPPbKP/DGxqtf7UyXFiqtBhS1o16B8ca/rt6f1epyAPjBn3Y5AEZtf3jshNxpY0dhsYBlAsQYCICXTkHZHQ4DgL8ajzq7iuPBhOWwgLbAiTcSaElzomVAMXplwzWRrAjkuuwoMQwsRgRQcdBlZSH3qpk3t3967rk1E4GLrhxRb5r+8uIYgRsB4qEq9v2mVsaXhx5FsU9VN6Qq51Z50XoGXr4q0fwMY+xb/VzO41uOZsLwlGau0rCFQtaWcJWGpzTrzgVge3zri+tr+LNv1Zw5V33lcExecegFkVHliSOqXsTVXPfIN9UX1BH1j2ffvPaqXhn4JCMt6j/n2h6I6Ei4IHwxiBgABAwfsYB7d+Nde97A/+NdMK+lNn97wXekPBNKa6a0hvQ1lPQfU1I3CSGZ0hpKa9iSISeMNU+urgk/+nItBt0T5pWoudcha7Wks3DfVSCfIivn7nZmL6t1zUgg0M8CB6iAiyULZ7Y9PGgG6l+sMRzJV3vEoYlBEwMRg/Z1w8q5u518M0K3aalZ/74CY7bkv//dippRg1ZAerjJY8bZHgEE3xUAsHL20lrMXlqLl+a2bdBCAejDaILgJjzJbhz000z4MCJE0Izl+woiIqKGl+a2ZQf2fXQN+XovDA4iwASRD+KDZiBp8w9CzD8woliiIqwQJMWUj2Wzlw50sJf+EvyEKZUuDSiMKpEoMpUnfbw+aAVGVEA+98DuMaZBN4UMPcfkqG5+qE2u/O3ugcDPtsMy2PAgp/qAiTsX/Hp3eNzFPH0++f8BWpzorRJD7KcAAAAASUVORK5CYII="
+ },
+ {
+ "title": "Site Vivo",
+ "uri": "http://www.vivo.com.br/conteudosmartphone",
+ "iconUri": "data:image/jpg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUFBQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wAARCAAgACADASIAAhEBAxEB/8QAGgAAAgIDAAAAAAAAAAAAAAAABQYBAwQHCP/EAC4QAAEDAwIEBQIHAAAAAAAAAAECAwQFBhEAIQcSMUETFFFhcSKBMkKCkqGxwf/EABkBAAIDAQAAAAAAAAAAAAAAAAMGAAECBP/EACQRAAIBAwQCAgMAAAAAAAAAAAECEQMEEgAFITEiQVFxMmGh/9oADAMBAAIRAxEAPwDrG7rkg25AEiVlx1eQywk/U4f8A7nSXFrXEW4k+ZpMJiFEV+BxYSAfgqyT8gasmQBcPEQpmDnjNuFPIenI2OnwVf3ozOnSKnJ8rDeUhnPIyyyoJKwO59Btn2Gky53F7lncswQMVVVMFiOzPxpko0KVsiqEDORJLcgA9CPnQGbVOJdvtGXUIrE+IjdxTaUr5R6kJwoD3wQNN9lXVCuaEXGR4UlsAuMk52PRST3ToLBRNgywtp59pxDwbcaWvmC9wMehznY++sOi01qlcRZSYIDbHmikIT0AW0lah8cxOpZX9ak6sC2JYKysZImYIPB4jkau4pUbimwKgMBIKiAY7BHXvgjQldRXa/F+WmouLEOWQ6ypXQJUCDj9RP7dTSLHrsC6mpER1C6c49zpmMPAEtE53Gc5x6ZGn29LUpd1QEx56VNvNEqYkN7LaJ649QcDIOxwO4B0jxba4kW2S1RqjGqEXP0guBKvulY5QfcHJ0W82rB/KmXTLIY/kCYkR7BgaNbbgtWl4OEfEKQ3RA6IPo/erJ9PumJeTtRapbstIfUtsBWW1D8pOD22ODo9Zdt1RmsP1+vPDzTpJbjpVlKCeqj2zjYY6DQZUHilUh4cmQ1BQepEhKMfdsc3862HSWpLFMjMzHQ7IbbSlxYJIUoDc7763tW3UzcM5RwAchnAGR98cn9Trm3C7dKIQMhJGJxkmB/PvX//2Q=="
+ }
+ ]
+}
+
+writeContent(init, JSON.stringify(content));
+
+// Support
+init = getFile(GAIA_DIR, 'apps', 'settings', 'resources', 'support.json');
+content = {
+ "onlinesupport": {
+ "href": "http://www.vivo.com.br/portalweb/appmanager/env/web?_nfls=false&_nfpb=true&_pageLabel=vcAtendMovelBook&WT.ac=portal.atendimento.movel",
+ "title": "Vivo"
+ },
+ "callsupport": [
+ {
+ "href": "tel:*8486",
+ "title": "*8486"
+ },
+ {
+ "href": "tel:1058",
+ "title": "1058"
+ }
+ ]
+}
+writeContent(init, JSON.stringify(content));
+
+// ICC / STK
+init = getFile(GAIA_DIR, 'apps', 'settings', 'resources', 'icc.json');
+content = {
+ "defaultURL": "http://www.mozilla.org/en-US/firefoxos/"
+}
+writeContent(init, JSON.stringify(content));
diff --git a/build/busybox-armv6l b/build/busybox-armv6l
new file mode 100644
index 0000000..5f34bbc
--- /dev/null
+++ b/build/busybox-armv6l
Binary files differ
diff --git a/build/install-gaia.py b/build/install-gaia.py
new file mode 100644
index 0000000..9ff2ae0
--- /dev/null
+++ b/build/install-gaia.py
@@ -0,0 +1,176 @@
+"""Usage: python %prog [ADB_PATH] [REMOTE_PATH]
+
+ADB_PATH is the path to the |adb| executable we should run.
+REMOTE_PATH is the path to push the gaia webapps directory to.
+
+Used by |make install-gaia| to push files to a device. You shouldn't run
+this file directly.
+
+"""
+
+import sys
+import os
+import hashlib
+import subprocess
+from tempfile import mkstemp
+
+def compute_local_hash(filename, hashes):
+ h = hashlib.sha1()
+ with open(filename,'rb') as f:
+ for chunk in iter(lambda: f.read(256 * h.block_size), b''):
+ h.update(chunk)
+ hashes[filename] = h.hexdigest()
+
+def compute_local_hashes_in_dir(dir, hashes):
+ def visit(arg, dirname, names):
+ for filename in [os.path.join(dirname, name) for name in names]:
+ if not os.path.isfile(filename):
+ continue
+ compute_local_hash(filename, hashes)
+
+ os.path.walk(dir, visit, None)
+
+def compute_local_hashes():
+ hashes = {}
+ compute_local_hashes_in_dir('webapps', hashes)
+ compute_local_hash('user.js', hashes)
+ return hashes
+
+def adb_push(local, remote):
+ global adb_cmd
+ subprocess.check_call([adb_cmd, 'push', local, remote])
+
+def adb_shell(cmd, ignore_error=False):
+ global adb_cmd
+
+ # Output the return code so we can check whether the command executed
+ # successfully.
+ new_cmd = cmd + '; echo "RETURN CODE: $?"'
+
+ # universal_newlines=True because adb shell returns CRLF separators.
+ proc = subprocess.Popen([adb_cmd, 'shell', new_cmd],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ universal_newlines=True)
+ (stdout, stderr) = proc.communicate()
+ if stderr.strip():
+ raise Exception('adb shell "%s" returned the following unexpected error: "%s"' %
+ (cmd, stderr.strip()))
+ if proc.returncode != 0:
+ raise Exception('adb shell "%s" exited with error %d' % (cmd, proc.returncode))
+
+ split = [line for line in stdout.split('\n') if line.strip()]
+ if not ignore_error and not split[-1].startswith('RETURN CODE: 0'):
+ raise Exception('adb shell "%s" did not complete successfully. Output:\n%s' % (cmd, stdout))
+
+ # Don't return the "RETURN CODE: 0" line!
+ return split[0:-1]
+
+
+def compute_remote_hashes():
+ hashes = {}
+ adb_out = adb_shell('cd /data/local && find . -type f | xargs sha1sum')
+ for (hash, filename) in [line.split() for line in adb_out]:
+ # Strip off './' from the filename.
+ if filename.startswith('./'):
+ filename = filename[2:]
+ else:
+ raise Exception('Unexpected filename %s' % filename)
+
+ hashes[filename] = hash
+ return hashes
+
+INDEXED_DB_FOLDER = 'indexedDB/'
+
+def remove_from_remote(local_hashes, remote_hashes):
+ """Remove any files from the remote device which don't appear in
+ local_hashes.
+
+ """
+
+ # Keep indexedDB content
+ to_keep = set()
+ for path in remote_hashes:
+ if path[:len(INDEXED_DB_FOLDER)] == INDEXED_DB_FOLDER:
+ to_keep.add(path)
+
+ to_remove = list(set(remote_hashes.keys()) - set(local_hashes.keys()) - to_keep)
+
+ if not to_remove:
+ return
+
+ print 'Removing from device:\n%s\n' % '\n'.join(to_remove)
+ # Chunk to_remove into 25 files at a time so we don't send too much over
+ # adb_shell at once.
+ for files in [to_remove[pos:pos + 25] for pos in xrange(0, len(to_remove), 25)]:
+ adb_shell('cd /data/local && rm -f %s' % ' '.join(files))
+
+def push_to_remote(local_hashes, remote_hashes):
+ global adb_cmd
+
+ to_push = set()
+ for (k, v) in local_hashes.items():
+ if k not in remote_hashes or remote_hashes[k] != local_hashes[k]:
+ to_push.add(k)
+
+ if not to_push:
+ return
+
+ print 'Pushing to device:\n%s' % '\n'.join(list(to_push))
+
+ tmpfile, tmpfilename = mkstemp()
+ try:
+ subprocess.check_call(['tar', '-czf', tmpfilename] + list(to_push))
+ adb_push(tmpfilename, '/data/local')
+ basename = os.path.basename(tmpfilename)
+ adb_shell('cd /data/local && tar -xzf %s && rm %s' % (basename, basename))
+ finally:
+ os.remove(tmpfilename)
+
+def install_gaia_fast():
+ os.chdir('profile')
+ try:
+ local_hashes = compute_local_hashes()
+ remote_hashes = compute_remote_hashes()
+ remove_from_remote(local_hashes, remote_hashes)
+ push_to_remote(local_hashes, remote_hashes)
+ finally:
+ os.chdir('..')
+
+def install_gaia_slow():
+ global adb_cmd, remote_path
+ webapps_path = remote_path + '/webapps'
+ adb_shell("rm -r " + webapps_path, ignore_error=True)
+ adb_shell("rm /data/local/user.js", ignore_error=True)
+ adb_push('profile/webapps', webapps_path)
+ adb_push('profile/user.js', '/data/local')
+
+def install_gaia():
+ global remote_path
+ try:
+ if remote_path == "/system/b2g":
+ # XXX Force slow method until we fix the fast one to support
+ # files in both /system/b2g and /data/local
+ # install_gaia_fast()
+ install_gaia_slow()
+ else:
+ install_gaia_fast()
+ except:
+ # If anything goes wrong, fall back to the slow method.
+ install_gaia_slow()
+
+if __name__ == '__main__':
+ if len(sys.argv) > 3:
+ print >>sys.stderr, 'Too many arguments!\n'
+ print >>sys.stderr, \
+ 'Usage: python %s [ADB_PATH] [REMOTE_PATH]\n' % __FILE__
+ sys.exit(1)
+
+ adb_cmd = 'adb'
+ remote_path = '/data/local/webapps'
+ if len(sys.argv) >= 2:
+ adb_cmd = sys.argv[1]
+ if len(sys.argv) >= 3:
+ remote_path = sys.argv[2]
+
+ install_gaia()
diff --git a/build/multilocale.py b/build/multilocale.py
new file mode 100644
index 0000000..60387b3
--- /dev/null
+++ b/build/multilocale.py
@@ -0,0 +1,248 @@
+#!/usr/bin/env python
+
+import os
+import shutil
+import fnmatch
+import json
+import re
+from optparse import OptionParser
+import logging
+
+section_line = re.compile('\[(?P<section>.*)\]')
+import_line = re.compile('@import url\((?P<filename>.*)\)')
+property_line = re.compile('(?P<id>.*)\s*[:=]\s*(?P<value>.*)')
+
+def _get_locales(filename):
+ locales_list = json.load(open(filename), encoding="utf-8")
+ return locales_list.keys()
+
+
+def find_files(dirs, pattern):
+ matches = []
+ for dir in dirs:
+ for current, dirnames, filenames in os.walk(dir):
+ for filename in fnmatch.filter(filenames, pattern):
+ matches.append(os.path.join(current, filename))
+ return matches
+
+
+def parse_manifest_properties(filename):
+ with open(filename) as f:
+ data = f.readlines()
+ strings = {
+ "default": {},
+ "entry_points": {},
+ }
+ for line in data:
+ m = property_line.search(line)
+ if not m or line.strip().startswith('#'):
+ continue
+ value = m.group('value').strip()
+ if '.' in m.group('id'):
+ entry_point, key = m.group('id').split('.',1)
+ if entry_point not in strings["entry_points"]:
+ strings["entry_points"][entry_point] = {}
+ strings["entry_points"][entry_point][key.strip()] = value
+ else:
+ key = m.group('id')
+ strings["default"][key.strip()] = value
+ return strings
+
+
+def parse_ini(filename):
+ log = logging.getLogger(__name__)
+ with open(filename) as f:
+ data = f.readlines()
+ section = 'default'
+ imports = { section: [] }
+ for line in data:
+ if line.strip() == "" or line.startswith('!') or line.startswith('#'):
+ continue
+ elif line.strip().startswith('['): # Section header
+ section = section_line.search(line).group('section')
+ imports[section] = []
+ elif '@import' in line: # Import lines
+ property_file = import_line.search(line).group('filename')
+ imports[section].append(property_file)
+ else:
+ log.warn('parse_ini - found a line with contents '
+ 'unaccounted for "%s"', line.strip())
+ return imports
+
+
+def serialize_ini(outfile, imports):
+ def _section(locale):
+ return "[%s]" % locale
+ def _import(path):
+ return "@import url(%s)" % path
+ output = []
+ for locale, paths in imports.items():
+ if locale == "default":
+ for path in paths:
+ output.insert(0, _import(path))
+ continue
+ output.append(_section(locale))
+ for path in paths:
+ output.append(_import(path))
+ with open(outfile, 'w') as o:
+ o.write("\n".join(output))
+
+
+def add_locale_imports(locales, ini_file):
+ """Recreate an ini file with all locales sections"""
+ log = logging.getLogger(__name__)
+ imports = {
+ "default": parse_ini(ini_file)["default"]
+ }
+ for locale in locales:
+ log.info("adding %s to %s" % (locale, ini_file))
+ imports[locale] = []
+ for path in imports["default"]:
+ locale_path = path.replace("en-US", locale)
+ imports[locale].append(locale_path)
+ log.debug("added %s" % locale_path)
+ serialize_ini(ini_file, imports)
+ log.info("updated %s saved" % ini_file)
+
+
+def copy_properties(source, locales, ini_file):
+ log = logging.getLogger(__name__)
+ ini_dirname = os.path.dirname(ini_file)
+ imports = parse_ini(ini_file)
+ for locale in locales:
+ log.info("copying %s files as per %s" % (locale, ini_file))
+ for path in imports[locale]:
+ target_path = os.path.join(ini_dirname, path)
+ # apps/browser/locales/browser.fr.properties becomes
+ # apps/browser/browser.properties
+ source_path = target_path.replace('/locales', '') \
+ .replace('.%s' % locale, '')
+ source_path = os.path.join(source, locale, source_path)
+ if not os.path.exists(source_path):
+ log.warn('%s does not exist' % source_path)
+ continue
+ shutil.copy(source_path, target_path)
+ log.debug("copied %s to %s" % (source_path, target_path))
+
+
+def add_locale_manifest(source, locales, manifest_file):
+ log = logging.getLogger(__name__)
+ with open(manifest_file) as f:
+ manifest = json.load(f, encoding="utf-8")
+ for locale in locales:
+ log.info("adding %s to %s" % (locale, manifest_file))
+ manifest_properties = os.path.join(source, locale,
+ os.path.dirname(manifest_file),
+ 'manifest.properties')
+ log.debug("getting strings from %s" % manifest_properties)
+ if not os.path.exists(manifest_properties):
+ log.warn("%s does not exist" % manifest_properties)
+ continue
+ strings = parse_manifest_properties(manifest_properties)
+ if "entry_points" in manifest:
+ for name, ep in manifest["entry_points"].items():
+ if "locales" not in ep:
+ continue
+ log.debug("adding to entry_points.%s.locales" % name)
+ if name not in strings["entry_points"]:
+ log.warn("%s.* strings are missing from %s" %
+ (name, manifest_properties))
+ continue
+ ep["locales"][locale] = {}
+ ep["locales"][locale].update(strings["entry_points"][name])
+ if "locales" in manifest:
+ log.debug("adding to locales")
+ manifest["locales"][locale] = {}
+ manifest["locales"][locale].update(strings["default"])
+ f.close()
+ with open(manifest_file, 'w') as o:
+ json.dump(manifest, o, encoding="utf-8", indent=2)
+ log.debug("updated %s saved" % manifest_file)
+
+
+def setup_logging(volume=1, console=True, filename=None):
+ logger = logging.getLogger(__name__)
+ levels = [logging.DEBUG,
+ logging.INFO,
+ logging.WARNING,
+ logging.ERROR,
+ logging.CRITICAL][::1]
+ if volume > len(levels):
+ volume = len(levels) - 1
+ elif volume < 0:
+ volume = 0
+ logger.setLevel(levels[len(levels)-volume])
+ if console:
+ console_handler = logging.StreamHandler()
+ console_formatter = logging.Formatter('%(levelname)s: %(message)s')
+ console_handler.setFormatter(console_formatter)
+ logger.addHandler(console_handler)
+ if filename:
+ file_handler = logging.FileHandler(filename)
+ file_formatter = logging.Formatter('%(asctime) - %(levelname)s: %(message)s')
+ file_handler.addFormatter(file_formatter)
+ logger.addHandler(file_handler)
+
+
+def main():
+ parser = OptionParser("%prog [OPTIONS] [LOCALES...] - create multilocale Gaia")
+ parser.add_option("-v", "--verbose",
+ action="count", dest="verbose", default=2,
+ help="use more to make louder")
+ parser.add_option("-i", "--ini",
+ action="store_true", dest="onlyini", default=False,
+ help=("just edit the ini files and exit; "
+ "use this with DEBUG=1 make profile"))
+ parser.add_option("--target",
+ action="append", dest="target",
+ help=("path to directory to make changes in "
+ "(more than one is fine)"))
+ parser.add_option("--source",
+ action="store", dest="source",
+ help="path to the l10n basedir")
+ parser.add_option("--config",
+ action="store", dest="config_file",
+ help=("path to the languages.json config file; "
+ "will be used instead of LOCALES"))
+
+ options, locales = parser.parse_args()
+
+ setup_logging(volume=options.verbose)
+ log = logging.getLogger(__name__)
+
+ if options.config_file is not None:
+ locales = _get_locales(options.config_file)
+ log.debug("config file specified; ignoring any locales passed as args")
+ elif len(locales) == 0:
+ parser.error("You need to specify --config or pass the list of locales")
+ if options.target is None:
+ parser.error("You need to specify at least one --target")
+ if options.source is None and not options.onlyini:
+ parser.error("You need to specify --source (unless you meant --ini)")
+
+ if "en-US" in locales:
+ locales.remove("en-US")
+ ini_files = find_files(options.target, "*.ini")
+
+ # 1. link properties files from the inis
+ for ini_file in ini_files:
+ log.info("########## adding locale import rules to %s" % ini_file)
+ add_locale_imports(locales, ini_file)
+
+ if options.onlyini:
+ parser.exit(1)
+
+ # 2. copy properties files as per the inis
+ for ini_file in ini_files:
+ log.info("########## copying locale files as per %s" % ini_file)
+ copy_properties(options.source, locales, ini_file)
+
+ # 3. edit manifests
+ manifest_files = find_files(options.target, 'manifest.webapp')
+ for manifest_file in manifest_files:
+ log.info("########## adding localized names to %s" % manifest_file)
+ add_locale_manifest(options.source, locales, manifest_file)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/build/offline-cache.js b/build/offline-cache.js
new file mode 100644
index 0000000..abec495
--- /dev/null
+++ b/build/offline-cache.js
@@ -0,0 +1,159 @@
+let Namespace = CC('@mozilla.org/network/application-cache-namespace;1',
+ 'nsIApplicationCacheNamespace',
+ 'init');
+const nsICache = Ci.nsICache;
+const nsIApplicationCache = Ci.nsIApplicationCache;
+const applicationCacheService = Cc['@mozilla.org/network/application-cache-service;1']
+ .getService(Ci.nsIApplicationCacheService);
+
+function log(str) {
+ dump(' +-+ OfflineCache: ' + str + '\n');
+}
+
+/*
+ * Compile Gaia into an offline cache sqlite database.
+ */
+function storeCache(applicationCache, url, file, itemType) {
+ let session = Services.cache.createSession(applicationCache.clientID,
+ nsICache.STORE_OFFLINE, true);
+ session.asyncOpenCacheEntry(url, nsICache.ACCESS_WRITE, {
+ onCacheEntryAvailable: function (cacheEntry, accessGranted, status) {
+ cacheEntry.setMetaDataElement('request-method', 'GET');
+ cacheEntry.setMetaDataElement('response-head', 'HTTP/1.1 200 OK\r\n');
+ // Force an update. the default expiration time is way too far in the future:
+ //cacheEntry.setExpirationTime(0);
+
+ let outputStream = cacheEntry.openOutputStream(0);
+
+ // Input-Output stream machinery in order to push nsIFile content into cache
+ let inputStream = Cc['@mozilla.org/network/file-input-stream;1']
+ .createInstance(Ci.nsIFileInputStream);
+ inputStream.init(file, 1, -1, null);
+ let bufferedOutputStream = Cc['@mozilla.org/network/buffered-output-stream;1']
+ .createInstance(Ci.nsIBufferedOutputStream);
+ bufferedOutputStream.init(outputStream, 1024);
+ bufferedOutputStream.writeFrom(inputStream, inputStream.available());
+ bufferedOutputStream.flush();
+ bufferedOutputStream.close();
+ outputStream.close();
+ inputStream.close();
+
+ cacheEntry.markValid();
+ log (file.path + ' -> ' + url + ' (' + itemType + ')');
+ applicationCache.markEntry(url, itemType);
+ cacheEntry.close();
+ }
+ });
+}
+
+function getCachedURLs(origin, appcacheFile) {
+ let urls = [];
+ let lines = getFileContent(appcacheFile).split(/\r?\n/);
+ for (let i = 0; i < lines.length; i++) {
+ let line = lines[i];
+ if (/^#/.test(line) || !line.length)
+ continue;
+ if (line == 'CACHE MANIFEST')
+ continue;
+ if (line == 'CACHE:')
+ continue;
+ if (line == 'NETWORK:')
+ break;
+ // Prepend webapp origin in case of absolute path
+ if (line[0] == '/')
+ urls.push(origin + line.substring(1));
+ // Just pass along the url, if we have one
+ else if (line.substr(0, 4) == 'http')
+ urls.push(line);
+ else
+ throw new Error('Invalid line in appcache manifest:\n' + line +
+ '\nFrom: ' + appcacheFile.path);
+ }
+ return urls;
+}
+
+let webapps = getJSON(getFile(PROFILE_DIR, 'webapps', 'webapps.json'));
+
+Gaia.externalWebapps.forEach(function (webapp) {
+ // Process only webapp with a `appcache_path` field in their manifest.
+ if (!('appcache_path' in webapp.manifest))
+ return;
+
+ // Get the nsIFile for the appcache file by using `origin` file and
+ // `appcache_path` field of the manifest in order to find it in `cache/`.
+ let originDomain = webapp.origin.replace(/^https?:\/\//, '');
+ let appcachePath = 'cache/' + originDomain + webapp.manifest.appcache_path;
+ let appcacheURL = webapp.origin +
+ webapp.manifest.appcache_path.replace(/^\//, '');
+ let appcacheFile = webapp.sourceDirectoryFile.clone();
+ appcachePath.split('/').forEach(function (name) {
+ appcacheFile.append(name);
+ });
+ if (!appcacheFile.exists())
+ throw new Error('Unable to find application cache manifest: ' +
+ appcacheFile.path);
+
+ // Retrieve generated webapp id from platform profile file build by
+ // webapp-manifest.js; in order to allow the webapp to use offline cache.
+ let appId = webapps[webapp.sourceDirectoryName].localId;
+ let principal = Services.scriptSecurityManager.getAppCodebasePrincipal(
+ Services.io.newURI(webapp.origin, null, null),
+ appId, false);
+ Services.perms.addFromPrincipal(principal, 'offline-app',
+ Ci.nsIPermissionManager.ALLOW_ACTION);
+
+ // Get the url for the manifest. At some points the root
+ // domain should be extracted from manifest.webapp.
+ // See netwerk/cache/nsDiskCacheDeviceSQL.cpp : AppendJARIdentifier
+ // The group ID contains application id and 'f' for not being hosted in
+ // a browser element, but a mozbrowser iframe.
+ let groupID = appcacheURL + '#' + appId + '+f';
+ let applicationCache = applicationCacheService.createApplicationCache(groupID);
+ applicationCache.activate();
+
+ log ('Compiling (' + webapp.domain + ')');
+
+ let urls = getCachedURLs(webapp.origin, appcacheFile);
+ urls.forEach(function appendFile(url) {
+ // Get this nsIFile out of its relative path
+ let path = url.replace(/https?:\/\//, '');
+ let file = webapp.sourceDirectoryFile.clone();
+ file.append('cache');
+ let paths = path.split('/');
+ for (let i = 0; i < paths.length; i++) {
+ file.append(paths[i]);
+ }
+
+ if (!file.exists()) {
+ let msg = 'File ' + file.path + ' exists in the manifest but does not ' +
+ 'points to a real file.';
+ throw new Error(msg);
+ }
+
+ // TODO: use ITEM_IMPLICIT for launch_path, if it occurs to be important.
+ let itemType = nsIApplicationCache.ITEM_EXPLICIT;
+ storeCache(applicationCache, url, file, itemType);
+ });
+
+ // Store the appcache file
+ storeCache(applicationCache, appcacheURL, appcacheFile,
+ nsIApplicationCache.ITEM_MANIFEST);
+
+ // NETWORK:
+ // http://*
+ // https://*
+ let array = Cc['@mozilla.org/array;1'].createInstance(Ci.nsIMutableArray);
+ let bypass = Ci.nsIApplicationCacheNamespace.NAMESPACE_BYPASS;
+ array.appendElement(new Namespace(bypass, 'http://*/', ''), false);
+ array.appendElement(new Namespace(bypass, 'https://*/', ''), false);
+ applicationCache.addNamespaces(array);
+});
+
+
+// Wait for cache to be filled before quitting
+if (Gaia.engine === 'xpcshell') {
+ let thread = Services.tm.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+}
diff --git a/build/optimize-clean.js b/build/optimize-clean.js
new file mode 100644
index 0000000..14dbcf0
--- /dev/null
+++ b/build/optimize-clean.js
@@ -0,0 +1,28 @@
+
+function debug(str) {
+ //dump(' -*- l10n-clean.js: ' + str + '\n');
+}
+
+debug('Begin');
+
+Gaia.webapps.forEach(function(webapp) {
+ // if BUILD_APP_NAME isn't `*`, we only accept one webapp
+ if (BUILD_APP_NAME != '*' && webapp.sourceDirectoryName != BUILD_APP_NAME)
+ return;
+
+ debug(webapp.sourceDirectoryName);
+
+ let re = new RegExp('\\.html\\.' + GAIA_DEFAULT_LOCALE + '$');
+ let files = ls(webapp.sourceDirectoryFile, true);
+ files.forEach(function(file) {
+ if (
+ re.test(file.leafName) ||
+ file.leafName.indexOf(Gaia.aggregatePrefix) === 0
+ ) {
+ file.remove(false);
+ }
+ });
+});
+
+debug('End');
+
diff --git a/build/otoro-install-busybox.sh b/build/otoro-install-busybox.sh
new file mode 100755
index 0000000..8fa138b
--- /dev/null
+++ b/build/otoro-install-busybox.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+# This shell script installs busybox on the device.
+# This lets us take the fast path in install-gaia.py.
+
+# Remount file system with read/write permissions
+adb shell "mount -o rw,remount -t rootfs /"
+adb shell "mkdir -p /system/vendor/bin"
+adb push busybox-armv6l /vendor/bin/busybox
+adb shell "chmod 555 /vendor/bin/busybox"
+
+# Perform the symbolic links
+adb shell "for x in \`busybox --list\`; do ln -s /vendor/bin/busybox /vendor/bin/\$x; done"
+
+# Remount file system with read-only permissions
+adb shell "mount -o ro,remount -t rootfs /"
+
diff --git a/build/payment-prefs.js b/build/payment-prefs.js
new file mode 100644
index 0000000..214fd4e
--- /dev/null
+++ b/build/payment-prefs.js
@@ -0,0 +1,5 @@
+pref("dom.payment.provider.0.name", "firefoxmarket");
+pref("dom.payment.provider.0.description", "marketplace.firefox.com");
+pref("dom.payment.provider.0.uri", "https://marketplace.firefox.com/mozpay/?req=");
+pref("dom.payment.provider.0.type", "mozilla/payments/pay/v1");
+pref("dom.payment.provider.0.requestMethod", "GET");
diff --git a/build/preferences.js b/build/preferences.js
new file mode 100644
index 0000000..8787a3b
--- /dev/null
+++ b/build/preferences.js
@@ -0,0 +1,100 @@
+
+'use strict';
+
+function debug(msg) {
+ //dump('-*- preferences.js ' + msg + '\n');
+}
+
+const prefs = [];
+
+let homescreen = HOMESCREEN + (GAIA_PORT ? GAIA_PORT : '');
+prefs.push(["browser.manifestURL", homescreen + "/manifest.webapp"]);
+if (homescreen.substring(0,6) == "app://") { // B2G bug 773884
+ homescreen += "/index.html";
+}
+prefs.push(["browser.homescreenURL", homescreen]);
+
+let domains = [];
+domains.push(GAIA_DOMAIN);
+
+Gaia.webapps.forEach(function (webapp) {
+ domains.push(webapp.domain);
+});
+
+prefs.push(["network.http.max-connections-per-server", 15]);
+
+// for https://bugzilla.mozilla.org/show_bug.cgi?id=811605 to let user know what prefs is for ril debugging
+prefs.push(["ril.debugging.enabled", false]);
+
+if (LOCAL_DOMAINS) {
+ prefs.push(["network.dns.localDomains", domains.join(",")]);
+}
+
+if (DEBUG) {
+ prefs.push(["marionette.defaultPrefs.enabled", true]);
+ prefs.push(["b2g.remote-js.enabled", true]);
+ prefs.push(["b2g.remote-js.port", 4242]);
+ prefs.push(["javascript.options.showInConsole", true]);
+ prefs.push(["nglayout.debug.disable_xul_cache", true]);
+ prefs.push(["browser.dom.window.dump.enabled", true]);
+ prefs.push(["javascript.options.strict", true]);
+ prefs.push(["dom.report_all_js_exceptions", true]);
+ prefs.push(["nglayout.debug.disable_xul_fastload", true]);
+ prefs.push(["extensions.autoDisableScopes", 0]);
+ prefs.push(["browser.startup.homepage", homescreen]);
+
+ prefs.push(["dom.mozBrowserFramesEnabled", true]);
+ prefs.push(["b2g.ignoreXFrameOptions", true]);
+ prefs.push(["dom.sms.enabled", true]);
+ prefs.push(["dom.mozContacts.enabled", true]);
+ prefs.push(["dom.mozSettings.enabled", true]);
+ prefs.push(["device.storage.enabled", true]);
+ prefs.push(["devtools.chrome.enabled", true]);
+ prefs.push(["webgl.verbose", true]);
+
+ // Preferences for httpd
+ // (Use JSON.stringify in order to avoid taking care of `\` escaping)
+ prefs.push(["extensions.gaia.dir", GAIA_DIR]);
+ prefs.push(["extensions.gaia.domain", GAIA_DOMAIN]);
+ prefs.push(["extensions.gaia.port", parseInt(GAIA_PORT.replace(/:/g, ""))]);
+ prefs.push(["extensions.gaia.app_src_dirs", GAIA_APP_SRCDIRS]);
+ prefs.push(["extensions.gaia.locales_debug_path", GAIA_LOCALES_PATH]);
+ let appPathList = [];
+ Gaia.webapps.forEach(function (webapp) {
+ appPathList.push(webapp.sourceAppDirectoryName + '/' +
+ webapp.sourceDirectoryName);
+ });
+ prefs.push(["extensions.gaia.app_relative_path", appPathList.join(' ')]);
+
+ // Identity debug messages
+ prefs.push(["toolkit.identity.debug", true]);
+}
+
+function writePrefs() {
+ let userJs = getFile(GAIA_DIR, 'profile', 'user.js');
+ let content = prefs.map(function (entry) {
+ return 'user_pref("' + entry[0] + '", ' + JSON.stringify(entry[1]) + ');';
+ }).join('\n');
+ writeContent(userJs, content + "\n");
+ debug("\n" + content);
+}
+
+function setPrefs() {
+ prefs.forEach(function(entry) {
+ if (typeof entry[1] == "string") {
+ Services.prefs.setCharPref(entry[0], entry[1]);
+ } else if (typeof entry[1] == "boolean") {
+ Services.prefs.setBoolPref(entry[0], entry[1]);
+ } else if (typeof entry[1] == "number") {
+ Services.prefs.setIntPref(entry[0], entry[1]);
+ } else {
+ throw new Error("Unsupported pref type: " + typeof entry[1]);
+ }
+ });
+}
+
+if (Gaia.engine === "xpcshell") {
+ writePrefs();
+} else if (Gaia.engine === "b2g") {
+ setPrefs();
+}
diff --git a/build/settings.py b/build/settings.py
new file mode 100644
index 0000000..dd8c1b6
--- /dev/null
+++ b/build/settings.py
@@ -0,0 +1,236 @@
+#!/usr/bin/python
+#
+# This script generates the settings.json file which is stored into the b2g profile
+
+import base64
+import json
+import optparse
+import os
+import sys
+
+settings = {
+ "accessibility.invert": False,
+ "accessibility.screenreader": False,
+ "alarm.enabled": False,
+ "alert-sound.enabled": True,
+ "alert-vibration.enabled": True,
+ "app.reportCrashes": "ask",
+ "app.update.interval": 86400,
+ "audio.volume.alarm": 15,
+ "audio.volume.bt_sco": 15,
+ "audio.volume.dtmf": 15,
+ "audio.volume.content": 15,
+ "audio.volume.notification": 15,
+ "audio.volume.tts": 15,
+ "audio.volume.telephony": 5,
+ "bluetooth.enabled": False,
+ "bluetooth.debugging.enabled": False,
+ "camera.shutter.enabled": True,
+ "clear.remote-windows.data": False,
+ "debug.grid.enabled": False,
+ "debug.oop.disabled": False,
+ "debug.fps.enabled": False,
+ "debug.ttl.enabled": False,
+ "debug.log-animations.enabled": False,
+ "debug.paint-flashing.enabled": False,
+ "debug.peformancedata.shared": False,
+ "deviceinfo.firmware_revision": "",
+ "deviceinfo.hardware": "",
+ "deviceinfo.os": "",
+ "deviceinfo.platform_build_id": "",
+ "deviceinfo.platform_version": "",
+ "deviceinfo.software": "",
+ "deviceinfo.update_channel": "",
+ "gaia.system.checkForUpdates": False,
+ "geolocation.enabled": True,
+ "keyboard.layouts.english": True,
+ "keyboard.layouts.dvorak": False,
+ "keyboard.layouts.otherlatins": False,
+ "keyboard.layouts.cyrillic": False,
+ "keyboard.layouts.arabic": False,
+ "keyboard.layouts.hebrew": False,
+ "keyboard.layouts.zhuyin": False,
+ "keyboard.layouts.pinyin": False,
+ "keyboard.layouts.greek": False,
+ "keyboard.layouts.japanese": False,
+ "keyboard.layouts.portuguese": False,
+ "keyboard.layouts.spanish": False,
+ "keyboard.vibration": False,
+ "keyboard.clicksound": False,
+ "keyboard.wordsuggestion": False,
+ "keyboard.current": "en",
+ "language.current": "en-US",
+ "lockscreen.passcode-lock.code": "0000",
+ "lockscreen.passcode-lock.timeout": 0,
+ "lockscreen.passcode-lock.enabled": False,
+ "lockscreen.notifications-preview.enabled": True,
+ "lockscreen.enabled": True,
+ "lockscreen.locked": True,
+ "lockscreen.unlock-sound.enabled": False,
+ "mail.sent-sound.enabled": True,
+ "operatorvariant.mcc": 0,
+ "operatorvariant.mnc": 0,
+ "ril.iccInfo.mbdn":"",
+ "ril.sms.strict7BitEncoding.enabled": False,
+ "ril.cellbroadcast.searchlist": "",
+ "debug.console.enabled": False,
+ "phone.ring.keypad": True,
+ "powersave.enabled": False,
+ "powersave.threshold": 0,
+ "privacy.donottrackheader.enabled": False,
+ "ril.callwaiting.enabled": None,
+ "ril.cf.enabled": False,
+ "ril.data.enabled": False,
+ "ril.data.apn": "",
+ "ril.data.carrier": "",
+ "ril.data.passwd": "",
+ "ril.data.httpProxyHost": "",
+ "ril.data.httpProxyPort": 0,
+ "ril.data.mmsc": "",
+ "ril.data.mmsproxy": "",
+ "ril.data.mmsport": 0,
+ "ril.data.roaming_enabled": False,
+ "ril.data.user": "",
+ "ril.mms.apn": "",
+ "ril.mms.carrier": "",
+ "ril.mms.httpProxyHost": "",
+ "ril.mms.httpProxyPort": "",
+ "ril.mms.mmsc": "",
+ "ril.mms.mmsport": "",
+ "ril.mms.mmsproxy": "",
+ "ril.mms.passwd": "",
+ "ril.mms.user": "",
+ "ril.radio.preferredNetworkType": "",
+ "ril.radio.disabled": False,
+ "ril.supl.apn": "",
+ "ril.supl.carrier": "",
+ "ril.supl.httpProxyHost": "",
+ "ril.supl.httpProxyPort": "",
+ "ril.supl.passwd": "",
+ "ril.supl.user": "",
+ "ril.sms.strict7BitEncoding.enabled": False,
+ "ring.enabled": True,
+ "screen.automatic-brightness": True,
+ "screen.brightness": 1,
+ "screen.timeout": 60,
+ "tethering.usb.enabled": False,
+ "tethering.usb.ip": "192.168.0.1",
+ "tethering.usb.prefix": "24",
+ "tethering.usb.dhcpserver.startip": "192.168.0.10",
+ "tethering.usb.dhcpserver.endip": "192.168.0.30",
+ "tethering.wifi.enabled": False,
+ "tethering.wifi.ip": "192.168.1.1",
+ "tethering.wifi.prefix": "24",
+ "tethering.wifi.dhcpserver.startip": "192.168.1.10",
+ "tethering.wifi.dhcpserver.endip": "192.168.1.30",
+ "tethering.wifi.ssid": "FirefoxHotspot",
+ "tethering.wifi.security.type": "open",
+ "tethering.wifi.security.password": "1234567890",
+ "tethering.wifi.connectedClients": 0,
+ "tethering.usb.connectedClients": 0,
+ "time.nitz.automatic-update.enabled": True,
+ "time.timezone": None,
+ "ums.enabled": False,
+ "ums.mode": 0,
+ "vibration.enabled": True,
+ "wifi.enabled": True,
+ "wifi.disabled_by_wakelock": False,
+ "wifi.notification": False,
+ "icc.displayTextTimeout": 40000,
+ "icc.inputTextTimeout": 40000
+}
+
+def main():
+ parser = optparse.OptionParser(description="Generate initial settings.json file")
+ parser.add_option( "--override", help="JSON files for custom settings overrides")
+ parser.add_option( "--homescreen", help="specify the homescreen URL")
+ parser.add_option( "--ftu", help="specify the ftu manifest URL")
+ parser.add_option("-c", "--console", help="indicate if the console should be enabled", action="store_true")
+ parser.add_option("-o", "--output", help="specify the name of the output file")
+ parser.add_option("-w", "--wallpaper", help="specify the name of the wallpaper file")
+ parser.add_option("-v", "--verbose", help="increase output verbosity", action="store_true")
+ parser.add_option( "--noftu", help="bypass the ftu app", action="store_true")
+ parser.add_option( "--locale", help="specify the default locale to use")
+ parser.add_option( "--enable-debugger", help="enable remote debugger (and ADB for VARIANT=user builds)", action="store_true")
+ (options, args) = parser.parse_args(sys.argv[1:])
+
+ verbose = options.verbose
+
+ if options.homescreen:
+ homescreen_url = options.homescreen
+ else:
+ homescreen_url = "app://homescreen.gaiamobile.org/manifest.webapp"
+
+ if options.ftu:
+ ftu_url = options.ftu
+ else:
+ ftu_url = "app://communications.gaiamobile.org/manifest.webapp"
+
+ if options.output:
+ settings_filename = options.output
+ else:
+ settings_filename = "profile/settings.json"
+
+ if options.wallpaper:
+ wallpaper_filename = options.wallpaper
+ else:
+ wallpaper_filename = "build/wallpaper.jpg"
+
+ enable_debugger = (options.enable_debugger == True)
+
+ if verbose:
+ print "Console:", options.console
+ print "Homescreen URL:", homescreen_url
+ print "Ftu URL:", ftu_url
+ print "Setting Filename:",settings_filename
+ print "Wallpaper Filename:", wallpaper_filename
+ print "Enable Debugger:", enable_debugger
+
+ # Set the default console output
+ if options.console:
+ settings["debug.console.enabled"] = options.console
+
+ # Set the homescreen URL
+ settings["homescreen.manifestURL"] = homescreen_url
+
+ # Set the ftu manifest URL
+ if not options.noftu:
+ settings["ftu.manifestURL"] = ftu_url
+
+ # Set the default locale
+ if options.locale:
+ settings["language.current"] = options.locale
+
+ settings["devtools.debugger.remote-enabled"] = enable_debugger;
+
+ # Grab wallpaper.jpg and convert it into a base64 string
+ wallpaper_file = open(wallpaper_filename, "rb")
+ wallpaper_base64 = base64.b64encode(wallpaper_file.read())
+ settings["wallpaper.image"] = "data:image/jpeg;base64," + wallpaper_base64.decode("utf-8")
+
+ # Grab ringer_classic_courier.opus and convert it into a base64 string
+ ringtone_name = "shared/resources/media/ringtones/ringer_classic_courier.opus"
+ ringtone_file = open(ringtone_name, "rb");
+ ringtone_base64 = base64.b64encode(ringtone_file.read())
+ settings["dialer.ringtone"] = "data:audio/ogg;base64," + ringtone_base64.decode("utf-8")
+ settings["dialer.ringtone.name"] = "ringer_classic_courier.opus"
+
+ # Grab notifier_bell.opus and convert it into a base64 string
+ notification_name = "shared/resources/media/notifications/notifier_bell.opus"
+ notification_file = open(notification_name, "rb");
+ notification_base64 = base64.b64encode(notification_file.read())
+ settings["notification.ringtone"] = "data:audio/ogg;base64," + notification_base64.decode("utf-8")
+ settings["notification.ringtone.name"] = "notifier_bell.opus"
+
+ if options.override and os.path.exists(options.override):
+ try:
+ overrides = json.load(open(options.override))
+ for key, val in overrides.items():
+ settings[key] = val
+ except Exception, e:
+ print "Error while applying override setting file: %s\n%s" % (options.override, e)
+
+ json.dump(settings, open(settings_filename, "wb"))
+
+if __name__ == "__main__":
+ main()
diff --git a/build/ua-override-prefs.js b/build/ua-override-prefs.js
new file mode 100644
index 0000000..af5c354
--- /dev/null
+++ b/build/ua-override-prefs.js
@@ -0,0 +1,102 @@
+
+// Send these sites a custom user-agent. Bugs to remove each override after
+// evangelism are included.
+pref("general.useragent.override.facebook.com", "\(Mobile#(Android; Mobile"); // bug 827635
+pref("general.useragent.override.youtube.com", "\(Mobile#(Android; Mobile"); // bug 827636
+pref("general.useragent.override.yelp.com", "\(Mobile#(Android; Mobile"); // bug 799884
+pref("general.useragent.override.dailymotion.com", "\(Mobile#(Android; Mobile"); // bug 827638
+pref("general.useragent.override.accounts.google.com", "\(Mobile#(Android; Mobile"); // bug 805164
+pref("general.useragent.override.maps.google.com", "\(Mobile#(Android; Mobile"); // bug 802981
+pref("general.useragent.override.uol.com.br", "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"); // bug 826330
+pref("general.useragent.override.live.com", "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"); // bug 826332
+pref("general.useragent.override.globo.com", "\(Mobile#(Android; Mobile"); // bug 826335
+pref("general.useragent.override.yahoo.com", "\(Mobile#(Android; Mobile"); // bug 826338
+pref("general.useragent.override.mercadolivre.com.br", "\(Mobile#(Android; Mobile"); // bug 826342
+pref("general.useragent.override.ig.com.br", "\(Mobile#(Android; Mobile"); // bug 826343
+pref("general.useragent.override.abril.com.br", "\(Mobile#(Android; Mobile"); // bug 826344
+pref("general.useragent.override.msn.com", "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"); // bug 826347
+pref("general.useragent.override.linkedin.com", "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"); // bug 826348
+pref("general.useragent.override.itau.com.br", "\(Mobile#(Android; Mobile"); // bug 826353
+pref("general.useragent.override.tumblr.com", "\(Mobile#(Android; Mobile"); // bug 826361
+pref("general.useragent.override.4shared.com", "\(Mobile#(Android; Mobile"); // bug 826502
+pref("general.useragent.override.orkut.com.br", "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"); // bug 826504
+pref("general.useragent.override.r7.com", "\(Mobile#(Android; Mobile"); // bug 826510
+pref("general.useragent.override.amazon.com", "\(Mobile#(Android; Mobile"); // bug 826512
+pref("general.useragent.override.estadao.com.br", "\(Mobile#(Android; Mobile"); // bug 826514
+pref("general.useragent.override.letras.mus.br", "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"); // bug 826517
+pref("general.useragent.override.bb.com.br", "\(Mobile#(Android; Mobile"); // bug 826711
+pref("general.useragent.override.orkut.com", "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"); // bug 826712
+pref("general.useragent.override.noticias.uol.com.br", "\(Mobile#(Android; Mobile"); // bug 826715
+pref("general.useragent.override.olx.com.br", "\(Mobile#(Android; Mobile"); // bug 826720
+pref("general.useragent.override.bancobrasil.com.br", "\(Mobile#(Android; Mobile"); // bug 826736
+pref("general.useragent.override.techtudo.com.br", "\(Mobile#(Android; Mobile"); // bug 826845
+pref("general.useragent.override.clickjogos.uol.com.br", "\(Mobile#(Android; Mobile"); // bug 826949
+pref("general.useragent.override.ebay.com", "\(Mobile#(Android; Mobile");// bug 826958
+pref("general.useragent.override.bing.com", "\(Mobile#(Android; Mobile"); // bug 827622
+pref("general.useragent.override.tam.com.br", "\(Mobile#(Android; Mobile"); // bug 827623
+pref("general.useragent.override.pontofrio.com.br", "\(Mobile#(Android; Mobile"); // bug 827624
+pref("general.useragent.override.pagseguro.uol.com.br", "\(Mobile#(Android; Mobile"); // bug 827625
+pref("general.useragent.override.magazineluiza.com.br", "\(Mobile#(Android; Mobile"); // bug 827626
+pref("general.useragent.override.bol.uol.com.br", "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"); // bug 827627
+pref("general.useragent.override.groupon.com.br", "\(Mobile#(Android; Mobile"); // bug 827628
+pref("general.useragent.override.vagalume.com.br", "\(Mobile#(Android; Mobile"); // bug 827630
+pref("general.useragent.override.climatempo.com.br", "\(Mobile#(Android; Mobile"); // bug 827631
+pref("general.useragent.override.tecmundo.com.br", "\(Mobile#(Android; Mobile"); // bug 827632
+pref("general.useragent.override.hao123.com", "\(Mobile#(Android; Mobile"); // bug 827633
+pref("general.useragent.override.imdb.com", "\(Mobile#(Android; Mobile"); // bug 827634
+pref("general.useragent.override.lancenet.com.br", "\(Mobile#(Android; Mobile"); // bug 827576
+pref("general.useragent.override.webmotors.com.br", "\(Mobile#(Android; Mobile"); // bug 827573
+pref("general.useragent.override.mercadolibre.com.co", "\(Mobile#(Android; Mobile"); // bug 827661
+pref("general.useragent.override.elespectador.com", "\(Mobile#(Android; Mobile"); // bug 827664
+pref("general.useragent.override.slideshare.net", "\(Mobile#(Android; Mobile"); // bug 827666
+pref("general.useragent.override.scribd.com", "\(Mobile#(Android; Mobile"); // bug 827668
+pref("general.useragent.override.elpais.com.co", "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"); // bug 827670
+pref("general.useragent.override.olx.com.co", "\(Mobile#(Android; Mobile"); // bug 827672
+pref("general.useragent.override.avianca.com", "\(Mobile#(Android; Mobile"); // bug 827674
+pref("general.useragent.override.dropbox.com", "\(Mobile#(Android; Mobile"); // bug 827676
+pref("general.useragent.override.marca.com", "\(Mobile#(Android; Mobile"); // bug 827678
+pref("general.useragent.override.wp.pl", "\(Mobile#(Android; Mobile"); // bug 828351
+pref("general.useragent.override.gazeta.pl", "\(Mobile#(Android; Mobile"); // bug 828354
+pref("general.useragent.override.o2.pl", "\(Mobile#(Android; Mobile"); // bug 828356
+pref("general.useragent.override.ceneo.pl", "\(Mobile#(Android; Mobile"); // bug 828358
+pref("general.useragent.override.sport.pl", "\(Mobile#(Android; Mobile"); // bug 828360
+pref("general.useragent.override.tvn24.pl", "\(Mobile#(Android; Mobile"); // bug 828362
+pref("general.useragent.override.nk.pl", "\(Mobile#(Android; Mobile"); // bug 828364
+pref("general.useragent.override.wyborcza.biz", "\(Mobile#(Android; Mobile"); // bug 828366
+pref("general.useragent.override.money.pl", "\(Mobile#(Android; Mobile"); // bug 828369
+pref("general.useragent.override.ingbank.pl", "\(Mobile#(Android; Mobile"); // bug 828371
+pref("general.useragent.override.tablica.pl", "\(Mobile#(Android; Mobile"); // bug 828374
+pref("general.useragent.override.plotek.pl", "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"); // bug 828376
+pref("general.useragent.override.wyborcza.pl", "\(Mobile#(Android; Mobile"); // bug 828378
+pref("general.useragent.override.deser.pl", "\(Mobile#(Android; Mobile"); // bug 828380
+pref("general.useragent.override.as.com", "\(Mobile#(Android; Mobile"); // bug 828383
+pref("general.useragent.override.ebay.es", "\(Mobile#(Android; Mobile"); // bug 828386
+pref("general.useragent.override.amazon.es", "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"); // bug 828388
+pref("general.useragent.override.20minutos.es", "\(Mobile#(Android; Mobile"); // bug 828390
+pref("general.useragent.override.infojobs.net", "\(Mobile#(Android; Mobile"); // bug 828392
+pref("general.useragent.override.vimeo.com", "\(Mobile#(Android; Mobile"); // bug 828394
+pref("general.useragent.override.elconfidencial.com", "\(Mobile#(Android; Mobile"); // bug 828397
+pref("general.useragent.override.antena3.com", "\(Mobile#(Android; Mobile"); // bug 828399
+pref("general.useragent.override.ingdirect.es", "\(Mobile#(Android; Mobile"); // bug 828401
+pref("general.useragent.override.fotocasa.es", "\(Mobile#(Android; Mobile"); // bug 828403
+pref("general.useragent.override.orange.es", "\(Mobile#(Android; Mobile"); // bug 828406
+pref("general.useragent.override.stackoverflow.com", "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"); // bug 828408
+pref("general.useragent.override.amazon.co.uk", "\(Mobile#(Android; Mobile"); // bug 828412
+pref("general.useragent.override.paginasamarillas.es", "\(Mobile#(Android; Mobile"); // bug 828414
+pref("general.useragent.override.loteriasyapuestas.es", "\(Mobile#(Android; Mobile"); // bug 828416
+pref("general.useragent.override.bbva.es", "\(Mobile#(Android; Mobile"); // bug 828418
+pref("general.useragent.override.booking.com", "\(Mobile#(Android; Mobile"); // bug 828420
+pref("general.useragent.override.publico.es", "\(Mobile#(Android; Mobile"); // bug 828422
+pref("general.useragent.override.mercadolibre.com.ve", "\(Mobile#(Android; Mobile"); // bug 828425
+pref("general.useragent.override.lapatilla.com", "\(Mobile#(Android; Mobile"); // bug 828428
+pref("general.useragent.override.meridiano.com.ve", "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"); // bug 828430
+pref("general.useragent.override.espn.go.com", "\(Mobile#(Android; Mobile"); // bug 828431
+pref("general.useragent.override.olx.com.ve", "\(Mobile#(Android; Mobile"); // bug 828433
+pref("general.useragent.override.rincondelvago.com", "\(Mobile#(Android; Mobile"); // bug 828435
+pref("general.useragent.override.avn.info.ve", "\(Mobile#(Android; Mobile"); // bug 828437
+pref("general.useragent.override.movistar.com.ve", "\(Mobile#(Android; Mobile"); // bug 828439
+pref("general.useragent.override.laverdad.com", "\(Mobile#(Android; Mobile"); // bug 828441
+pref("general.useragent.override.despegar.com.ve", "\(Mobile#(Android; Mobile"); // bug 828443
+pref("general.useragent.override.bumeran.com.ve", "\(Mobile#(Android; Mobile"); // bug 828445
+pref("general.useragent.override.petardas.com", "\(Mobile#(Android; Mobile"); // bug 828448
+pref("general.useragent.override.mail.google.com", "\(Mobile#(Android; Mobile"); // bug 827869
diff --git a/build/utils.js b/build/utils.js
new file mode 100644
index 0000000..5b7a58b
--- /dev/null
+++ b/build/utils.js
@@ -0,0 +1,222 @@
+const { 'classes': Cc, 'interfaces': Ci, 'results': Cr, 'utils': Cu,
+ 'Constructor': CC } = Components;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.import('resource://gre/modules/FileUtils.jsm');
+Cu.import('resource://gre/modules/Services.jsm');
+
+function isSubjectToBranding(path) {
+ return /shared[\/\\][a-zA-Z]+[\/\\]branding$/.test(path) ||
+ /branding[\/\\]initlogo.png/.test(path);
+}
+
+function getSubDirectories(directory) {
+ let appsDir = new FileUtils.File(GAIA_DIR);
+ appsDir.append(directory);
+
+ let dirs = [];
+ let files = appsDir.directoryEntries;
+ while (files.hasMoreElements()) {
+ let file = files.getNext().QueryInterface(Ci.nsILocalFile);
+ if (file.isDirectory()) {
+ dirs.push(file.leafName);
+ }
+ }
+ return dirs;
+}
+
+/**
+ * Returns an array of nsIFile's for a given directory
+ *
+ * @param {nsIFile} dir directory to read.
+ * @param {boolean} recursive set to true in order to walk recursively.
+ * @param {RegExp} exclude optional filter to exclude file/directories.
+ *
+ * @return {Array} list of nsIFile's.
+ */
+function ls(dir, recursive, exclude) {
+ let results = [];
+ let files = dir.directoryEntries;
+ while (files.hasMoreElements()) {
+ let file = files.getNext().QueryInterface(Ci.nsILocalFile);
+ if (!exclude || !exclude.test(file.leafName)) {
+ results.push(file);
+ if (recursive && file.isDirectory()) {
+ results = results.concat(ls(file, true, exclude));
+ }
+ }
+ }
+ return results;
+}
+
+function getFileContent(file) {
+ try {
+ let fileStream = Cc['@mozilla.org/network/file-input-stream;1']
+ .createInstance(Ci.nsIFileInputStream);
+ fileStream.init(file, 1, 0, false);
+
+ let converterStream = Cc['@mozilla.org/intl/converter-input-stream;1']
+ .createInstance(Ci.nsIConverterInputStream);
+ converterStream.init(fileStream, 'utf-8', fileStream.available(),
+ Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
+
+ let out = {};
+ let count = fileStream.available();
+ converterStream.readString(count, out);
+
+ var content = out.value;
+ converterStream.close();
+ fileStream.close();
+ } catch (e) {
+ let msg = (file && file.path) ? '\nfile not found: ' + file.path : '';
+ throw new Error(' -*- build/utils.js: ' + e + msg + '\n');
+ }
+ return content;
+}
+
+function writeContent(file, content) {
+ var fileStream = Cc['@mozilla.org/network/file-output-stream;1']
+ .createInstance(Ci.nsIFileOutputStream);
+ fileStream.init(file, 0x02 | 0x08 | 0x20, 0666, 0);
+
+ let converterStream = Cc['@mozilla.org/intl/converter-output-stream;1']
+ .createInstance(Ci.nsIConverterOutputStream);
+
+ converterStream.init(fileStream, 'utf-8', 0, 0);
+ converterStream.writeString(content);
+ converterStream.close();
+}
+
+// Return an nsIFile by joining paths given as arguments
+// First path has to be an absolute one
+function getFile() {
+ try {
+ let file = new FileUtils.File(arguments[0]);
+ if (arguments.length > 1) {
+ for (let i = 1; i < arguments.length; i++) {
+ file.append(arguments[i]);
+ }
+ }
+ return file;
+ } catch(e) {
+ throw new Error(' -*- build/utils.js: Invalid file path (' +
+ Array.slice(arguments).join(', ') + ')\n' + e + '\n');
+ }
+}
+
+function ensureFolderExists(file) {
+ if (!file.exists()) {
+ try {
+ file.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt('0755', 8));
+ } catch (e if e.result == Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
+ // Bug 808513: Ignore races between `if exists() then create()`.
+ return;
+ }
+ }
+}
+
+function getJSON(file) {
+ try {
+ let content = getFileContent(file);
+ return JSON.parse(content);
+ } catch (e) {
+ dump('Invalid JSON file : ' + file.path + '\n');
+ throw e;
+ }
+}
+
+function makeWebappsObject(dirs) {
+ return {
+ forEach: function(fun) {
+ let appSrcDirs = dirs.split(' ');
+ appSrcDirs.forEach(function parseDirectory(directoryName) {
+ let directories = getSubDirectories(directoryName);
+ directories.forEach(function readManifests(dir) {
+ let manifestFile = getFile(GAIA_DIR, directoryName, dir,
+ 'manifest.webapp');
+ let updateFile = getFile(GAIA_DIR, directoryName, dir,
+ 'update.webapp');
+ // Ignore directories without manifest
+ if (!manifestFile.exists() && !updateFile.exists()) {
+ return;
+ }
+
+ let manifest = manifestFile.exists() ? manifestFile : updateFile;
+ let domain = dir + '.' + GAIA_DOMAIN;
+
+ let webapp = {
+ manifest: getJSON(manifest),
+ manifestFile: manifest,
+ url: GAIA_SCHEME + domain + (GAIA_PORT ? GAIA_PORT : ''),
+ domain: domain,
+ sourceDirectoryFile: manifestFile.parent,
+ sourceDirectoryName: dir,
+ sourceAppDirectoryName: directoryName
+ };
+
+ // External webapps have a `metadata.json` file
+ let metaData = webapp.sourceDirectoryFile.clone();
+ metaData.append('metadata.json');
+ if (metaData.exists()) {
+ webapp.metaData = getJSON(metaData);
+ }
+
+ fun(webapp);
+ });
+ });
+ }
+ };
+}
+
+let externalAppsDirs = ['external-apps'];
+
+// External apps are built differently from other apps by webapp-manifests.js,
+// and we need apps that are both external and dogfood to be treated like
+// external apps (to properly test external apps on dogfood devices), so we
+// segregate them into their own directory that we add to the list of external
+// apps dirs here when building a dogfood profile.
+if (DOGFOOD === '1') {
+ externalAppsDirs.push('external-dogfood-apps');
+}
+
+const Gaia = {
+ engine: GAIA_ENGINE,
+ sharedFolder: getFile(GAIA_DIR, 'shared'),
+ webapps: makeWebappsObject(GAIA_APP_SRCDIRS),
+ externalWebapps: makeWebappsObject(externalAppsDirs.join(' ')),
+ aggregatePrefix: 'gaia_build_'
+};
+
+function registerProfileDirectory() {
+ let directoryProvider = {
+ getFile: function provider_getFile(prop, persistent) {
+ persistent.value = true;
+ if (prop != 'ProfD' && prop != 'ProfLDS') {
+ throw Cr.NS_ERROR_FAILURE;
+ }
+
+ return new FileUtils.File(PROFILE_DIR);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIDirectoryServiceProvider,
+ Ci.nsISupports])
+ };
+
+ Cc['@mozilla.org/file/directory_service;1']
+ .getService(Ci.nsIProperties)
+ .QueryInterface(Ci.nsIDirectoryService)
+ .registerProvider(directoryProvider);
+}
+
+if (Gaia.engine === 'xpcshell') {
+ registerProfileDirectory();
+}
+
+function gaiaOriginURL(name) {
+ return GAIA_SCHEME + name + '.' + GAIA_DOMAIN + (GAIA_PORT ? GAIA_PORT : '');
+}
+
+function gaiaManifestURL(name) {
+ return gaiaOriginURL(name) + '/manifest.webapp';
+}
+
diff --git a/build/wallpaper.jpg b/build/wallpaper.jpg
new file mode 100644
index 0000000..d60fc2e
--- /dev/null
+++ b/build/wallpaper.jpg
Binary files differ
diff --git a/build/webapp-manifests.js b/build/webapp-manifests.js
new file mode 100644
index 0000000..6bd79b9
--- /dev/null
+++ b/build/webapp-manifests.js
@@ -0,0 +1,179 @@
+const INSTALL_TIME = 132333986000; // Match this to value in applications-data.js
+
+function debug(msg) {
+ //dump('-*- webapp-manifest.js: ' + msg + '\n');
+}
+
+let webappsTargetDir = Cc['@mozilla.org/file/local;1']
+ .createInstance(Ci.nsILocalFile);
+webappsTargetDir.initWithPath(PROFILE_DIR);
+// Create profile folder if doesn't exists
+if (!webappsTargetDir.exists())
+ webappsTargetDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt('0755', 8));
+// Create webapps folder if doesn't exists
+webappsTargetDir.append('webapps');
+if (!webappsTargetDir.exists())
+ webappsTargetDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt('0755', 8));
+
+let manifests = {};
+let id = 1;
+
+function copyRec(source, target) {
+ let results = [];
+ let files = source.directoryEntries;
+ if (!target.exists())
+ target.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt('0755', 8));
+
+ while (files.hasMoreElements()) {
+ let file = files.getNext().QueryInterface(Ci.nsILocalFile);
+ if (file.isDirectory()) {
+ let subFolder = target.clone();
+ subFolder.append(file.leafName);
+ copyRec(file, subFolder);
+ } else {
+ file.copyTo(target, file.leafName);
+ }
+ }
+}
+
+// Returns the nsIPrincipal compliant integer
+// from the "type" property in manifests.
+function getAppStatus(status) {
+ let appStatus = 1; // By default, apps are installed
+ switch (status) {
+ case "certified":
+ appStatus = 3;
+ break;
+ case "privileged":
+ appStatus = 2;
+ break;
+ case "web":
+ default:
+ appStatus = 1;
+ break;
+ }
+ return appStatus;
+}
+
+Gaia.webapps.forEach(function (webapp) {
+ // If BUILD_APP_NAME isn't `*`, we only accept one webapp
+ if (BUILD_APP_NAME != '*' && webapp.sourceDirectoryName != BUILD_APP_NAME)
+ return;
+
+ // Compute webapp folder name in profile
+ let webappTargetDirName = webapp.domain;
+
+ // Copy webapp's manifest to the profile
+ let webappTargetDir = webappsTargetDir.clone();
+ webappTargetDir.append(webappTargetDirName);
+ webapp.manifestFile.copyTo(webappTargetDir, 'manifest.webapp');
+
+ // Add webapp's entry to the webapps global manifest.
+ // appStatus == 3 means this is a certified app.
+ // appStatus == 2 means this is a privileged app.
+ // appStatus == 1 means this is an installed (unprivileged) app
+
+ let url = webapp.url;
+ manifests[webappTargetDirName] = {
+ origin: url,
+ installOrigin: url,
+ receipt: null,
+ installTime: INSTALL_TIME,
+ manifestURL: url + '/manifest.webapp',
+ appStatus: getAppStatus(webapp.manifest.type),
+ localId: id++
+ };
+
+});
+
+// Process external webapps from /gaia/external-app/ folder
+Gaia.externalWebapps.forEach(function (webapp) {
+ // If BUILD_APP_NAME isn't `*`, we only accept one webapp
+ if (BUILD_APP_NAME != '*' && webapp.sourceDirectoryName != BUILD_APP_NAME)
+ return;
+
+ if (!webapp.metaData) {
+ return;
+ }
+
+ // Compute webapp folder name in profile
+ let webappTargetDirName = webapp.sourceDirectoryName;
+
+ // Copy webapp's manifest to the profile
+ let webappTargetDir = webappsTargetDir.clone();
+ webappTargetDir.append(webappTargetDirName);
+
+ let origin;
+ let installOrigin;
+ let manifestURL;
+
+ let removable;
+
+ // In case of packaged app, just copy `application.zip` and `update.webapp`
+ let appPackage = webapp.sourceDirectoryFile.clone();
+ appPackage.append('application.zip');
+ if (appPackage.exists()) {
+ let updateManifest = webapp.sourceDirectoryFile.clone();
+ updateManifest.append('update.webapp');
+ if (!updateManifest.exists()) {
+ throw new Error('External packaged webapp `' + webapp.domain + ' is ' +
+ 'missing an `update.webapp` file. This JSON file ' +
+ 'contains a `package_path` attribute specifying where ' +
+ 'to download the application zip package from the origin ' +
+ 'specified in `metadata.json` file.');
+ }
+ appPackage.copyTo(webappTargetDir, 'application.zip');
+ updateManifest.copyTo(webappTargetDir, 'update.webapp');
+ removable = true;
+ origin = webapp.metaData.origin;
+ installOrigin = webapp.metaData.installOrigin;
+ manifestURL = webapp.metaData.manifestURL;
+ } else {
+ webapp.manifestFile.copyTo(webappTargetDir, 'manifest.webapp');
+ origin = webapp.metaData.origin;
+ installOrigin = webapp.metaData.origin;
+ manifestURL = webapp.metaData.origin + 'manifest.webapp';
+
+ // This is an hosted app. Check if there is an offline cache.
+ let srcCacheFolder = webapp.sourceDirectoryFile.clone();
+ srcCacheFolder.append('cache');
+ if (srcCacheFolder.exists()) {
+ let cacheManifest = srcCacheFolder.clone();
+ cacheManifest.append('manifest.appcache');
+ if (!cacheManifest.exists())
+ throw new Error('External webapp `' + webapp.domain + '` has a cache ' +
+ 'directory without `manifest.appcache` file.');
+
+ // Copy recursively the whole cache folder to webapp folder
+ let targetCacheFolder = webappTargetDir.clone();
+ targetCacheFolder.append('cache');
+ copyRec(srcCacheFolder, targetCacheFolder);
+ }
+ }
+
+ let etag = webapp.metaData.etag || null;
+ let packageEtag = webapp.metaData.packageEtag || null;
+
+ // Add webapp's entry to the webapps global manifest
+ manifests[webappTargetDirName] = {
+ origin: origin,
+ installOrigin: installOrigin,
+ receipt: null,
+ installTime: 132333986000,
+ manifestURL: manifestURL,
+ removable: removable,
+ localId: id++,
+ etag: etag,
+ packageEtag: packageEtag,
+ appStatus: getAppStatus(webapp.metaData.type || "web"),
+ };
+
+});
+
+// Write webapps global manifest
+let manifestFile = webappsTargetDir.clone();
+manifestFile.append('webapps.json');
+
+// stringify json with 2 spaces indentation
+writeContent(manifestFile, JSON.stringify(manifests, null, 2) + '\n');
+
diff --git a/build/webapp-optimize.js b/build/webapp-optimize.js
new file mode 100644
index 0000000..8258e3d
--- /dev/null
+++ b/build/webapp-optimize.js
@@ -0,0 +1,386 @@
+
+function debug(str) {
+ //dump(' -*- webapp-optimize.js: ' + str + '\n');
+}
+
+
+/**
+ * Expose a global `win' object and load `l10n.js' in it --
+ * note: the `?reload' trick ensures we don't load a cached `l10njs' library.
+ */
+
+var win = { navigator: {} };
+Services.scriptloader.loadSubScript('file:///' + GAIA_DIR +
+ '/shared/js/l10n.js?reload=' + new Date().getTime(), win);
+
+
+/**
+ * Locale list -- by default, only the default one
+ */
+
+var l10nLocales = [GAIA_DEFAULT_LOCALE];
+var l10nDictionary = {
+ locales: {},
+ default_locale: GAIA_DEFAULT_LOCALE
+};
+l10nDictionary.locales[GAIA_DEFAULT_LOCALE] = {};
+
+/**
+ * whitelist by app name for javascript asset aggregation.
+ */
+const JS_AGGREGATION_WHITELIST = [
+ 'calendar'
+];
+
+/**
+ * Helpers
+ */
+
+function optimize_getFileContent(webapp, htmlFile, relativePath) {
+ let paths = relativePath.split('/');
+ let file;
+
+ // get starting directory: webapp root, HTML file or /shared/
+ if (/^\//.test(relativePath)) {
+ paths.shift();
+ file = webapp.sourceDirectoryFile.clone();
+ } else {
+ file = htmlFile.parent.clone();
+ }
+ if (paths[0] == 'shared') {
+ file = getFile(GAIA_DIR);
+ }
+
+ paths.forEach(function appendPath(name) {
+ file.append(name);
+ if (isSubjectToBranding(file.path)) {
+ file.append((OFFICIAL == 1) ? 'official' : 'unofficial');
+ }
+ });
+
+ try {
+ return getFileContent(file);
+ } catch (e) {
+ dump(file.path + ' could not be found.\n');
+ return '';
+ }
+}
+
+/**
+ * Aggregates javascript files by type to reduce the IO overhead.
+ * Depending on the script tags there are two files made:
+ *
+ * - defered scripts (<script defer src= ...) :
+ * $(Gaia.aggregatePrefix)defer_$(html_filename).js
+ *
+ * - normal scripts (<script src=...) :
+ * $(Gaia.aggregatePrefix)$(html_filename).js
+ *
+ *
+ * Also it is possible to skip aggregation on a per script basis:
+ *
+ * <script src="..." data-skip-optimize defer></script>
+ *
+ *
+ * This function is somewhat conservative about what it will aggregate and will
+ * only group scripts found the documents <head> section.
+ *
+ * @param {HTMLDocument} doc DOM document of the file.
+ * @param {Object} webapp details of current web app.
+ * @param {NSFile} htmlFile filename/path of the document.
+ */
+function optimize_aggregateJsResources(doc, webapp, htmlFile) {
+ // Everyone should be putting their scripts in head with defer.
+ // The best case is that only l10n.js is put into a normal.
+ let scripts = Array.slice(
+ doc.head.querySelectorAll('script[src]')
+ );
+
+ let deferred = {
+ prefix: 'defer_',
+ content: '',
+ lastNode: null
+ };
+
+ let normal = {
+ prefix: '',
+ content: '',
+ lastNode: null
+ };
+
+ scripts.forEach(function(script, idx) {
+ let html = script.outerHTML;
+
+ // per-script out see comment in function header.
+ if ('skipOptimize' in script.dataset) {
+ // remove from scripts so it will not be commented out...
+ debug(
+ '[optimize ' + webapp.sourceDirectoryName + '] ' +
+ 'skipping script "' + html + '"'
+ );
+ scripts.splice(idx, 1);
+ return;
+ }
+
+ // we inject the whole outerHTML into the comment for debugging so
+ // if there is something valuable in the html that effects the script
+ // that broke the app it should be fairly easy to tell what happened.
+ let content = '; /* "' + html + ' "*/\n\n';
+
+ // fetch the whole file append it to the comment.
+ content += optimize_getFileContent(webapp, htmlFile, script.src);
+
+ let config = normal;
+
+ if (script.defer)
+ config = deferred;
+
+ config.content += content;
+ config.lastNode = script;
+
+ // some apps (email) use version in the script types
+ // (text/javascript;version=x).
+ //
+ // If we don't have the same version in the aggregate the
+ // app will not load correctly.
+ if (script.type.indexOf('version') !== -1) {
+ config.type = script.type;
+ }
+ });
+
+ // root name like index or oncall, etc...
+ let baseName = htmlFile.path.split('/').pop().split('.')[0];
+
+ // used as basis for aggregated scripts...
+ let rootDirectory = htmlFile.parent;
+
+ // find the absolute root of the app's html file.
+ let rootUrl = htmlFile.parent.path;
+ rootUrl = rootUrl.replace(webapp.manifestFile.parent.path, '');
+ // the above will yield something like: '', '/facebook/', '/contacts/', etc...
+
+ function writeAggregatedScript(config) {
+ // skip if we don't have any content to write.
+ if (!config.content)
+ return;
+
+ // prefix the file we are about to write content to.
+ let scriptBaseName =
+ Gaia.aggregatePrefix + config.prefix + baseName + '.js';
+
+ let target = rootDirectory.clone();
+ target.append(scriptBaseName);
+
+ debug('writing aggregated source file: ' + target.path);
+
+ // write the contents of the aggregated script
+ writeContent(target, config.content);
+
+ let script = doc.createElement('script');
+ let lastScript = config.lastNode;
+
+ script.src = rootUrl + '/' + scriptBaseName;
+ script.defer = lastScript.defer;
+ // use the config's type if given (for text/javascript;version=x)
+ script.type = config.type || lastScript.type;
+
+ debug('writing to path="' + target.path + '" src="' + script.src + '"');
+
+ // insert after the last script node of this type...
+ let parent = lastScript.parentNode;
+ parent.insertBefore(script, lastScript.nextSibling);
+ }
+
+ writeAggregatedScript(deferred);
+ writeAggregatedScript(normal);
+
+ function commentScript(script) {
+ script.outerHTML = '<!-- ' + script.outerHTML + ' -->';
+ }
+
+ // comment out all scripts
+ scripts.forEach(commentScript);
+}
+
+function optimize_embedl10nResources(doc, dictionary) {
+ // remove all external l10n resource nodes
+ var resources = doc.querySelectorAll('link[type="application/l10n"]');
+ for (let i = 0; i < resources.length; i++) {
+ let res = resources[i].outerHTML;
+ resources[i].outerHTML = '<!-- ' + res + ' -->';
+ }
+
+ // put the current dictionary in an inline JSON script
+ let script = doc.createElement('script');
+ script.type = 'application/l10n';
+ script.innerHTML = '\n ' + JSON.stringify(dictionary) + '\n';
+ doc.documentElement.appendChild(script);
+}
+
+function optimize_serializeHTMLDocument(doc, file) {
+ debug('saving: ' + file.path);
+
+ // the doctype string should always be '<!DOCTYPE html>' but just in case...
+ let doctypeStr = '';
+ let dt = doc.doctype;
+ if (dt && dt.name) {
+ doctypeStr = '<!DOCTYPE ' + dt.name;
+ if (dt.publicId) {
+ doctypeStr += ' PUBLIC ' + dt.publicId;
+ }
+ if (dt.systemId) {
+ doctypeStr += ' ' + dt.systemId;
+ }
+ doctypeStr += '>\n';
+ }
+
+ // outerHTML breaks the formating, so let's use innerHTML instead
+ let htmlStr = '<html';
+ let docElt = doc.documentElement;
+ let attrs = docElt.attributes;
+ for (let i = 0; i < attrs.length; i++) {
+ htmlStr += ' ' + attrs[i].nodeName.toLowerCase() +
+ '="' + attrs[i].nodeValue + '"';
+ }
+ let innerHTML = docElt.innerHTML.replace(/ \n*<\/body>\n*/, ' </body>\n');
+ htmlStr += '>\n ' + innerHTML + '\n</html>\n';
+
+ writeContent(file, doctypeStr + htmlStr);
+}
+
+function optimize_compile(webapp, file) {
+ let mozL10n = win.navigator.mozL10n;
+
+ let processedLocales = 0;
+ let dictionary = l10nDictionary;
+
+ // catch console.[log|warn|info] calls and redirect them to `dump()'
+ // XXX for some reason, this won't work if gDEBUG >= 2 in l10n.js
+ function optimize_dump(str) {
+ dump(file.path.replace(GAIA_DIR, '') + ': ' + str + '\n');
+ }
+
+ win.console = {
+ log: optimize_dump,
+ warn: optimize_dump,
+ info: optimize_dump
+ };
+
+ // catch the XHR in `loadResource' and use a local file reader instead
+ win.XMLHttpRequest = function() {
+ debug('loadResource');
+
+ function open(type, url, async) {
+ this.readyState = 4;
+ this.status = 200;
+ this.responseText = optimize_getFileContent(webapp, file, url);
+ }
+
+ function send() {
+ this.onreadystatechange();
+ }
+
+ return {
+ open: open,
+ send: send,
+ onreadystatechange: null
+ };
+ };
+
+ // catch the `localized' event dispatched by `fireL10nReadyEvent()'
+ win.dispatchEvent = function() {
+ processedLocales++;
+ debug('fireL10nReadyEvent - ' +
+ processedLocales + '/' + l10nLocales.length);
+
+ let docElt = win.document.documentElement;
+ dictionary.locales[mozL10n.language.code] = mozL10n.dictionary;
+
+ if (processedLocales < l10nLocales.length) {
+ // load next locale
+ mozL10n.language.code = l10nLocales[processedLocales];
+ } else {
+ // we expect the last locale to be the default one:
+ // set the lang/dir attributes of the current document
+ docElt.dir = mozL10n.language.direction;
+ docElt.lang = mozL10n.language.code;
+
+ // save localized document
+ let newPath = file.path + '.' + GAIA_DEFAULT_LOCALE;
+ let newFile = new FileUtils.File(newPath);
+ optimize_embedl10nResources(win.document, dictionary);
+
+ if (JS_AGGREGATION_WHITELIST.indexOf(webapp.sourceDirectoryName) !== -1) {
+ optimize_aggregateJsResources(win.document, webapp, newFile);
+ dump(
+ '[optimize] aggregating javascript for : "' +
+ webapp.sourceDirectoryName + '" \n'
+ );
+ }
+
+ optimize_serializeHTMLDocument(win.document, newFile);
+ }
+ };
+
+ // load and parse the HTML document
+ let DOMParser = CC('@mozilla.org/xmlextras/domparser;1', 'nsIDOMParser');
+ win.document = (new DOMParser()).
+ parseFromString(getFileContent(file), 'text/html');
+
+ // if this HTML document uses l10n.js, pre-localize it --
+ // selecting a language triggers `XMLHttpRequest' and `dispatchEvent' above
+ if (win.document.querySelector('script[src$="l10n.js"]')) {
+ debug('localizing: ' + file.path);
+ mozL10n.language.code = l10nLocales[processedLocales];
+ }
+}
+
+
+/**
+ * Pre-translate all HTML files for the default locale
+ */
+
+debug('Begin');
+
+if (GAIA_INLINE_LOCALES === '1') {
+ l10nLocales = [];
+ l10nDictionary.locales = {};
+
+ // LOCALES_FILE is a relative path by default: shared/resources/languages.json
+ // -- but it can be an absolute path when doing a multilocale build.
+ // LOCALES_FILE is using unix separator, ensure working fine on win32
+ let abs_path_chunks = [GAIA_DIR].concat(LOCALES_FILE.split('/'));
+ let file = getFile.apply(null, abs_path_chunks);
+ if (!file.exists()) {
+ file = getFile(LOCALES_FILE);
+ }
+ let locales = JSON.parse(getFileContent(file));
+
+ // we keep the default locale order for `l10nDictionary.locales',
+ // but we ensure the default locale comes last in `l10nLocales'.
+ for (let lang in locales) {
+ if (lang != GAIA_DEFAULT_LOCALE) {
+ l10nLocales.push(lang);
+ }
+ l10nDictionary.locales[lang] = {};
+ }
+ l10nLocales.push(GAIA_DEFAULT_LOCALE);
+}
+
+Gaia.webapps.forEach(function(webapp) {
+ // if BUILD_APP_NAME isn't `*`, we only accept one webapp
+ if (BUILD_APP_NAME != '*' && webapp.sourceDirectoryName != BUILD_APP_NAME)
+ return;
+
+ debug(webapp.sourceDirectoryName);
+
+ let files = ls(webapp.sourceDirectoryFile, true, /^(shared|tests?)$/);
+ files.forEach(function(file) {
+ if (/\.html$/.test(file.leafName)) {
+ optimize_compile(webapp, file);
+ }
+ });
+});
+
+debug('End');
+
diff --git a/build/webapp-zip.js b/build/webapp-zip.js
new file mode 100644
index 0000000..1c6df44
--- /dev/null
+++ b/build/webapp-zip.js
@@ -0,0 +1,293 @@
+
+function debug(msg) {
+ //dump('-*- webapps-zip.js ' + msg + '\n');
+}
+
+// Header values usefull for zip xpcom component
+const PR_RDONLY = 0x01;
+const PR_WRONLY = 0x02;
+const PR_RDWR = 0x04;
+const PR_CREATE_FILE = 0x08;
+const PR_APPEND = 0x10;
+const PR_TRUNCATE = 0x20;
+const PR_SYNC = 0x40;
+const PR_EXCL = 0x80;
+
+/**
+ * Add a file or a directory, recursively, to a zip file
+ *
+ * @param {nsIZipWriter} zip zip xpcom instance.
+ * @param {String} pathInZip relative path to use in zip.
+ * @param {nsIFile} file file xpcom to add.
+ */
+function addToZip(zip, pathInZip, file) {
+ if (isSubjectToBranding(file.path)) {
+ file.append((OFFICIAL == 1) ? 'official' : 'unofficial');
+ }
+
+ if (!file.exists())
+ throw new Error('Can\'t add inexistent file to zip : ' + file.path);
+
+ // nsIZipWriter should not receive any path starting with `/`,
+ // it would put files in a folder with empty name...
+ pathInZip = pathInZip.replace(/^\/+/, '');
+
+ // Case 1/ Regular file
+ if (file.isFile()) {
+ try {
+ debug(' +file to zip ' + pathInZip);
+
+ if (/\.html$/.test(file.leafName)) {
+ // this file might have been pre-translated for the default locale
+ let l10nFile = file.parent.clone();
+ l10nFile.append(file.leafName + '.' + GAIA_DEFAULT_LOCALE);
+ if (l10nFile.exists()) {
+ zip.addEntryFile(pathInZip,
+ Ci.nsIZipWriter.COMPRESSION_DEFAULT,
+ l10nFile,
+ false);
+ return;
+ }
+ }
+
+ let re = new RegExp('\\.html\\.' + GAIA_DEFAULT_LOCALE);
+ if (!zip.hasEntry(pathInZip) && !re.test(file.leafName)) {
+ zip.addEntryFile(pathInZip,
+ Ci.nsIZipWriter.COMPRESSION_DEFAULT,
+ file,
+ false);
+ }
+ } catch (e) {
+ throw new Error('Unable to add following file in zip: ' +
+ file.path + '\n' + e);
+ }
+ }
+ // Case 2/ Directory
+ else if (file.isDirectory()) {
+ debug(' +directory to zip ' + pathInZip);
+
+ if (!zip.hasEntry(pathInZip))
+ zip.addEntryDirectory(pathInZip, file.lastModifiedTime, false);
+
+ // Append a `/` at end of relative path if it isn't already here
+ if (pathInZip.substr(-1) !== '/')
+ pathInZip += '/';
+
+ let files = ls(file);
+ files.forEach(function(subFile) {
+ let subPath = pathInZip + subFile.leafName;
+ addToZip(zip, subPath, subFile);
+ });
+ }
+}
+
+/**
+ * Copy a "Building Block" (i.e. shared style resource)
+ *
+ * @param {nsIZipWriter} zip zip xpcom instance.
+ * @param {String} blockName name of the building block to copy.
+ * @param {String} dirName name of the shared directory to use.
+ */
+function copyBuildingBlock(zip, blockName, dirName) {
+ let dirPath = '/shared/' + dirName + '/';
+
+ // Compute the nsIFile for this shared style
+ let styleFolder = Gaia.sharedFolder.clone();
+ styleFolder.append(dirName);
+ let cssFile = styleFolder.clone();
+ if (!styleFolder.exists()) {
+ throw new Error('Using inexistent shared style: ' + blockName);
+ }
+
+ cssFile.append(blockName + '.css');
+ addToZip(zip, dirPath + blockName + '.css', cssFile);
+
+ // Copy everything but index.html and any other HTML page into the
+ // style/<block> folder.
+ let subFolder = styleFolder.clone();
+ subFolder.append(blockName);
+ ls(subFolder, true).forEach(function(file) {
+ let relativePath = file.getRelativeDescriptor(styleFolder);
+ // Ignore HTML files at style root folder
+ if (relativePath.match(/^[^\/]+\.html$/))
+ return;
+ // Do not process directory as `addToZip` will add files recursively
+ if (file.isDirectory())
+ return;
+ addToZip(zip, dirPath + relativePath, file);
+ });
+}
+
+let webappsTargetDir = Cc['@mozilla.org/file/local;1']
+ .createInstance(Ci.nsILocalFile);
+webappsTargetDir.initWithPath(PROFILE_DIR);
+
+// Create profile folder if doesn't exists
+ensureFolderExists(webappsTargetDir);
+
+// Create webapps folder if doesn't exists
+webappsTargetDir.append('webapps');
+ensureFolderExists(webappsTargetDir);
+
+Gaia.webapps.forEach(function(webapp) {
+ // If BUILD_APP_NAME isn't `*`, we only accept one webapp
+ if (BUILD_APP_NAME != '*' && webapp.sourceDirectoryName != BUILD_APP_NAME)
+ return;
+
+ // Compute webapp folder name in profile
+ let webappTargetDir = webappsTargetDir.clone();
+ webappTargetDir.append(webapp.domain);
+ ensureFolderExists(webappTargetDir);
+
+ let zip = Cc['@mozilla.org/zipwriter;1'].createInstance(Ci.nsIZipWriter);
+
+ let mode = PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE;
+
+ let zipFile = webappTargetDir.clone();
+ zipFile.append('application.zip');
+ zip.open(zipFile, mode);
+
+ // Add webapp folder to the zip
+ debug('# Create zip for: ' + webapp.domain);
+ let files = ls(webapp.sourceDirectoryFile);
+ files.forEach(function(file) {
+ // Ignore l10n files if they have been inlined
+ if (GAIA_INLINE_LOCALES &&
+ (file.leafName === 'locales' || file.leafName === 'locales.ini'))
+ return;
+ // Ignore files from /shared directory (these files were created by
+ // Makefile code). Also ignore files in the /test directory.
+ if (file.leafName !== 'shared' && file.leafName !== 'test')
+ addToZip(zip, '/' + file.leafName, file);
+ });
+
+ // Put shared files, but copy only files actually used by the webapp.
+ // We search for shared file usage by parsing webapp source code.
+ let EXTENSIONS_WHITELIST = ['html'];
+ let SHARED_USAGE =
+ /<(?:script|link).+=['"]\.?\.?\/?shared\/([^\/]+)\/([^''\s]+)("|')/g;
+
+ let used = {
+ js: [], // List of JS file paths to copy
+ locales: [], // List of locale names to copy
+ resources: [], // List of resources to copy
+ styles: [], // List of stable style names to copy
+ unstable_styles: [] // List of unstable style names to copy
+ };
+
+ let files = ls(webapp.sourceDirectoryFile, true);
+ files.filter(function(file) {
+ // Process only files that may require a shared file
+ let extension = file.leafName
+ .substr(file.leafName.lastIndexOf('.') + 1)
+ .toLowerCase();
+ return file.isFile() && EXTENSIONS_WHITELIST.indexOf(extension) != -1;
+ }).
+ forEach(function(file) {
+ // Grep files to find shared/* usages
+ let content = getFileContent(file);
+ while ((matches = SHARED_USAGE.exec(content)) !== null) {
+ let kind = matches[1]; // js | locales | resources | style
+ let path = matches[2];
+ switch (kind) {
+ case 'js':
+ if (used.js.indexOf(path) == -1)
+ used.js.push(path);
+ break;
+ case 'locales':
+ if (!GAIA_INLINE_LOCALES) {
+ let localeName = path.substr(0, path.lastIndexOf('.'));
+ if (used.locales.indexOf(localeName) == -1) {
+ used.locales.push(localeName);
+ }
+ }
+ break;
+ case 'resources':
+ if (used.resources.indexOf(path) == -1) {
+ used.resources.push(path);
+ }
+ break;
+ case 'style':
+ let styleName = path.substr(0, path.lastIndexOf('.'));
+ if (used.styles.indexOf(styleName) == -1)
+ used.styles.push(styleName);
+ break;
+ case 'style_unstable':
+ let unstableStyleName = path.substr(0, path.lastIndexOf('.'));
+ if (used.unstable_styles.indexOf(unstableStyleName) == -1)
+ used.unstable_styles.push(unstableStyleName);
+ break;
+ }
+ }
+ });
+
+ used.js.forEach(function(path) {
+ // Compute the nsIFile for this shared JS file
+ let file = Gaia.sharedFolder.clone();
+ file.append('js');
+ path.split('/').forEach(function(segment) {
+ file.append(segment);
+ });
+ if (!file.exists()) {
+ throw new Error('Using inexistent shared JS file: ' + path + ' from: ' +
+ webapp.domain);
+ }
+ addToZip(zip, '/shared/js/' + path, file);
+ });
+
+ used.locales.forEach(function(name) {
+ // Compute the nsIFile for this shared locale
+ let localeFolder = Gaia.sharedFolder.clone();
+ localeFolder.append('locales');
+ let ini = localeFolder.clone();
+ localeFolder.append(name);
+ if (!localeFolder.exists()) {
+ throw new Error('Using inexistent shared locale: ' + name + ' from: ' +
+ webapp.domain);
+ }
+ ini.append(name + '.ini');
+ if (!ini.exists())
+ throw new Error(name + ' locale doesn`t have `.ini` file.');
+
+ // Add the .ini file
+ addToZip(zip, '/shared/locales/' + name + '.ini', ini);
+ // And the locale folder itself
+ addToZip(zip, '/shared/locales/' + name, localeFolder);
+ });
+
+ used.resources.forEach(function(path) {
+ // Compute the nsIFile for this shared resource file
+ let file = Gaia.sharedFolder.clone();
+ file.append('resources');
+ path.split('/').forEach(function(segment) {
+ file.append(segment);
+ if (isSubjectToBranding(file.path)) {
+ file.append((OFFICIAL == 1) ? 'official' : 'unofficial');
+ }
+ });
+ if (!file.exists()) {
+ throw new Error('Using inexistent shared resource: ' + path +
+ ' from: ' + webapp.domain + '\n');
+ return;
+ }
+ addToZip(zip, '/shared/resources/' + path, file);
+ });
+
+ used.styles.forEach(function(name) {
+ try {
+ copyBuildingBlock(zip, name, 'style');
+ } catch (e) {
+ throw new Error(e + ' from: ' + webapp.domain);
+ }
+ });
+
+ used.unstable_styles.forEach(function(name) {
+ try {
+ copyBuildingBlock(zip, name, 'style_unstable');
+ } catch (e) {
+ throw new Error(e + ' from: ' + webapp.domain);
+ }
+ });
+
+ zip.close();
+});