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) {