From a45226d132a84ac57546f91c064a52df8226c71e Mon Sep 17 00:00:00 2001 From: Mark Finkle Date: Wed, 16 Oct 2013 17:02:36 -0400 Subject: [PATCH 01/26] Bug 927253 - [geckoview] NPE in GeckoNetworkManager r=blassey --- mobile/android/base/GeckoView.java | 1 + 1 file changed, 1 insertion(+) diff --git a/mobile/android/base/GeckoView.java b/mobile/android/base/GeckoView.java index fa6eb551e484..9d5896adcba2 100644 --- a/mobile/android/base/GeckoView.java +++ b/mobile/android/base/GeckoView.java @@ -55,6 +55,7 @@ public class GeckoView extends LayerView Clipboard.init(context); HardwareUtils.init(context); + GeckoNetworkManager.getInstance().init(context); GeckoLoader.loadMozGlue(); BrowserDB.setEnableContentProviders(false); From d7adf0ffe8eacf8100a9c9bf3d9d1c154b807eb7 Mon Sep 17 00:00:00 2001 From: Mark Finkle Date: Wed, 16 Oct 2013 17:02:39 -0400 Subject: [PATCH 02/26] Bug 927451 - GeckoView profile folder is not created r=blassey r=wesj --- mobile/android/base/GeckoProfile.java | 9 +++++++-- mobile/android/base/GeckoView.java | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/mobile/android/base/GeckoProfile.java b/mobile/android/base/GeckoProfile.java index 6bfd937edd4d..ad514657719b 100644 --- a/mobile/android/base/GeckoProfile.java +++ b/mobile/android/base/GeckoProfile.java @@ -313,8 +313,13 @@ public final class GeckoProfile { } public synchronized File getDir() { + forceCreate(); + return mDir; + } + + public synchronized GeckoProfile forceCreate() { if (mDir != null) { - return mDir; + return this; } try { @@ -330,7 +335,7 @@ public final class GeckoProfile { } catch (IOException ioe) { Log.e(LOGTAG, "Error getting profile dir", ioe); } - return mDir; + return this; } public File getFile(String aFile) { diff --git a/mobile/android/base/GeckoView.java b/mobile/android/base/GeckoView.java index 9d5896adcba2..22d268566e16 100644 --- a/mobile/android/base/GeckoView.java +++ b/mobile/android/base/GeckoView.java @@ -76,7 +76,7 @@ public class GeckoView extends LayerView ThreadUtils.setUiThread(Thread.currentThread(), new Handler()); initializeView(GeckoAppShell.getEventDispatcher()); - GeckoProfile profile = GeckoProfile.get(context); + GeckoProfile profile = GeckoProfile.get(context).forceCreate(); BrowserDB.initialize(profile.getName()); if (GeckoThread.checkAndSetLaunchState(GeckoThread.LaunchState.Launching, GeckoThread.LaunchState.Launched)) { From 69af33ef30d9ca318a29382f25da31855e1d277b Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Wed, 9 Oct 2013 16:18:00 -0700 Subject: [PATCH 03/26] Bug 900522 - Part 1: Make ANDROID_RESFILES a moz.build-only variable. r=gps This depends on Bug 923306, which I think is close to r=glandium. Since this is more moz.build than Android, r?=gps. --- build/mobile/robocop/Makefile.in | 4 ---- build/mobile/robocop/moz.build | 3 +++ build/mobile/sutagent/android/Makefile.in | 10 ---------- build/mobile/sutagent/android/fencp/Makefile.in | 8 -------- build/mobile/sutagent/android/fencp/moz.build | 7 +++++++ build/mobile/sutagent/android/ffxcp/Makefile.in | 8 -------- build/mobile/sutagent/android/ffxcp/moz.build | 7 +++++++ build/mobile/sutagent/android/moz.build | 9 +++++++++ build/mobile/sutagent/android/watcher/Makefile.in | 11 ----------- build/mobile/sutagent/android/watcher/moz.build | 10 ++++++++++ config/rules.mk | 1 + js/src/config/rules.mk | 1 + python/mozbuild/mozbuild/frontend/emitter.py | 1 + python/mozbuild/mozbuild/frontend/sandbox_symbols.py | 7 +++++++ 14 files changed, 46 insertions(+), 41 deletions(-) diff --git a/build/mobile/robocop/Makefile.in b/build/mobile/robocop/Makefile.in index 0dce46c528f5..f1b8ce0b87a1 100644 --- a/build/mobile/robocop/Makefile.in +++ b/build/mobile/robocop/Makefile.in @@ -12,10 +12,6 @@ ANDROID_EXTRA_JARS += \ $(srcdir)/robotium-solo-4.2.jar \ $(NULL) -ANDROID_RESFILES = \ - res/values/strings.xml \ - $(NULL) - ANDROID_ASSETS_DIR := $(TESTPATH)/assets _JAVA_HARNESS := \ diff --git a/build/mobile/robocop/moz.build b/build/mobile/robocop/moz.build index e6e0c96feefb..e3d5078e0521 100644 --- a/build/mobile/robocop/moz.build +++ b/build/mobile/robocop/moz.build @@ -6,3 +6,6 @@ MODULE = 'robocop' +ANDROID_RESFILES = [ + 'res/values/strings.xml', +] diff --git a/build/mobile/sutagent/android/Makefile.in b/build/mobile/sutagent/android/Makefile.in index c1201ed7ce4e..5dc76b877611 100644 --- a/build/mobile/sutagent/android/Makefile.in +++ b/build/mobile/sutagent/android/Makefile.in @@ -20,16 +20,6 @@ JAVAFILES = \ WifiConfiguration.java \ $(NULL) -ANDROID_RESFILES = \ - res/drawable/icon.png \ - res/drawable/ateamlogo.png \ - res/drawable/ic_stat_first.png \ - res/drawable/ic_stat_neterror.png \ - res/drawable/ic_stat_warning.png \ - res/layout/main.xml \ - res/values/strings.xml \ - $(NULL) - ANDROID_EXTRA_JARS = \ $(srcdir)/network-libs/commons-net-2.0.jar \ $(srcdir)/network-libs/jmdns.jar \ diff --git a/build/mobile/sutagent/android/fencp/Makefile.in b/build/mobile/sutagent/android/fencp/Makefile.in index 1e3a9eb5ab6c..e833b263dcb7 100644 --- a/build/mobile/sutagent/android/fencp/Makefile.in +++ b/build/mobile/sutagent/android/fencp/Makefile.in @@ -11,14 +11,6 @@ JAVAFILES = \ FileCursor.java \ $(NULL) -ANDROID_RESFILES = \ - res/drawable-hdpi/icon.png \ - res/drawable-ldpi/icon.png \ - res/drawable-mdpi/icon.png \ - res/layout/main.xml \ - res/values/strings.xml \ - $(NULL) - include $(topsrcdir)/config/rules.mk tools:: $(ANDROID_APK_NAME).apk diff --git a/build/mobile/sutagent/android/fencp/moz.build b/build/mobile/sutagent/android/fencp/moz.build index bba1d688b73e..e87b69e5cd4c 100644 --- a/build/mobile/sutagent/android/fencp/moz.build +++ b/build/mobile/sutagent/android/fencp/moz.build @@ -6,3 +6,10 @@ MODULE = 'FenCP' +ANDROID_RESFILES = [ + 'res/drawable-hdpi/icon.png', + 'res/drawable-ldpi/icon.png', + 'res/drawable-mdpi/icon.png', + 'res/layout/main.xml', + 'res/values/strings.xml', +] diff --git a/build/mobile/sutagent/android/ffxcp/Makefile.in b/build/mobile/sutagent/android/ffxcp/Makefile.in index 957398d7ba8d..ba51e14bcb61 100644 --- a/build/mobile/sutagent/android/ffxcp/Makefile.in +++ b/build/mobile/sutagent/android/ffxcp/Makefile.in @@ -11,14 +11,6 @@ JAVAFILES = \ FileCursor.java \ $(NULL) -ANDROID_RESFILES = \ - res/drawable-hdpi/icon.png \ - res/drawable-ldpi/icon.png \ - res/drawable-mdpi/icon.png \ - res/layout/main.xml \ - res/values/strings.xml \ - $(NULL) - include $(topsrcdir)/config/rules.mk tools:: $(ANDROID_APK_NAME).apk diff --git a/build/mobile/sutagent/android/ffxcp/moz.build b/build/mobile/sutagent/android/ffxcp/moz.build index 11b4ec2cd3db..b7774ea2a89e 100644 --- a/build/mobile/sutagent/android/ffxcp/moz.build +++ b/build/mobile/sutagent/android/ffxcp/moz.build @@ -6,3 +6,10 @@ MODULE = 'FfxCP' +ANDROID_RESFILES = [ + 'res/drawable-hdpi/icon.png', + 'res/drawable-ldpi/icon.png', + 'res/drawable-mdpi/icon.png', + 'res/layout/main.xml', + 'res/values/strings.xml', +] diff --git a/build/mobile/sutagent/android/moz.build b/build/mobile/sutagent/android/moz.build index 58a08666cfb5..ff3709c3906d 100644 --- a/build/mobile/sutagent/android/moz.build +++ b/build/mobile/sutagent/android/moz.build @@ -6,3 +6,12 @@ MODULE = 'sutAgentAndroid' +ANDROID_RESFILES = [ + 'res/drawable/ateamlogo.png', + 'res/drawable/ic_stat_first.png', + 'res/drawable/ic_stat_neterror.png', + 'res/drawable/ic_stat_warning.png', + 'res/drawable/icon.png', + 'res/layout/main.xml', + 'res/values/strings.xml', +] diff --git a/build/mobile/sutagent/android/watcher/Makefile.in b/build/mobile/sutagent/android/watcher/Makefile.in index 22903a9cdc9b..fc8f9e5c356a 100644 --- a/build/mobile/sutagent/android/watcher/Makefile.in +++ b/build/mobile/sutagent/android/watcher/Makefile.in @@ -12,17 +12,6 @@ JAVAFILES = \ WatcherService.java \ $(NULL) -ANDROID_RESFILES = \ - res/drawable-hdpi/icon.png \ - res/drawable-hdpi/ateamlogo.png \ - res/drawable-ldpi/icon.png \ - res/drawable-ldpi/ateamlogo.png \ - res/drawable-mdpi/icon.png \ - res/drawable-mdpi/ateamlogo.png \ - res/layout/main.xml \ - res/values/strings.xml \ - $(NULL) - include $(topsrcdir)/config/rules.mk tools:: $(ANDROID_APK_NAME).apk diff --git a/build/mobile/sutagent/android/watcher/moz.build b/build/mobile/sutagent/android/watcher/moz.build index d45d52c91a33..4f704b34fa71 100644 --- a/build/mobile/sutagent/android/watcher/moz.build +++ b/build/mobile/sutagent/android/watcher/moz.build @@ -6,3 +6,13 @@ MODULE = 'Watcher' +ANDROID_RESFILES = [ + 'res/drawable-hdpi/ateamlogo.png', + 'res/drawable-hdpi/icon.png', + 'res/drawable-ldpi/ateamlogo.png', + 'res/drawable-ldpi/icon.png', + 'res/drawable-mdpi/ateamlogo.png', + 'res/drawable-mdpi/icon.png', + 'res/layout/main.xml', + 'res/values/strings.xml', +] diff --git a/config/rules.mk b/config/rules.mk index ef332bcca305..268b0c769afd 100644 --- a/config/rules.mk +++ b/config/rules.mk @@ -21,6 +21,7 @@ INCLUDED_RULES_MK = 1 # present. If they are, this is a violation of the separation of # responsibility between Makefile.in and mozbuild files. _MOZBUILD_EXTERNAL_VARIABLES := \ + ANDROID_RESFILES \ CMMSRCS \ CPP_UNIT_TESTS \ DIRS \ diff --git a/js/src/config/rules.mk b/js/src/config/rules.mk index ef332bcca305..268b0c769afd 100644 --- a/js/src/config/rules.mk +++ b/js/src/config/rules.mk @@ -21,6 +21,7 @@ INCLUDED_RULES_MK = 1 # present. If they are, this is a violation of the separation of # responsibility between Makefile.in and mozbuild files. _MOZBUILD_EXTERNAL_VARIABLES := \ + ANDROID_RESFILES \ CMMSRCS \ CPP_UNIT_TESTS \ DIRS \ diff --git a/python/mozbuild/mozbuild/frontend/emitter.py b/python/mozbuild/mozbuild/frontend/emitter.py index df8a7f5027cb..eebb87f041f1 100644 --- a/python/mozbuild/mozbuild/frontend/emitter.py +++ b/python/mozbuild/mozbuild/frontend/emitter.py @@ -139,6 +139,7 @@ class TreeMetadataEmitter(LoggingMixin): passthru = VariablePassthru(sandbox) varmap = dict( # Makefile.in : moz.build + ANDROID_RESFILES='ANDROID_RESFILES', ASFILES='ASFILES', CMMSRCS='CMMSRCS', CPPSRCS='CPP_SOURCES', diff --git a/python/mozbuild/mozbuild/frontend/sandbox_symbols.py b/python/mozbuild/mozbuild/frontend/sandbox_symbols.py index c6240f7b60ed..3b4fc2090786 100644 --- a/python/mozbuild/mozbuild/frontend/sandbox_symbols.py +++ b/python/mozbuild/mozbuild/frontend/sandbox_symbols.py @@ -43,6 +43,13 @@ from mozbuild.util import ( VARIABLES = { # Variables controlling reading of other frontend files. + 'ANDROID_RESFILES': (StrictOrderingOnAppendList, list, [], + """Android resource files. + + This variable contains a list of files to package into a 'res' + directory and merge into an APK file. + """, 'export'), + 'ASFILES': (StrictOrderingOnAppendList, list, [], """Assembly file sources. From e0f324428d9a45d5f89488877b1d2cc3d89f4833 Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Wed, 9 Oct 2013 16:19:00 -0700 Subject: [PATCH 04/26] Bug 900522 - Part 2: Use ANDROID_RESFILES in mobile/android/base/moz.build. r=gps This defines ANDROID_RESFILES in mobile/android/base/moz.build but does not use the default processing from java-build.mk. * * * Bug 900522 - Part 1: Make ANDROID_RESFILES a moz.build-only variable. r=gps --- config/makefiles/java-build.mk | 2 + js/src/config/makefiles/java-build.mk | 2 + mobile/android/base/Makefile.in | 789 +----------------- mobile/android/base/android-services-files.mk | 47 -- mobile/android/base/android-services.mozbuild | 28 + mobile/android/base/moz.build | 598 +++++++++++++ .../tests/background/junit3/Makefile.in | 1 - .../junit3/android-services-files.mk | 8 - .../junit3/android-services.mozbuild | 7 + .../android/tests/background/junit3/moz.build | 2 + 10 files changed, 648 insertions(+), 836 deletions(-) create mode 100644 mobile/android/base/android-services.mozbuild create mode 100644 mobile/android/tests/background/junit3/android-services.mozbuild diff --git a/config/makefiles/java-build.mk b/config/makefiles/java-build.mk index 0f7dba426949..179063b8fd5d 100644 --- a/config/makefiles/java-build.mk +++ b/config/makefiles/java-build.mk @@ -8,6 +8,7 @@ ifndef INCLUDED_JAVA_BUILD_MK #{ ifdef ANDROID_RESFILES #{ +ifndef IGNORE_ANDROID_RESFILES #{ res-dep := .deps-copy-java-res GENERATED_DIRS += res @@ -25,6 +26,7 @@ res-dep-preqs := \ $(res-dep): $(res-dep-preqs) $(call copy_dir,$(srcdir)/res,$(CURDIR)/res) @$(TOUCH) $@ +endif #} IGNORE_ANDROID_RESFILES endif #} ANDROID_RESFILES diff --git a/js/src/config/makefiles/java-build.mk b/js/src/config/makefiles/java-build.mk index 0f7dba426949..179063b8fd5d 100644 --- a/js/src/config/makefiles/java-build.mk +++ b/js/src/config/makefiles/java-build.mk @@ -8,6 +8,7 @@ ifndef INCLUDED_JAVA_BUILD_MK #{ ifdef ANDROID_RESFILES #{ +ifndef IGNORE_ANDROID_RESFILES #{ res-dep := .deps-copy-java-res GENERATED_DIRS += res @@ -25,6 +26,7 @@ res-dep-preqs := \ $(res-dep): $(res-dep-preqs) $(call copy_dir,$(srcdir)/res,$(CURDIR)/res) @$(TOUCH) $@ +endif #} IGNORE_ANDROID_RESFILES endif #} ANDROID_RESFILES diff --git a/mobile/android/base/Makefile.in b/mobile/android/base/Makefile.in index 989822ff454c..e8aede9807e0 100644 --- a/mobile/android/base/Makefile.in +++ b/mobile/android/base/Makefile.in @@ -416,789 +416,12 @@ ICON_PATH = $(topsrcdir)/$(MOZ_BRANDING_DIRECTORY)/content/icon48.png ICON_PATH_HDPI = $(topsrcdir)/$(MOZ_BRANDING_DIRECTORY)/content/icon64.png endif -RES_LAYOUT = \ - $(SYNC_RES_LAYOUT) \ - res/layout/arrow_popup.xml \ - res/layout/autocomplete_list.xml \ - res/layout/autocomplete_list_item.xml \ - res/layout/bookmark_edit.xml \ - res/layout/bookmark_folder_row.xml \ - res/layout/bookmark_item_row.xml \ - res/layout/browser_search.xml \ - res/layout/browser_toolbar.xml \ - res/layout/datetime_picker.xml \ - res/layout/doorhanger.xml \ - res/layout/doorhanger_button.xml \ - res/layout/find_in_page_content.xml \ - res/layout/font_size_preference.xml \ - res/layout/gecko_app.xml \ - res/layout/home_bookmarks_page.xml \ - res/layout/home_empty_page.xml \ - res/layout/home_empty_reading_page.xml \ - res/layout/home_item_row.xml \ - res/layout/home_header_row.xml \ - res/layout/home_history_page.xml \ - res/layout/home_history_tabs_indicator.xml \ - res/layout/home_last_tabs_page.xml \ - res/layout/home_history_list.xml \ - res/layout/home_most_recent_page.xml \ - res/layout/home_pager.xml \ - res/layout/home_reading_list_page.xml \ - res/layout/home_search_item_row.xml \ - res/layout/home_banner.xml \ - res/layout/home_suggestion_prompt.xml \ - res/layout/home_top_sites_page.xml \ - res/layout/icon_grid.xml \ - res/layout/icon_grid_item.xml \ - res/layout/web_app.xml \ - res/layout/launch_app_list.xml \ - res/layout/launch_app_listitem.xml \ - res/layout/menu_action_bar.xml \ - res/layout/menu_item_action_view.xml \ - res/layout/menu_popup.xml \ - res/layout/notification_icon_text.xml \ - res/layout/notification_progress.xml \ - res/layout/notification_progress_text.xml \ - res/layout/pin_site_dialog.xml \ - res/layout/preference_rightalign_icon.xml \ - res/layout/preference_search_engine.xml \ - res/layout/preference_search_tip.xml \ - res/layout/site_setting_item.xml \ - res/layout/site_setting_title.xml \ - res/layout/shared_ui_components.xml \ - res/layout/site_identity.xml \ - res/layout/remote_tabs_child.xml \ - res/layout/remote_tabs_group.xml \ - res/layout/search_engine_row.xml \ - res/layout/tab_menu_strip.xml \ - res/layout/tabs_panel.xml \ - res/layout/tabs_counter.xml \ - res/layout/tabs_panel_header.xml \ - res/layout/tabs_panel_indicator.xml \ - res/layout/tabs_item_cell.xml \ - res/layout/tabs_item_row.xml \ - res/layout/text_selection_handles.xml \ - res/layout/top_sites_grid_item_view.xml \ - res/layout/two_line_page_row.xml \ - res/layout/list_item_header.xml \ - res/layout/select_dialog_list.xml \ - res/layout/select_dialog_multichoice.xml \ - res/layout/select_dialog_singlechoice.xml \ - res/layout/simple_dropdown_item_1line.xml \ - res/layout/suggestion_item.xml \ - res/layout/validation_message.xml \ - res/layout/videoplayer.xml \ - $(NULL) - -RES_LAYOUT_LARGE_V11 = \ - res/layout-large-v11/browser_toolbar.xml \ - res/layout-large-v11/home_pager.xml \ - $(NULL) - -RES_LAYOUT_LARGE_LAND_V11 = \ - res/layout-large-land-v11/home_history_page.xml \ - res/layout-large-land-v11/home_history_tabs_indicator.xml \ - res/layout-large-land-v11/home_history_list.xml \ - res/layout-large-land-v11/tabs_panel.xml \ - res/layout-large-land-v11/tabs_panel_header.xml \ - res/layout-large-land-v11/tabs_panel_footer.xml \ - $(NULL) - -RES_LAYOUT_XLARGE_V11 = \ - res/layout-xlarge-v11/font_size_preference.xml \ - res/layout-xlarge-v11/home_history_page.xml \ - res/layout-xlarge-v11/home_history_tabs_indicator.xml \ - res/layout-xlarge-v11/home_history_list.xml \ - res/layout-xlarge-v11/remote_tabs_child.xml \ - res/layout-xlarge-v11/remote_tabs_group.xml \ - $(NULL) - -RES_VALUES = \ - $(SYNC_RES_VALUES) \ - res/values/attrs.xml \ - res/values/arrays.xml \ - res/values/colors.xml \ - res/values/dimens.xml \ - res/values/integers.xml \ - res/values/layout.xml \ - res/values/styles.xml \ - res/values/themes.xml \ - $(NULL) - -RES_VALUES_LAND = \ - res/values-land/integers.xml \ - res/values-land/layout.xml \ - res/values-land/styles.xml \ - $(NULL) - -RES_VALUES_V11 = \ - $(SYNC_RES_VALUES_V11) \ - res/values-v11/colors.xml \ - res/values-v11/dimens.xml \ - res/values-v11/styles.xml \ - res/values-v11/themes.xml \ - $(NULL) - -RES_VALUES_LARGE_V11 = \ - $(SYNC_RES_VALUES_LARGE_V11) \ - res/values-large-v11/dimens.xml \ - res/values-large-v11/layout.xml \ - res/values-large-v11/styles.xml \ - res/values-large-v11/themes.xml \ - $(NULL) - -RES_VALUES_LARGE_LAND_V11 = \ - res/values-large-land-v11/dimens.xml \ - res/values-large-land-v11/styles.xml \ - $(NULL) - -RES_VALUES_XLARGE_V11 = \ - res/values-xlarge-v11/dimens.xml \ - res/values-xlarge-v11/integers.xml \ - res/values-xlarge-v11/styles.xml \ - $(NULL) - -RES_VALUES_XLARGE_LAND_V11 = \ - res/values-xlarge-land-v11/dimens.xml \ - res/values-xlarge-land-v11/styles.xml \ - $(NULL) - -RES_VALUES_V14 = \ - res/values-v14/styles.xml \ - $(NULL) - -RES_VALUES_V16 = \ - res/values-v16/styles.xml \ - $(NULL) - -RES_XML = \ - res/xml/preferences.xml \ - res/xml/preferences_customize.xml \ - res/xml/preferences_display.xml \ - res/xml/preferences_search.xml \ - res/xml/preferences_privacy.xml \ - res/xml/preferences_vendor.xml \ - res/xml/preferences_devtools.xml \ - res/xml/searchable.xml \ - $(SYNC_RES_XML) \ - $(NULL) - -RES_XML_V11 = \ - res/xml-v11/preferences_customize.xml \ - res/xml-v11/preference_headers.xml \ - res/xml-v11/preferences_customize_tablet.xml \ - res/xml-v11/preferences.xml \ - $(NULL) - -RES_ANIM = \ - res/anim/popup_show.xml \ - res/anim/popup_hide.xml \ - res/anim/grow_fade_in.xml \ - res/anim/grow_fade_in_center.xml \ - res/anim/progress_spinner.xml \ - res/anim/shrink_fade_out.xml \ - $(NULL) - -RES_DRAWABLE_MDPI = \ - $(SYNC_RES_DRAWABLE_MDPI) \ - res/drawable-mdpi/blank.png \ - res/drawable-mdpi/favicon.png \ - res/drawable-mdpi/folder.png \ - res/drawable-mdpi/abouthome_thumbnail.png \ - res/drawable-mdpi/alert_addon.png \ - res/drawable-mdpi/alert_app.png \ - res/drawable-mdpi/alert_download.png \ - res/drawable-mdpi/alert_camera.png \ - res/drawable-mdpi/alert_mic.png \ - res/drawable-mdpi/alert_mic_camera.png \ - res/drawable-mdpi/arrow_popup_bg.9.png \ - res/drawable-mdpi/autocomplete_list_bg.9.png \ - res/drawable-mdpi/bookmark_folder_closed.png \ - res/drawable-mdpi/bookmark_folder_opened.png \ - res/drawable-mdpi/desktop_notification.png \ - res/drawable-mdpi/grid_icon_bg_activated.9.png \ - res/drawable-mdpi/grid_icon_bg_focused.9.png \ - res/drawable-mdpi/home_tab_menu_strip.9.png \ - res/drawable-mdpi/ic_menu_addons_filler.png \ - res/drawable-mdpi/ic_menu_bookmark_add.png \ - res/drawable-mdpi/ic_menu_bookmark_remove.png \ - res/drawable-mdpi/ic_menu_character_encoding.png \ - res/drawable-mdpi/close.png \ - res/drawable-mdpi/ic_menu_forward.png \ - res/drawable-mdpi/ic_menu_guest.png \ - res/drawable-mdpi/ic_menu_new_private_tab.png \ - res/drawable-mdpi/ic_menu_new_tab.png \ - res/drawable-mdpi/ic_menu_reload.png \ - res/drawable-mdpi/ic_status_logo.png \ - res/drawable-mdpi/ic_url_bar_go.png \ - res/drawable-mdpi/ic_url_bar_reader.png \ - res/drawable-mdpi/ic_url_bar_search.png \ - res/drawable-mdpi/ic_url_bar_star.png \ - res/drawable-mdpi/ic_url_bar_tab.png \ - res/drawable-mdpi/icon_bookmarks_empty.png \ - res/drawable-mdpi/icon_last_tabs.png \ - res/drawable-mdpi/icon_last_tabs_empty.png \ - res/drawable-mdpi/icon_most_recent.png \ - res/drawable-mdpi/icon_most_recent_empty.png \ - res/drawable-mdpi/icon_most_visited.png \ - res/drawable-mdpi/icon_openinapp.png \ - res/drawable-mdpi/icon_pageaction.png \ - res/drawable-mdpi/icon_reading_list_empty.png \ - res/drawable-mdpi/progress_spinner.png \ - res/drawable-mdpi/play.png \ - res/drawable-mdpi/pause.png \ - res/drawable-mdpi/tab_indicator_divider.9.png \ - res/drawable-mdpi/tab_indicator_selected.9.png \ - res/drawable-mdpi/tab_indicator_selected_focused.9.png \ - res/drawable-mdpi/spinner_default.9.png \ - res/drawable-mdpi/spinner_focused.9.png \ - res/drawable-mdpi/spinner_pressed.9.png \ - res/drawable-mdpi/tab_new.png \ - res/drawable-mdpi/tab_new_pb.png \ - res/drawable-mdpi/tab_close.png \ - res/drawable-mdpi/tab_thumbnail_default.png \ - res/drawable-mdpi/tab_thumbnail_shadow.png \ - res/drawable-mdpi/tabs_count.png \ - res/drawable-mdpi/tabs_count_foreground.png \ - res/drawable-mdpi/url_bar_entry_default.9.png \ - res/drawable-mdpi/url_bar_entry_default_pb.9.png \ - res/drawable-mdpi/url_bar_entry_pressed.9.png \ - res/drawable-mdpi/url_bar_entry_pressed_pb.9.png \ - res/drawable-mdpi/tip_addsearch.png \ - res/drawable-mdpi/toast.9.png \ - res/drawable-mdpi/toast_button_focused.9.png \ - res/drawable-mdpi/toast_button_pressed.9.png \ - res/drawable-mdpi/toast_divider.9.png \ - res/drawable-mdpi/find_close.png \ - res/drawable-mdpi/find_next.png \ - res/drawable-mdpi/find_prev.png \ - res/drawable-mdpi/larry.png \ - res/drawable-mdpi/lock_identified.png \ - res/drawable-mdpi/lock_verified.png \ - res/drawable-mdpi/menu.png \ - res/drawable-mdpi/menu_pb.png \ - res/drawable-mdpi/menu_panel_bg.9.png \ - res/drawable-mdpi/menu_popup_bg.9.png \ - res/drawable-mdpi/menu_popup_arrow_bottom.png \ - res/drawable-mdpi/menu_popup_arrow_top.png \ - res/drawable-mdpi/menu_item_check.png \ - res/drawable-mdpi/menu_item_more.png \ - res/drawable-mdpi/menu_item_uncheck.png \ - res/drawable-mdpi/pin.png \ - res/drawable-mdpi/shield.png \ - res/drawable-mdpi/shield_doorhanger.png \ - res/drawable-mdpi/tabs_normal.png \ - res/drawable-mdpi/tabs_private.png \ - res/drawable-mdpi/tabs_synced.png \ - res/drawable-mdpi/top_site_add.png \ - res/drawable-mdpi/urlbar_stop.png \ - res/drawable-mdpi/reader.png \ - res/drawable-mdpi/reader_cropped.png \ - res/drawable-mdpi/reader_active.png \ - res/drawable-mdpi/reading_list.png \ - res/drawable-mdpi/validation_arrow.png \ - res/drawable-mdpi/validation_arrow_inverted.png \ - res/drawable-mdpi/validation_bg.9.png \ - res/drawable-mdpi/bookmarkdefaults_favicon_support.png \ - res/drawable-mdpi/bookmarkdefaults_favicon_addons.png \ - res/drawable-mdpi/handle_end.png \ - res/drawable-mdpi/handle_middle.png \ - res/drawable-mdpi/handle_start.png \ - res/drawable-mdpi/scrollbar.png \ - res/drawable-mdpi/shadow.png \ - res/drawable-mdpi/start.png \ - res/drawable-mdpi/marketplace.png \ - res/drawable-mdpi/history_tabs_indicator_selected.9.png \ - res/drawable-mdpi/warning.png \ - res/drawable-mdpi/warning_doorhanger.png \ - $(NULL) - -RES_DRAWABLE_LDPI = \ - $(SYNC_RES_DRAWABLE_LDPI) \ - $(NULL) - -RES_DRAWABLE_HDPI = \ - $(SYNC_RES_DRAWABLE_HDPI) \ - res/drawable-hdpi/blank.png \ - res/drawable-hdpi/favicon.png \ - res/drawable-hdpi/folder.png \ - res/drawable-hdpi/home_bg.png \ - res/drawable-hdpi/home_star.png \ - res/drawable-hdpi/grid_icon_bg_activated.9.png \ - res/drawable-hdpi/grid_icon_bg_focused.9.png \ - res/drawable-hdpi/abouthome_thumbnail.png \ - res/drawable-hdpi/alert_addon.png \ - res/drawable-hdpi/alert_app.png \ - res/drawable-hdpi/alert_download.png \ - res/drawable-hdpi/bookmark_folder_closed.png \ - res/drawable-hdpi/bookmark_folder_opened.png \ - res/drawable-hdpi/alert_camera.png \ - res/drawable-hdpi/alert_mic.png \ - res/drawable-hdpi/alert_mic_camera.png \ - res/drawable-hdpi/arrow_popup_bg.9.png \ - res/drawable-hdpi/home_tab_menu_strip.9.png \ - res/drawable-hdpi/ic_menu_addons_filler.png \ - res/drawable-hdpi/ic_menu_bookmark_add.png \ - res/drawable-hdpi/ic_menu_bookmark_remove.png \ - res/drawable-hdpi/ic_menu_character_encoding.png \ - res/drawable-hdpi/close.png \ - res/drawable-hdpi/ic_menu_forward.png \ - res/drawable-hdpi/ic_menu_guest.png \ - res/drawable-hdpi/ic_menu_new_private_tab.png \ - res/drawable-hdpi/ic_menu_new_tab.png \ - res/drawable-hdpi/ic_menu_reload.png \ - res/drawable-hdpi/ic_status_logo.png \ - res/drawable-hdpi/ic_url_bar_go.png \ - res/drawable-hdpi/ic_url_bar_reader.png \ - res/drawable-hdpi/ic_url_bar_search.png \ - res/drawable-hdpi/ic_url_bar_star.png \ - res/drawable-hdpi/ic_url_bar_tab.png \ - res/drawable-hdpi/icon_bookmarks_empty.png \ - res/drawable-hdpi/icon_last_tabs.png \ - res/drawable-hdpi/icon_last_tabs_empty.png \ - res/drawable-hdpi/icon_most_recent.png \ - res/drawable-hdpi/icon_most_recent_empty.png \ - res/drawable-hdpi/icon_most_visited.png \ - res/drawable-hdpi/icon_openinapp.png \ - res/drawable-hdpi/icon_pageaction.png \ - res/drawable-hdpi/icon_reading_list_empty.png \ - res/drawable-hdpi/tab_indicator_divider.9.png \ - res/drawable-hdpi/tab_indicator_selected.9.png \ - res/drawable-hdpi/tab_indicator_selected_focused.9.png \ - res/drawable-hdpi/spinner_default.9.png \ - res/drawable-hdpi/spinner_focused.9.png \ - res/drawable-hdpi/spinner_pressed.9.png \ - res/drawable-hdpi/tab_new.png \ - res/drawable-hdpi/tab_new_pb.png \ - res/drawable-hdpi/tab_close.png \ - res/drawable-hdpi/tab_thumbnail_default.png \ - res/drawable-hdpi/tab_thumbnail_shadow.png \ - res/drawable-hdpi/tabs_count.png \ - res/drawable-hdpi/tabs_count_foreground.png \ - res/drawable-hdpi/url_bar_entry_default.9.png \ - res/drawable-hdpi/url_bar_entry_default_pb.9.png \ - res/drawable-hdpi/url_bar_entry_pressed.9.png \ - res/drawable-hdpi/url_bar_entry_pressed_pb.9.png \ - res/drawable-hdpi/tip_addsearch.png \ - res/drawable-hdpi/find_close.png \ - res/drawable-hdpi/find_next.png \ - res/drawable-hdpi/find_prev.png \ - res/drawable-hdpi/larry.png \ - res/drawable-hdpi/lock_identified.png \ - res/drawable-hdpi/lock_verified.png \ - res/drawable-hdpi/menu.png \ - res/drawable-hdpi/menu_pb.png \ - res/drawable-hdpi/menu_panel_bg.9.png \ - res/drawable-hdpi/menu_popup_bg.9.png \ - res/drawable-hdpi/menu_popup_arrow_bottom.png \ - res/drawable-hdpi/menu_popup_arrow_top.png \ - res/drawable-hdpi/menu_item_check.png \ - res/drawable-hdpi/menu_item_more.png \ - res/drawable-hdpi/menu_item_uncheck.png \ - res/drawable-hdpi/pin.png \ - res/drawable-hdpi/play.png \ - res/drawable-hdpi/pause.png \ - res/drawable-hdpi/shield.png \ - res/drawable-hdpi/shield_doorhanger.png \ - res/drawable-hdpi/tabs_normal.png \ - res/drawable-hdpi/tabs_private.png \ - res/drawable-hdpi/tabs_synced.png \ - res/drawable-hdpi/top_site_add.png \ - res/drawable-hdpi/urlbar_stop.png \ - res/drawable-hdpi/reader.png \ - res/drawable-hdpi/reader_cropped.png \ - res/drawable-hdpi/reader_active.png \ - res/drawable-hdpi/reading_list.png \ - res/drawable-hdpi/validation_arrow.png \ - res/drawable-hdpi/validation_arrow_inverted.png \ - res/drawable-hdpi/validation_bg.9.png \ - res/drawable-hdpi/handle_end.png \ - res/drawable-hdpi/handle_middle.png \ - res/drawable-hdpi/handle_start.png \ - res/drawable-hdpi/history_tabs_indicator_selected.9.png \ - res/drawable-hdpi/warning.png \ - res/drawable-hdpi/warning_doorhanger.png \ - $(NULL) - -RES_DRAWABLE_XHDPI = \ - res/drawable-xhdpi/blank.png \ - res/drawable-xhdpi/favicon.png \ - res/drawable-xhdpi/folder.png \ - res/drawable-xhdpi/abouthome_thumbnail.png \ - res/drawable-xhdpi/url_bar_entry_default.9.png \ - res/drawable-xhdpi/url_bar_entry_default_pb.9.png \ - res/drawable-xhdpi/url_bar_entry_pressed.9.png \ - res/drawable-xhdpi/url_bar_entry_pressed_pb.9.png \ - res/drawable-xhdpi/alert_addon.png \ - res/drawable-xhdpi/alert_app.png \ - res/drawable-xhdpi/alert_download.png \ - res/drawable-xhdpi/bookmark_folder_closed.png \ - res/drawable-xhdpi/bookmark_folder_opened.png \ - res/drawable-xhdpi/alert_camera.png \ - res/drawable-xhdpi/alert_mic.png \ - res/drawable-xhdpi/alert_mic_camera.png \ - res/drawable-xhdpi/arrow_popup_bg.9.png \ - res/drawable-xhdpi/home_tab_menu_strip.9.png \ - res/drawable-xhdpi/grid_icon_bg_activated.9.png \ - res/drawable-xhdpi/grid_icon_bg_focused.9.png \ - res/drawable-xhdpi/ic_menu_addons_filler.png \ - res/drawable-xhdpi/ic_menu_bookmark_add.png \ - res/drawable-xhdpi/ic_menu_bookmark_remove.png \ - res/drawable-xhdpi/close.png \ - res/drawable-xhdpi/ic_menu_character_encoding.png \ - res/drawable-xhdpi/ic_menu_forward.png \ - res/drawable-xhdpi/ic_menu_guest.png \ - res/drawable-xhdpi/ic_menu_new_private_tab.png \ - res/drawable-xhdpi/ic_menu_new_tab.png \ - res/drawable-xhdpi/ic_menu_reload.png \ - res/drawable-xhdpi/ic_status_logo.png \ - res/drawable-xhdpi/ic_url_bar_go.png \ - res/drawable-xhdpi/ic_url_bar_reader.png \ - res/drawable-xhdpi/ic_url_bar_search.png \ - res/drawable-xhdpi/ic_url_bar_star.png \ - res/drawable-xhdpi/ic_url_bar_tab.png \ - res/drawable-xhdpi/icon_bookmarks_empty.png \ - res/drawable-xhdpi/icon_last_tabs.png \ - res/drawable-xhdpi/icon_last_tabs_empty.png \ - res/drawable-xhdpi/icon_most_recent.png \ - res/drawable-xhdpi/icon_most_recent_empty.png \ - res/drawable-xhdpi/icon_most_visited.png \ - res/drawable-xhdpi/icon_openinapp.png \ - res/drawable-xhdpi/icon_pageaction.png \ - res/drawable-xhdpi/icon_reading_list_empty.png \ - res/drawable-xhdpi/spinner_default.9.png \ - res/drawable-xhdpi/spinner_focused.9.png \ - res/drawable-xhdpi/spinner_pressed.9.png \ - res/drawable-xhdpi/tab_new.png \ - res/drawable-xhdpi/tab_new_pb.png \ - res/drawable-xhdpi/tab_close.png \ - res/drawable-xhdpi/tab_thumbnail_default.png \ - res/drawable-xhdpi/tab_thumbnail_shadow.png \ - res/drawable-xhdpi/tabs_count.png \ - res/drawable-xhdpi/tabs_count_foreground.png \ - res/drawable-xhdpi/tip_addsearch.png \ - res/drawable-xhdpi/find_close.png \ - res/drawable-xhdpi/find_next.png \ - res/drawable-xhdpi/find_prev.png \ - res/drawable-xhdpi/top_site_add.png \ - res/drawable-xhdpi/urlbar_stop.png \ - res/drawable-xhdpi/reader.png \ - res/drawable-xhdpi/reader_cropped.png \ - res/drawable-xhdpi/reader_active.png \ - res/drawable-xhdpi/reading_list.png \ - res/drawable-xhdpi/larry.png \ - res/drawable-xhdpi/lock_identified.png \ - res/drawable-xhdpi/lock_verified.png \ - res/drawable-xhdpi/menu.png \ - res/drawable-xhdpi/menu_pb.png \ - res/drawable-xhdpi/menu_panel_bg.9.png \ - res/drawable-xhdpi/menu_popup_bg.9.png \ - res/drawable-xhdpi/menu_popup_arrow_bottom.png \ - res/drawable-xhdpi/menu_popup_arrow_top.png \ - res/drawable-xhdpi/menu_item_check.png \ - res/drawable-xhdpi/menu_item_more.png \ - res/drawable-xhdpi/menu_item_uncheck.png \ - res/drawable-xhdpi/pin.png \ - res/drawable-xhdpi/play.png \ - res/drawable-xhdpi/pause.png \ - res/drawable-xhdpi/shield.png \ - res/drawable-xhdpi/shield_doorhanger.png \ - res/drawable-xhdpi/tab_indicator_divider.9.png \ - res/drawable-xhdpi/tab_indicator_selected.9.png \ - res/drawable-xhdpi/tab_indicator_selected_focused.9.png \ - res/drawable-xhdpi/tabs_normal.png \ - res/drawable-xhdpi/tabs_private.png \ - res/drawable-xhdpi/tabs_synced.png \ - res/drawable-xhdpi/validation_arrow.png \ - res/drawable-xhdpi/validation_arrow_inverted.png \ - res/drawable-xhdpi/validation_bg.9.png \ - res/drawable-xhdpi/handle_end.png \ - res/drawable-xhdpi/handle_middle.png \ - res/drawable-xhdpi/handle_start.png \ - res/drawable-xhdpi/history_tabs_indicator_selected.9.png \ - res/drawable-xhdpi/warning.png \ - res/drawable-xhdpi/warning_doorhanger.png \ - $(NULL) - -RES_DRAWABLE_MDPI_V11 = \ - res/drawable-mdpi-v11/alert_addon.png \ - res/drawable-mdpi-v11/alert_app.png \ - res/drawable-mdpi-v11/alert_download.png \ - res/drawable-mdpi-v11/alert_camera.png \ - res/drawable-mdpi-v11/alert_mic.png \ - res/drawable-mdpi-v11/alert_mic_camera.png \ - res/drawable-mdpi-v11/firefox_settings_alert.png \ - res/drawable-mdpi-v11/ic_menu_addons.png \ - res/drawable-mdpi-v11/ic_menu_apps.png \ - res/drawable-mdpi-v11/ic_menu_back.png \ - res/drawable-mdpi-v11/ic_menu_bookmark_add.png \ - res/drawable-mdpi-v11/ic_menu_bookmark_remove.png \ - res/drawable-mdpi-v11/ic_menu_desktop_mode_off.png \ - res/drawable-mdpi-v11/ic_menu_desktop_mode_on.png \ - res/drawable-mdpi-v11/ic_menu_downloads.png \ - res/drawable-mdpi-v11/ic_menu_find_in_page.png \ - res/drawable-mdpi-v11/ic_menu_forward.png \ - res/drawable-mdpi-v11/ic_menu_new_private_tab.png \ - res/drawable-mdpi-v11/ic_menu_new_tab.png \ - res/drawable-mdpi-v11/ic_menu_reload.png \ - res/drawable-mdpi-v11/ic_menu_save_as_pdf.png \ - res/drawable-mdpi-v11/ic_menu_settings.png \ - res/drawable-mdpi-v11/ic_menu_share.png \ - res/drawable-mdpi-v11/ic_menu_tools.png \ - res/drawable-mdpi-v11/ic_menu_quit.png \ - res/drawable-mdpi-v11/ic_status_logo.png \ - $(NULL) - -RES_DRAWABLE_HDPI_V11 = \ - res/drawable-hdpi-v11/alert_addon.png \ - res/drawable-hdpi-v11/alert_app.png \ - res/drawable-hdpi-v11/alert_download.png \ - res/drawable-hdpi-v11/alert_camera.png \ - res/drawable-hdpi-v11/alert_mic.png \ - res/drawable-hdpi-v11/alert_mic_camera.png \ - res/drawable-hdpi-v11/firefox_settings_alert.png \ - res/drawable-hdpi-v11/ic_menu_addons.png \ - res/drawable-hdpi-v11/ic_menu_apps.png \ - res/drawable-hdpi-v11/ic_menu_back.png \ - res/drawable-hdpi-v11/ic_menu_bookmark_add.png \ - res/drawable-hdpi-v11/ic_menu_bookmark_remove.png \ - res/drawable-hdpi-v11/ic_menu_desktop_mode_off.png \ - res/drawable-hdpi-v11/ic_menu_desktop_mode_on.png \ - res/drawable-hdpi-v11/ic_menu_downloads.png \ - res/drawable-hdpi-v11/ic_menu_find_in_page.png \ - res/drawable-hdpi-v11/ic_menu_forward.png \ - res/drawable-hdpi-v11/ic_menu_new_private_tab.png \ - res/drawable-hdpi-v11/ic_menu_new_tab.png \ - res/drawable-hdpi-v11/ic_menu_reload.png \ - res/drawable-hdpi-v11/ic_menu_save_as_pdf.png \ - res/drawable-hdpi-v11/ic_menu_settings.png \ - res/drawable-hdpi-v11/ic_menu_share.png \ - res/drawable-hdpi-v11/ic_menu_tools.png \ - res/drawable-hdpi-v11/ic_menu_quit.png \ - res/drawable-hdpi-v11/ic_status_logo.png \ - $(NULL) - -RES_DRAWABLE_XHDPI_V11 = \ - res/drawable-xhdpi-v11/alert_addon.png \ - res/drawable-xhdpi-v11/alert_app.png \ - res/drawable-xhdpi-v11/alert_download.png \ - res/drawable-xhdpi-v11/alert_camera.png \ - res/drawable-xhdpi-v11/alert_mic.png \ - res/drawable-xhdpi-v11/alert_mic_camera.png \ - res/drawable-xhdpi-v11/firefox_settings_alert.png \ - res/drawable-xhdpi-v11/ic_menu_addons.png \ - res/drawable-xhdpi-v11/ic_menu_apps.png \ - res/drawable-xhdpi-v11/ic_menu_back.png \ - res/drawable-xhdpi-v11/ic_menu_bookmark_add.png \ - res/drawable-xhdpi-v11/ic_menu_bookmark_remove.png \ - res/drawable-xhdpi-v11/ic_menu_desktop_mode_off.png \ - res/drawable-xhdpi-v11/ic_menu_desktop_mode_on.png \ - res/drawable-xhdpi-v11/ic_menu_downloads.png \ - res/drawable-xhdpi-v11/ic_menu_find_in_page.png \ - res/drawable-xhdpi-v11/ic_menu_forward.png \ - res/drawable-xhdpi-v11/ic_menu_new_private_tab.png \ - res/drawable-xhdpi-v11/ic_menu_new_tab.png \ - res/drawable-xhdpi-v11/ic_menu_reload.png \ - res/drawable-xhdpi-v11/ic_menu_save_as_pdf.png \ - res/drawable-xhdpi-v11/ic_menu_settings.png \ - res/drawable-xhdpi-v11/ic_menu_share.png \ - res/drawable-xhdpi-v11/ic_menu_tools.png \ - res/drawable-xhdpi-v11/ic_menu_quit.png \ - res/drawable-xhdpi-v11/ic_status_logo.png \ - $(NULL) - -RES_DRAWABLE_LARGE_LAND_V11 = \ - res/drawable-large-land-v11/home_history_tabs_indicator.xml \ - $(NULL) - -RES_DRAWABLE_LARGE_MDPI_V11 = \ - res/drawable-large-mdpi-v11/arrow_popup_bg.9.png \ - res/drawable-large-mdpi-v11/ic_menu_reload.png \ - res/drawable-large-mdpi-v11/ic_menu_forward.png \ - res/drawable-large-mdpi-v11/menu.png \ - $(NULL) - -RES_DRAWABLE_LARGE_HDPI_V11 = \ - res/drawable-large-hdpi-v11/arrow_popup_bg.9.png \ - res/drawable-large-hdpi-v11/ic_menu_reload.png \ - res/drawable-large-hdpi-v11/ic_menu_forward.png \ - res/drawable-large-hdpi-v11/menu.png \ - $(NULL) - -RES_DRAWABLE_LARGE_XHDPI_V11 = \ - res/drawable-large-xhdpi-v11/arrow_popup_bg.9.png \ - res/drawable-large-xhdpi-v11/ic_menu_reload.png \ - res/drawable-large-xhdpi-v11/ic_menu_forward.png \ - res/drawable-large-xhdpi-v11/menu.png \ - $(NULL) - -RES_DRAWABLE_XLARGE_V11 = \ - res/drawable-xlarge-v11/home_history_tabs_indicator.xml \ - $(NULL) - -RES_DRAWABLE_XLARGE_MDPI_V11 = \ - res/drawable-xlarge-mdpi-v11/ic_menu_bookmark_add.png \ - res/drawable-xlarge-mdpi-v11/ic_menu_bookmark_remove.png \ - $(NULL) - -RES_DRAWABLE_XLARGE_HDPI_V11 = \ - res/drawable-xlarge-hdpi-v11/ic_menu_bookmark_add.png \ - res/drawable-xlarge-hdpi-v11/ic_menu_bookmark_remove.png \ - $(NULL) - -RES_DRAWABLE_XLARGE_XHDPI_V11 = \ - res/drawable-xlarge-xhdpi-v11/ic_menu_bookmark_add.png \ - res/drawable-xlarge-xhdpi-v11/ic_menu_bookmark_remove.png \ - $(NULL) - -RES_COLOR = \ - res/color/primary_text.xml \ - res/color/primary_text_inverse.xml \ - res/color/secondary_text.xml \ - res/color/secondary_text_inverse.xml \ - res/color/select_item_multichoice.xml \ - res/color/tertiary_text.xml \ - res/color/tertiary_text_inverse.xml \ - res/color/top_sites_grid_item_title.xml \ - res/color/url_bar_title.xml \ - res/color/url_bar_title_hint.xml \ - $(NULL) - -RES_MENU = \ - res/menu/browser_app_menu.xml \ - res/menu/gecko_app_menu.xml \ - res/menu/home_contextmenu.xml \ - res/menu/titlebar_contextmenu.xml \ - res/menu/top_sites_contextmenu.xml \ - res/menu-large-v11/browser_app_menu.xml \ - res/menu-v11/browser_app_menu.xml \ - res/menu-xlarge-v11/browser_app_menu.xml \ - $(NULL) - JAVA_CLASSPATH = $(ANDROID_SDK)/android.jar ifdef MOZ_CRASHREPORTER FENNEC_JAVA_FILES += CrashReporter.java -RES_DRAWABLE_MDPI += res/drawable-mdpi/crash_reporter.png -RES_LAYOUT += res/layout/crash_reporter.xml endif -RES_DRAWABLE += \ - $(SYNC_RES_DRAWABLE) \ - res/drawable/action_bar_button.xml \ - res/drawable/action_bar_button_inverse.xml \ - res/drawable/top_sites_thumbnail_bg.xml \ - res/drawable/url_bar_bg.xml \ - res/drawable/url_bar_entry.xml \ - res/drawable/url_bar_nav_button.xml \ - res/drawable/icon_grid_item_bg.xml \ - res/drawable/url_bar_right_edge.xml \ - res/drawable/bookmark_folder.xml \ - res/drawable/divider_horizontal.xml \ - res/drawable/divider_vertical.xml \ - res/drawable/favicon_bg.xml \ - res/drawable/handle_end_level.xml \ - res/drawable/handle_start_level.xml \ - res/drawable/home_history_tabs_indicator.xml \ - res/drawable/home_page_title_background.xml \ - res/drawable/home_banner.xml \ - res/drawable/ic_menu_back.xml \ - res/drawable/ic_menu_desktop_mode_off.xml \ - res/drawable/ic_menu_desktop_mode_on.xml \ - res/drawable/ic_menu_quit.xml \ - res/drawable/menu_item_state.xml \ - res/drawable/menu_level.xml \ - res/drawable/remote_tabs_child_divider.xml \ - res/drawable/shaped_button.xml \ - res/drawable/site_security_level.xml \ - res/drawable/spinner.xml \ - res/drawable/suggestion_selector.xml \ - res/drawable/tab_new_level.xml \ - res/drawable/tab_row.xml \ - res/drawable/tab_thumbnail.xml \ - res/drawable/tabs_panel_indicator.xml \ - res/drawable/textbox_bg.xml \ - res/drawable/toast_button.xml \ - res/drawable/webapp_titlebar_bg.xml \ - $(NULL) - -RESOURCES = \ - $(RES_ANIM) \ - $(RES_COLOR) \ - $(RES_DRAWABLE) \ - $(RES_DRAWABLE_HDPI) \ - $(RES_DRAWABLE_HDPI_V11) \ - $(RES_DRAWABLE_LARGE_LAND_V11) \ - $(RES_DRAWABLE_LARGE_HDPI_V11) \ - $(RES_DRAWABLE_LARGE_MDPI_V11) \ - $(RES_DRAWABLE_LARGE_XHDPI_V11) \ - $(RES_DRAWABLE_LDPI) \ - $(RES_DRAWABLE_MDPI) \ - $(RES_DRAWABLE_MDPI_V11) \ - $(RES_DRAWABLE_XHDPI) \ - $(RES_DRAWABLE_XHDPI_V11) \ - $(RES_DRAWABLE_XLARGE_V11) \ - $(RES_DRAWABLE_XLARGE_HDPI_V11) \ - $(RES_DRAWABLE_XLARGE_MDPI_V11) \ - $(RES_DRAWABLE_XLARGE_XHDPI_V11) \ - $(RES_LAYOUT) \ - $(RES_LAYOUT_LARGE_LAND_V11) \ - $(RES_LAYOUT_LARGE_V11) \ - $(RES_LAYOUT_XLARGE_LAND_V11) \ - $(RES_LAYOUT_XLARGE_V11) \ - $(RES_MENU) \ - $(RES_VALUES) \ - $(RES_VALUES_LAND) \ - $(RES_VALUES_LAND_V14) \ - $(RES_VALUES_LARGE_LAND_V11) \ - $(RES_VALUES_LARGE_V11) \ - $(RES_VALUES_V11) \ - $(RES_VALUES_V14) \ - $(RES_VALUES_V16) \ - $(RES_VALUES_XLARGE_LAND_V11) \ - $(RES_VALUES_XLARGE_V11) \ - $(RES_XML) \ - $(RES_XML_V11) \ - $(NULL) - -RES_DIRS= \ - res/layout \ - res/layout-large-v11 \ - res/layout-large-land-v11 \ - res/layout-xlarge-v11 \ - res/values \ - res/values-v11 \ - res/values-large-v11 \ - res/values-xlarge-land-v11 \ - res/values-xlarge-v11 \ - res/values-v14 \ - res/values-v16 \ - res/xml \ - res/xml-v11 \ - res/anim \ - res/drawable-ldpi \ - res/drawable-mdpi \ - res/drawable-hdpi \ - res/drawable-xhdpi \ - res/drawable \ - res/drawable-mdpi-v11 \ - res/drawable-hdpi-v11 \ - res/drawable-xhdpi-v11 \ - res/drawable-large-land-v11 \ - res/drawable-large-mdpi-v11 \ - res/drawable-large-hdpi-v11 \ - res/drawable-large-xhdpi-v11 \ - res/drawable-xlarge-v11 \ - res/drawable-xlarge-mdpi-v11 \ - res/drawable-xlarge-hdpi-v11 \ - res/drawable-xlarge-xhdpi-v11 \ - res/color \ - res/menu \ - res/menu-v11 \ - res/menu-large-v11 \ - res/menu-xlarge-v11 \ - $(NULL) - ALL_JARS = \ jars/gecko-browser.jar \ jars/gecko-mozglue.jar \ @@ -1280,6 +503,10 @@ endif include $(topsrcdir)/config/makefiles/java-build.mk +# We process ANDROID_RESFILES specially for now; the following flag +# disables the default processing. +IGNORE_ANDROID_RESFILES=1 + include $(topsrcdir)/config/rules.mk # Override the Java settings with some specific android settings @@ -1360,12 +587,14 @@ res/drawable-xxhdpi/icon.png: $(ICON_PATH_XXHDPI) $(NSINSTALL) -D res/drawable-xxhdpi cp $(ICON_PATH_XXHDPI) $@ -$(call mkdir_deps,$(RES_DIRS)): $(subst res/,$(srcdir)/resources/,$(RESOURCES)) Makefile +ANDROID_RESDIRS := $(subst resources/,res/,$(sort $(dir $(ANDROID_RESFILES)))) + +$(call mkdir_deps,$(ANDROID_RESDIRS)): $(ANDROID_RESFILES) Makefile $(RM) -r $(@D) $(NSINSTALL) -D $(@D) $(TOUCH) $@ -$(RESOURCES): $(call mkdir_deps,$(RES_DIRS)) $(subst res/,$(srcdir)/resources/,$(RESOURCES)) +$(subst resources/,res/,$(ANDROID_RESFILES)): $(call mkdir_deps,$(ANDROID_RESDIRS)) $(ANDROID_RESFILES) @echo "creating $@" $(NSINSTALL) $(subst res/,$(srcdir)/resources/,$@) $(dir $@) @@ -1383,7 +612,7 @@ all_resources = \ res/values/strings.xml \ $(MULTILOCALE_STRINGS_XML_FILES) \ AndroidManifest.xml \ - $(RESOURCES) \ + $(subst resources/,res/,$(ANDROID_RESFILES)) \ $(NULL) R.java: $(all_resources) diff --git a/mobile/android/base/android-services-files.mk b/mobile/android/base/android-services-files.mk index 6bc2a42693d0..f107f3fd9cfb 100644 --- a/mobile/android/base/android-services-files.mk +++ b/mobile/android/base/android-services-files.mk @@ -301,53 +301,6 @@ SYNC_JAVA_FILES := \ sync/Utils.java \ $(NULL) -SYNC_RES_DRAWABLE := \ - res/drawable/pin_background.xml \ - $(NULL) - -SYNC_RES_DRAWABLE_LDPI := \ - $(NULL) - -SYNC_RES_DRAWABLE_MDPI := \ - res/drawable-mdpi/desktop.png \ - res/drawable-mdpi/mobile.png \ - $(NULL) - -SYNC_RES_DRAWABLE_HDPI := \ - $(NULL) - -SYNC_RES_LAYOUT := \ - res/layout/sync_account.xml \ - res/layout/sync_list_item.xml \ - res/layout/sync_redirect_to_setup.xml \ - res/layout/sync_send_tab.xml \ - res/layout/sync_setup.xml \ - res/layout/sync_setup_failure.xml \ - res/layout/sync_setup_jpake_waiting.xml \ - res/layout/sync_setup_nointernet.xml \ - res/layout/sync_setup_pair.xml \ - res/layout/sync_setup_success.xml \ - res/layout/sync_setup_webview.xml \ - $(NULL) - -SYNC_RES_VALUES := \ - res/values/sync_styles.xml \ - $(NULL) - -SYNC_RES_VALUES_V11 := \ - res/values-v11/sync_styles.xml \ - $(NULL) - -SYNC_RES_VALUES_LARGE_V11 := \ - res/values-large-v11/sync_styles.xml \ - $(NULL) - -SYNC_RES_XML := \ - res/xml/sync_authenticator.xml \ - res/xml/sync_syncadapter.xml \ - res/xml/sync_options.xml \ - $(NULL) - SYNC_THIRDPARTY_JAVA_FILES := \ httpclientandroidlib/androidextra/HttpClientAndroidLog.java \ httpclientandroidlib/annotation/GuardedBy.java \ diff --git a/mobile/android/base/android-services.mozbuild b/mobile/android/base/android-services.mozbuild new file mode 100644 index 000000000000..43defbace1d5 --- /dev/null +++ b/mobile/android/base/android-services.mozbuild @@ -0,0 +1,28 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +ANDROID_RESFILES += [ + 'resources/drawable-mdpi/desktop.png', + 'resources/drawable-mdpi/mobile.png', + 'resources/drawable/pin_background.xml', + 'resources/layout/sync_account.xml', + 'resources/layout/sync_list_item.xml', + 'resources/layout/sync_redirect_to_setup.xml', + 'resources/layout/sync_send_tab.xml', + 'resources/layout/sync_setup.xml', + 'resources/layout/sync_setup_failure.xml', + 'resources/layout/sync_setup_jpake_waiting.xml', + 'resources/layout/sync_setup_nointernet.xml', + 'resources/layout/sync_setup_pair.xml', + 'resources/layout/sync_setup_success.xml', + 'resources/layout/sync_setup_webview.xml', + 'resources/values-large-v11/sync_styles.xml', + 'resources/values-v11/sync_styles.xml', + 'resources/values/sync_styles.xml', + 'resources/xml/sync_authenticator.xml', + 'resources/xml/sync_options.xml', + 'resources/xml/sync_syncadapter.xml', +] diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build index a018c31256d5..5f5148560e2b 100644 --- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -5,3 +5,601 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. DIRS += ['locales'] + +include('android-services.mozbuild') + +ANDROID_RESFILES += [ + 'resources/anim/grow_fade_in.xml', + 'resources/anim/grow_fade_in_center.xml', + 'resources/anim/popup_hide.xml', + 'resources/anim/popup_show.xml', + 'resources/anim/progress_spinner.xml', + 'resources/anim/shrink_fade_out.xml', + 'resources/color/primary_text.xml', + 'resources/color/primary_text_inverse.xml', + 'resources/color/secondary_text.xml', + 'resources/color/secondary_text_inverse.xml', + 'resources/color/select_item_multichoice.xml', + 'resources/color/tertiary_text.xml', + 'resources/color/tertiary_text_inverse.xml', + 'resources/color/top_sites_grid_item_title.xml', + 'resources/color/url_bar_title.xml', + 'resources/color/url_bar_title_hint.xml', + 'resources/drawable-hdpi-v11/alert_addon.png', + 'resources/drawable-hdpi-v11/alert_app.png', + 'resources/drawable-hdpi-v11/alert_camera.png', + 'resources/drawable-hdpi-v11/alert_download.png', + 'resources/drawable-hdpi-v11/alert_mic.png', + 'resources/drawable-hdpi-v11/alert_mic_camera.png', + 'resources/drawable-hdpi-v11/firefox_settings_alert.png', + 'resources/drawable-hdpi-v11/ic_menu_addons.png', + 'resources/drawable-hdpi-v11/ic_menu_apps.png', + 'resources/drawable-hdpi-v11/ic_menu_back.png', + 'resources/drawable-hdpi-v11/ic_menu_bookmark_add.png', + 'resources/drawable-hdpi-v11/ic_menu_bookmark_remove.png', + 'resources/drawable-hdpi-v11/ic_menu_desktop_mode_off.png', + 'resources/drawable-hdpi-v11/ic_menu_desktop_mode_on.png', + 'resources/drawable-hdpi-v11/ic_menu_downloads.png', + 'resources/drawable-hdpi-v11/ic_menu_find_in_page.png', + 'resources/drawable-hdpi-v11/ic_menu_forward.png', + 'resources/drawable-hdpi-v11/ic_menu_new_private_tab.png', + 'resources/drawable-hdpi-v11/ic_menu_new_tab.png', + 'resources/drawable-hdpi-v11/ic_menu_quit.png', + 'resources/drawable-hdpi-v11/ic_menu_reload.png', + 'resources/drawable-hdpi-v11/ic_menu_save_as_pdf.png', + 'resources/drawable-hdpi-v11/ic_menu_settings.png', + 'resources/drawable-hdpi-v11/ic_menu_share.png', + 'resources/drawable-hdpi-v11/ic_menu_tools.png', + 'resources/drawable-hdpi-v11/ic_status_logo.png', + 'resources/drawable-hdpi/abouthome_thumbnail.png', + 'resources/drawable-hdpi/alert_addon.png', + 'resources/drawable-hdpi/alert_app.png', + 'resources/drawable-hdpi/alert_camera.png', + 'resources/drawable-hdpi/alert_download.png', + 'resources/drawable-hdpi/alert_mic.png', + 'resources/drawable-hdpi/alert_mic_camera.png', + 'resources/drawable-hdpi/arrow_popup_bg.9.png', + 'resources/drawable-hdpi/blank.png', + 'resources/drawable-hdpi/bookmark_folder_closed.png', + 'resources/drawable-hdpi/bookmark_folder_opened.png', + 'resources/drawable-hdpi/close.png', + 'resources/drawable-hdpi/favicon.png', + 'resources/drawable-hdpi/find_close.png', + 'resources/drawable-hdpi/find_next.png', + 'resources/drawable-hdpi/find_prev.png', + 'resources/drawable-hdpi/folder.png', + 'resources/drawable-hdpi/grid_icon_bg_activated.9.png', + 'resources/drawable-hdpi/grid_icon_bg_focused.9.png', + 'resources/drawable-hdpi/handle_end.png', + 'resources/drawable-hdpi/handle_middle.png', + 'resources/drawable-hdpi/handle_start.png', + 'resources/drawable-hdpi/history_tabs_indicator_selected.9.png', + 'resources/drawable-hdpi/home_bg.png', + 'resources/drawable-hdpi/home_star.png', + 'resources/drawable-hdpi/home_tab_menu_strip.9.png', + 'resources/drawable-hdpi/ic_menu_addons_filler.png', + 'resources/drawable-hdpi/ic_menu_bookmark_add.png', + 'resources/drawable-hdpi/ic_menu_bookmark_remove.png', + 'resources/drawable-hdpi/ic_menu_character_encoding.png', + 'resources/drawable-hdpi/ic_menu_forward.png', + 'resources/drawable-hdpi/ic_menu_guest.png', + 'resources/drawable-hdpi/ic_menu_new_private_tab.png', + 'resources/drawable-hdpi/ic_menu_new_tab.png', + 'resources/drawable-hdpi/ic_menu_reload.png', + 'resources/drawable-hdpi/ic_status_logo.png', + 'resources/drawable-hdpi/ic_url_bar_go.png', + 'resources/drawable-hdpi/ic_url_bar_reader.png', + 'resources/drawable-hdpi/ic_url_bar_search.png', + 'resources/drawable-hdpi/ic_url_bar_star.png', + 'resources/drawable-hdpi/ic_url_bar_tab.png', + 'resources/drawable-hdpi/icon_bookmarks_empty.png', + 'resources/drawable-hdpi/icon_last_tabs.png', + 'resources/drawable-hdpi/icon_last_tabs_empty.png', + 'resources/drawable-hdpi/icon_most_recent.png', + 'resources/drawable-hdpi/icon_most_recent_empty.png', + 'resources/drawable-hdpi/icon_most_visited.png', + 'resources/drawable-hdpi/icon_openinapp.png', + 'resources/drawable-hdpi/icon_pageaction.png', + 'resources/drawable-hdpi/icon_reading_list_empty.png', + 'resources/drawable-hdpi/larry.png', + 'resources/drawable-hdpi/lock_identified.png', + 'resources/drawable-hdpi/lock_verified.png', + 'resources/drawable-hdpi/menu.png', + 'resources/drawable-hdpi/menu_item_check.png', + 'resources/drawable-hdpi/menu_item_more.png', + 'resources/drawable-hdpi/menu_item_uncheck.png', + 'resources/drawable-hdpi/menu_panel_bg.9.png', + 'resources/drawable-hdpi/menu_pb.png', + 'resources/drawable-hdpi/menu_popup_arrow_bottom.png', + 'resources/drawable-hdpi/menu_popup_arrow_top.png', + 'resources/drawable-hdpi/menu_popup_bg.9.png', + 'resources/drawable-hdpi/pause.png', + 'resources/drawable-hdpi/pin.png', + 'resources/drawable-hdpi/play.png', + 'resources/drawable-hdpi/reader.png', + 'resources/drawable-hdpi/reader_active.png', + 'resources/drawable-hdpi/reader_cropped.png', + 'resources/drawable-hdpi/reading_list.png', + 'resources/drawable-hdpi/shield.png', + 'resources/drawable-hdpi/shield_doorhanger.png', + 'resources/drawable-hdpi/spinner_default.9.png', + 'resources/drawable-hdpi/spinner_focused.9.png', + 'resources/drawable-hdpi/spinner_pressed.9.png', + 'resources/drawable-hdpi/tab_close.png', + 'resources/drawable-hdpi/tab_indicator_divider.9.png', + 'resources/drawable-hdpi/tab_indicator_selected.9.png', + 'resources/drawable-hdpi/tab_indicator_selected_focused.9.png', + 'resources/drawable-hdpi/tab_new.png', + 'resources/drawable-hdpi/tab_new_pb.png', + 'resources/drawable-hdpi/tab_thumbnail_default.png', + 'resources/drawable-hdpi/tab_thumbnail_shadow.png', + 'resources/drawable-hdpi/tabs_count.png', + 'resources/drawable-hdpi/tabs_count_foreground.png', + 'resources/drawable-hdpi/tabs_normal.png', + 'resources/drawable-hdpi/tabs_private.png', + 'resources/drawable-hdpi/tabs_synced.png', + 'resources/drawable-hdpi/tip_addsearch.png', + 'resources/drawable-hdpi/top_site_add.png', + 'resources/drawable-hdpi/url_bar_entry_default.9.png', + 'resources/drawable-hdpi/url_bar_entry_default_pb.9.png', + 'resources/drawable-hdpi/url_bar_entry_pressed.9.png', + 'resources/drawable-hdpi/url_bar_entry_pressed_pb.9.png', + 'resources/drawable-hdpi/urlbar_stop.png', + 'resources/drawable-hdpi/validation_arrow.png', + 'resources/drawable-hdpi/validation_arrow_inverted.png', + 'resources/drawable-hdpi/validation_bg.9.png', + 'resources/drawable-hdpi/warning.png', + 'resources/drawable-hdpi/warning_doorhanger.png', + 'resources/drawable-large-hdpi-v11/arrow_popup_bg.9.png', + 'resources/drawable-large-hdpi-v11/ic_menu_forward.png', + 'resources/drawable-large-hdpi-v11/ic_menu_reload.png', + 'resources/drawable-large-hdpi-v11/menu.png', + 'resources/drawable-large-land-v11/home_history_tabs_indicator.xml', + 'resources/drawable-large-mdpi-v11/arrow_popup_bg.9.png', + 'resources/drawable-large-mdpi-v11/ic_menu_forward.png', + 'resources/drawable-large-mdpi-v11/ic_menu_reload.png', + 'resources/drawable-large-mdpi-v11/menu.png', + 'resources/drawable-large-xhdpi-v11/arrow_popup_bg.9.png', + 'resources/drawable-large-xhdpi-v11/ic_menu_forward.png', + 'resources/drawable-large-xhdpi-v11/ic_menu_reload.png', + 'resources/drawable-large-xhdpi-v11/menu.png', + 'resources/drawable-mdpi-v11/alert_addon.png', + 'resources/drawable-mdpi-v11/alert_app.png', + 'resources/drawable-mdpi-v11/alert_camera.png', + 'resources/drawable-mdpi-v11/alert_download.png', + 'resources/drawable-mdpi-v11/alert_mic.png', + 'resources/drawable-mdpi-v11/alert_mic_camera.png', + 'resources/drawable-mdpi-v11/firefox_settings_alert.png', + 'resources/drawable-mdpi-v11/ic_menu_addons.png', + 'resources/drawable-mdpi-v11/ic_menu_apps.png', + 'resources/drawable-mdpi-v11/ic_menu_back.png', + 'resources/drawable-mdpi-v11/ic_menu_bookmark_add.png', + 'resources/drawable-mdpi-v11/ic_menu_bookmark_remove.png', + 'resources/drawable-mdpi-v11/ic_menu_desktop_mode_off.png', + 'resources/drawable-mdpi-v11/ic_menu_desktop_mode_on.png', + 'resources/drawable-mdpi-v11/ic_menu_downloads.png', + 'resources/drawable-mdpi-v11/ic_menu_find_in_page.png', + 'resources/drawable-mdpi-v11/ic_menu_forward.png', + 'resources/drawable-mdpi-v11/ic_menu_new_private_tab.png', + 'resources/drawable-mdpi-v11/ic_menu_new_tab.png', + 'resources/drawable-mdpi-v11/ic_menu_quit.png', + 'resources/drawable-mdpi-v11/ic_menu_reload.png', + 'resources/drawable-mdpi-v11/ic_menu_save_as_pdf.png', + 'resources/drawable-mdpi-v11/ic_menu_settings.png', + 'resources/drawable-mdpi-v11/ic_menu_share.png', + 'resources/drawable-mdpi-v11/ic_menu_tools.png', + 'resources/drawable-mdpi-v11/ic_status_logo.png', + 'resources/drawable-mdpi/abouthome_thumbnail.png', + 'resources/drawable-mdpi/alert_addon.png', + 'resources/drawable-mdpi/alert_app.png', + 'resources/drawable-mdpi/alert_camera.png', + 'resources/drawable-mdpi/alert_download.png', + 'resources/drawable-mdpi/alert_mic.png', + 'resources/drawable-mdpi/alert_mic_camera.png', + 'resources/drawable-mdpi/arrow_popup_bg.9.png', + 'resources/drawable-mdpi/autocomplete_list_bg.9.png', + 'resources/drawable-mdpi/blank.png', + 'resources/drawable-mdpi/bookmark_folder_closed.png', + 'resources/drawable-mdpi/bookmark_folder_opened.png', + 'resources/drawable-mdpi/bookmarkdefaults_favicon_addons.png', + 'resources/drawable-mdpi/bookmarkdefaults_favicon_support.png', + 'resources/drawable-mdpi/close.png', + 'resources/drawable-mdpi/desktop_notification.png', + 'resources/drawable-mdpi/favicon.png', + 'resources/drawable-mdpi/find_close.png', + 'resources/drawable-mdpi/find_next.png', + 'resources/drawable-mdpi/find_prev.png', + 'resources/drawable-mdpi/folder.png', + 'resources/drawable-mdpi/grid_icon_bg_activated.9.png', + 'resources/drawable-mdpi/grid_icon_bg_focused.9.png', + 'resources/drawable-mdpi/handle_end.png', + 'resources/drawable-mdpi/handle_middle.png', + 'resources/drawable-mdpi/handle_start.png', + 'resources/drawable-mdpi/history_tabs_indicator_selected.9.png', + 'resources/drawable-mdpi/home_tab_menu_strip.9.png', + 'resources/drawable-mdpi/ic_menu_addons_filler.png', + 'resources/drawable-mdpi/ic_menu_bookmark_add.png', + 'resources/drawable-mdpi/ic_menu_bookmark_remove.png', + 'resources/drawable-mdpi/ic_menu_character_encoding.png', + 'resources/drawable-mdpi/ic_menu_forward.png', + 'resources/drawable-mdpi/ic_menu_guest.png', + 'resources/drawable-mdpi/ic_menu_new_private_tab.png', + 'resources/drawable-mdpi/ic_menu_new_tab.png', + 'resources/drawable-mdpi/ic_menu_reload.png', + 'resources/drawable-mdpi/ic_status_logo.png', + 'resources/drawable-mdpi/ic_url_bar_go.png', + 'resources/drawable-mdpi/ic_url_bar_reader.png', + 'resources/drawable-mdpi/ic_url_bar_search.png', + 'resources/drawable-mdpi/ic_url_bar_star.png', + 'resources/drawable-mdpi/ic_url_bar_tab.png', + 'resources/drawable-mdpi/icon_bookmarks_empty.png', + 'resources/drawable-mdpi/icon_last_tabs.png', + 'resources/drawable-mdpi/icon_last_tabs_empty.png', + 'resources/drawable-mdpi/icon_most_recent.png', + 'resources/drawable-mdpi/icon_most_recent_empty.png', + 'resources/drawable-mdpi/icon_most_visited.png', + 'resources/drawable-mdpi/icon_openinapp.png', + 'resources/drawable-mdpi/icon_pageaction.png', + 'resources/drawable-mdpi/icon_reading_list_empty.png', + 'resources/drawable-mdpi/larry.png', + 'resources/drawable-mdpi/lock_identified.png', + 'resources/drawable-mdpi/lock_verified.png', + 'resources/drawable-mdpi/marketplace.png', + 'resources/drawable-mdpi/menu.png', + 'resources/drawable-mdpi/menu_item_check.png', + 'resources/drawable-mdpi/menu_item_more.png', + 'resources/drawable-mdpi/menu_item_uncheck.png', + 'resources/drawable-mdpi/menu_panel_bg.9.png', + 'resources/drawable-mdpi/menu_pb.png', + 'resources/drawable-mdpi/menu_popup_arrow_bottom.png', + 'resources/drawable-mdpi/menu_popup_arrow_top.png', + 'resources/drawable-mdpi/menu_popup_bg.9.png', + 'resources/drawable-mdpi/pause.png', + 'resources/drawable-mdpi/pin.png', + 'resources/drawable-mdpi/play.png', + 'resources/drawable-mdpi/progress_spinner.png', + 'resources/drawable-mdpi/reader.png', + 'resources/drawable-mdpi/reader_active.png', + 'resources/drawable-mdpi/reader_cropped.png', + 'resources/drawable-mdpi/reading_list.png', + 'resources/drawable-mdpi/scrollbar.png', + 'resources/drawable-mdpi/shadow.png', + 'resources/drawable-mdpi/shield.png', + 'resources/drawable-mdpi/shield_doorhanger.png', + 'resources/drawable-mdpi/spinner_default.9.png', + 'resources/drawable-mdpi/spinner_focused.9.png', + 'resources/drawable-mdpi/spinner_pressed.9.png', + 'resources/drawable-mdpi/start.png', + 'resources/drawable-mdpi/tab_close.png', + 'resources/drawable-mdpi/tab_indicator_divider.9.png', + 'resources/drawable-mdpi/tab_indicator_selected.9.png', + 'resources/drawable-mdpi/tab_indicator_selected_focused.9.png', + 'resources/drawable-mdpi/tab_new.png', + 'resources/drawable-mdpi/tab_new_pb.png', + 'resources/drawable-mdpi/tab_thumbnail_default.png', + 'resources/drawable-mdpi/tab_thumbnail_shadow.png', + 'resources/drawable-mdpi/tabs_count.png', + 'resources/drawable-mdpi/tabs_count_foreground.png', + 'resources/drawable-mdpi/tabs_normal.png', + 'resources/drawable-mdpi/tabs_private.png', + 'resources/drawable-mdpi/tabs_synced.png', + 'resources/drawable-mdpi/tip_addsearch.png', + 'resources/drawable-mdpi/toast.9.png', + 'resources/drawable-mdpi/toast_button_focused.9.png', + 'resources/drawable-mdpi/toast_button_pressed.9.png', + 'resources/drawable-mdpi/toast_divider.9.png', + 'resources/drawable-mdpi/top_site_add.png', + 'resources/drawable-mdpi/url_bar_entry_default.9.png', + 'resources/drawable-mdpi/url_bar_entry_default_pb.9.png', + 'resources/drawable-mdpi/url_bar_entry_pressed.9.png', + 'resources/drawable-mdpi/url_bar_entry_pressed_pb.9.png', + 'resources/drawable-mdpi/urlbar_stop.png', + 'resources/drawable-mdpi/validation_arrow.png', + 'resources/drawable-mdpi/validation_arrow_inverted.png', + 'resources/drawable-mdpi/validation_bg.9.png', + 'resources/drawable-mdpi/warning.png', + 'resources/drawable-mdpi/warning_doorhanger.png', + 'resources/drawable-xhdpi-v11/alert_addon.png', + 'resources/drawable-xhdpi-v11/alert_app.png', + 'resources/drawable-xhdpi-v11/alert_camera.png', + 'resources/drawable-xhdpi-v11/alert_download.png', + 'resources/drawable-xhdpi-v11/alert_mic.png', + 'resources/drawable-xhdpi-v11/alert_mic_camera.png', + 'resources/drawable-xhdpi-v11/firefox_settings_alert.png', + 'resources/drawable-xhdpi-v11/ic_menu_addons.png', + 'resources/drawable-xhdpi-v11/ic_menu_apps.png', + 'resources/drawable-xhdpi-v11/ic_menu_back.png', + 'resources/drawable-xhdpi-v11/ic_menu_bookmark_add.png', + 'resources/drawable-xhdpi-v11/ic_menu_bookmark_remove.png', + 'resources/drawable-xhdpi-v11/ic_menu_desktop_mode_off.png', + 'resources/drawable-xhdpi-v11/ic_menu_desktop_mode_on.png', + 'resources/drawable-xhdpi-v11/ic_menu_downloads.png', + 'resources/drawable-xhdpi-v11/ic_menu_find_in_page.png', + 'resources/drawable-xhdpi-v11/ic_menu_forward.png', + 'resources/drawable-xhdpi-v11/ic_menu_new_private_tab.png', + 'resources/drawable-xhdpi-v11/ic_menu_new_tab.png', + 'resources/drawable-xhdpi-v11/ic_menu_quit.png', + 'resources/drawable-xhdpi-v11/ic_menu_reload.png', + 'resources/drawable-xhdpi-v11/ic_menu_save_as_pdf.png', + 'resources/drawable-xhdpi-v11/ic_menu_settings.png', + 'resources/drawable-xhdpi-v11/ic_menu_share.png', + 'resources/drawable-xhdpi-v11/ic_menu_tools.png', + 'resources/drawable-xhdpi-v11/ic_status_logo.png', + 'resources/drawable-xhdpi/abouthome_thumbnail.png', + 'resources/drawable-xhdpi/alert_addon.png', + 'resources/drawable-xhdpi/alert_app.png', + 'resources/drawable-xhdpi/alert_camera.png', + 'resources/drawable-xhdpi/alert_download.png', + 'resources/drawable-xhdpi/alert_mic.png', + 'resources/drawable-xhdpi/alert_mic_camera.png', + 'resources/drawable-xhdpi/arrow_popup_bg.9.png', + 'resources/drawable-xhdpi/blank.png', + 'resources/drawable-xhdpi/bookmark_folder_closed.png', + 'resources/drawable-xhdpi/bookmark_folder_opened.png', + 'resources/drawable-xhdpi/close.png', + 'resources/drawable-xhdpi/favicon.png', + 'resources/drawable-xhdpi/find_close.png', + 'resources/drawable-xhdpi/find_next.png', + 'resources/drawable-xhdpi/find_prev.png', + 'resources/drawable-xhdpi/folder.png', + 'resources/drawable-xhdpi/grid_icon_bg_activated.9.png', + 'resources/drawable-xhdpi/grid_icon_bg_focused.9.png', + 'resources/drawable-xhdpi/handle_end.png', + 'resources/drawable-xhdpi/handle_middle.png', + 'resources/drawable-xhdpi/handle_start.png', + 'resources/drawable-xhdpi/history_tabs_indicator_selected.9.png', + 'resources/drawable-xhdpi/home_tab_menu_strip.9.png', + 'resources/drawable-xhdpi/ic_menu_addons_filler.png', + 'resources/drawable-xhdpi/ic_menu_bookmark_add.png', + 'resources/drawable-xhdpi/ic_menu_bookmark_remove.png', + 'resources/drawable-xhdpi/ic_menu_character_encoding.png', + 'resources/drawable-xhdpi/ic_menu_forward.png', + 'resources/drawable-xhdpi/ic_menu_guest.png', + 'resources/drawable-xhdpi/ic_menu_new_private_tab.png', + 'resources/drawable-xhdpi/ic_menu_new_tab.png', + 'resources/drawable-xhdpi/ic_menu_reload.png', + 'resources/drawable-xhdpi/ic_status_logo.png', + 'resources/drawable-xhdpi/ic_url_bar_go.png', + 'resources/drawable-xhdpi/ic_url_bar_reader.png', + 'resources/drawable-xhdpi/ic_url_bar_search.png', + 'resources/drawable-xhdpi/ic_url_bar_star.png', + 'resources/drawable-xhdpi/ic_url_bar_tab.png', + 'resources/drawable-xhdpi/icon_bookmarks_empty.png', + 'resources/drawable-xhdpi/icon_last_tabs.png', + 'resources/drawable-xhdpi/icon_last_tabs_empty.png', + 'resources/drawable-xhdpi/icon_most_recent.png', + 'resources/drawable-xhdpi/icon_most_recent_empty.png', + 'resources/drawable-xhdpi/icon_most_visited.png', + 'resources/drawable-xhdpi/icon_openinapp.png', + 'resources/drawable-xhdpi/icon_pageaction.png', + 'resources/drawable-xhdpi/icon_reading_list_empty.png', + 'resources/drawable-xhdpi/larry.png', + 'resources/drawable-xhdpi/lock_identified.png', + 'resources/drawable-xhdpi/lock_verified.png', + 'resources/drawable-xhdpi/menu.png', + 'resources/drawable-xhdpi/menu_item_check.png', + 'resources/drawable-xhdpi/menu_item_more.png', + 'resources/drawable-xhdpi/menu_item_uncheck.png', + 'resources/drawable-xhdpi/menu_panel_bg.9.png', + 'resources/drawable-xhdpi/menu_pb.png', + 'resources/drawable-xhdpi/menu_popup_arrow_bottom.png', + 'resources/drawable-xhdpi/menu_popup_arrow_top.png', + 'resources/drawable-xhdpi/menu_popup_bg.9.png', + 'resources/drawable-xhdpi/pause.png', + 'resources/drawable-xhdpi/pin.png', + 'resources/drawable-xhdpi/play.png', + 'resources/drawable-xhdpi/reader.png', + 'resources/drawable-xhdpi/reader_active.png', + 'resources/drawable-xhdpi/reader_cropped.png', + 'resources/drawable-xhdpi/reading_list.png', + 'resources/drawable-xhdpi/shield.png', + 'resources/drawable-xhdpi/shield_doorhanger.png', + 'resources/drawable-xhdpi/spinner_default.9.png', + 'resources/drawable-xhdpi/spinner_focused.9.png', + 'resources/drawable-xhdpi/spinner_pressed.9.png', + 'resources/drawable-xhdpi/tab_close.png', + 'resources/drawable-xhdpi/tab_indicator_divider.9.png', + 'resources/drawable-xhdpi/tab_indicator_selected.9.png', + 'resources/drawable-xhdpi/tab_indicator_selected_focused.9.png', + 'resources/drawable-xhdpi/tab_new.png', + 'resources/drawable-xhdpi/tab_new_pb.png', + 'resources/drawable-xhdpi/tab_thumbnail_default.png', + 'resources/drawable-xhdpi/tab_thumbnail_shadow.png', + 'resources/drawable-xhdpi/tabs_count.png', + 'resources/drawable-xhdpi/tabs_count_foreground.png', + 'resources/drawable-xhdpi/tabs_normal.png', + 'resources/drawable-xhdpi/tabs_private.png', + 'resources/drawable-xhdpi/tabs_synced.png', + 'resources/drawable-xhdpi/tip_addsearch.png', + 'resources/drawable-xhdpi/top_site_add.png', + 'resources/drawable-xhdpi/url_bar_entry_default.9.png', + 'resources/drawable-xhdpi/url_bar_entry_default_pb.9.png', + 'resources/drawable-xhdpi/url_bar_entry_pressed.9.png', + 'resources/drawable-xhdpi/url_bar_entry_pressed_pb.9.png', + 'resources/drawable-xhdpi/urlbar_stop.png', + 'resources/drawable-xhdpi/validation_arrow.png', + 'resources/drawable-xhdpi/validation_arrow_inverted.png', + 'resources/drawable-xhdpi/validation_bg.9.png', + 'resources/drawable-xhdpi/warning.png', + 'resources/drawable-xhdpi/warning_doorhanger.png', + 'resources/drawable-xlarge-hdpi-v11/ic_menu_bookmark_add.png', + 'resources/drawable-xlarge-hdpi-v11/ic_menu_bookmark_remove.png', + 'resources/drawable-xlarge-mdpi-v11/ic_menu_bookmark_add.png', + 'resources/drawable-xlarge-mdpi-v11/ic_menu_bookmark_remove.png', + 'resources/drawable-xlarge-v11/home_history_tabs_indicator.xml', + 'resources/drawable-xlarge-xhdpi-v11/ic_menu_bookmark_add.png', + 'resources/drawable-xlarge-xhdpi-v11/ic_menu_bookmark_remove.png', + 'resources/drawable/action_bar_button.xml', + 'resources/drawable/action_bar_button_inverse.xml', + 'resources/drawable/bookmark_folder.xml', + 'resources/drawable/divider_horizontal.xml', + 'resources/drawable/divider_vertical.xml', + 'resources/drawable/favicon_bg.xml', + 'resources/drawable/handle_end_level.xml', + 'resources/drawable/handle_start_level.xml', + 'resources/drawable/home_banner.xml', + 'resources/drawable/home_history_tabs_indicator.xml', + 'resources/drawable/home_page_title_background.xml', + 'resources/drawable/ic_menu_back.xml', + 'resources/drawable/ic_menu_desktop_mode_off.xml', + 'resources/drawable/ic_menu_desktop_mode_on.xml', + 'resources/drawable/ic_menu_quit.xml', + 'resources/drawable/icon_grid_item_bg.xml', + 'resources/drawable/menu_item_state.xml', + 'resources/drawable/menu_level.xml', + 'resources/drawable/remote_tabs_child_divider.xml', + 'resources/drawable/shaped_button.xml', + 'resources/drawable/site_security_level.xml', + 'resources/drawable/spinner.xml', + 'resources/drawable/suggestion_selector.xml', + 'resources/drawable/tab_new_level.xml', + 'resources/drawable/tab_row.xml', + 'resources/drawable/tab_thumbnail.xml', + 'resources/drawable/tabs_panel_indicator.xml', + 'resources/drawable/textbox_bg.xml', + 'resources/drawable/toast_button.xml', + 'resources/drawable/top_sites_thumbnail_bg.xml', + 'resources/drawable/url_bar_bg.xml', + 'resources/drawable/url_bar_entry.xml', + 'resources/drawable/url_bar_nav_button.xml', + 'resources/drawable/url_bar_right_edge.xml', + 'resources/drawable/webapp_titlebar_bg.xml', + 'resources/layout-large-land-v11/home_history_list.xml', + 'resources/layout-large-land-v11/home_history_page.xml', + 'resources/layout-large-land-v11/home_history_tabs_indicator.xml', + 'resources/layout-large-land-v11/tabs_panel.xml', + 'resources/layout-large-land-v11/tabs_panel_footer.xml', + 'resources/layout-large-land-v11/tabs_panel_header.xml', + 'resources/layout-large-v11/browser_toolbar.xml', + 'resources/layout-large-v11/home_pager.xml', + 'resources/layout-xlarge-v11/font_size_preference.xml', + 'resources/layout-xlarge-v11/home_history_list.xml', + 'resources/layout-xlarge-v11/home_history_page.xml', + 'resources/layout-xlarge-v11/home_history_tabs_indicator.xml', + 'resources/layout-xlarge-v11/remote_tabs_child.xml', + 'resources/layout-xlarge-v11/remote_tabs_group.xml', + 'resources/layout/arrow_popup.xml', + 'resources/layout/autocomplete_list.xml', + 'resources/layout/autocomplete_list_item.xml', + 'resources/layout/bookmark_edit.xml', + 'resources/layout/bookmark_folder_row.xml', + 'resources/layout/bookmark_item_row.xml', + 'resources/layout/browser_search.xml', + 'resources/layout/browser_toolbar.xml', + 'resources/layout/datetime_picker.xml', + 'resources/layout/doorhanger.xml', + 'resources/layout/doorhanger_button.xml', + 'resources/layout/find_in_page_content.xml', + 'resources/layout/font_size_preference.xml', + 'resources/layout/gecko_app.xml', + 'resources/layout/home_banner.xml', + 'resources/layout/home_bookmarks_page.xml', + 'resources/layout/home_empty_page.xml', + 'resources/layout/home_empty_reading_page.xml', + 'resources/layout/home_header_row.xml', + 'resources/layout/home_history_list.xml', + 'resources/layout/home_history_page.xml', + 'resources/layout/home_history_tabs_indicator.xml', + 'resources/layout/home_item_row.xml', + 'resources/layout/home_last_tabs_page.xml', + 'resources/layout/home_most_recent_page.xml', + 'resources/layout/home_pager.xml', + 'resources/layout/home_reading_list_page.xml', + 'resources/layout/home_search_item_row.xml', + 'resources/layout/home_suggestion_prompt.xml', + 'resources/layout/home_top_sites_page.xml', + 'resources/layout/icon_grid.xml', + 'resources/layout/icon_grid_item.xml', + 'resources/layout/launch_app_list.xml', + 'resources/layout/launch_app_listitem.xml', + 'resources/layout/list_item_header.xml', + 'resources/layout/menu_action_bar.xml', + 'resources/layout/menu_item_action_view.xml', + 'resources/layout/menu_popup.xml', + 'resources/layout/notification_icon_text.xml', + 'resources/layout/notification_progress.xml', + 'resources/layout/notification_progress_text.xml', + 'resources/layout/pin_site_dialog.xml', + 'resources/layout/preference_rightalign_icon.xml', + 'resources/layout/preference_search_engine.xml', + 'resources/layout/preference_search_tip.xml', + 'resources/layout/remote_tabs_child.xml', + 'resources/layout/remote_tabs_group.xml', + 'resources/layout/search_engine_row.xml', + 'resources/layout/select_dialog_list.xml', + 'resources/layout/select_dialog_multichoice.xml', + 'resources/layout/select_dialog_singlechoice.xml', + 'resources/layout/shared_ui_components.xml', + 'resources/layout/simple_dropdown_item_1line.xml', + 'resources/layout/site_identity.xml', + 'resources/layout/site_setting_item.xml', + 'resources/layout/site_setting_title.xml', + 'resources/layout/suggestion_item.xml', + 'resources/layout/tab_menu_strip.xml', + 'resources/layout/tabs_counter.xml', + 'resources/layout/tabs_item_cell.xml', + 'resources/layout/tabs_item_row.xml', + 'resources/layout/tabs_panel.xml', + 'resources/layout/tabs_panel_header.xml', + 'resources/layout/tabs_panel_indicator.xml', + 'resources/layout/text_selection_handles.xml', + 'resources/layout/top_sites_grid_item_view.xml', + 'resources/layout/two_line_page_row.xml', + 'resources/layout/validation_message.xml', + 'resources/layout/videoplayer.xml', + 'resources/layout/web_app.xml', + 'resources/menu-large-v11/browser_app_menu.xml', + 'resources/menu-v11/browser_app_menu.xml', + 'resources/menu-xlarge-v11/browser_app_menu.xml', + 'resources/menu/browser_app_menu.xml', + 'resources/menu/gecko_app_menu.xml', + 'resources/menu/home_contextmenu.xml', + 'resources/menu/titlebar_contextmenu.xml', + 'resources/menu/top_sites_contextmenu.xml', + 'resources/values-land/integers.xml', + 'resources/values-land/layout.xml', + 'resources/values-land/styles.xml', + 'resources/values-large-land-v11/dimens.xml', + 'resources/values-large-land-v11/styles.xml', + 'resources/values-large-v11/dimens.xml', + 'resources/values-large-v11/layout.xml', + 'resources/values-large-v11/styles.xml', + 'resources/values-large-v11/themes.xml', + 'resources/values-v11/colors.xml', + 'resources/values-v11/dimens.xml', + 'resources/values-v11/styles.xml', + 'resources/values-v11/themes.xml', + 'resources/values-v14/styles.xml', + 'resources/values-v16/styles.xml', + 'resources/values-xlarge-land-v11/dimens.xml', + 'resources/values-xlarge-land-v11/styles.xml', + 'resources/values-xlarge-v11/dimens.xml', + 'resources/values-xlarge-v11/integers.xml', + 'resources/values-xlarge-v11/styles.xml', + 'resources/values/arrays.xml', + 'resources/values/attrs.xml', + 'resources/values/colors.xml', + 'resources/values/dimens.xml', + 'resources/values/integers.xml', + 'resources/values/layout.xml', + 'resources/values/styles.xml', + 'resources/values/themes.xml', + 'resources/xml-v11/preference_headers.xml', + 'resources/xml-v11/preferences.xml', + 'resources/xml-v11/preferences_customize.xml', + 'resources/xml-v11/preferences_customize_tablet.xml', + 'resources/xml/preferences.xml', + 'resources/xml/preferences_customize.xml', + 'resources/xml/preferences_devtools.xml', + 'resources/xml/preferences_display.xml', + 'resources/xml/preferences_privacy.xml', + 'resources/xml/preferences_search.xml', + 'resources/xml/preferences_vendor.xml', + 'resources/xml/searchable.xml', +] + +if CONFIG['MOZ_CRASHREPORTER']: + ANDROID_RESFILES += [ + 'resources/drawable-mdpi/crash_reporter.png', + 'resources/layout/crash_reporter.xml', + ] diff --git a/mobile/android/tests/background/junit3/Makefile.in b/mobile/android/tests/background/junit3/Makefile.in index 93509e11d41f..2c8bbe3f7bbf 100644 --- a/mobile/android/tests/background/junit3/Makefile.in +++ b/mobile/android/tests/background/junit3/Makefile.in @@ -21,7 +21,6 @@ include $(srcdir)/android-services-files.mk # BACKGROUND_TESTS_{JAVA,RES}_FILES are defined in android-services-files.mk. JAVAFILES := $(BACKGROUND_TESTS_JAVA_FILES) -ANDROID_RESFILES := $(BACKGROUND_TESTS_RES_FILES) # The test APK needs to know the contents of the target APK while not # being linked against them. This is a best effort to avoid getting diff --git a/mobile/android/tests/background/junit3/android-services-files.mk b/mobile/android/tests/background/junit3/android-services-files.mk index 0aa58422a7ca..8fe805608836 100644 --- a/mobile/android/tests/background/junit3/android-services-files.mk +++ b/mobile/android/tests/background/junit3/android-services-files.mk @@ -100,11 +100,3 @@ BACKGROUND_TESTS_JAVA_FILES := \ src/testhelpers/WBORepository.java \ $(NULL) -BACKGROUND_TESTS_RES_FILES := \ - res/drawable-hdpi/icon.png \ - res/drawable-ldpi/icon.png \ - res/drawable-mdpi/icon.png \ - res/layout/main.xml \ - res/values/strings.xml \ - $(NULL) - diff --git a/mobile/android/tests/background/junit3/android-services.mozbuild b/mobile/android/tests/background/junit3/android-services.mozbuild new file mode 100644 index 000000000000..2f723537c970 --- /dev/null +++ b/mobile/android/tests/background/junit3/android-services.mozbuild @@ -0,0 +1,7 @@ +ANDROID_RESFILES += [ + 'res/drawable-hdpi/icon.png', + 'res/drawable-ldpi/icon.png', + 'res/drawable-mdpi/icon.png', + 'res/layout/main.xml', + 'res/values/strings.xml', +] diff --git a/mobile/android/tests/background/junit3/moz.build b/mobile/android/tests/background/junit3/moz.build index c271ec3908ce..0538e5e09e79 100644 --- a/mobile/android/tests/background/junit3/moz.build +++ b/mobile/android/tests/background/junit3/moz.build @@ -3,3 +3,5 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. + +include('android-services.mozbuild') From 2f1f13d1520ab733637e7b524b823a7d01367ffd Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Wed, 9 Oct 2013 16:19:00 -0700 Subject: [PATCH 05/26] Bug 900522 - Part 3: Add passthru ANDROID_GENERATED_RESFILES. r=gps This defines all of the Android resources in moz.build files (although some are still generated by mobile/android/base/Makefile.in). --- config/rules.mk | 1 + js/src/config/rules.mk | 1 + mobile/android/base/Makefile.in | 6 +----- mobile/android/base/moz.build | 8 ++++++++ python/mozbuild/mozbuild/frontend/emitter.py | 1 + python/mozbuild/mozbuild/frontend/sandbox_symbols.py | 9 +++++++++ 6 files changed, 21 insertions(+), 5 deletions(-) diff --git a/config/rules.mk b/config/rules.mk index 268b0c769afd..d6bee7f5f250 100644 --- a/config/rules.mk +++ b/config/rules.mk @@ -21,6 +21,7 @@ INCLUDED_RULES_MK = 1 # present. If they are, this is a violation of the separation of # responsibility between Makefile.in and mozbuild files. _MOZBUILD_EXTERNAL_VARIABLES := \ + ANDROID_GENERATED_RESFILES \ ANDROID_RESFILES \ CMMSRCS \ CPP_UNIT_TESTS \ diff --git a/js/src/config/rules.mk b/js/src/config/rules.mk index 268b0c769afd..d6bee7f5f250 100644 --- a/js/src/config/rules.mk +++ b/js/src/config/rules.mk @@ -21,6 +21,7 @@ INCLUDED_RULES_MK = 1 # present. If they are, this is a violation of the separation of # responsibility between Makefile.in and mozbuild files. _MOZBUILD_EXTERNAL_VARIABLES := \ + ANDROID_GENERATED_RESFILES \ ANDROID_RESFILES \ CMMSRCS \ CPP_UNIT_TESTS \ diff --git a/mobile/android/base/Makefile.in b/mobile/android/base/Makefile.in index e8aede9807e0..5ea9d1aeb030 100644 --- a/mobile/android/base/Makefile.in +++ b/mobile/android/base/Makefile.in @@ -605,14 +605,10 @@ res/values/strings.xml: $(call mkdir_deps,res/values) # rebuild gecko.ap_ if any of them change. MULTILOCALE_STRINGS_XML_FILES := $(wildcard res/values-*/strings.xml) all_resources = \ - res/drawable-mdpi/icon.png \ - res/drawable-hdpi/icon.png \ - res/drawable-xhdpi/icon.png \ - res/drawable-xxhdpi/icon.png \ - res/values/strings.xml \ $(MULTILOCALE_STRINGS_XML_FILES) \ AndroidManifest.xml \ $(subst resources/,res/,$(ANDROID_RESFILES)) \ + $(ANDROID_GENERATED_RESFILES) \ $(NULL) R.java: $(all_resources) diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build index 5f5148560e2b..9fceddbfaef5 100644 --- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -8,6 +8,14 @@ DIRS += ['locales'] include('android-services.mozbuild') +ANDROID_GENERATED_RESFILES += [ + 'res/drawable-hdpi/icon.png', + 'res/drawable-mdpi/icon.png', + 'res/drawable-xhdpi/icon.png', + 'res/drawable-xxhdpi/icon.png', + 'res/values/strings.xml', +] + ANDROID_RESFILES += [ 'resources/anim/grow_fade_in.xml', 'resources/anim/grow_fade_in_center.xml', diff --git a/python/mozbuild/mozbuild/frontend/emitter.py b/python/mozbuild/mozbuild/frontend/emitter.py index eebb87f041f1..ed622596b1a6 100644 --- a/python/mozbuild/mozbuild/frontend/emitter.py +++ b/python/mozbuild/mozbuild/frontend/emitter.py @@ -139,6 +139,7 @@ class TreeMetadataEmitter(LoggingMixin): passthru = VariablePassthru(sandbox) varmap = dict( # Makefile.in : moz.build + ANDROID_GENERATED_RESFILES='ANDROID_GENERATED_RESFILES', ANDROID_RESFILES='ANDROID_RESFILES', ASFILES='ASFILES', CMMSRCS='CMMSRCS', diff --git a/python/mozbuild/mozbuild/frontend/sandbox_symbols.py b/python/mozbuild/mozbuild/frontend/sandbox_symbols.py index 3b4fc2090786..14059198091a 100644 --- a/python/mozbuild/mozbuild/frontend/sandbox_symbols.py +++ b/python/mozbuild/mozbuild/frontend/sandbox_symbols.py @@ -43,6 +43,15 @@ from mozbuild.util import ( VARIABLES = { # Variables controlling reading of other frontend files. + 'ANDROID_GENERATED_RESFILES': (StrictOrderingOnAppendList, list, [], + """Android resource files generated as part of the build. + + This variable contains a list of files that are expected to be + generated (often by preprocessing) into a 'res' directory as + part of the build process, and subsequently merged into an APK + file. + """, 'export'), + 'ANDROID_RESFILES': (StrictOrderingOnAppendList, list, [], """Android resource files. From aa3b0a9d6b70d7417f47beca2bc69df290ca36d5 Mon Sep 17 00:00:00 2001 From: Chenxia Liu Date: Thu, 11 Apr 2013 09:53:15 +0300 Subject: [PATCH 06/26] Bug 914773 - Robocop: upgrade to robotium-solo-4.3.jar. r=gbrown --- build/mobile/robocop/Makefile.in | 2 +- build/mobile/robocop/README | 6 +++--- build/mobile/robocop/robotium-solo-4.2.jar | Bin 104697 -> 0 bytes build/mobile/robocop/robotium-solo-4.3.jar | Bin 0 -> 106466 bytes 4 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 build/mobile/robocop/robotium-solo-4.2.jar create mode 100644 build/mobile/robocop/robotium-solo-4.3.jar diff --git a/build/mobile/robocop/Makefile.in b/build/mobile/robocop/Makefile.in index f1b8ce0b87a1..3d8986fde568 100644 --- a/build/mobile/robocop/Makefile.in +++ b/build/mobile/robocop/Makefile.in @@ -9,7 +9,7 @@ dir-tests := $(DEPTH)/$(mobile-tests) ANDROID_APK_NAME := robocop-debug ANDROID_EXTRA_JARS += \ - $(srcdir)/robotium-solo-4.2.jar \ + $(srcdir)/robotium-solo-4.3.jar \ $(NULL) ANDROID_ASSETS_DIR := $(TESTPATH)/assets diff --git a/build/mobile/robocop/README b/build/mobile/robocop/README index d01691d9ca9e..171bb54963b8 100644 --- a/build/mobile/robocop/README +++ b/build/mobile/robocop/README @@ -1,11 +1,11 @@ Robocop is a Mozilla project which uses Robotium to test Firefox on Android devices. -Robotium is an open source tool licensed under the Apache 2.0 license and the original +Robotium is an open source tool licensed under the Apache 2.0 license and the original source can be found here: http://code.google.com/p/robotium/ -We are including robotium-solo-4.2.jar as a binary and are not modifying it in any way -from the original download found at: +We are including robotium-solo-4.3.jar as a binary and are not modifying it in any way +from the original download found at: http://code.google.com/p/robotium/ Firefox for Android developers should read the documentation in diff --git a/build/mobile/robocop/robotium-solo-4.2.jar b/build/mobile/robocop/robotium-solo-4.2.jar deleted file mode 100644 index 349f7516a0bf0d63a845577032429047073aeb72..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 104697 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

s^gkBc*=uP)AT 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 diff --git a/build/mobile/robocop/robotium-solo-4.3.jar b/build/mobile/robocop/robotium-solo-4.3.jar new file mode 100644 index 0000000000000000000000000000000000000000..2bee9cb5f95ea5c4309640cf19fdced9fb2fa706 GIT binary patch literal 106466 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 Date: Fri, 11 Oct 2013 14:30:31 -0700 Subject: [PATCH 07/26] Bug 925892: add "channel=searchbar" parameter to Google plugin to distinguish search bar searches, r=MattN --HG-- extra : transplant_source : %CD%D4%0BqxCt%82%BA%80%CE%E6%C3%E1%F3V%96xo%81 --- browser/components/search/content/search.xml | 6 +++--- browser/components/search/test/browser_google.js | 7 +++++++ browser/locales/en-US/searchplugins/google.xml | 1 + 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/browser/components/search/content/search.xml b/browser/components/search/content/search.xml index 2a7f3337ba9d..ce3cdc15f171 100644 --- a/browser/components/search/content/search.xml +++ b/browser/components/search/content/search.xml @@ -482,7 +482,7 @@ } // null parameter below specifies HTML response for search - var submission = this.currentEngine.getSubmission(aData); + var submission = this.currentEngine.getSubmission(aData, null, "searchbar"); BrowserSearch.recordSearchInHealthReport(this.currentEngine.name, "searchbar"); openUILinkIn(submission.uri.spec, aWhere, null, submission.postData); ]]> @@ -531,14 +531,14 @@ var engine = this.currentEngine; var connector = Services.io.QueryInterface(Components.interfaces.nsISpeculativeConnect); - var searchURI = engine.getSubmission("dummy").uri; + var searchURI = engine.getSubmission("dummy", null, "searchbar").uri; let callbacks = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor) .getInterface(Components.interfaces.nsIWebNavigation) .QueryInterface(Components.interfaces.nsILoadContext); connector.speculativeConnect(searchURI, callbacks); if (engine.supportsResponseType(SUGGEST_TYPE)) { - var suggestURI = engine.getSubmission("dummy", SUGGEST_TYPE).uri; + var suggestURI = engine.getSubmission("dummy", SUGGEST_TYPE, "searchbar").uri; if (suggestURI.prePath != searchURI.prePath) connector.speculativeConnect(suggestURI, callbacks); } diff --git a/browser/components/search/test/browser_google.js b/browser/components/search/test/browser_google.js index 94b4b6e09040..98e9f904cdbd 100644 --- a/browser/components/search/test/browser_google.js +++ b/browser/components/search/test/browser_google.js @@ -80,6 +80,8 @@ function test() { is(url, base + "&channel=rcs", "Check context menu search URL for 'foo'"); url = engine.getSubmission("foo", null, "keyword").uri.spec; is(url, base + "&channel=fflb", "Check keyword search URL for 'foo'"); + url = engine.getSubmission("foo", null, "searchbar").uri.spec; + is(url, base + "&channel=sb", "Check search bar search URL for 'foo'"); // Check search suggestion URL. url = engine.getSubmission("foo", "application/x-suggestions+json").uri.spec; @@ -147,6 +149,11 @@ function test() { "value": "fflb", "purpose": "keyword", }, + { + "name": "channel", + "value": "sb", + "purpose": "searchbar", + }, { "name": "channel", "value": "np", diff --git a/browser/locales/en-US/searchplugins/google.xml b/browser/locales/en-US/searchplugins/google.xml index 29a9c5f088b5..68c9a68d420f 100644 --- a/browser/locales/en-US/searchplugins/google.xml +++ b/browser/locales/en-US/searchplugins/google.xml @@ -25,6 +25,7 @@ #endif + From b39261f4acec33e7daeb1a70b4fd132add815f08 Mon Sep 17 00:00:00 2001 From: Gavin Sharp Date: Fri, 11 Oct 2013 15:00:11 -0700 Subject: [PATCH 08/26] Bug 925898: properly pass the "homepage" reason to getSubmission calls for about:home, r=MattN --HG-- extra : transplant_source : %10i%ADC%D2W%C0%C0%99LM%9AR%D3%09%B29%10%F1%25 --- browser/components/search/test/Makefile.in | 1 + .../components/search/test/browser_google.js | 2 + .../search/test/browser_google_behavior.js | 187 ++++++++++++++++++ browser/modules/AboutHome.jsm | 2 +- 4 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 browser/components/search/test/browser_google_behavior.js diff --git a/browser/components/search/test/Makefile.in b/browser/components/search/test/Makefile.in index 5a926f5ba09b..1981cf4c6483 100644 --- a/browser/components/search/test/Makefile.in +++ b/browser/components/search/test/Makefile.in @@ -5,6 +5,7 @@ ifdef ENABLE_TESTS pp_mochitest_browser_files := \ browser_google.js \ + browser_google_behavior.js \ $(NULL) pp_mochitest_browser_files_PATH := $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir) pp_mochitest_browser_files_FLAGS := -DMOZ_DISTRIBUTION_ID=$(MOZ_DISTRIBUTION_ID) diff --git a/browser/components/search/test/browser_google.js b/browser/components/search/test/browser_google.js index 98e9f904cdbd..ebfe94bf8f95 100644 --- a/browser/components/search/test/browser_google.js +++ b/browser/components/search/test/browser_google.js @@ -82,6 +82,8 @@ function test() { is(url, base + "&channel=fflb", "Check keyword search URL for 'foo'"); url = engine.getSubmission("foo", null, "searchbar").uri.spec; is(url, base + "&channel=sb", "Check search bar search URL for 'foo'"); + url = engine.getSubmission("foo", null, "homepage").uri.spec; + is(url, base + "&channel=np&source=hp", "Check homepage search URL for 'foo'"); // Check search suggestion URL. url = engine.getSubmission("foo", "application/x-suggestions+json").uri.spec; diff --git a/browser/components/search/test/browser_google_behavior.js b/browser/components/search/test/browser_google_behavior.js new file mode 100644 index 000000000000..fab22637b765 --- /dev/null +++ b/browser/components/search/test/browser_google_behavior.js @@ -0,0 +1,187 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Test Google search plugin URLs + */ + +"use strict"; + +const BROWSER_SEARCH_PREF = "browser.search."; + +const MOZ_PARAM_LOCALE = /\{moz:locale\}/g; +const MOZ_PARAM_DIST_ID = /\{moz:distributionID\}/g; +const MOZ_PARAM_OFFICIAL = /\{moz:official\}/g; + +// Custom search parameters +#ifdef MOZ_OFFICIAL_BRANDING +const MOZ_OFFICIAL = "official"; +#else +const MOZ_OFFICIAL = "unofficial"; +#endif + +#if MOZ_UPDATE_CHANNEL == beta +const GOOGLE_CLIENT = "firefox-beta"; +#elif MOZ_UPDATE_CHANNEL == aurora +const GOOGLE_CLIENT = "firefox-aurora"; +#elif MOZ_UPDATE_CHANNEL == nightly +const GOOGLE_CLIENT = "firefox-nightly"; +#else +const GOOGLE_CLIENT = "firefox-a"; +#endif + +#expand const MOZ_DISTRIBUTION_ID = __MOZ_DISTRIBUTION_ID__; + +function getLocale() { + const localePref = "general.useragent.locale"; + return getLocalizedPref(localePref, Services.prefs.getCharPref(localePref)); +} + +function getLocalizedPref(aPrefName, aDefault) { + try { + return Services.prefs.getComplexValue(aPrefName, Ci.nsIPrefLocalizedString).data; + } catch (ex) { + return aDefault; + } + + return aDefault; +} + +function test() { + let engine = Services.search.getEngineByName("Google"); + ok(engine, "Google is installed"); + + is(Services.search.defaultEngine, engine, "Check that Google is the default search engine"); + + let distributionID; + try { + distributionID = Services.prefs.getCharPref(BROWSER_SEARCH_PREF + "distributionID"); + } catch (ex) { + distributionID = MOZ_DISTRIBUTION_ID; + } + + let base = "https://www.google.com/search?q=foo&ie=utf-8&oe=utf-8&aq=t&rls={moz:distributionID}:{moz:locale}:{moz:official}&client=" + GOOGLE_CLIENT; + base = base.replace(MOZ_PARAM_LOCALE, getLocale()); + base = base.replace(MOZ_PARAM_DIST_ID, distributionID); + base = base.replace(MOZ_PARAM_OFFICIAL, MOZ_OFFICIAL); + + let url; + + // Test search URLs (including purposes). + url = engine.getSubmission("foo").uri.spec; + is(url, base, "Check search URL for 'foo'"); + + waitForExplicitFinish(); + + var gCurrTest; + var gTests = [ + { + name: "context menu search", + searchURL: base + "&channel=rcs", + run: function () { + // Simulate a contextmenu search + // FIXME: This is a bit "low-level"... + BrowserSearch.loadSearch("foo", false, "contextmenu"); + } + }, + { + name: "keyword search", + searchURL: base + "&channel=fflb", + run: function () { + gURLBar.value = "? foo"; + gURLBar.focus(); + EventUtils.synthesizeKey("VK_RETURN", {}); + } + }, + { + name: "search bar search", + searchURL: base + "&channel=sb", + run: function () { + let sb = BrowserSearch.searchBar; + sb.focus(); + sb.value = "foo"; + registerCleanupFunction(function () { + sb.value = ""; + }); + EventUtils.synthesizeKey("VK_RETURN", {}); + } + }, + { + name: "home page search", + searchURL: base + "&channel=np&source=hp", + run: function () { + // load about:home, but remove the listener first so it doesn't + // get in the way + gBrowser.removeProgressListener(listener); + gBrowser.loadURI("about:home"); + info("Waiting for about:home load"); + tab.linkedBrowser.addEventListener("load", function load(event) { + if (event.originalTarget != tab.linkedBrowser.contentDocument || + event.target.location.href == "about:blank") { + info("skipping spurious load event"); + return; + } + tab.linkedBrowser.removeEventListener("load", load, true); + + // Observe page setup + let doc = gBrowser.contentDocument; + let mutationObserver = new MutationObserver(function (mutations) { + for (let mutation of mutations) { + if (mutation.attributeName == "searchEngineName") { + // Re-add the listener, and perform a search + gBrowser.addProgressListener(listener); + doc.getElementById("searchText").value = "foo"; + doc.getElementById("searchSubmit").click(); + } + } + }); + mutationObserver.observe(doc.documentElement, { attributes: true }); + }, true); + } + } + ]; + + function nextTest() { + if (gTests.length) { + gCurrTest = gTests.shift(); + info("Running : " + gCurrTest.name); + executeSoon(gCurrTest.run); + } else { + finish(); + } + } + + let tab = gBrowser.selectedTab = gBrowser.addTab(); + + let listener = { + onStateChange: function onStateChange(webProgress, req, flags, status) { + info("onStateChange"); + // Only care about top-level document starts + let docStart = Ci.nsIWebProgressListener.STATE_IS_DOCUMENT | + Ci.nsIWebProgressListener.STATE_START; + if (!(flags & docStart) || !webProgress.isTopLevel) + return; + + info("received document start"); + + ok(req instanceof Ci.nsIChannel, "req is a channel"); + is(req.originalURI.spec, gCurrTest.searchURL, "search URL was loaded"); + info("Actual URI: " + req.URI.spec); + + req.cancel(Components.results.NS_ERROR_FAILURE); + + executeSoon(nextTest); + } + } + + registerCleanupFunction(function () { + gBrowser.removeProgressListener(listener); + gBrowser.removeTab(tab); + }); + + tab.linkedBrowser.addEventListener("load", function load() { + tab.linkedBrowser.removeEventListener("load", load, true); + gBrowser.addProgressListener(listener); + nextTest(); + }, true); +} diff --git a/browser/modules/AboutHome.jsm b/browser/modules/AboutHome.jsm index 96af6678df8f..98c443d11cdd 100644 --- a/browser/modules/AboutHome.jsm +++ b/browser/modules/AboutHome.jsm @@ -169,7 +169,7 @@ let AboutHome = { window.BrowserSearch.recordSearchInHealthReport(data.engineName, "abouthome"); #endif // Trigger a search through nsISearchEngine.getSubmission() - let submission = Services.search.currentEngine.getSubmission(data.searchTerms); + let submission = Services.search.currentEngine.getSubmission(data.searchTerms, null, "homepage"); window.loadURI(submission.uri.spec, null, submission.postData); break; } From f5d0e880796fca1b7a6a7e5a3826bca2f9278bea Mon Sep 17 00:00:00 2001 From: Gaia Pushbot Date: Wed, 16 Oct 2013 13:55:23 -0700 Subject: [PATCH 09/26] Bumping gaia.json for 1 gaia-central revision(s) a=gaia-bump ======== https://hg.mozilla.org/integration/gaia-central/rev/38632017a95b Author: Mike Pennisi Desc: Bug 907177 - [Clock] Add integration tests r=iliu --- b2g/config/gaia.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index 014c6a494942..4b4543e069b3 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -1,4 +1,4 @@ { - "revision": "86e06b1db110e34eb66826d3b1bdee3a5d57b3a7", + "revision": "38632017a95b76f8b74544e7f94237368241b8af", "repo_path": "/integration/gaia-central" } From 2a5d6a7ebf19ba228e13ef7c03f5eeb9615fd8c6 Mon Sep 17 00:00:00 2001 From: Ryan VanderMeulen Date: Wed, 16 Oct 2013 17:12:54 -0400 Subject: [PATCH 10/26] Backed out 4 changesets (bug 887699). Backed out changeset 5c878c48d732 (bug 887699) Backed out changeset 57b03d7055e8 (bug 887699) Backed out changeset ea06175feb4f (bug 887699) Backed out changeset 516d0f14f7fd (bug 887699) --- dom/network/interfaces/nsIDOMNetworkStats.idl | 11 +- .../interfaces/nsIDOMNetworkStatsManager.idl | 68 ++- .../nsINetworkStatsServiceProxy.idl | 8 +- dom/network/src/NetworkStatsDB.jsm | 417 +++++++++--------- dom/network/src/NetworkStatsManager.js | 87 +--- dom/network/src/NetworkStatsManager.manifest | 11 +- dom/network/src/NetworkStatsService.jsm | 415 ++++++++--------- dom/network/src/NetworkStatsServiceProxy.js | 10 +- dom/network/src/TCPSocket.js | 10 +- .../tests/test_networkstats_basics.html | 288 ++++++++---- .../test_networkstats_enabled_no_perm.html | 8 +- .../tests/unit_stats/test_networkstats_db.js | 351 +++++++-------- .../unit_stats/test_networkstats_service.js | 56 +-- .../test_networkstats_service_proxy.js | 80 +--- .../protocol/websocket/WebSocketChannel.cpp | 25 +- netwerk/protocol/websocket/WebSocketChannel.h | 11 +- 16 files changed, 852 insertions(+), 1004 deletions(-) diff --git a/dom/network/interfaces/nsIDOMNetworkStats.idl b/dom/network/interfaces/nsIDOMNetworkStats.idl index b5e3fc445ab1..f3419d93fe2e 100644 --- a/dom/network/interfaces/nsIDOMNetworkStats.idl +++ b/dom/network/interfaces/nsIDOMNetworkStats.idl @@ -4,8 +4,6 @@ #include "nsISupports.idl" -interface nsIDOMMozNetworkStatsInterface; - [scriptable, builtinclass, uuid(3b16fe17-5583-483a-b486-b64a3243221c)] interface nsIDOMMozNetworkStatsData : nsISupports { @@ -14,7 +12,7 @@ interface nsIDOMMozNetworkStatsData : nsISupports readonly attribute jsval date; // Date. }; -[scriptable, builtinclass, uuid(b6fc4b14-628d-4c99-bf4e-e4ed56916cbe)] +[scriptable, builtinclass, uuid(6613ea55-b99c-44f9-91bf-d07da10b9b74)] interface nsIDOMMozNetworkStats : nsISupports { /** @@ -24,12 +22,13 @@ interface nsIDOMMozNetworkStats : nsISupports readonly attribute DOMString manifestURL; /** - * Network the returned data belongs to. + * Can be 'mobile', 'wifi' or null. + * If null, stats for both mobile and wifi are returned. */ - readonly attribute nsIDOMMozNetworkStatsInterface network; + readonly attribute DOMString connectionType; /** - * Stats for a network. + * Stats for connectionType */ readonly attribute jsval data; // array of NetworkStatsData. // one element per day. diff --git a/dom/network/interfaces/nsIDOMNetworkStatsManager.idl b/dom/network/interfaces/nsIDOMNetworkStatsManager.idl index d0ba3c9a9acf..ec7b663f5b70 100644 --- a/dom/network/interfaces/nsIDOMNetworkStatsManager.idl +++ b/dom/network/interfaces/nsIDOMNetworkStatsManager.idl @@ -6,65 +6,57 @@ interface nsIDOMDOMRequest; -/** - * Represents a data interface for which the manager is recording statistics. - */ -[scriptable, uuid(f540615b-d803-43ff-8200-2a9d145a5645)] -interface nsIDOMMozNetworkStatsInterface : nsISupports +dictionary NetworkStatsOptions { - readonly attribute long type; - /** - * Id value is '0' for wifi or the iccid for mobile (SIM). + * Connection type used to filter which network stats will be returned: + * 'mobile', 'wifi' or null. + * If null, stats for both mobile and wifi are returned. + * + * Manifest URL used to retrieve network stats per app. + * If null, system stats (regardless of the app) are returned. */ - readonly attribute DOMString id; + DOMString connectionType; + DOMString manifestURL; + jsval start; // date + jsval end; // date }; -[scriptable, uuid(5fbdcae6-a2cd-47b3-929f-83ac75bd4881)] +[scriptable, uuid(87529a6c-aef6-11e1-a595-4f034275cfa6)] interface nsIDOMMozNetworkStatsManager : nsISupports { /** - * Constants for known interface types. - */ - const long WIFI = 0; - const long MOBILE = 1; - - /** - * Find samples between two dates start and end, both included. + * Query network statistics. * - * If manifestURL is provided, per-app usage is retrieved, - * otherwise the target will be system usage. + * If options.connectionType is not provided, return statistics for all known + * network interfaces. * - * If success, the request result will be an nsIDOMMozNetworkStats object. + * If options.manifestURL is not provided, return statistics regardless of the app. + * + * If successful, the request result will be an nsIDOMMozNetworkStats object. + * + * If network stats are not available for some dates, then rxBytes & + * txBytes are undefined for those dates. */ - nsIDOMDOMRequest getSamples(in nsIDOMMozNetworkStatsInterface network, - in jsval start, - in jsval end, - [optional] in DOMString manifestURL); + nsIDOMDOMRequest getNetworkStats(in jsval options); /** - * Remove all stats related with the provided network from DB. + * Return available connection types. */ - nsIDOMDOMRequest clearStats(in nsIDOMMozNetworkStatsInterface network); + readonly attribute jsval connectionTypes; // array of DOMStrings. /** - * Remove all stats in the database. + * Clear all stats from DB. */ - nsIDOMDOMRequest clearAllStats(); + nsIDOMDOMRequest clearAllData(); /** - * Return currently available networks. + * Time in seconds between samples stored in database. */ - readonly attribute jsval availableNetworks; // array of nsIDOMMozNetworkStatsInterface. + readonly attribute long sampleRate; /** - * Minimum time in milliseconds between samples stored in the database. + * Maximum number of samples stored in the database per connection type. */ - readonly attribute long sampleRate; - - /** - * Time in milliseconds recorded by the API until present time. All samples - * older than maxStorageAge from now are deleted. - */ - readonly attribute long long maxStorageAge; + readonly attribute long maxStorageSamples; }; diff --git a/dom/network/interfaces/nsINetworkStatsServiceProxy.idl b/dom/network/interfaces/nsINetworkStatsServiceProxy.idl index 904e3742b0b6..3be8d7562874 100644 --- a/dom/network/interfaces/nsINetworkStatsServiceProxy.idl +++ b/dom/network/interfaces/nsINetworkStatsServiceProxy.idl @@ -4,8 +4,6 @@ #include "nsISupports.idl" -interface nsINetworkInterface; - [scriptable, function, uuid(5f821529-1d80-4ab5-a933-4e1b3585b6bc)] interface nsINetworkStatsServiceProxyCallback : nsISupports { @@ -16,20 +14,20 @@ interface nsINetworkStatsServiceProxyCallback : nsISupports void notify(in boolean aResult, in jsval aMessage); }; -[scriptable, uuid(facef032-3fd9-4509-a396-83d94c1a11ae)] +[scriptable, uuid(8fbd115d-f590-474c-96dc-e2b6803ca975)] interface nsINetworkStatsServiceProxy : nsISupports { /* * An interface used to record per-app traffic data. * @param aAppId app id - * @param aNetworkInterface network + * @param aConnectionType network connection type (0 for wifi, 1 for mobile) * @param aTimeStamp time stamp * @param aRxBytes received data amount * @param aTxBytes transmitted data amount * @param aCallback an optional callback */ void saveAppStats(in unsigned long aAppId, - in nsINetworkInterface aNetwork, + in long aConnectionType, in unsigned long long aTimeStamp, in unsigned long long aRxBytes, in unsigned long long aTxBytes, diff --git a/dom/network/src/NetworkStatsDB.jsm b/dom/network/src/NetworkStatsDB.jsm index 93071c4c2b57..dd12521f3120 100644 --- a/dom/network/src/NetworkStatsDB.jsm +++ b/dom/network/src/NetworkStatsDB.jsm @@ -16,7 +16,8 @@ Cu.import("resource://gre/modules/IndexedDBHelper.jsm"); const DB_NAME = "net_stats"; const DB_VERSION = 2; -const STORE_NAME = "net_stats"; +const STORE_NAME = "net_stats"; // Deprecated. Use "net_stats_v2" instead. +const STORE_NAME_V2 = "net_stats_v2"; // Constant defining the maximum values allowed per interface. If more, older // will be erased. @@ -25,11 +26,12 @@ const VALUES_MAX_LENGTH = 6 * 30; // Constant defining the rate of the samples. Daily. const SAMPLE_RATE = 1000 * 60 * 60 * 24; -this.NetworkStatsDB = function NetworkStatsDB() { +this.NetworkStatsDB = function NetworkStatsDB(aConnectionTypes) { if (DEBUG) { debug("Constructor"); } - this.initDBHelper(DB_NAME, DB_VERSION, [STORE_NAME]); + this._connectionTypes = aConnectionTypes; + this.initDBHelper(DB_NAME, DB_VERSION, [STORE_NAME_V2]); } NetworkStatsDB.prototype = { @@ -42,7 +44,7 @@ NetworkStatsDB.prototype = { function errorCb(error) { txnCb(error, null); } - return this.newTxn(txn_type, STORE_NAME, callback, successCb, errorCb); + return this.newTxn(txn_type, STORE_NAME_V2, callback, successCb, errorCb); }, upgradeSchema: function upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) { @@ -67,57 +69,69 @@ NetworkStatsDB.prototype = { if (DEBUG) { debug("Created object stores and indexes"); } + + // There could be a time delay between the point when the network + // interface comes up and the point when the database is initialized. + // In this short interval some traffic data are generated but are not + // registered by the first sample. The initialization of the database + // should make up the missing sample. + let stats = []; + for (let connection in this._connectionTypes) { + let connectionType = this._connectionTypes[connection].name; + let timestamp = this.normalizeDate(new Date()); + stats.push({ connectionType: connectionType, + timestamp: timestamp, + rxBytes: 0, + txBytes: 0, + rxTotalBytes: 0, + txTotalBytes: 0 }); + } + this._saveStats(aTransaction, objectStore, stats); + if (DEBUG) { + debug("Database initialized"); + } } else if (currVersion == 1) { // In order to support per-app traffic data storage, the original // objectStore needs to be replaced by a new objectStore with new // key path ("appId") and new index ("appId"). - // Also, since now networks are identified by their - // [networkId, networkType] not just by their connectionType, - // to modify the keyPath is mandatory to delete the object store - // and create it again. Old data is going to be deleted because the - // networkId for each sample can not be set. - db.deleteObjectStore(STORE_NAME); + let newObjectStore; + newObjectStore = db.createObjectStore(STORE_NAME_V2, { keyPath: ["appId", "connectionType", "timestamp"] }); + newObjectStore.createIndex("appId", "appId", { unique: false }); + newObjectStore.createIndex("connectionType", "connectionType", { unique: false }); + newObjectStore.createIndex("timestamp", "timestamp", { unique: false }); + newObjectStore.createIndex("rxBytes", "rxBytes", { unique: false }); + newObjectStore.createIndex("txBytes", "txBytes", { unique: false }); + newObjectStore.createIndex("rxTotalBytes", "rxTotalBytes", { unique: false }); + newObjectStore.createIndex("txTotalBytes", "txTotalBytes", { unique: false }); + if (DEBUG) { + debug("Created new object stores and indexes"); + } - objectStore = db.createObjectStore(STORE_NAME, { keyPath: ["appId", "network", "timestamp"] }); - objectStore.createIndex("appId", "appId", { unique: false }); - objectStore.createIndex("network", "network", { unique: false }); - objectStore.createIndex("networkType", "networkType", { unique: false }); - objectStore.createIndex("timestamp", "timestamp", { unique: false }); - objectStore.createIndex("rxBytes", "rxBytes", { unique: false }); - objectStore.createIndex("txBytes", "txBytes", { unique: false }); - objectStore.createIndex("rxTotalBytes", "rxTotalBytes", { unique: false }); - objectStore.createIndex("txTotalBytes", "txTotalBytes", { unique: false }); + // Copy the data from the original objectStore to the new objectStore. + objectStore = aTransaction.objectStore(STORE_NAME); + objectStore.openCursor().onsuccess = function(event) { + let cursor = event.target.result; + if (!cursor) { + // Delete the original object store. + db.deleteObjectStore(STORE_NAME); + return; + } - debug("Created object stores and indexes for version 2"); + let oldStats = cursor.value; + let newStats = { appId: 0, + connectionType: oldStats.connectionType, + timestamp: oldStats.timestamp, + rxBytes: oldStats.rxBytes, + txBytes: oldStats.txBytes, + rxTotalBytes: oldStats.rxTotalBytes, + txTotalBytes: oldStats.txTotalBytes }; + this._saveStats(aTransaction, newObjectStore, newStats); + cursor.continue(); + }.bind(this); } } }, - importData: function importData(aStats) { - let stats = { appId: aStats.appId, - network: [aStats.networkId, aStats.networkType], - timestamp: aStats.timestamp, - rxBytes: aStats.rxBytes, - txBytes: aStats.txBytes, - rxTotalBytes: aStats.rxTotalBytes, - txTotalBytes: aStats.txTotalBytes }; - - return stats; - }, - - exportData: function exportData(aStats) { - let stats = { appId: aStats.appId, - networkId: aStats.network[0], - networkType: aStats.network[1], - timestamp: aStats.timestamp, - rxBytes: aStats.rxBytes, - txBytes: aStats.txBytes, - rxTotalBytes: aStats.rxTotalBytes, - txTotalBytes: aStats.txTotalBytes }; - - return stats; - }, - normalizeDate: function normalizeDate(aDate) { // Convert to UTC according to timezone and // filter timestamp to get SAMPLE_RATE precission @@ -126,42 +140,29 @@ NetworkStatsDB.prototype = { return timestamp; }, - saveStats: function saveStats(aStats, aResultCb) { - let timestamp = this.normalizeDate(aStats.date); + saveStats: function saveStats(stats, aResultCb) { + let timestamp = this.normalizeDate(stats.date); - let stats = { appId: aStats.appId, - networkId: aStats.networkId, - networkType: aStats.networkType, - timestamp: timestamp, - rxBytes: (aStats.appId == 0) ? 0 : aStats.rxBytes, - txBytes: (aStats.appId == 0) ? 0 : aStats.txBytes, - rxTotalBytes: (aStats.appId == 0) ? aStats.rxBytes : 0, - txTotalBytes: (aStats.appId == 0) ? aStats.txBytes : 0 }; + stats = { appId: stats.appId, + connectionType: stats.connectionType, + timestamp: timestamp, + rxBytes: (stats.appId == 0) ? 0 : stats.rxBytes, + txBytes: (stats.appId == 0) ? 0 : stats.txBytes, + rxTotalBytes: (stats.appId == 0) ? stats.rxBytes : 0, + txTotalBytes: (stats.appId == 0) ? stats.txBytes : 0 }; - stats = this.importData(stats); - - this.dbNewTxn("readwrite", function(aTxn, aStore) { + this.dbNewTxn("readwrite", function(txn, store) { if (DEBUG) { debug("Filtered time: " + new Date(timestamp)); debug("New stats: " + JSON.stringify(stats)); } - let request = aStore.index("network").openCursor(stats.network, "prev"); + let request = store.index("connectionType").openCursor(stats.connectionType, "prev"); request.onsuccess = function onsuccess(event) { let cursor = event.target.result; if (!cursor) { // Empty, so save first element. - - // There could be a time delay between the point when the network - // interface comes up and the point when the database is initialized. - // In this short interval some traffic data are generated but are not - // registered by the first sample. - if (stats.appId == 0) { - stats.rxBytes = stats.rxTotalBytes; - stats.txBytes = stats.txTotalBytes; - } - - this._saveStats(aTxn, aStore, stats); + this._saveStats(txn, store, stats); return; } @@ -176,10 +177,10 @@ NetworkStatsDB.prototype = { } // Remove stats previous to now - VALUE_MAX_LENGTH - this._removeOldStats(aTxn, aStore, stats.appId, stats.network, stats.timestamp); + this._removeOldStats(txn, store, stats.appId, stats.connectionType, stats.timestamp); // Process stats before save - this._processSamplesDiff(aTxn, aStore, cursor, stats); + this._processSamplesDiff(txn, store, cursor, stats); }.bind(this); }.bind(this), aResultCb); }, @@ -188,21 +189,20 @@ NetworkStatsDB.prototype = { * This function check that stats are saved in the database following the sample rate. * In this way is easier to find elements when stats are requested. */ - _processSamplesDiff: function _processSamplesDiff(aTxn, aStore, aLastSampleCursor, aNewSample) { - let lastSample = aLastSampleCursor.value; + _processSamplesDiff: function _processSamplesDiff(txn, store, lastSampleCursor, newSample) { + let lastSample = lastSampleCursor.value; // Get difference between last and new sample. - let diff = (aNewSample.timestamp - lastSample.timestamp) / SAMPLE_RATE; + let diff = (newSample.timestamp - lastSample.timestamp) / SAMPLE_RATE; if (diff % 1) { // diff is decimal, so some error happened because samples are stored as a multiple // of SAMPLE_RATE - aTxn.abort(); + txn.abort(); throw new Error("Error processing samples"); } if (DEBUG) { - debug("New: " + aNewSample.timestamp + " - Last: " + - lastSample.timestamp + " - diff: " + diff); + debug("New: " + newSample.timestamp + " - Last: " + lastSample.timestamp + " - diff: " + diff); } // If the incoming data is obtained from netd (|newSample.appId| is 0), @@ -210,15 +210,15 @@ NetworkStatsDB.prototype = { // |txTotalBytes|/|rxTotalBytes| and the last |txTotalBytes|/|rxTotalBytes|. // Else, the incoming data is per-app data (|newSample.appId| is not 0), // the |txBytes|/|rxBytes| is directly the new |txBytes|/|rxBytes|. - if (aNewSample.appId == 0) { - let rxDiff = aNewSample.rxTotalBytes - lastSample.rxTotalBytes; - let txDiff = aNewSample.txTotalBytes - lastSample.txTotalBytes; + if (newSample.appId == 0) { + let rxDiff = newSample.rxTotalBytes - lastSample.rxTotalBytes; + let txDiff = newSample.txTotalBytes - lastSample.txTotalBytes; if (rxDiff < 0 || txDiff < 0) { - rxDiff = aNewSample.rxTotalBytes; - txDiff = aNewSample.txTotalBytes; + rxDiff = newSample.rxTotalBytes; + txDiff = newSample.txTotalBytes; } - aNewSample.rxBytes = rxDiff; - aNewSample.txBytes = txDiff; + newSample.rxBytes = rxDiff; + newSample.txBytes = txDiff; } if (diff == 1) { @@ -227,12 +227,11 @@ NetworkStatsDB.prototype = { // If the incoming data is per-data data, new |rxTotalBytes|/|txTotalBytes| // needs to be obtained by adding new |rxBytes|/|txBytes| to last // |rxTotalBytes|/|txTotalBytes|. - if (aNewSample.appId != 0) { - aNewSample.rxTotalBytes = aNewSample.rxBytes + lastSample.rxTotalBytes; - aNewSample.txTotalBytes = aNewSample.txBytes + lastSample.txTotalBytes; + if (newSample.appId != 0) { + newSample.rxTotalBytes = newSample.rxBytes + lastSample.rxTotalBytes; + newSample.txTotalBytes = newSample.txBytes + lastSample.txTotalBytes; } - - this._saveStats(aTxn, aStore, aNewSample); + this._saveStats(txn, store, newSample); return; } if (diff > 1) { @@ -245,20 +244,19 @@ NetworkStatsDB.prototype = { let data = []; for (let i = diff - 2; i >= 0; i--) { - let time = aNewSample.timestamp - SAMPLE_RATE * (i + 1); - let sample = { appId: aNewSample.appId, - network: aNewSample.network, - timestamp: time, - rxBytes: 0, - txBytes: 0, - rxTotalBytes: lastSample.rxTotalBytes, - txTotalBytes: lastSample.txTotalBytes }; - + let time = newSample.timestamp - SAMPLE_RATE * (i + 1); + let sample = {appId: newSample.appId, + connectionType: newSample.connectionType, + timestamp: time, + rxBytes: 0, + txBytes: 0, + rxTotalBytes: lastSample.rxTotalBytes, + txTotalBytes: lastSample.txTotalBytes}; data.push(sample); } - data.push(aNewSample); - this._saveStats(aTxn, aStore, data); + data.push(newSample); + this._saveStats(txn, store, data); return; } if (diff == 0 || diff < 0) { @@ -268,163 +266,91 @@ NetworkStatsDB.prototype = { // If diff < 0, clock or timezone changed back. Place data in the last sample. - lastSample.rxBytes += aNewSample.rxBytes; - lastSample.txBytes += aNewSample.txBytes; + lastSample.rxBytes += newSample.rxBytes; + lastSample.txBytes += newSample.txBytes; // If incoming data is obtained from netd, last |rxTotalBytes|/|txTotalBytes| // needs to get updated by replacing the new |rxTotalBytes|/|txTotalBytes|. - if (aNewSample.appId == 0) { - lastSample.rxTotalBytes = aNewSample.rxTotalBytes; - lastSample.txTotalBytes = aNewSample.txTotalBytes; + if (newSample.appId == 0) { + lastSample.rxTotalBytes = newSample.rxTotalBytes; + lastSample.txTotalBytes = newSample.txTotalBytes; } else { // Else, the incoming data is per-app data, old |rxTotalBytes|/ // |txTotalBytes| needs to get updated by adding the new // |rxBytes|/|txBytes| to last |rxTotalBytes|/|txTotalBytes|. - lastSample.rxTotalBytes += aNewSample.rxBytes; - lastSample.txTotalBytes += aNewSample.txBytes; + lastSample.rxTotalBytes += newSample.rxBytes; + lastSample.txTotalBytes += newSample.txBytes; } if (DEBUG) { debug("Update: " + JSON.stringify(lastSample)); } - let req = aLastSampleCursor.update(lastSample); + let req = lastSampleCursor.update(lastSample); } }, - _saveStats: function _saveStats(aTxn, aStore, aNetworkStats) { + _saveStats: function _saveStats(txn, store, networkStats) { if (DEBUG) { - debug("_saveStats: " + JSON.stringify(aNetworkStats)); + debug("_saveStats: " + JSON.stringify(networkStats)); } - if (Array.isArray(aNetworkStats)) { - let len = aNetworkStats.length - 1; + if (Array.isArray(networkStats)) { + let len = networkStats.length - 1; for (let i = 0; i <= len; i++) { - aStore.put(aNetworkStats[i]); + store.put(networkStats[i]); } } else { - aStore.put(aNetworkStats); + store.put(networkStats); } }, - _removeOldStats: function _removeOldStats(aTxn, aStore, aAppId, aNetwork, aDate) { + _removeOldStats: function _removeOldStats(txn, store, appId, connType, date) { // Callback function to remove old items when new ones are added. - let filterDate = aDate - (SAMPLE_RATE * VALUES_MAX_LENGTH - 1); - let lowerFilter = [aAppId, aNetwork, 0]; - let upperFilter = [aAppId, aNetwork, filterDate]; + let filterDate = date - (SAMPLE_RATE * VALUES_MAX_LENGTH - 1); + let lowerFilter = [appId, connType, 0]; + let upperFilter = [appId, connType, filterDate]; let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); - let lastSample = null; - let self = this; - - aStore.openCursor(range).onsuccess = function(event) { + store.openCursor(range).onsuccess = function(event) { var cursor = event.target.result; if (cursor) { - lastSample = cursor.value; cursor.delete(); cursor.continue(); - return; } - - // If all samples for a network are removed, an empty sample - // has to be saved to keep the totalBytes in order to compute - // future samples because system counters are not set to 0. - // Thus, if there are no samples left, the last sample removed - // will be saved again after setting its bytes to 0. - let request = aStore.index("network").openCursor(aNetwork); - request.onsuccess = function onsuccess(event) { - let cursor = event.target.result; - if (!cursor && lastSample != null) { - let timestamp = new Date(); - timestamp = self.normalizeDate(timestamp); - lastSample.timestamp = timestamp; - lastSample.rxBytes = 0; - lastSample.txBytes = 0; - self._saveStats(aTxn, aStore, lastSample); - } - }; - }; + }.bind(this); }, - clearInterfaceStats: function clearInterfaceStats(aNetwork, aResultCb) { - let network = [aNetwork.id, aNetwork.type]; - let self = this; - - // Clear and save an empty sample to keep sync with system counters - this.dbNewTxn("readwrite", function(aTxn, aStore) { - let sample = null; - let request = aStore.index("network").openCursor(network, "prev"); - request.onsuccess = function onsuccess(event) { - let cursor = event.target.result; - if (cursor) { - if (!sample) { - sample = cursor.value; - } - - cursor.delete(); - cursor.continue(); - return; - } - - if (sample) { - let timestamp = new Date(); - timestamp = self.normalizeDate(timestamp); - sample.timestamp = timestamp; - sample.appId = 0; - sample.rxBytes = 0; - sample.txBytes = 0; - - self._saveStats(aTxn, aStore, sample); - } - }; + clear: function clear(aResultCb) { + this.dbNewTxn("readwrite", function(txn, store) { + if (DEBUG) { + debug("Going to clear all!"); + } + store.clear(); }, aResultCb); }, - clearStats: function clearStats(aNetworks, aResultCb) { - let index = 0; - let stats = []; - let self = this; - - let callback = function(aError, aResult) { - index++; - - if (!aError && index < aNetworks.length) { - self.clearInterfaceStats(aNetworks[index], callback); - return; - } - - aResultCb(aError, aResult); - }; - - if (!aNetworks[index]) { - aResultCb(null, true); - return; - } - this.clearInterfaceStats(aNetworks[index], callback); - }, - - find: function find(aResultCb, aNetwork, aStart, aEnd, aAppId, aManifestURL) { + find: function find(aResultCb, aOptions) { let offset = (new Date()).getTimezoneOffset() * 60 * 1000; - let start = this.normalizeDate(aStart); - let end = this.normalizeDate(aEnd); + let start = this.normalizeDate(aOptions.start); + let end = this.normalizeDate(aOptions.end); if (DEBUG) { - debug("Find samples for appId: " + aAppId + " network " + - JSON.stringify(aNetwork) + " from " + start + " until " + end); + debug("Find: appId: " + aOptions.appId + " connectionType:" + + aOptions.connectionType + " start: " + start + " end: " + end); debug("Start time: " + new Date(start)); debug("End time: " + new Date(end)); } - this.dbNewTxn("readonly", function(aTxn, aStore) { - let network = [aNetwork.id, aNetwork.type]; - let lowerFilter = [aAppId, network, start]; - let upperFilter = [aAppId, network, end]; + this.dbNewTxn("readonly", function(txn, store) { + let lowerFilter = [aOptions.appId, aOptions.connectionType, start]; + let upperFilter = [aOptions.appId, aOptions.connectionType, end]; let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); let data = []; - if (!aTxn.result) { - aTxn.result = {}; + if (!txn.result) { + txn.result = {}; } - let request = aStore.openCursor(range).onsuccess = function(event) { + let request = store.openCursor(range).onsuccess = function(event) { var cursor = event.target.result; if (cursor){ data.push({ rxBytes: cursor.value.rxBytes, @@ -438,11 +364,66 @@ NetworkStatsDB.prototype = { // now - VALUES_MAX_LENGTH, fill with empty samples. this.fillResultSamples(start + offset, end + offset, data); - aTxn.result.manifestURL = aManifestURL; - aTxn.result.network = aNetwork; - aTxn.result.start = aStart; - aTxn.result.end = aEnd; - aTxn.result.data = data; + txn.result.manifestURL = aOptions.manifestURL; + txn.result.connectionType = aOptions.connectionType; + txn.result.start = aOptions.start; + txn.result.end = aOptions.end; + txn.result.data = data; + }.bind(this); + }.bind(this), aResultCb); + }, + + findAll: function findAll(aResultCb, aOptions) { + let offset = (new Date()).getTimezoneOffset() * 60 * 1000; + let start = this.normalizeDate(aOptions.start); + let end = this.normalizeDate(aOptions.end); + + if (DEBUG) { + debug("FindAll: appId: " + aOptions.appId + + " start: " + start + " end: " + end + "\n"); + } + + let self = this; + this.dbNewTxn("readonly", function(txn, store) { + let lowerFilter = start; + let upperFilter = end; + let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); + + let data = []; + + if (!txn.result) { + txn.result = {}; + } + + let request = store.index("timestamp").openCursor(range).onsuccess = function(event) { + var cursor = event.target.result; + if (cursor) { + if (cursor.value.appId != aOptions.appId) { + cursor.continue(); + return; + } + + if (data.length > 0 && + data[data.length - 1].date.getTime() == cursor.value.timestamp + offset) { + // Time is the same, so add values. + data[data.length - 1].rxBytes += cursor.value.rxBytes; + data[data.length - 1].txBytes += cursor.value.txBytes; + } else { + data.push({ rxBytes: cursor.value.rxBytes, + txBytes: cursor.value.txBytes, + date: new Date(cursor.value.timestamp + offset) }); + } + cursor.continue(); + return; + } + + this.fillResultSamples(start + offset, end + offset, data); + + txn.result.manifestURL = aOptions.manifestURL; + txn.result.connectionType = aOptions.connectionType; + txn.result.start = aOptions.start; + txn.result.end = aOptions.end; + txn.result.data = data; }.bind(this); }.bind(this), aResultCb); }, @@ -480,9 +461,9 @@ NetworkStatsDB.prototype = { }, logAllRecords: function logAllRecords(aResultCb) { - this.dbNewTxn("readonly", function(aTxn, aStore) { - aStore.mozGetAll().onsuccess = function onsuccess(event) { - aTxn.result = event.target.result; + this.dbNewTxn("readonly", function(txn, store) { + store.mozGetAll().onsuccess = function onsuccess(event) { + txn.result = event.target.result; }; }, aResultCb); }, diff --git a/dom/network/src/NetworkStatsManager.js b/dom/network/src/NetworkStatsManager.js index 57ea361e3a7d..f1d65e93bb6f 100644 --- a/dom/network/src/NetworkStatsManager.js +++ b/dom/network/src/NetworkStatsManager.js @@ -55,38 +55,9 @@ NetworkStatsData.prototype = { QueryInterface : XPCOMUtils.generateQI([nsIDOMMozNetworkStatsData]) }; -// NetworkStatsInterface -const NETWORKSTATSINTERFACE_CONTRACTID = "@mozilla.org/networkstatsinterface;1"; -const NETWORKSTATSINTERFACE_CID = Components.ID("{f540615b-d803-43ff-8200-2a9d145a5645}"); -const nsIDOMMozNetworkStatsInterface = Components.interfaces.nsIDOMMozNetworkStatsInterface; - -function NetworkStatsInterface(aNetwork) { - if (DEBUG) { - debug("NetworkStatsInterface Constructor"); - } - this.type = aNetwork.type; - this.id = aNetwork.id; -} - -NetworkStatsInterface.prototype = { - __exposedProps__: { - id: 'r', - type: 'r', - }, - - classID : NETWORKSTATSINTERFACE_CID, - classInfo : XPCOMUtils.generateCI({classID: NETWORKSTATSINTERFACE_CID, - contractID: NETWORKSTATSINTERFACE_CONTRACTID, - classDescription: "NetworkStatsInterface", - interfaces: [nsIDOMMozNetworkStatsInterface], - flags: nsIClassInfo.DOM_OBJECT}), - - QueryInterface : XPCOMUtils.generateQI([nsIDOMMozNetworkStatsInterface]) -} - // NetworkStats const NETWORKSTATS_CONTRACTID = "@mozilla.org/networkstats;1"; -const NETWORKSTATS_CID = Components.ID("{b6fc4b14-628d-4c99-bf4e-e4ed56916cbe}"); +const NETWORKSTATS_CID = Components.ID("{6613ea55-b99c-44f9-91bf-d07da10b9b74}"); const nsIDOMMozNetworkStats = Components.interfaces.nsIDOMMozNetworkStats; function NetworkStats(aWindow, aStats) { @@ -94,7 +65,7 @@ function NetworkStats(aWindow, aStats) { debug("NetworkStats Constructor"); } this.manifestURL = aStats.manifestURL || null; - this.network = new NetworkStatsInterface(aStats.network); + this.connectionType = aStats.connectionType || null; this.start = aStats.start || null; this.end = aStats.end || null; @@ -107,7 +78,7 @@ function NetworkStats(aWindow, aStats) { NetworkStats.prototype = { __exposedProps__: { manifestURL: 'r', - network: 'r', + connectionType: 'r', start: 'r', end: 'r', data: 'r', @@ -121,14 +92,13 @@ NetworkStats.prototype = { flags: nsIClassInfo.DOM_OBJECT}), QueryInterface : XPCOMUtils.generateQI([nsIDOMMozNetworkStats, - nsIDOMMozNetworkStatsData, - nsIDOMMozNetworkStatsInterface]) + nsIDOMMozNetworkStatsData]) } // NetworkStatsManager const NETWORKSTATSMANAGER_CONTRACTID = "@mozilla.org/networkStatsManager;1"; -const NETWORKSTATSMANAGER_CID = Components.ID("{5fbdcae6-a2cd-47b3-929f-83ac75bd4881}"); +const NETWORKSTATSMANAGER_CID = Components.ID("{87529a6c-aef6-11e1-a595-4f034275cfa6}"); const nsIDOMMozNetworkStatsManager = Components.interfaces.nsIDOMMozNetworkStatsManager; function NetworkStatsManager() { @@ -146,64 +116,42 @@ NetworkStatsManager.prototype = { } }, - getSamples: function getSamples(aNetwork, aStart, aEnd, aManifestURL) { + getNetworkStats: function getNetworkStats(aOptions) { this.checkPrivileges(); - if (aStart.constructor.name !== "Date" || - aEnd.constructor.name !== "Date" || - aStart > aEnd) { + if (!aOptions.start || !aOptions.end || + aOptions.start > aOptions.end) { throw Components.results.NS_ERROR_INVALID_ARG; } let request = this.createRequest(); cpmm.sendAsyncMessage("NetworkStats:Get", - { network: aNetwork, - start: aStart, - end: aEnd, - manifestURL: aManifestURL, - id: this.getRequestId(request) }); + {data: aOptions, id: this.getRequestId(request)}); return request; }, - clearStats: function clearStats(aNetwork) { + clearAllData: function clearAllData() { this.checkPrivileges(); let request = this.createRequest(); cpmm.sendAsyncMessage("NetworkStats:Clear", - { network: aNetwork, - id: this.getRequestId(request) }); - return request; - }, - - clearAllStats: function clearAllStats() { - this.checkPrivileges(); - - let request = this.createRequest(); - cpmm.sendAsyncMessage("NetworkStats:ClearAll", {id: this.getRequestId(request)}); return request; }, - get availableNetworks() { + get connectionTypes() { this.checkPrivileges(); - - let result = ObjectWrapper.wrap(cpmm.sendSyncMessage("NetworkStats:Networks")[0], this._window); - let networks = this.data = Cu.createArrayIn(this._window); - for (let i = 0; i < result.length; i++) { - networks.push(new NetworkStatsInterface(result[i])); - } - - return networks; + return ObjectWrapper.wrap(cpmm.sendSyncMessage("NetworkStats:Types")[0], this._window); }, get sampleRate() { this.checkPrivileges(); - return cpmm.sendSyncMessage("NetworkStats:SampleRate")[0]; + return cpmm.sendSyncMessage("NetworkStats:SampleRate")[0] / 1000; }, - get maxStorageAge() { + get maxStorageSamples() { this.checkPrivileges(); - return cpmm.sendSyncMessage("NetworkStats:MaxStorageAge")[0]; + return cpmm.sendSyncMessage("NetworkStats:MaxStorageSamples")[0]; }, receiveMessage: function(aMessage) { @@ -235,7 +183,6 @@ NetworkStatsManager.prototype = { break; case "NetworkStats:Clear:Return": - case "NetworkStats:ClearAll:Return": if (msg.error) { Services.DOMRequest.fireError(req, msg.error); return; @@ -275,8 +222,7 @@ NetworkStatsManager.prototype = { } this.initDOMRequestHelper(aWindow, ["NetworkStats:Get:Return", - "NetworkStats:Clear:Return", - "NetworkStats:ClearAll:Return"]); + "NetworkStats:Clear:Return"]); }, // Called from DOMRequestIpcHelper @@ -299,6 +245,5 @@ NetworkStatsManager.prototype = { } this.NSGetFactory = XPCOMUtils.generateNSGetFactory([NetworkStatsData, - NetworkStatsInterface, NetworkStats, NetworkStatsManager]); diff --git a/dom/network/src/NetworkStatsManager.manifest b/dom/network/src/NetworkStatsManager.manifest index 4e6e4d33c215..f0b82a323871 100644 --- a/dom/network/src/NetworkStatsManager.manifest +++ b/dom/network/src/NetworkStatsManager.manifest @@ -1,12 +1,9 @@ component {3b16fe17-5583-483a-b486-b64a3243221c} NetworkStatsManager.js contract @mozilla.org/networkStatsdata;1 {3b16fe17-5583-483a-b486-b64a3243221c} -component {b6fc4b14-628d-4c99-bf4e-e4ed56916cbe} NetworkStatsManager.js -contract @mozilla.org/networkStats;1 {b6fc4b14-628d-4c99-bf4e-e4ed56916cbe} +component {6613ea55-b99c-44f9-91bf-d07da10b9b74} NetworkStatsManager.js +contract @mozilla.org/networkStats;1 {6613ea55-b99c-44f9-91bf-d07da10b9b74} -component {f540615b-d803-43ff-8200-2a9d145a5645} NetworkStatsManager.js -contract @mozilla.org/networkstatsinterface;1 {f540615b-d803-43ff-8200-2a9d145a5645} - -component {5fbdcae6-a2cd-47b3-929f-83ac75bd4881} NetworkStatsManager.js -contract @mozilla.org/networkStatsManager;1 {5fbdcae6-a2cd-47b3-929f-83ac75bd4881} +component {87529a6c-aef6-11e1-a595-4f034275cfa6} NetworkStatsManager.js +contract @mozilla.org/networkStatsManager;1 {87529a6c-aef6-11e1-a595-4f034275cfa6} category JavaScript-navigator-property mozNetworkStats @mozilla.org/networkStatsManager;1 diff --git a/dom/network/src/NetworkStatsService.jsm b/dom/network/src/NetworkStatsService.jsm index 8e325faa4c42..6b187d39788e 100644 --- a/dom/network/src/NetworkStatsService.jsm +++ b/dom/network/src/NetworkStatsService.jsm @@ -5,11 +5,7 @@ "use strict"; const DEBUG = false; -function debug(s) { - if (DEBUG) { - dump("-*- NetworkStatsService: " + s + "\n"); - } -} +function debug(s) { dump("-*- NetworkStatsService: " + s + "\n"); } const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; @@ -26,6 +22,7 @@ const TOPIC_INTERFACE_REGISTERED = "network-interface-registered"; const TOPIC_INTERFACE_UNREGISTERED = "network-interface-unregistered"; const NET_TYPE_WIFI = Ci.nsINetworkInterface.NETWORK_TYPE_WIFI; const NET_TYPE_MOBILE = Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE; +const NET_TYPE_UNKNOWN = Ci.nsINetworkInterface.NETWORK_TYPE_UNKNOWN; // The maximum traffic amount can be saved in the |cachedAppStats|. const MAX_CACHED_TRAFFIC = 500 * 1000 * 1000; // 500 MB @@ -42,19 +39,11 @@ XPCOMUtils.defineLazyServiceGetter(this, "appsService", "@mozilla.org/AppsService;1", "nsIAppsService"); -XPCOMUtils.defineLazyServiceGetter(this, "gSettingsService", - "@mozilla.org/settingsService;1", - "nsISettingsService"); - -XPCOMUtils.defineLazyGetter(this, "gRadioInterface", function () { - let ril = Cc["@mozilla.org/ril;1"].getService(Ci["nsIRadioInterfaceLayer"]); - // TODO: Bug 923382 - B2G Multi-SIM: support multiple SIM cards for network metering. - return ril.getRadioInterface(0); -}); - this.NetworkStatsService = { init: function() { - debug("Service started"); + if (DEBUG) { + debug("Service started"); + } Services.obs.addObserver(this, "xpcom-shutdown", false); Services.obs.addObserver(this, TOPIC_INTERFACE_REGISTERED, false); @@ -63,41 +52,24 @@ this.NetworkStatsService = { this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - // Object to store network interfaces, each network interface is composed - // by a network object (network type and network Id) and a interfaceName - // that contains the name of the physical interface (wlan0, rmnet0, etc.). - // The network type can be 0 for wifi or 1 for mobile. On the other hand, - // the network id is '0' for wifi or the iccid for mobile (SIM). - // Each networkInterface is placed in the _networks object by the index of - // 'networkId + networkType'. - // - // _networks object allows to map available network interfaces at low level - // (wlan0, rmnet0, etc.) to a network. It's not mandatory to have a - // networkInterface per network but can't exist a networkInterface not - // being mapped to a network. + this._connectionTypes = Object.create(null); + this._connectionTypes[NET_TYPE_WIFI] = { name: "wifi", + network: Object.create(null) }; + this._connectionTypes[NET_TYPE_MOBILE] = { name: "mobile", + network: Object.create(null) }; - this._networks = Object.create(null); - - // There is no way to know a priori if wifi connection is available, - // just when the wifi driver is loaded, but it is unloaded when - // wifi is switched off. So wifi connection is hardcoded - let netId = this.getNetworkId('0', NET_TYPE_WIFI); - this._networks[netId] = { network: { id: '0', - type: NET_TYPE_WIFI }, - interfaceName: null }; this.messages = ["NetworkStats:Get", "NetworkStats:Clear", - "NetworkStats:ClearAll", - "NetworkStats:Networks", + "NetworkStats:Types", "NetworkStats:SampleRate", - "NetworkStats:MaxStorageAge"]; + "NetworkStats:MaxStorageSamples"]; - this.messages.forEach(function(aMsgName) { - ppmm.addMessageListener(aMsgName, this); + this.messages.forEach(function(msgName) { + ppmm.addMessageListener(msgName, this); }, this); - this._db = new NetworkStatsDB(); + this._db = new NetworkStatsDB(this._connectionTypes); // Stats for all interfaces are updated periodically this.timer.initWithCallback(this, this._db.sampleRate, @@ -116,56 +88,58 @@ this.NetworkStatsService = { return; } - debug("receiveMessage " + aMessage.name); - + if (DEBUG) { + debug("receiveMessage " + aMessage.name); + } let mm = aMessage.target; let msg = aMessage.json; switch (aMessage.name) { case "NetworkStats:Get": - this.getSamples(mm, msg); + this.getStats(mm, msg); break; case "NetworkStats:Clear": - this.clearInterfaceStats(mm, msg); - break; - case "NetworkStats:ClearAll": this.clearDB(mm, msg); break; - case "NetworkStats:Networks": - return this.availableNetworks(); + case "NetworkStats:Types": + // This message is sync. + let types = []; + for (let i in this._connectionTypes) { + types.push(this._connectionTypes[i].name); + } + return types; case "NetworkStats:SampleRate": // This message is sync. return this._db.sampleRate; - case "NetworkStats:MaxStorageAge": + case "NetworkStats:MaxStorageSamples": // This message is sync. - return this._db.maxStorageSamples * this._db.sampleRate; + return this._db.maxStorageSamples; } }, - observe: function observe(aSubject, aTopic, aData) { - switch (aTopic) { + observe: function observe(subject, topic, data) { + switch (topic) { case TOPIC_INTERFACE_REGISTERED: case TOPIC_INTERFACE_UNREGISTERED: - // If new interface is registered (notified from NetworkManager), // the stats are updated for the new interface without waiting to - // complete the updating period. - - let network = aSubject.QueryInterface(Ci.nsINetworkInterface); - debug("Network " + network.name + " of type " + network.type + " status change"); - - let netId = this.convertNetworkInterface(network); - if (!netId) { - break; + // complete the updating period + let network = subject.QueryInterface(Ci.nsINetworkInterface); + if (DEBUG) { + debug("Network " + network.name + " of type " + network.type + " status change"); + } + if (this._connectionTypes[network.type]) { + this._connectionTypes[network.type].network = network; + this.updateStats(network.type); } - - this.updateStats(netId); break; case "xpcom-shutdown": - debug("Service shutdown"); + if (DEBUG) { + debug("Service shutdown"); + } - this.messages.forEach(function(aMsgName) { - ppmm.removeMessageListener(aMsgName, this); + this.messages.forEach(function(msgName) { + ppmm.removeMessageListener(msgName, this); }, this); Services.obs.removeObserver(this, "xpcom-shutdown"); @@ -186,53 +160,10 @@ this.NetworkStatsService = { * nsITimerCallback * Timer triggers the update of all stats */ - notify: function(aTimer) { + notify: function(timer) { this.updateAllStats(); }, - /* - * nsINetworkStatsService - */ - - convertNetworkInterface: function(aNetwork) { - if (aNetwork.type != NET_TYPE_MOBILE && - aNetwork.type != NET_TYPE_WIFI) { - return null; - } - - let id = '0'; - if (aNetwork.type == NET_TYPE_MOBILE) { - // Bug 904542 will provide the serviceId to map the iccId with the - // nsINetworkInterface of the NetworkManager. Now, lets assume that - // network is mapped with the current iccId of the single SIM. - id = gRadioInterface.rilContext.iccInfo.iccid; - } - - let netId = this.getNetworkId(id, aNetwork.type); - - if (!this._networks[netId]) { - this._networks[netId] = Object.create(null); - this._networks[netId].network = { id: id, - type: aNetwork.type }; - } - - this._networks[netId].interfaceName = aNetwork.name; - return netId; - }, - - getNetworkId: function getNetworkId(aIccId, aNetworkType) { - return aIccId + '' + aNetworkType; - }, - - availableNetworks: function availableNetworks() { - let result = []; - for (let netId in this._networks) { - result.push(this._networks[netId].network); - } - - return result; - }, - /* * Function called from manager to get stats from database. * In order to return updated stats, first is performed a call to @@ -241,71 +172,69 @@ this.NetworkStatsService = { * Then, depending on the request (stats per appId or total stats) * it retrieve them from database and return to the manager. */ - getSamples: function getSamples(mm, msg) { - let self = this; - let network = msg.network; - let netId = this.getNetworkId(network.id, network.type); + getStats: function getStats(mm, msg) { + this.updateAllStats(function onStatsUpdated(aResult, aMessage) { - if (!this._networks[netId]) { - mm.sendAsyncMessage("NetworkStats:Get:Return", - { id: msg.id, error: "Invalid connectionType", result: null }); - return; - } + let data = msg.data; - let appId = 0; - let manifestURL = msg.manifestURL; - if (manifestURL) { - appId = appsService.getAppLocalIdByManifestURL(manifestURL); + let options = { appId: 0, + connectionType: data.connectionType, + start: data.start, + end: data.end }; - if (!appId) { - mm.sendAsyncMessage("NetworkStats:Get:Return", - { id: msg.id, error: "Invalid manifestURL", result: null }); + let manifestURL = data.manifestURL; + if (manifestURL) { + let appId = appsService.getAppLocalIdByManifestURL(manifestURL); + if (DEBUG) { + debug("get appId: " + appId + " from manifestURL: " + manifestURL); + } + + if (!appId) { + mm.sendAsyncMessage("NetworkStats:Get:Return", + { id: msg.id, error: "Invalid manifestURL", result: null }); + return; + } + + options.appId = appId; + options.manifestURL = manifestURL; + } + + if (DEBUG) { + debug("getStats for options: " + JSON.stringify(options)); + } + + if (!options.connectionType || options.connectionType.length == 0) { + this._db.findAll(function onStatsFound(error, result) { + mm.sendAsyncMessage("NetworkStats:Get:Return", + { id: msg.id, error: error, result: result }); + }, options); return; } - } - let start = new Date(msg.start); - let end = new Date(msg.end); + for (let i in this._connectionTypes) { + if (this._connectionTypes[i].name == options.connectionType) { + this._db.find(function onStatsFound(error, result) { + mm.sendAsyncMessage("NetworkStats:Get:Return", + { id: msg.id, error: error, result: result }); + }, options); + return; + } + } - this.updateStats(netId, function onStatsUpdated(aResult, aMessage) { - debug("getstats for network " + network.id + " of type " + network.type); - debug("appId: " + appId + " from manifestURL: " + manifestURL); + mm.sendAsyncMessage("NetworkStats:Get:Return", + { id: msg.id, error: "Invalid connectionType", result: null }); - self._db.find(function onStatsFound(aError, aResult) { - mm.sendAsyncMessage("NetworkStats:Get:Return", - { id: msg.id, error: aError, result: aResult }); - }, network, start, end, appId, manifestURL); - - }); - }, - - clearInterfaceStats: function clearInterfaceStats(mm, msg) { - let network = msg.network; - let netId = this.getNetworkId(network.id, network.type); - - debug("clear stats for network " + network.id + " of type " + network.type); - - if (!this._networks[netId]) { - mm.sendAsyncMessage("NetworkStats:Clear:Return", - { id: msg.id, error: "Invalid networkType", result: null }); - return; - } - - this._db.clearInterfaceStats(network, function onDBCleared(aError, aResult) { - mm.sendAsyncMessage("NetworkStats:Clear:Return", - { id: msg.id, error: aError, result: aResult }); - }); + }.bind(this)); }, clearDB: function clearDB(mm, msg) { - let networks = this.availableNetworks(); - this._db.clearStats(networks, function onDBCleared(aError, aResult) { - mm.sendAsyncMessage("NetworkStats:ClearAll:Return", - { id: msg.id, error: aError, result: aResult }); + this._db.clear(function onDBCleared(error, result) { + mm.sendAsyncMessage("NetworkStats:Clear:Return", + { id: msg.id, error: error, result: result }); }); }, - updateAllStats: function updateAllStats(aCallback) { + updateAllStats: function updateAllStats(callback) { // Update |cachedAppStats|. this.updateCachedAppStats(); @@ -318,19 +247,18 @@ this.NetworkStatsService = { // the connection type is already in the queue it is not appended again, // else it is pushed in 'elements' array, which later will be pushed to // the queue array. - for (let netId in this._networks) { - lastElement = { netId: netId, - queueIndex: this.updateQueueIndex(netId)}; - + for (let i in this._connectionTypes) { + lastElement = { type: i, + queueIndex: this.updateQueueIndex(i)}; if (lastElement.queueIndex == -1) { - elements.push({netId: lastElement.netId, callbacks: []}); + elements.push({type: lastElement.type, callbacks: []}); } } if (elements.length > 0) { // If length of elements is greater than 0, callback is set to // the last element. - elements[elements.length - 1].callbacks.push(aCallback); + elements[elements.length - 1].callbacks.push(callback); this.updateQueue = this.updateQueue.concat(elements); } else { // Else, it means that all connection types are already in the queue to @@ -338,14 +266,16 @@ this.NetworkStatsService = { // the element in the main queue with the index of the last 'lastElement'. // But before is checked that element is still in the queue because it can // be processed while generating 'elements' array. - let element = this.updateQueue[lastElement.queueIndex]; - if (aCallback && - (!element || element.netId != lastElement.netId)) { - aCallback(); + + if (!this.updateQueue[lastElement.queueIndex] || + this.updateQueue[lastElement.queueIndex].type != lastElement.queueIndex) { + if (callback) { + callback(); + } return; } - this.updateQueue[lastElement.queueIndex].callbacks.push(aCallback); + this.updateQueue[lastElement.queueIndex].callbacks.push(callback); } // Call the function that process the elements of the queue. @@ -356,14 +286,14 @@ this.NetworkStatsService = { } }, - updateStats: function updateStats(aNetId, aCallback) { - // Check if the connection is in the main queue, push a new element + updateStats: function updateStats(connectionType, callback) { + // Check if the connection type is in the main queue, push a new element // if it is not being processed or add a callback if it is. - let index = this.updateQueueIndex(aNetId); + let index = this.updateQueueIndex(connectionType); if (index == -1) { - this.updateQueue.push({netId: aNetId, callbacks: [aCallback]}); + this.updateQueue.push({type: connectionType, callbacks: [callback]}); } else { - this.updateQueue[index].callbacks.push(aCallback); + this.updateQueue[index].callbacks.push(callback); } // Call the function that process the elements of the queue. @@ -371,11 +301,16 @@ this.NetworkStatsService = { }, /* - * Find if a connection is in the main queue array and return its + * Find if a connection type is in the main queue array and return its * index, if it is not in the array return -1. */ - updateQueueIndex: function updateQueueIndex(aNetId) { - return this.updateQueue.map(function(e) { return e.netId; }).indexOf(aNetId); + updateQueueIndex: function updateQueueIndex(type) { + for (let i in this.updateQueue) { + if (this.updateQueue[i].type == type) { + return i; + } + } + return -1; }, /* @@ -412,64 +347,64 @@ this.NetworkStatsService = { } // Call the update function for the next element. - this.update(this.updateQueue[0].netId, this.processQueue.bind(this)); + this.update(this.updateQueue[0].type, this.processQueue.bind(this)); }, - update: function update(aNetId, aCallback) { + update: function update(connectionType, callback) { // Check if connection type is valid. - if (!this._networks[aNetId]) { - if (aCallback) { - aCallback(false, "Invalid network " + aNetId); + if (!this._connectionTypes[connectionType]) { + if (callback) { + callback(false, "Invalid network type " + connectionType); } return; } - let interfaceName = this._networks[aNetId].interfaceName; - debug("Update stats for " + interfaceName); + if (DEBUG) { + debug("Update stats for " + this._connectionTypes[connectionType].name); + } // Request stats to NetworkManager, which will get stats from netd, passing // 'networkStatsAvailable' as a callback. - if (interfaceName) { - networkManager.getNetworkInterfaceStats(interfaceName, - this.networkStatsAvailable.bind(this, aCallback, aNetId)); + let networkName = this._connectionTypes[connectionType].network.name; + if (networkName) { + networkManager.getNetworkInterfaceStats(networkName, + this.networkStatsAvailable.bind(this, callback, connectionType)); return; } - - if (aCallback) { - aCallback(true, "ok"); + if (callback) { + callback(true, "ok"); } }, /* * Callback of request stats. Store stats in database. */ - networkStatsAvailable: function networkStatsAvailable(aCallback, aNetId, - aResult, aRxBytes, - aTxBytes, aDate) { - if (!aResult) { - if (aCallback) { - aCallback(false, "Netd IPC error"); + networkStatsAvailable: function networkStatsAvailable(callback, connType, result, rxBytes, txBytes, date) { + if (!result) { + if (callback) { + callback(false, "Netd IPC error"); } return; } - let stats = { appId: 0, - networkId: this._networks[aNetId].network.id, - networkType: this._networks[aNetId].network.type, - date: aDate, - rxBytes: aTxBytes, - txBytes: aRxBytes }; + let stats = { appId: 0, + connectionType: this._connectionTypes[connType].name, + date: date, + rxBytes: rxBytes, + txBytes: txBytes }; - debug("Update stats for: " + JSON.stringify(stats)); - - this._db.saveStats(stats, function onSavedStats(aError, aResult) { - if (aCallback) { - if (aError) { - aCallback(false, aError); + if (DEBUG) { + debug("Update stats for " + stats.connectionType + ": rx=" + stats.rxBytes + + " tx=" + stats.txBytes + " timestamp=" + stats.date); + } + this._db.saveStats(stats, function onSavedStats(error, result) { + if (callback) { + if (error) { + callback(false, error); return; } - aCallback(true, "OK"); + callback(true, "OK"); } }); }, @@ -477,34 +412,26 @@ this.NetworkStatsService = { /* * Function responsible for receiving per-app stats. */ - saveAppStats: function saveAppStats(aAppId, aNetwork, aTimeStamp, aRxBytes, aTxBytes, aCallback) { - let netId = this.convertNetworkInterface(aNetwork); - if (!netId) { - if (aCallback) { - aCallback.notify(false, "Invalid network type"); - } - return; + saveAppStats: function saveAppStats(aAppId, aConnectionType, aTimeStamp, aRxBytes, aTxBytes, aCallback) { + if (DEBUG) { + debug("saveAppStats: " + aAppId + " " + aConnectionType + " " + + aTimeStamp + " " + aRxBytes + " " + aTxBytes); } - debug("saveAppStats: " + aAppId + " " + netId + " " + - aTimeStamp + " " + aRxBytes + " " + aTxBytes); - // Check if |aAppId| and |aConnectionType| are valid. - if (!aAppId || !this._networks[netId]) { - debug("Invalid appId or network interface"); + if (!aAppId || aConnectionType == NET_TYPE_UNKNOWN) { return; } let stats = { appId: aAppId, - networkId: this._networks[netId].network.id, - networkType: this._networks[netId].network.type, + connectionType: this._connectionTypes[aConnectionType].name, date: new Date(aTimeStamp), rxBytes: aRxBytes, txBytes: aTxBytes }; // Generate an unique key from |appId| and |connectionType|, // which is used to retrieve data in |cachedAppStats|. - let key = stats.appId + "" + netId; + let key = stats.appId + stats.connectionType; // |cachedAppStats| only keeps the data with the same date. // If the incoming date is different from |cachedAppStatsDate|, @@ -551,15 +478,19 @@ this.NetworkStatsService = { appStats.txBytes > MAX_CACHED_TRAFFIC) { this._db.saveStats(appStats, function (error, result) { - debug("Application stats inserted in indexedDB"); + if (DEBUG) { + debug("Application stats inserted in indexedDB"); + } } ); delete this.cachedAppStats[key]; } }, - updateCachedAppStats: function updateCachedAppStats(aCallback) { - debug("updateCachedAppStats: " + this.cachedAppStatsDate); + updateCachedAppStats: function updateCachedAppStats(callback) { + if (DEBUG) { + debug("updateCachedAppStats: " + this.cachedAppStatsDate); + } let stats = Object.keys(this.cachedAppStats); if (stats.length == 0) { @@ -578,16 +509,16 @@ this.NetworkStatsService = { if (index == stats.length - 1) { this.cachedAppStats = Object.create(null); - if (!aCallback) { + if (!callback) { return; } if (error) { - aCallback(false, error); + callback(false, error); return; } - aCallback(true, "ok"); + callback(true, "ok"); return; } @@ -603,17 +534,17 @@ this.NetworkStatsService = { }, logAllRecords: function logAllRecords() { - this._db.logAllRecords(function onResult(aError, aResult) { - if (aError) { - debug("Error: " + aError); + this._db.logAllRecords(function onResult(error, result) { + if (error) { + debug("Error: " + error); return; } debug("===== LOG ====="); - debug("There are " + aResult.length + " items"); - debug(JSON.stringify(aResult)); + debug("There are " + result.length + " items"); + debug(JSON.stringify(result)); }); - }, + } }; NetworkStatsService.init(); diff --git a/dom/network/src/NetworkStatsServiceProxy.js b/dom/network/src/NetworkStatsServiceProxy.js index 9bf93f29a4e3..084d5d1d930e 100644 --- a/dom/network/src/NetworkStatsServiceProxy.js +++ b/dom/network/src/NetworkStatsServiceProxy.js @@ -29,15 +29,15 @@ NetworkStatsServiceProxy.prototype = { * Function called in the protocol layer (HTTP, FTP, WebSocket ...etc) * to pass the per-app stats to NetworkStatsService. */ - saveAppStats: function saveAppStats(aAppId, aNetwork, aTimeStamp, + saveAppStats: function saveAppStats(aAppId, aConnectionType, aTimeStamp, aRxBytes, aTxBytes, aCallback) { if (DEBUG) { - debug("saveAppStats: " + aAppId + " connectionType " + aNetwork.type + - " " + aTimeStamp + " " + aRxBytes + " " + aTxBytes); + debug("saveAppStats: " + aAppId + " " + aConnectionType + " " + + aTimeStamp + " " + aRxBytes + " " + aTxBytes); } - NetworkStatsService.saveAppStats(aAppId, aNetwork, aTimeStamp, - aRxBytes, aTxBytes, aCallback); + NetworkStatsService.saveAppStats(aAppId, aConnectionType, aTimeStamp, + aRxBytes, aTxBytes, aCallback); }, classID : NETWORKSTATSSERVICEPROXY_CID, diff --git a/dom/network/src/TCPSocket.js b/dom/network/src/TCPSocket.js index b4e4cf5fb949..94b2a86fbd94 100644 --- a/dom/network/src/TCPSocket.js +++ b/dom/network/src/TCPSocket.js @@ -167,7 +167,7 @@ TCPSocket.prototype = { _txBytes: 0, _rxBytes: 0, _appId: Ci.nsIScriptSecurityManager.NO_APP_ID, - _activeNetwork: null, + _connectionType: Ci.nsINetworkInterface.NETWORK_TYPE_UNKNOWN, #endif // Public accessors. @@ -347,7 +347,7 @@ TCPSocket.prototype = { LOG("Error: Ci.nsINetworkStatsServiceProxy service is not available."); return; } - nssProxy.saveAppStats(this._appId, this._activeNetwork, Date.now(), + nssProxy.saveAppStats(this._appId, this._connectionType, Date.now(), this._rxBytes, this._txBytes); // Reset the counters once the statistics is saved to NetworkStatsServiceProxy. @@ -530,12 +530,12 @@ TCPSocket.prototype = { that._initStream(that._binaryType); #ifdef MOZ_WIDGET_GONK - // Set _activeNetwork, which is only required for network statistics. + // Set _connectionType, which is only required for network statistics. // Note that nsINetworkManager, as well as nsINetworkStatsServiceProxy, is // Gonk-specific. let networkManager = Cc["@mozilla.org/network/manager;1"].getService(Ci.nsINetworkManager); - if (networkManager) { - that._activeNetwork = networkManager.active; + if (networkManager && networkManager.active) { + that._connectionType = networkManager.active.type; } #endif diff --git a/dom/network/tests/test_networkstats_basics.html b/dom/network/tests/test_networkstats_basics.html index 171aa4eb79b0..22321bb40ed5 100644 --- a/dom/network/tests/test_networkstats_basics.html +++ b/dom/network/tests/test_networkstats_basics.html @@ -12,34 +12,45 @@

 
 
diff --git a/dom/network/tests/unit_stats/test_networkstats_db.js b/dom/network/tests/unit_stats/test_networkstats_db.js index b478f881dd90..b52d8f26e522 100644 --- a/dom/network/tests/unit_stats/test_networkstats_db.js +++ b/dom/network/tests/unit_stats/test_networkstats_db.js @@ -5,7 +5,7 @@ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; Cu.import("resource://gre/modules/NetworkStatsDB.jsm"); -const netStatsDb = new NetworkStatsDB(); +const netStatsDb = new NetworkStatsDB(this); function filterTimestamp(date) { var sampleRate = netStatsDb.sampleRate; @@ -13,15 +13,6 @@ function filterTimestamp(date) { return Math.floor((date.getTime() - offset) / sampleRate) * sampleRate; } -function getNetworks() { - return [{ id: '0', type: Ci.nsIDOMMozNetworkStatsManager.WIFI }, - { id: '1234', type: Ci.nsIDOMMozNetworkStatsManager.MOBILE }]; -} - -function compareNetworks(networkA, networkB) { - return (networkA[0] == networkB[0] && networkA[1] == networkB[1]); -} - add_test(function test_sampleRate() { var sampleRate = netStatsDb.sampleRate; do_check_true(sampleRate > 0); @@ -98,31 +89,20 @@ add_test(function test_fillResultSamples_noEmptyData() { }); add_test(function test_clear() { - var networks = getNetworks(); - netStatsDb.clearStats(networks, function (error, result) { - do_check_eq(error, null); - run_next_test(); - }); -}); - -add_test(function test_clear_interface() { - var networks = getNetworks(); - netStatsDb.clearInterfaceStats(networks[0], function (error, result) { + netStatsDb.clear(function (error, result) { do_check_eq(error, null); run_next_test(); }); }); add_test(function test_internalSaveStats_singleSample() { - var networks = getNetworks(); - - var stats = { appId: 0, - network: [networks[0].id, networks[0].type], - timestamp: Date.now(), - rxBytes: 0, - txBytes: 0, - rxTotalBytes: 1234, - txTotalBytes: 1234 }; + var stats = {appId: 0, + connectionType: "wifi", + timestamp: Date.now(), + rxBytes: 0, + txBytes: 0, + rxTotalBytes: 1234, + txTotalBytes: 1234}; netStatsDb.dbNewTxn("readwrite", function(txn, store) { netStatsDb._saveStats(txn, store, stats); @@ -133,7 +113,7 @@ add_test(function test_internalSaveStats_singleSample() { do_check_eq(error, null); do_check_eq(result.length, 1); do_check_eq(result[0].appId, stats.appId); - do_check_true(compareNetworks(result[0].network, stats.network)); + do_check_eq(result[0].connectionType, stats.connectionType); do_check_eq(result[0].timestamp, stats.timestamp); do_check_eq(result[0].rxBytes, stats.rxBytes); do_check_eq(result[0].txBytes, stats.txBytes); @@ -145,23 +125,19 @@ add_test(function test_internalSaveStats_singleSample() { }); add_test(function test_internalSaveStats_arraySamples() { - var networks = getNetworks(); - - netStatsDb.clearStats(networks, function (error, result) { + netStatsDb.clear(function (error, result) { do_check_eq(error, null); - var network = [networks[0].id, networks[0].type]; - var samples = 2; var stats = []; for (var i = 0; i < samples; i++) { - stats.push({ appId: 0, - network: network, - timestamp: Date.now() + (10 * i), - rxBytes: 0, - txBytes: 0, - rxTotalBytes: 1234, - txTotalBytes: 1234 }); + stats.push({appId: 0, + connectionType: "wifi", + timestamp: Date.now() + (10 * i), + rxBytes: 0, + txBytes: 0, + rxTotalBytes: 1234, + txTotalBytes: 1234}); } netStatsDb.dbNewTxn("readwrite", function(txn, store) { @@ -171,16 +147,12 @@ add_test(function test_internalSaveStats_arraySamples() { netStatsDb.logAllRecords(function(error, result) { do_check_eq(error, null); - - // Result has one sample more than samples because clear inserts - // an empty sample to keep totalBytes synchronized with netd counters - result.shift(); do_check_eq(result.length, samples); var success = true; - for (var i = 1; i < samples; i++) { + for (var i = 0; i < samples; i++) { if (result[i].appId != stats[i].appId || - !compareNetworks(result[i].network, stats[i].network) || + result[i].connectionType != stats[i].connectionType || result[i].timestamp != stats[i].timestamp || result[i].rxBytes != stats[i].rxBytes || result[i].txBytes != stats[i].txBytes || @@ -198,31 +170,28 @@ add_test(function test_internalSaveStats_arraySamples() { }); add_test(function test_internalRemoveOldStats() { - var networks = getNetworks(); - - netStatsDb.clearStats(networks, function (error, result) { + netStatsDb.clear(function (error, result) { do_check_eq(error, null); - var network = [networks[0].id, networks[0].type]; var samples = 10; var stats = []; for (var i = 0; i < samples - 1; i++) { - stats.push({ appId: 0, - network: network, timestamp: Date.now() + (10 * i), - rxBytes: 0, txBytes: 0, - rxTotalBytes: 1234, txTotalBytes: 1234 }); + stats.push({appId: 0, + connectionType: "wifi", timestamp: Date.now() + (10 * i), + rxBytes: 0, txBytes: 0, + rxTotalBytes: 1234, txTotalBytes: 1234}); } - stats.push({ appId: 0, - network: network, timestamp: Date.now() + (10 * samples), - rxBytes: 0, txBytes: 0, - rxTotalBytes: 1234, txTotalBytes: 1234 }); + stats.push({appId: 0, + connectionType: "wifi", timestamp: Date.now() + (10 * samples), + rxBytes: 0, txBytes: 0, + rxTotalBytes: 1234, txTotalBytes: 1234}); netStatsDb.dbNewTxn("readwrite", function(txn, store) { netStatsDb._saveStats(txn, store, stats); - var date = stats[stats.length - 1].timestamp + var date = stats[stats.length -1].timestamp + (netStatsDb.sampleRate * netStatsDb.maxStorageSamples - 1) - 1; - netStatsDb._removeOldStats(txn, store, 0, network, date); + netStatsDb._removeOldStats(txn, store, 0, "wifi", date); }, function(error, result) { do_check_eq(error, null); @@ -236,14 +205,14 @@ add_test(function test_internalRemoveOldStats() { }); }); -function processSamplesDiff(networks, lastStat, newStat, callback) { - netStatsDb.clearStats(networks, function (error, result){ +function processSamplesDiff(lastStat, newStat, callback) { + netStatsDb.clear(function (error, result){ do_check_eq(error, null); netStatsDb.dbNewTxn("readwrite", function(txn, store) { netStatsDb._saveStats(txn, store, lastStat); }, function(error, result) { netStatsDb.dbNewTxn("readwrite", function(txn, store) { - let request = store.index("network").openCursor(newStat.network, "prev"); + let request = store.index("connectionType").openCursor(newStat.connectionType, "prev"); request.onsuccess = function onsuccess(event) { let cursor = event.target.result; do_check_neq(cursor, null); @@ -261,26 +230,22 @@ function processSamplesDiff(networks, lastStat, newStat, callback) { } add_test(function test_processSamplesDiffSameSample() { - var networks = getNetworks(); - var network = [networks[0].id, networks[0].type]; - var sampleRate = netStatsDb.sampleRate; var date = filterTimestamp(new Date()); + var lastStat = {appId: 0, + connectionType: "wifi", timestamp: date, + rxBytes: 0, txBytes: 0, + rxTotalBytes: 1234, txTotalBytes: 1234}; - var lastStat = { appId: 0, - network: network, timestamp: date, - rxBytes: 0, txBytes: 0, - rxTotalBytes: 1234, txTotalBytes: 1234 }; + var newStat = {appId: 0, + connectionType: "wifi", timestamp: date, + rxBytes: 0, txBytes: 0, + rxTotalBytes: 2234, txTotalBytes: 2234}; - var newStat = { appId: 0, - network: network, timestamp: date, - rxBytes: 0, txBytes: 0, - rxTotalBytes: 2234, txTotalBytes: 2234 }; - - processSamplesDiff(networks, lastStat, newStat, function(result) { + processSamplesDiff(lastStat, newStat, function(result) { do_check_eq(result.length, 1); do_check_eq(result[0].appId, newStat.appId); - do_check_true(compareNetworks(result[0].network, newStat.network)); + do_check_eq(result[0].connectionType, newStat.connectionType); do_check_eq(result[0].timestamp, newStat.timestamp); do_check_eq(result[0].rxBytes, newStat.rxTotalBytes - lastStat.rxTotalBytes); do_check_eq(result[0].txBytes, newStat.txTotalBytes - lastStat.txTotalBytes); @@ -291,26 +256,22 @@ add_test(function test_processSamplesDiffSameSample() { }); add_test(function test_processSamplesDiffNextSample() { - var networks = getNetworks(); - var network = [networks[0].id, networks[0].type]; - var sampleRate = netStatsDb.sampleRate; var date = filterTimestamp(new Date()); + var lastStat = {appId: 0, + connectionType: "wifi", timestamp: date, + rxBytes: 0, txBytes: 0, + rxTotalBytes: 1234, txTotalBytes: 1234}; - var lastStat = { appId: 0, - network: network, timestamp: date, - rxBytes: 0, txBytes: 0, - rxTotalBytes: 1234, txTotalBytes: 1234 }; + var newStat = {appId: 0, + connectionType: "wifi", timestamp: date + sampleRate, + rxBytes: 0, txBytes: 0, + rxTotalBytes: 500, txTotalBytes: 500}; - var newStat = { appId: 0, - network: network, timestamp: date + sampleRate, - rxBytes: 0, txBytes: 0, - rxTotalBytes: 500, txTotalBytes: 500 }; - - processSamplesDiff(networks, lastStat, newStat, function(result) { + processSamplesDiff(lastStat, newStat, function(result) { do_check_eq(result.length, 2); do_check_eq(result[1].appId, newStat.appId); - do_check_true(compareNetworks(result[1].network, newStat.network)); + do_check_eq(result[1].connectionType, newStat.connectionType); do_check_eq(result[1].timestamp, newStat.timestamp); do_check_eq(result[1].rxBytes, newStat.rxTotalBytes); do_check_eq(result[1].txBytes, newStat.txTotalBytes); @@ -321,25 +282,23 @@ add_test(function test_processSamplesDiffNextSample() { }); add_test(function test_processSamplesDiffSamplesLost() { - var networks = getNetworks(); - var network = [networks[0].id, networks[0].type]; var samples = 5; var sampleRate = netStatsDb.sampleRate; var date = filterTimestamp(new Date()); - var lastStat = { appId: 0, - network: network, timestamp: date, - rxBytes: 0, txBytes: 0, - rxTotalBytes: 1234, txTotalBytes: 1234 }; + var lastStat = {appId: 0, + connectionType: "wifi", timestamp: date, + rxBytes: 0, txBytes: 0, + rxTotalBytes: 1234, txTotalBytes: 1234}; - var newStat = { appId: 0, - network: network, timestamp: date + (sampleRate * samples), - rxBytes: 0, txBytes: 0, - rxTotalBytes: 2234, txTotalBytes: 2234 }; + var newStat = {appId: 0, + connectionType: "wifi", timestamp: date + (sampleRate * samples), + rxBytes: 0, txBytes: 0, + rxTotalBytes: 2234, txTotalBytes: 2234}; - processSamplesDiff(networks, lastStat, newStat, function(result) { + processSamplesDiff(lastStat, newStat, function(result) { do_check_eq(result.length, samples + 1); do_check_eq(result[0].appId, newStat.appId); - do_check_true(compareNetworks(result[samples].network, newStat.network)); + do_check_eq(result[samples].connectionType, newStat.connectionType); do_check_eq(result[samples].timestamp, newStat.timestamp); do_check_eq(result[samples].rxBytes, newStat.rxTotalBytes - lastStat.rxTotalBytes); do_check_eq(result[samples].txBytes, newStat.txTotalBytes - lastStat.txTotalBytes); @@ -350,17 +309,13 @@ add_test(function test_processSamplesDiffSamplesLost() { }); add_test(function test_saveStats() { - var networks = getNetworks(); - var network = [networks[0].id, networks[0].type]; + var stats = {appId: 0, + connectionType: "wifi", + date: new Date(), + rxBytes: 2234, + txBytes: 2234}; - var stats = { appId: 0, - networkId: networks[0].id, - networkType: networks[0].type, - date: new Date(), - rxBytes: 2234, - txBytes: 2234}; - - netStatsDb.clearStats(networks, function (error, result) { + netStatsDb.clear(function (error, result) { do_check_eq(error, null); netStatsDb.saveStats(stats, function(error, result) { do_check_eq(error, null); @@ -368,7 +323,7 @@ add_test(function test_saveStats() { do_check_eq(error, null); do_check_eq(result.length, 1); do_check_eq(result[0].appId, stats.appId); - do_check_true(compareNetworks(result[0].network, network)); + do_check_eq(result[0].connectionType, stats.connectionType); let timestamp = filterTimestamp(stats.date); do_check_eq(result[0].timestamp, timestamp); do_check_eq(result[0].rxBytes, 0); @@ -382,44 +337,35 @@ add_test(function test_saveStats() { }); add_test(function test_saveAppStats() { - var networks = getNetworks(); - var network = [networks[0].id, networks[0].type]; + var stats = {appId: 1, + connectionType: "wifi", + date: new Date(), + rxBytes: 2234, + txBytes: 2234}; - var stats = { appId: 1, - networkId: networks[0].id, - networkType: networks[0].type, - date: new Date(), - rxBytes: 2234, - txBytes: 2234}; - - netStatsDb.clearStats(networks, function (error, result) { + netStatsDb.clear(function (error, result) { do_check_eq(error, null); netStatsDb.saveStats(stats, function(error, result) { do_check_eq(error, null); netStatsDb.logAllRecords(function(error, result) { do_check_eq(error, null); - // The clear function clears all records of the datbase but - // inserts a new element for each [appId, connectionId, connectionType] - // record to keep the track of rxTotalBytes / txTotalBytes. - // So at this point, we have two records, one for the appId 0 used in - // past tests and the new one for appId 1 - do_check_eq(result.length, 2); - do_check_eq(result[1].appId, stats.appId); - do_check_true(compareNetworks(result[1].network, network)); + do_check_eq(result.length, 1); + do_check_eq(result[0].appId, stats.appId); + do_check_eq(result[0].connectionType, stats.connectionType); let timestamp = filterTimestamp(stats.date); - do_check_eq(result[1].timestamp, timestamp); - do_check_eq(result[1].rxBytes, stats.rxBytes); - do_check_eq(result[1].txBytes, stats.txBytes); - do_check_eq(result[1].rxTotalBytes, 0); - do_check_eq(result[1].txTotalBytes, 0); + do_check_eq(result[0].timestamp, timestamp); + do_check_eq(result[0].rxBytes, stats.rxBytes); + do_check_eq(result[0].txBytes, stats.txBytes); + do_check_eq(result[0].rxTotalBytes, 0); + do_check_eq(result[0].txTotalBytes, 0); run_next_test(); }); }); }); }); -function prepareFind(network, stats, callback) { - netStatsDb.clearStats(network, function (error, result) { +function prepareFind(stats, callback) { + netStatsDb.clear(function (error, result) { do_check_eq(error, null); netStatsDb.dbNewTxn("readwrite", function(txn, store) { netStatsDb._saveStats(txn, store, stats); @@ -430,11 +376,6 @@ function prepareFind(network, stats, callback) { } add_test(function test_find () { - var networks = getNetworks(); - var networkWifi = [networks[0].id, networks[0].type]; - var networkMobile = [networks[1].id, networks[1].type]; // Fake mobile interface - var appId = 0; - var samples = 5; var sampleRate = netStatsDb.sampleRate; var start = Date.now(); @@ -443,39 +384,46 @@ add_test(function test_find () { start = new Date(start - sampleRate); var stats = []; for (var i = 0; i < samples; i++) { - stats.push({ appId: appId, - network: networkWifi, timestamp: saveDate + (sampleRate * i), - rxBytes: 0, txBytes: 10, - rxTotalBytes: 0, txTotalBytes: 0 }); + stats.push({appId: 0, + connectionType: "wifi", timestamp: saveDate + (sampleRate * i), + rxBytes: 0, txBytes: 10, + rxTotalBytes: 0, txTotalBytes: 0}); - stats.push({ appId: appId, - network: networkMobile, timestamp: saveDate + (sampleRate * i), - rxBytes: 0, txBytes: 10, - rxTotalBytes: 0, txTotalBytes: 0 }); + stats.push({appId: 0, + connectionType: "mobile", timestamp: saveDate + (sampleRate * i), + rxBytes: 0, txBytes: 10, + rxTotalBytes: 0, txTotalBytes: 0}); } - prepareFind(networks[0], stats, function(error, result) { + prepareFind(stats, function(error, result) { do_check_eq(error, null); netStatsDb.find(function (error, result) { do_check_eq(error, null); - do_check_eq(result.network.id, networks[0].id); - do_check_eq(result.network.type, networks[0].type); + do_check_eq(result.connectionType, "wifi"); do_check_eq(result.start.getTime(), start.getTime()); do_check_eq(result.end.getTime(), end.getTime()); do_check_eq(result.data.length, samples + 1); do_check_eq(result.data[0].rxBytes, null); do_check_eq(result.data[1].rxBytes, 0); do_check_eq(result.data[samples].rxBytes, 0); - run_next_test(); - }, networks[0], start, end, appId); + + netStatsDb.findAll(function (error, result) { + do_check_eq(error, null); + do_check_eq(result.connectionType, null); + do_check_eq(result.start.getTime(), start.getTime()); + do_check_eq(result.end.getTime(), end.getTime()); + do_check_eq(result.data.length, samples + 1); + do_check_eq(result.data[0].rxBytes, null); + do_check_eq(result.data[1].rxBytes, 0); + do_check_eq(result.data[1].txBytes, 20); + do_check_eq(result.data[samples].rxBytes, 0); + run_next_test(); + }, {appId: 0, start: start, end: end}); + }, {start: start, end: end, connectionType: "wifi", appId: 0}); }); }); add_test(function test_findAppStats () { - var networks = getNetworks(); - var networkWifi = [networks[0].id, networks[0].type]; - var networkMobile = [networks[1].id, networks[1].type]; // Fake mobile interface - var samples = 5; var sampleRate = netStatsDb.sampleRate; var start = Date.now(); @@ -484,63 +432,69 @@ add_test(function test_findAppStats () { start = new Date(start - sampleRate); var stats = []; for (var i = 0; i < samples; i++) { - stats.push({ appId: 1, - network: networkWifi, timestamp: saveDate + (sampleRate * i), - rxBytes: 0, txBytes: 10, - rxTotalBytes: 0, txTotalBytes: 0 }); + stats.push({appId: 1, + connectionType: "wifi", timestamp: saveDate + (sampleRate * i), + rxBytes: 0, txBytes: 10, + rxTotalBytes: 0, txTotalBytes: 0}); - stats.push({ appId: 1, - network: networkMobile, timestamp: saveDate + (sampleRate * i), - rxBytes: 0, txBytes: 10, - rxTotalBytes: 0, txTotalBytes: 0 }); + stats.push({appId: 1, + connectionType: "mobile", timestamp: saveDate + (sampleRate * i), + rxBytes: 0, txBytes: 10, + rxTotalBytes: 0, txTotalBytes: 0}); } - prepareFind(networks[0], stats, function(error, result) { + prepareFind(stats, function(error, result) { do_check_eq(error, null); netStatsDb.find(function (error, result) { do_check_eq(error, null); - do_check_eq(result.network.id, networks[0].id); - do_check_eq(result.network.type, networks[0].type); + do_check_eq(result.connectionType, "wifi"); do_check_eq(result.start.getTime(), start.getTime()); do_check_eq(result.end.getTime(), end.getTime()); do_check_eq(result.data.length, samples + 1); do_check_eq(result.data[0].rxBytes, null); do_check_eq(result.data[1].rxBytes, 0); do_check_eq(result.data[samples].rxBytes, 0); - run_next_test(); - }, networks[0], start, end, 1); + + netStatsDb.findAll(function (error, result) { + do_check_eq(error, null); + do_check_eq(result.connectionType, null); + do_check_eq(result.start.getTime(), start.getTime()); + do_check_eq(result.end.getTime(), end.getTime()); + do_check_eq(result.data.length, samples + 1); + do_check_eq(result.data[0].rxBytes, null); + do_check_eq(result.data[1].rxBytes, 0); + do_check_eq(result.data[1].txBytes, 20); + do_check_eq(result.data[samples].rxBytes, 0); + run_next_test(); + }, {start: start, end: end, appId: 1}); + }, {start: start, end: end, connectionType: "wifi", appId: 1}); }); }); add_test(function test_saveMultipleAppStats () { - var networks = getNetworks(); - var networkWifi = networks[0]; - var networkMobile = networks[1]; // Fake mobile interface - var saveDate = filterTimestamp(new Date()); var cached = Object.create(null); - cached['1wifi'] = { appId: 1, date: new Date(), - networkId: networkWifi.id, networkType: networkWifi.type, - rxBytes: 0, txBytes: 10 }; + cached['1wifi'] = {appId: 1, + connectionType: "wifi", date: new Date(), + rxBytes: 0, txBytes: 10}; - cached['1mobile'] = { appId: 1, date: new Date(), - networkId: networkMobile.id, networkType: networkMobile.type, - rxBytes: 0, txBytes: 10 }; + cached['1mobile'] = {appId: 1, + connectionType: "mobile", date: new Date(), + rxBytes: 0, txBytes: 10}; - cached['2wifi'] = { appId: 2, date: new Date(), - networkId: networkWifi.id, networkType: networkWifi.type, - rxBytes: 0, txBytes: 10 }; + cached['2wifi'] = {appId: 2, + connectionType: "wifi", date: new Date(), + rxBytes: 0, txBytes: 10}; - cached['2mobile'] = { appId: 2, date: new Date(), - networkId: networkMobile.id, networkType: networkMobile.type, - rxBytes: 0, txBytes: 10 }; + cached['2mobile'] = {appId: 2, + connectionType: "mobile", date: new Date(), + rxBytes: 0, txBytes: 10}; let keys = Object.keys(cached); let index = 0; - networks.push(networkMobile); - netStatsDb.clearStats(networks, function (error, result) { + netStatsDb.clear(function (error, result) { do_check_eq(error, null); netStatsDb.saveStats(cached[keys[index]], function callback(error, result) { @@ -548,17 +502,10 @@ add_test(function test_saveMultipleAppStats () { if (index == keys.length - 1) { netStatsDb.logAllRecords(function(error, result) { - // Again, result has two samples more than expected samples because - // clear inserts one empty sample for each network to keep totalBytes - // synchronized with netd counters. so the first two samples have to - // be discarted. - result.shift(); - result.shift(); - do_check_eq(error, null); do_check_eq(result.length, 4); do_check_eq(result[0].appId, 1); - do_check_true(compareNetworks(result[0].network,[networkWifi.id, networkWifi.type])); + do_check_eq(result[0].connectionType, 'mobile'); do_check_eq(result[0].rxBytes, 0); do_check_eq(result[0].txBytes, 10); run_next_test(); diff --git a/dom/network/tests/unit_stats/test_networkstats_service.js b/dom/network/tests/unit_stats/test_networkstats_service.js index d38906802739..569b2f7ba0d1 100644 --- a/dom/network/tests/unit_stats/test_networkstats_service.js +++ b/dom/network/tests/unit_stats/test_networkstats_service.js @@ -4,56 +4,48 @@ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; add_test(function test_clearDB() { - var networks = NetworkStatsService.availableNetworks(); - NetworkStatsService._db.clearStats(networks, function onDBCleared(error, result) { + NetworkStatsService._db.clear(function onDBCleared(error, result) { do_check_eq(result, null); run_next_test(); }); }); -function getNetworkId() { - var network = (NetworkStatsService.availableNetworks())[0]; - return NetworkStatsService.getNetworkId(network.id, network.type); -} add_test(function test_networkStatsAvailable_ok() { - var netId = getNetworkId(); NetworkStatsService.networkStatsAvailable(function (success, msg) { do_check_eq(success, true); run_next_test(); - }, netId, true, 1234, 4321, new Date()); + }, Ci.nsINetworkInterface.NETWORK_TYPE_WIFI, true, 1234, 4321, new Date()); }); add_test(function test_networkStatsAvailable_failure() { - var netId = getNetworkId(); NetworkStatsService.networkStatsAvailable(function (success, msg) { do_check_eq(success, false); run_next_test(); - }, netId, false, 1234, 4321, new Date()); + }, Ci.nsINetworkInterface.NETWORK_TYPE_WIFI, false, 1234, 4321, new Date()); }); -add_test(function test_update_invalidNetwork() { +add_test(function test_update_invalidConnection() { NetworkStatsService.update(-1, function (success, msg) { do_check_eq(success, false); - do_check_eq(msg, "Invalid network -1"); + do_check_eq(msg, "Invalid network type -1"); run_next_test(); }); }); add_test(function test_update() { - var netId = getNetworkId(); - NetworkStatsService.update(netId, function (success, msg) { + NetworkStatsService.update(Ci.nsINetworkInterface.NETWORK_TYPE_WIFI, function (success, msg) { do_check_eq(success, true); run_next_test(); }); }); add_test(function test_updateQueueIndex() { - NetworkStatsService.updateQueue = [{netId: 0, callbacks: null}, - {netId: 1, callbacks: null}, - {netId: 2, callbacks: null}, - {netId: 3, callbacks: null}, - {netId: 4, callbacks: null}]; + NetworkStatsService.updateQueue = [{type: 0, callbacks: null}, + {type: 1, callbacks: null}, + {type: 2, callbacks: null}, + {type: 3, callbacks: null}, + {type: 4, callbacks: null}]; var index = NetworkStatsService.updateQueueIndex(3); do_check_eq(index, 3); index = NetworkStatsService.updateQueueIndex(10); @@ -71,8 +63,7 @@ add_test(function test_updateAllStats() { }); add_test(function test_updateStats_ok() { - var netId = getNetworkId(); - NetworkStatsService.updateStats(netId, function(success, msg){ + NetworkStatsService.updateStats(Ci.nsINetworkInterface.NETWORK_TYPE_WIFI, function(success, msg){ do_check_eq(success, true); run_next_test(); }); @@ -86,20 +77,15 @@ add_test(function test_updateStats_failure() { }); add_test(function test_queue() { - // Fill networks with fake network interfaces + // Fill connections with fake network interfaces (wlan0 and rmnet0) // to enable netd async requests - var network = {id: "1234", type: Ci.nsIDOMMozNetworkStatsManager.MOBILE}; - var netId1 = NetworkStatsService.getNetworkId(network.id, network.type); - NetworkStatsService._networks[netId1] = { network: network, - interfaceName: "net1" }; + NetworkStatsService._connectionTypes[Ci.nsINetworkInterface.NETWORK_TYPE_WIFI] + .network.name = 'wlan0'; + NetworkStatsService._connectionTypes[Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE] + .network.name = 'rmnet0'; - network = {id: "5678", type: Ci.nsIDOMMozNetworkStatsManager.MOBILE}; - var netId2 = NetworkStatsService.getNetworkId(network.id, network.type); - NetworkStatsService._networks[netId2] = { network: network, - interfaceName: "net2" }; - - NetworkStatsService.updateStats(netId1); - NetworkStatsService.updateStats(netId2); + NetworkStatsService.updateStats(Ci.nsINetworkInterface.NETWORK_TYPE_WIFI); + NetworkStatsService.updateStats(Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE); do_check_eq(NetworkStatsService.updateQueue.length, 2); do_check_eq(NetworkStatsService.updateQueue[0].callbacks.length, 1); @@ -107,8 +93,8 @@ add_test(function test_queue() { return; }; - NetworkStatsService.updateStats(netId1, callback); - NetworkStatsService.updateStats(netId2, callback); + NetworkStatsService.updateStats(Ci.nsINetworkInterface.NETWORK_TYPE_WIFI, callback); + NetworkStatsService.updateStats(Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE, callback); do_check_eq(NetworkStatsService.updateQueue.length, 2); do_check_eq(NetworkStatsService.updateQueue[0].callbacks.length, 2); diff --git a/dom/network/tests/unit_stats/test_networkstats_service_proxy.js b/dom/network/tests/unit_stats/test_networkstats_service_proxy.js index d75ce0630281..8c97ec92f0a3 100644 --- a/dom/network/tests/unit_stats/test_networkstats_service_proxy.js +++ b/dom/network/tests/unit_stats/test_networkstats_service_proxy.js @@ -9,66 +9,33 @@ XPCOMUtils.defineLazyServiceGetter(this, "nssProxy", "@mozilla.org/networkstatsServiceProxy;1", "nsINetworkStatsServiceProxy"); -function mokConvertNetworkInterface() { - NetworkStatsService.convertNetworkInterface = function(aNetwork) { - if (aNetwork.type != Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE && - aNetwork.type != Ci.nsINetworkInterface.NETWORK_TYPE_WIFI) { - return null; - } - - let id = '0'; - if (aNetwork.type == Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE) { - id = '1234' - } - - let netId = this.getNetworkId(id, aNetwork.type); - - if (!this._networks[netId]) { - this._networks[netId] = Object.create(null); - this._networks[netId].network = { id: id, - type: aNetwork.type }; - } - - return netId; - }; -} - add_test(function test_saveAppStats() { var cachedAppStats = NetworkStatsService.cachedAppStats; var timestamp = NetworkStatsService.cachedAppStatsDate.getTime(); var samples = 5; - // Create to fake nsINetworkInterfaces. As nsINetworkInterface can not - // be instantiated, these two vars will emulate it by filling the properties - // that will be used. - var wifi = {type: Ci.nsINetworkInterface.NETWORK_TYPE_WIFI, id: "0"}; - var mobile = {type: Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE, id: "1234"}; - - // Insert fake mobile network interface in NetworkStatsService - var mobileNetId = NetworkStatsService.getNetworkId(mobile.id, mobile.type); - do_check_eq(Object.keys(cachedAppStats).length, 0); for (var i = 0; i < samples; i++) { - nssProxy.saveAppStats(1, wifi, timestamp, 10, 20); + nssProxy.saveAppStats(1, Ci.nsINetworkInterface.NETWORK_TYPE_WIFI, + timestamp, 10, 20); - nssProxy.saveAppStats(1, mobile, timestamp, 10, 20); + nssProxy.saveAppStats(1, Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE, + timestamp, 10, 20); } - var key1 = 1 + NetworkStatsService.getNetworkId(wifi.id, wifi.type); - var key2 = 1 + mobileNetId; + var key1 = 1 + 'wifi'; + var key2 = 1 + 'mobile'; do_check_eq(Object.keys(cachedAppStats).length, 2); do_check_eq(cachedAppStats[key1].appId, 1); - do_check_eq(cachedAppStats[key1].networkId, wifi.id); - do_check_eq(cachedAppStats[key1].networkType, wifi.type); + do_check_eq(cachedAppStats[key1].connectionType, 'wifi'); do_check_eq(new Date(cachedAppStats[key1].date).getTime() / 1000, Math.floor(timestamp / 1000)); do_check_eq(cachedAppStats[key1].rxBytes, 50); do_check_eq(cachedAppStats[key1].txBytes, 100); do_check_eq(cachedAppStats[key2].appId, 1); - do_check_eq(cachedAppStats[key2].networkId, mobile.id); - do_check_eq(cachedAppStats[key2].networkType, mobile.type); + do_check_eq(cachedAppStats[key2].connectionType, 'mobile'); do_check_eq(new Date(cachedAppStats[key2].date).getTime() / 1000, Math.floor(timestamp / 1000)); do_check_eq(cachedAppStats[key2].rxBytes, 50); @@ -80,11 +47,7 @@ add_test(function test_saveAppStats() { add_test(function test_saveAppStatsWithDifferentDates() { var today = NetworkStatsService.cachedAppStatsDate; var tomorrow = new Date(today.getTime() + (24 * 60 * 60 * 1000)); - - var wifi = {type: Ci.nsINetworkInterface.NETWORK_TYPE_WIFI, id: "0"}; - var mobile = {type: Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE, id: "1234"}; - - var key = 1 + NetworkStatsService.getNetworkId(wifi.id, wifi.type); + var key = 1 + 'wifi'; NetworkStatsService.updateCachedAppStats( function (success, msg) { @@ -92,20 +55,21 @@ add_test(function test_saveAppStatsWithDifferentDates() { do_check_eq(Object.keys(NetworkStatsService.cachedAppStats).length, 0); - nssProxy.saveAppStats(1, wifi, today.getTime(), 10, 20); + nssProxy.saveAppStats(1, Ci.nsINetworkInterface.NETWORK_TYPE_WIFI, + today.getTime(), 10, 20); - nssProxy.saveAppStats(1, mobile, today.getTime(), 10, 20); + nssProxy.saveAppStats(1, Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE, + today.getTime(), 10, 20); var saveAppStatsCb = { notify: function notify(success, message) { do_check_eq(success, true); var cachedAppStats = NetworkStatsService.cachedAppStats; - var key = 2 + NetworkStatsService.getNetworkId(mobile.id, mobile.type); + var key = 2 + 'mobile'; do_check_eq(Object.keys(cachedAppStats).length, 1); do_check_eq(cachedAppStats[key].appId, 2); - do_check_eq(cachedAppStats[key].networkId, mobile.id); - do_check_eq(cachedAppStats[key].networkType, mobile.type); + do_check_eq(cachedAppStats[key].connectionType, 'mobile'); do_check_eq(new Date(cachedAppStats[key].date).getTime() / 1000, Math.floor(tomorrow.getTime() / 1000)); do_check_eq(cachedAppStats[key].rxBytes, 30); @@ -115,7 +79,8 @@ add_test(function test_saveAppStatsWithDifferentDates() { } }; - nssProxy.saveAppStats(2, mobile, tomorrow.getTime(), 30, 40, saveAppStatsCb); + nssProxy.saveAppStats(2, Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE, + tomorrow.getTime(), 30, 40, saveAppStatsCb); } ); }); @@ -123,7 +88,6 @@ add_test(function test_saveAppStatsWithDifferentDates() { add_test(function test_saveAppStatsWithMaxCachedTraffic() { var timestamp = NetworkStatsService.cachedAppStatsDate.getTime(); var maxtraffic = NetworkStatsService.maxCachedTraffic; - var wifi = {type: Ci.nsINetworkInterface.NETWORK_TYPE_WIFI, id: "0"}; NetworkStatsService.updateCachedAppStats( function (success, msg) { @@ -132,11 +96,13 @@ add_test(function test_saveAppStatsWithMaxCachedTraffic() { var cachedAppStats = NetworkStatsService.cachedAppStats; do_check_eq(Object.keys(cachedAppStats).length, 0); - nssProxy.saveAppStats(1, wifi, timestamp, 10, 20); + nssProxy.saveAppStats(1, Ci.nsINetworkInterface.NETWORK_TYPE_WIFI, + timestamp, 10, 20); do_check_eq(Object.keys(cachedAppStats).length, 1); - nssProxy.saveAppStats(1, wifi, timestamp, maxtraffic, 20); + nssProxy.saveAppStats(1, Ci.nsINetworkInterface.NETWORK_TYPE_WIFI, + timestamp, maxtraffic, 20); do_check_eq(Object.keys(cachedAppStats).length, 0); @@ -149,9 +115,5 @@ function run_test() { Cu.import("resource://gre/modules/NetworkStatsService.jsm"); - // Function convertNetworkInterface of NetworkStatsService causes errors when dealing - // with RIL to get the iccid, so overwrite it. - mokConvertNetworkInterface(); - run_next_test(); } diff --git a/netwerk/protocol/websocket/WebSocketChannel.cpp b/netwerk/protocol/websocket/WebSocketChannel.cpp index ca4783ed584b..04bbfa963b7c 100644 --- a/netwerk/protocol/websocket/WebSocketChannel.cpp +++ b/netwerk/protocol/websocket/WebSocketChannel.cpp @@ -53,6 +53,7 @@ #include #ifdef MOZ_WIDGET_GONK +#include "nsINetworkManager.h" #include "nsINetworkStatsServiceProxy.h" #endif @@ -966,6 +967,7 @@ WebSocketChannel::WebSocketChannel() : mCountRecv(0), mCountSent(0), mAppId(0), + mConnectionType(NETWORK_NO_TYPE), mIsInBrowser(false) { NS_ABORT_IF_FALSE(NS_IsMainThread(), "not main thread"); @@ -1080,9 +1082,9 @@ WebSocketChannel::BeginOpen() NS_GetAppInfo(localChannel, &mAppId, &mIsInBrowser); } - // obtain active network + // obtain active connection type if (mAppId != NECKO_NO_APP_ID) { - GetActiveNetwork(); + GetConnectionType(&mConnectionType); } rv = localChannel->AsyncOpen(this, mHttpChannel); @@ -3272,7 +3274,7 @@ WebSocketChannel::OnDataAvailable(nsIRequest *aRequest, } nsresult -WebSocketChannel::GetActiveNetwork() +WebSocketChannel::GetConnectionType(int32_t *type) { #ifdef MOZ_WIDGET_GONK MOZ_ASSERT(NS_IsMainThread()); @@ -3281,11 +3283,15 @@ WebSocketChannel::GetActiveNetwork() nsCOMPtr networkManager = do_GetService("@mozilla.org/network/manager;1", &result); if (NS_FAILED(result) || !networkManager) { - mActiveNetwork = nullptr; - return NS_ERROR_UNEXPECTED; + *type = NETWORK_NO_TYPE; } - result = networkManager->GetActive(getter_AddRefs(mActiveNetwork)); + nsCOMPtr networkInterface; + result = networkManager->GetActive(getter_AddRefs(networkInterface)); + + if (networkInterface) { + result = networkInterface->GetType(type); + } return NS_OK; #else @@ -3297,8 +3303,9 @@ nsresult WebSocketChannel::SaveNetworkStats(bool enforce) { #ifdef MOZ_WIDGET_GONK - // Check if the active network and app id are valid. - if(!mActiveNetwork || mAppId == NECKO_NO_APP_ID) { + // Check if the connection type and app id are valid. + if(mConnectionType == NETWORK_NO_TYPE || + mAppId == NECKO_NO_APP_ID) { return NS_OK; } @@ -3322,7 +3329,7 @@ WebSocketChannel::SaveNetworkStats(bool enforce) return rv; } - mNetworkStatsServiceProxy->SaveAppStats(mAppId, mActiveNetwork, PR_Now() / 1000, + mNetworkStatsServiceProxy->SaveAppStats(mAppId, mConnectionType, PR_Now() / 1000, mCountRecv, mCountSent, nullptr); // Reset the counters after saving. diff --git a/netwerk/protocol/websocket/WebSocketChannel.h b/netwerk/protocol/websocket/WebSocketChannel.h index 745a95e479a4..9b62fcc16692 100644 --- a/netwerk/protocol/websocket/WebSocketChannel.h +++ b/netwerk/protocol/websocket/WebSocketChannel.h @@ -18,10 +18,6 @@ #include "nsIHttpChannelInternal.h" #include "BaseWebSocketChannel.h" -#ifdef MOZ_WIDGET_GONK -#include "nsINetworkManager.h" -#endif - #include "nsCOMPtr.h" #include "nsString.h" #include "nsDeque.h" @@ -258,17 +254,16 @@ private: // These members are used for network per-app metering (bug 855949) // Currently, they are only available on gonk. public: + const static int32_t NETWORK_NO_TYPE = -1; // default conntection type const static uint64_t NETWORK_STATS_THRESHOLD = 65536; private: uint64_t mCountRecv; uint64_t mCountSent; uint32_t mAppId; + int32_t mConnectionType; bool mIsInBrowser; -#ifdef MOZ_WIDGET_GONK - nsCOMPtr mActiveNetwork; -#endif - nsresult GetActiveNetwork(); + nsresult GetConnectionType(int32_t *); nsresult SaveNetworkStats(bool); void CountRecvBytes(uint64_t recvBytes) { From bdfe7c16bbfd17c81f073295b49204b8f9ac2de2 Mon Sep 17 00:00:00 2001 From: Gaia Pushbot Date: Wed, 16 Oct 2013 14:55:25 -0700 Subject: [PATCH 11/26] Bumping gaia.json for 2 gaia-central revision(s) a=gaia-bump ======== https://hg.mozilla.org/integration/gaia-central/rev/7699ac11141b Author: Kyle Machulis Desc: Merge pull request #12324 from qdot/915002-notification-dir Bug 915002 - Integration and Notification Tests for Notification BiDi Rendering ======== https://hg.mozilla.org/integration/gaia-central/rev/9fa542f2c22f Author: Kyle Machulis Desc: Bug 915002 - Add language/direction unit/integration tests for notifications --- b2g/config/gaia.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index 4b4543e069b3..5298150d895a 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -1,4 +1,4 @@ { - "revision": "38632017a95b76f8b74544e7f94237368241b8af", + "revision": "7699ac11141b025b6fb0beddec16783d53a1e0a5", "repo_path": "/integration/gaia-central" } From 0475b4f9c11ec0f98da5f5017b5e8db85906d90f Mon Sep 17 00:00:00 2001 From: Gaia Pushbot Date: Wed, 16 Oct 2013 16:05:24 -0700 Subject: [PATCH 12/26] Bumping gaia.json for 1 gaia-central revision(s) a=gaia-bump ======== https://hg.mozilla.org/integration/gaia-central/rev/62376ef5e1dd Author: Kyle Machulis Desc: Bug 927642 - Fix dir/lang integration tests for notifications --- b2g/config/gaia.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index 5298150d895a..e722d61ee69d 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -1,4 +1,4 @@ { - "revision": "7699ac11141b025b6fb0beddec16783d53a1e0a5", + "revision": "62376ef5e1ddb2b46557a37315054a1cb1b6fbe9", "repo_path": "/integration/gaia-central" } From 8fb91d666a0bf304ba31863655c00e9dac5dff2e Mon Sep 17 00:00:00 2001 From: Gaia Pushbot Date: Wed, 16 Oct 2013 16:15:25 -0700 Subject: [PATCH 13/26] Bumping gaia.json for 1 gaia-central revision(s) a=gaia-bump ======== https://hg.mozilla.org/integration/gaia-central/rev/5e851aef150b Author: Kyle Machulis Desc: Revert "Bug 927642 - Fix dir/lang integration tests for notifications" This reverts commit 9fc08c1790d49b0cb64e0c8e04da141cbd2fd2a1. --- b2g/config/gaia.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index e722d61ee69d..2ed75f789326 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -1,4 +1,4 @@ { - "revision": "62376ef5e1ddb2b46557a37315054a1cb1b6fbe9", + "revision": "5e851aef150b0c7d4236a89dd2e11e3320412ccc", "repo_path": "/integration/gaia-central" } From febecd8854ccc99e64f8740eed9ce93542da5efc Mon Sep 17 00:00:00 2001 From: Sam Foster Date: Wed, 16 Oct 2013 16:17:17 -0700 Subject: [PATCH 14/26] Bug 925425 - richgrid slots implementation, revised tests. r=mbrubeck --- browser/metro/base/content/Site.js | 4 +- browser/metro/base/content/bindings/grid.xml | 176 +++++++++++--- browser/metro/base/content/browser.css | 3 + .../base/content/startui/BookmarksView.js | 11 +- .../metro/base/content/startui/HistoryView.js | 11 +- .../base/content/startui/RemoteTabsView.js | 1 + browser/metro/base/content/startui/Start.xul | 30 ++- .../base/content/startui/TopSitesView.js | 17 +- .../base/tests/mochitest/browser_history.js | 13 +- .../base/tests/mochitest/browser_tilegrid.xul | 5 +- .../base/tests/mochitest/browser_tiles.js | 225 ++++++++++++++++-- browser/metro/theme/tiles.css | 20 ++ 12 files changed, 444 insertions(+), 72 deletions(-) diff --git a/browser/metro/base/content/Site.js b/browser/metro/base/content/Site.js index 8ebc4fc4cc9b..6907a2afe6c6 100644 --- a/browser/metro/base/content/Site.js +++ b/browser/metro/base/content/Site.js @@ -9,7 +9,7 @@ * link parameter/model object expected to have a .url property, and optionally .title */ function Site(aLink) { - if(!aLink.url) { + if (!aLink.url) { throw Cr.NS_ERROR_INVALID_ARG; } this._link = aLink; @@ -64,7 +64,7 @@ Site.prototype = { } } // is binding already applied? - if (aNode.refresh) { + if ('refresh' in aNode) { // just update it aNode.refresh(); } else { diff --git a/browser/metro/base/content/bindings/grid.xml b/browser/metro/base/content/bindings/grid.xml index ba2a68a38362..3f7d8a048e5c 100644 --- a/browser/metro/base/content/bindings/grid.xml +++ b/browser/metro/base/content/bindings/grid.xml @@ -27,7 +27,7 @@ null - + @@ -96,7 +96,7 @@ @@ -204,28 +204,105 @@ + + cnode.getAttribute("value")); + ]]> + + + + slotCount) { + let orphanNode = child; + child = orphanNode.nextSibling; + this.removeChild(orphanNode); + continue; + } + if (child.hasAttribute("value")) { + this._releaseSlot(child); + } + child = child.nextSibling; + childIndex++; } + // create our quota of item slots + for (let count = this.childElementCount; count < slotCount; count++) { + this.appendChild( this._createItemElement() ); + } + if (!aSkipArrange) this.arrangeItems(); ]]> + + + + 0; count--) { + this.appendChild( this._createItemElement() ); + } + return this.children[anIndex]; + ]]> + + + + + + + + + + + + + + + + @@ -233,19 +310,30 @@ 0 && !this.children[childIndex-1].hasAttribute("value")) { + insertedItem = this.children[childIndex-1]; + } else { + insertedItem = this.insertBefore(this._createItemElement(),existing); + } } + if (!insertedItem) { + insertedItem = this._slotAt(anIndex); + } + insertedItem.setAttribute("value", aValue); + insertedItem.setAttribute("label", aLabel); if (!aSkipArrange) this.arrangeItems(); - return addition; + return insertedItem; ]]> + @@ -266,7 +354,13 @@ null 0 5 + @@ -453,6 +548,7 @@ let itemDims = this._itemSize; let containerDims = this._containerSize; + let slotsCount = this.childElementCount; // reset the flags if (this._scheduledArrangeItemsTimerId) { @@ -468,25 +564,25 @@ if (this.hasAttribute("vertical")) { this._columnCount = Math.floor(containerDims.width / itemDims.width) || 1; - this._rowCount = Math.floor(this.itemCount / this._columnCount); + this._rowCount = Math.floor(slotsCount / this._columnCount); } else { - // We favor overflowing horizontally, not vertically (rows then colums) - // rows attribute = max rows - let maxRowCount = Math.min(this.getAttribute("rows") || Infinity, Math.floor(containerDims.height / itemDims.height)); - this._rowCount = Math.min(this.itemCount, maxRowCount); + // rows attribute is fixed number of rows + let maxRows = Math.floor(containerDims.height / itemDims.height); + this._rowCount = this.getAttribute("rows") ? + // fit indicated rows when possible + Math.min(maxRows, this.getAttribute("rows")) : + // at least 1 row + Math.min(maxRows, slotsCount) || 1; - // columns attribute = min cols - this._columnCount = this.itemCount ? - Math.max( - // at least 1 column when there are items - this.getAttribute("columns") || 1, - Math.ceil(this.itemCount / this._rowCount) - ) : this.getAttribute("columns") || 0; + // columns attribute is min number of cols + this._columnCount = Math.ceil(slotsCount / this._rowCount) || 1; + if (this.getAttribute("columns")) { + this._columnCount = Math.max(this._columnCount, this.getAttribute("columns")); + } } // width is typically auto, cap max columns by truncating items collection // or, setting max-width style property with overflow hidden - // '0' is an invalid value, just leave the property unset when 0 columns if (this._columnCount) { gridStyle.MozColumnCount = this._columnCount; } @@ -521,6 +617,12 @@ + @@ -626,7 +729,7 @@ if (aLabel) { item.setAttribute("label", aLabel); } - if(this.hasAttribute("tiletype")) { + if (this.hasAttribute("tiletype")) { item.setAttribute("tiletype", this.getAttribute("tiletype")); } return item; @@ -704,6 +807,7 @@ aItem.setAttribute("bending", true); ]]> + @@ -967,4 +1071,10 @@ + + + + + + diff --git a/browser/metro/base/content/browser.css b/browser/metro/base/content/browser.css index 6048bdd3307d..fc35b3189265 100644 --- a/browser/metro/base/content/browser.css +++ b/browser/metro/base/content/browser.css @@ -119,6 +119,9 @@ richgrid { } richgriditem { + -moz-binding: url("chrome://browser/content/bindings/grid.xml#richgrid-empty-item"); +} +richgriditem[value] { -moz-binding: url("chrome://browser/content/bindings/grid.xml#richgrid-item"); } diff --git a/browser/metro/base/content/startui/BookmarksView.js b/browser/metro/base/content/startui/BookmarksView.js index a19008ca6281..de3520638d4a 100644 --- a/browser/metro/base/content/startui/BookmarksView.js +++ b/browser/metro/base/content/startui/BookmarksView.js @@ -73,11 +73,11 @@ BookmarksView.prototype = Util.extend(Object.create(View.prototype), { }, _getItemForBookmarkId: function bv__getItemForBookmark(aBookmarkId) { - return this._set.querySelector("richgriditem[bookmarkId='" + aBookmarkId + "']"); + return this._set.querySelector("richgriditem[anonid='" + aBookmarkId + "']"); }, _getBookmarkIdForItem: function bv__getBookmarkForItem(aItem) { - return +aItem.getAttribute("bookmarkId"); + return +aItem.getAttribute("anonid"); }, _updateItemWithAttrs: function dv__updateItemWithAttrs(anItem, aAttrs) { @@ -142,6 +142,7 @@ BookmarksView.prototype = Util.extend(Object.create(View.prototype), { this._set.removeItemAt(this._set.itemCount - 1, true); } this._set.arrangeItems(); + this._set.removeAttribute("fade"); this._inBatch = false; rootNode.containerOpen = false; }, @@ -154,7 +155,8 @@ BookmarksView.prototype = Util.extend(Object.create(View.prototype), { }, clearBookmarks: function bv_clearBookmarks() { - this._set.clearAll(); + if ('clearAll' in this._set) + this._set.clearAll(); }, addBookmark: function bv_addBookmark(aBookmarkId, aPos) { @@ -162,7 +164,7 @@ BookmarksView.prototype = Util.extend(Object.create(View.prototype), { let uri = this._bookmarkService.getBookmarkURI(aBookmarkId); let title = this._bookmarkService.getItemTitle(aBookmarkId) || uri.spec; let item = this._set.insertItemAt(aPos || index, title, uri.spec, this._inBatch); - item.setAttribute("bookmarkId", aBookmarkId); + item.setAttribute("anonid", aBookmarkId); this._setContextActions(item); this._updateFavicon(item, uri); }, @@ -198,6 +200,7 @@ BookmarksView.prototype = Util.extend(Object.create(View.prototype), { let uri = this._bookmarkService.getBookmarkURI(aBookmarkId); let title = this._bookmarkService.getItemTitle(aBookmarkId) || uri.spec; + item.setAttribute("anonid", aBookmarkId); item.setAttribute("value", uri.spec); item.setAttribute("label", title); diff --git a/browser/metro/base/content/startui/HistoryView.js b/browser/metro/base/content/startui/HistoryView.js index 20818eb478d0..49ff84dc5c22 100644 --- a/browser/metro/base/content/startui/HistoryView.js +++ b/browser/metro/base/content/startui/HistoryView.js @@ -95,6 +95,7 @@ HistoryView.prototype = Util.extend(Object.create(View.prototype), { rootNode.containerOpen = false; this._set.arrangeItems(); + this._set.removeAttribute("fade"); if (this._inBatch > 0) this._inBatch--; }, @@ -130,6 +131,9 @@ HistoryView.prototype = Util.extend(Object.create(View.prototype), { let tileGroup = this._set; let selectedTiles = tileGroup.selectedItems; + // just arrange the grid once at the end of any action handling + this._inBatch = true; + switch (aActionName){ case "delete": Array.forEach(selectedTiles, function(aNode) { @@ -182,9 +186,11 @@ HistoryView.prototype = Util.extend(Object.create(View.prototype), { break; default: + this._inBatch = false; return; } + this._inBatch = false; // Send refresh event so all view are in sync. this._sendNeedsRefresh(); }, @@ -254,7 +260,8 @@ HistoryView.prototype = Util.extend(Object.create(View.prototype), { }, onClearHistory: function() { - this._set.clearAll(); + if ('clearAll' in this._set) + this._set.clearAll(); }, onPageChanged: function(aURI, aWhat, aValue) { @@ -264,7 +271,7 @@ HistoryView.prototype = Util.extend(Object.create(View.prototype), { let currIcon = item.getAttribute("iconURI"); if (currIcon != aValue) { item.setAttribute("iconURI", aValue); - if("refresh" in item) + if ("refresh" in item) item.refresh(); } } diff --git a/browser/metro/base/content/startui/RemoteTabsView.js b/browser/metro/base/content/startui/RemoteTabsView.js index 12db2f732f85..2bf8726b5967 100644 --- a/browser/metro/base/content/startui/RemoteTabsView.js +++ b/browser/metro/base/content/startui/RemoteTabsView.js @@ -93,6 +93,7 @@ RemoteTabsView.prototype = Util.extend(Object.create(View.prototype), { } this.setUIAccessVisible(show); this._set.arrangeItems(); + this._set.removeAttribute("fade"); }, destruct: function destruct() { diff --git a/browser/metro/base/content/startui/Start.xul b/browser/metro/base/content/startui/Start.xul index 10f7cfdfc870..ee14fa332980 100644 --- a/browser/metro/base/content/startui/Start.xul +++ b/browser/metro/base/content/startui/Start.xul @@ -48,7 +48,17 @@ &narrowTopSitesHeader.label; - + + + + + + + + + + + @@ -56,7 +66,10 @@ &narrowBookmarksHeader.label; - + + + + @@ -64,7 +77,11 @@ &narrowRecentHistoryHeader.label; - + + + + + #ifdef MOZ_SERVICES_SYNC @@ -73,7 +90,12 @@ &narrowRemoteTabsHeader.label; - + + + + + + #endif diff --git a/browser/metro/base/content/startui/TopSitesView.js b/browser/metro/base/content/startui/TopSitesView.js index 4a332f14fc96..5b9d4904f950 100644 --- a/browser/metro/base/content/startui/TopSitesView.js +++ b/browser/metro/base/content/startui/TopSitesView.js @@ -158,6 +158,9 @@ TopSitesView.prototype = Util.extend(Object.create(View.prototype), { }, updateTile: function(aTileNode, aSite, aArrangeGrid) { + if (!(aSite && aSite.url)) { + throw new Error("Invalid Site object passed to TopSitesView updateTile"); + } this._updateFavicon(aTileNode, Util.makeURI(aSite.url)); Task.spawn(function() { @@ -192,14 +195,11 @@ TopSitesView.prototype = Util.extend(Object.create(View.prototype), { tileset.clearAll(true); for (let site of sites) { - // call to private _createItemElement is a temp measure - // we'll eventually just request the next slot - let item = tileset._createItemElement(site.title, site.url); - - this.updateTile(item, site); - tileset.appendChild(item); + let slot = tileset.nextSlot(); + this.updateTile(slot, site); } tileset.arrangeItems(); + tileset.removeAttribute("fade"); this.isUpdating = false; }, @@ -244,7 +244,7 @@ TopSitesView.prototype = Util.extend(Object.create(View.prototype), { // nsIObservers observe: function (aSubject, aTopic, aState) { - switch(aTopic) { + switch (aTopic) { case "Metro:RefreshTopsiteThumbnail": this.forceReloadOfThumbnail(aState); break; @@ -269,7 +269,8 @@ TopSitesView.prototype = Util.extend(Object.create(View.prototype), { }, onClearHistory: function() { - this._set.clearAll(); + if ('clearAll' in this._set) + this._set.clearAll(); }, onPageChanged: function(aURI, aWhat, aValue) { diff --git a/browser/metro/base/tests/mochitest/browser_history.js b/browser/metro/base/tests/mochitest/browser_history.js index 4a5016e326c3..957a99c824f1 100644 --- a/browser/metro/base/tests/mochitest/browser_history.js +++ b/browser/metro/base/tests/mochitest/browser_history.js @@ -67,7 +67,7 @@ gTests.push({ ok(!item, "Item not in grid"); ok(!gStartView._pinHelper.isPinned(uriFromIndex(2)), "Item unpinned"); - ok(gStartView._set.itemCount === gStartView._limit, "Grid repopulated"); + is(gStartView._set.itemCount, gStartView._limit, "Grid repopulated"); // --------- unpin multiple items @@ -124,7 +124,7 @@ gTests.push({ item = gStartView._set.getItemsByUrl(uriFromIndex(2))[0]; ok(!item, "Item not in grid"); - ok(HistoryTestHelper._nodes[uriFromIndex(2)], "Item not deleted yet"); + ok(HistoryTestHelper._nodes[uriFromIndex(2)], "Item not actually deleted yet"); ok(!restoreButton.hidden, "Restore button is visible."); ok(gStartView._set.itemCount === gStartView._limit, "Grid repopulated"); @@ -150,9 +150,13 @@ gTests.push({ ok(!deleteButton.hidden, "Delete button is visible."); let promise = waitForCondition(() => !restoreButton.hidden); + let populateGridSpy = spyOnMethod(gStartView, "populateGrid"); + EventUtils.synthesizeMouse(deleteButton, 10, 10, {}, window); yield promise; + is(populateGridSpy.callCount, 1, "populateGrid was called in response to the deleting a tile"); + item = gStartView._set.getItemsByUrl(uriFromIndex(2))[0]; ok(!item, "Item not in grid"); @@ -163,11 +167,14 @@ gTests.push({ Elements.contextappbar.dismiss(); yield promise; + is(populateGridSpy.callCount, 1, "populateGrid not called when a removed item is actually deleted"); + populateGridSpy.restore(); + item = gStartView._set.getItemsByUrl(uriFromIndex(2))[0]; ok(!item, "Item not in grid"); ok(!HistoryTestHelper._nodes[uriFromIndex(2)], "Item RIP"); - ok(gStartView._set.itemCount === gStartView._limit, "Grid repopulated"); + is(gStartView._set.itemCount, gStartView._limit, "Grid repopulated"); // --------- delete multiple items and restore diff --git a/browser/metro/base/tests/mochitest/browser_tilegrid.xul b/browser/metro/base/tests/mochitest/browser_tilegrid.xul index 74c7e7a72ee0..e94535ce284e 100644 --- a/browser/metro/base/tests/mochitest/browser_tilegrid.xul +++ b/browser/metro/base/tests/mochitest/browser_tilegrid.xul @@ -17,6 +17,9 @@ + + + @@ -26,7 +29,7 @@ - + diff --git a/browser/metro/base/tests/mochitest/browser_tiles.js b/browser/metro/base/tests/mochitest/browser_tiles.js index 6603f92a5bb9..b79db9e835cd 100644 --- a/browser/metro/base/tests/mochitest/browser_tiles.js +++ b/browser/metro/base/tests/mochitest/browser_tiles.js @@ -9,6 +9,14 @@ function test() { }).then(runTests); } +function _checkIfBoundByRichGrid_Item(expected, node, idx) { + let binding = node.ownerDocument.defaultView.getComputedStyle(node).MozBinding; + let result = ('url("chrome://browser/content/bindings/grid.xml#richgrid-item")' == binding); + return (result == expected); +} +let isBoundByRichGrid_Item = _checkIfBoundByRichGrid_Item.bind(this, true); +let isNotBoundByRichGrid_Item = _checkIfBoundByRichGrid_Item.bind(this, false); + gTests.push({ desc: "richgrid binding is applied", run: function() { @@ -17,9 +25,9 @@ gTests.push({ let grid = doc.querySelector("#grid1"); ok(grid, "#grid1 is found"); is(typeof grid.clearSelection, "function", "#grid1 has the binding applied"); - is(grid.items.length, 2, "#grid1 has a 2 items"); is(grid.items[0].control, grid, "#grid1 item's control points back at #grid1'"); + ok(Array.every(grid.items, isBoundByRichGrid_Item), "All items are bound by richgrid-item"); } }); @@ -125,19 +133,28 @@ gTests.push({ gTests.push({ desc: "empty grid", run: function() { + // XXX grids have minSlots and may not be ever truly empty + let grid = doc.getElementById("emptyGrid"); grid.arrangeItems(); yield waitForCondition(() => !grid.isArranging); - // grid has rows=2 but 0 items + // grid has 2 rows, 6 slots, 0 items ok(grid.isBound, "binding was applied"); is(grid.itemCount, 0, "empty grid has 0 items"); - is(grid.rowCount, 0, "empty grid has 0 rows"); - is(grid.columnCount, 0, "empty grid has 0 cols"); + // minSlots attr. creates unpopulated slots + is(grid.rowCount, grid.getAttribute("rows"), "empty grid with rows-attribute has that number of rows"); + is(grid.columnCount, 3, "empty grid has expected number of columns"); - let columnsNode = grid._grid; - let cStyle = doc.defaultView.getComputedStyle(columnsNode); - is(cStyle.getPropertyValue("-moz-column-count"), "auto", "empty grid has -moz-column-count: auto"); + // remove rows attribute and allow space for the grid to find its own height + // for its number of slots + grid.removeAttribute("rows"); + grid.parentNode.style.height = 20+(grid.tileHeight*grid.minSlots)+"px"; + + grid.arrangeItems(); + yield waitForCondition(() => !grid.isArranging); + is(grid.rowCount, grid.minSlots, "empty grid has this.minSlots rows"); + is(grid.columnCount, 1, "empty grid has 1 column"); } }); @@ -211,16 +228,25 @@ gTests.push({ is(typeof grid.insertItemAt, "function", "insertItemAt is a function on the grid"); let arrangeStub = stubMethod(grid, "arrangeItems"); - let insertedItem = grid.insertItemAt(1, "inserted item", "http://example.com/inserted"); + let insertedAt0 = grid.insertItemAt(0, "inserted item 0", "http://example.com/inserted0"); + let insertedAt00 = grid.insertItemAt(0, "inserted item 00", "http://example.com/inserted00"); - ok(insertedItem, "insertItemAt gives back an item"); - is(grid.items[1], insertedItem, "item is inserted at the correct index"); - is(insertedItem.getAttribute("label"), "inserted item", "insertItemAt creates item with the correct label"); - is(insertedItem.getAttribute("value"), "http://example.com/inserted", "insertItemAt creates item with the correct url value"); - is(grid.items[2].getAttribute("id"), "grid3_item2", "following item ends up at the correct index"); - is(grid.itemCount, 3, "itemCount is incremented when we insertItemAt"); + ok(insertedAt0 && insertedAt00, "insertItemAt gives back an item"); - is(arrangeStub.callCount, 1, "arrangeItems is called when we insertItemAt"); + is(insertedAt0.getAttribute("label"), "inserted item 0", "insertItemAt creates item with the correct label"); + is(insertedAt0.getAttribute("value"), "http://example.com/inserted0", "insertItemAt creates item with the correct url value"); + + is(grid.items[0], insertedAt00, "item is inserted at the correct index"); + is(grid.children[0], insertedAt00, "first item occupies the first slot"); + is(grid.items[1], insertedAt0, "item is inserted at the correct index"); + is(grid.children[1], insertedAt0, "next item occupies the next slot"); + + is(grid.items[2].getAttribute("label"), "First item", "Old first item is now at index 2"); + is(grid.items[3].getAttribute("label"), "2nd item", "Old 2nd item is now at index 3"); + + is(grid.itemCount, 4, "itemCount is incremented when we insertItemAt"); + + is(arrangeStub.callCount, 2, "arrangeItems is called when we insertItemAt"); arrangeStub.restore(); } }); @@ -417,3 +443,172 @@ gTests.push({ doc.defaultView.removeEventListener("selectionchange", handler, false); } }); + +function gridSlotsSetup() { + let grid = this.grid = doc.createElement("richgrid"); + grid.setAttribute("minSlots", 6); + doc.documentElement.appendChild(grid); + is(grid.ownerDocument, doc, "created grid in the expected document"); +} +function gridSlotsTearDown() { + this.grid && this.grid.parentNode.removeChild(this.grid); +} + +gTests.push({ + desc: "richgrid slots init", + setUp: gridSlotsSetup, + run: function() { + let grid = this.grid; + // grid is initially populated with empty slots matching the minSlots attribute + is(grid.children.length, 6, "minSlots slots are created"); + is(grid.itemCount, 0, "slots do not count towards itemCount"); + ok(Array.every(grid.children, (node) => node.nodeName == 'richgriditem'), "slots have nodeName richgriditem"); + ok(Array.every(grid.children, isNotBoundByRichGrid_Item), "slots aren't bound by the richgrid-item binding"); + }, + tearDown: gridSlotsTearDown +}); + +gTests.push({ + desc: "richgrid using slots for items", + setUp: gridSlotsSetup, // creates grid with minSlots = num. slots = 6 + run: function() { + let grid = this.grid; + let numSlots = grid.getAttribute("minSlots"); + is(grid.children.length, numSlots); + // adding items occupies those slots + for (let idx of [0,1,2,3,4,5,6]) { + let slot = grid.children[idx]; + let item = grid.appendItem("item "+idx, "about:mozilla"); + if (idx < numSlots) { + is(grid.children.length, numSlots); + is(slot, item, "The same node is reused when an item is assigned to a slot"); + } else { + is(typeof slot, 'undefined'); + ok(item); + is(grid.children.length, grid.itemCount); + } + } + }, + tearDown: gridSlotsTearDown +}); + +gTests.push({ + desc: "richgrid assign and release slots", + setUp: function(){ + info("assign and release slots setUp"); + this.grid = doc.getElementById("slots_grid"); + this.grid.scrollIntoView(); + let rect = this.grid.getBoundingClientRect(); + info("slots grid at top: " + rect.top + ", window.pageYOffset: " + doc.defaultView.pageYOffset); + }, + run: function() { + let grid = this.grid; + // start with 5 of 6 slots occupied + for (let idx of [0,1,2,3,4]) { + let item = grid.appendItem("item "+idx, "about:mozilla"); + item.setAttribute("id", "test_item_"+idx); + } + is(grid.itemCount, 5); + is(grid.children.length, 6); // see setup, where we init with 6 slots + let firstItem = grid.items[0]; + + ok(firstItem.ownerDocument, "item has ownerDocument"); + is(doc, firstItem.ownerDocument, "item's ownerDocument is the document we expect"); + + is(firstItem, grid.children[0], "Item and assigned slot are one and the same"); + is(firstItem.control, grid, "Item is bound and its .control points back at the grid"); + + // before releasing, the grid should be nofified of clicks on that slot + let testWindow = grid.ownerDocument.defaultView; + + let rect = firstItem.getBoundingClientRect(); + { + let handleStub = stubMethod(grid, 'handleItemClick'); + // send click to item and wait for next tick; + sendElementTap(testWindow, firstItem); + yield waitForMs(0); + + is(handleStub.callCount, 1, "handleItemClick was called when we clicked an item"); + handleStub.restore(); + } + // _releaseSlot is semi-private, we don't expect consumers of the binding to call it + // but want to be sure it does what we expect + grid._releaseSlot(firstItem); + + is(grid.itemCount, 4, "Releasing a slot gives us one less item"); + is(firstItem, grid.children[0],"Released slot is still the same node we started with"); + + // after releasing, the grid should NOT be nofified of clicks + { + let handleStub = stubMethod(grid, 'handleItemClick'); + // send click to item and wait for next tick; + sendElementTap(testWindow, firstItem); + yield waitForMs(0); + + is(handleStub.callCount, 0, "handleItemClick was NOT called when we clicked a released slot"); + handleStub.restore(); + } + + ok(!firstItem.mozMatchesSelector("richgriditem[value]"), "Released slot doesn't match binding selector"); + ok(isNotBoundByRichGrid_Item(firstItem), "Released slot is no longer bound"); + + waitForCondition(() => isNotBoundByRichGrid_Item(firstItem)); + ok(true, "Slot eventually gets unbound"); + is(firstItem, grid.children[0], "Released slot is still at expected index in children collection"); + + let firstSlot = grid.children[0]; + firstItem = grid.insertItemAt(0, "New item 0", "about:blank"); + ok(firstItem == grid.items[0], "insertItemAt 0 creates item at expected index"); + ok(firstItem == firstSlot, "insertItemAt occupies the released slot with the new item"); + is(grid.itemCount, 5); + is(grid.children.length, 6); + is(firstItem.control, grid,"Item is bound and its .control points back at the grid"); + + let nextSlotIndex = grid.itemCount; + let lastItem = grid.insertItemAt(9, "New item 9", "about:blank"); + // Check we don't create sparse collection of items + is(lastItem, grid.children[nextSlotIndex], "Item is appended at the next index when an out of bounds index is provided"); + is(grid.children.length, 6); + is(grid.itemCount, 6); + + grid.appendItem("one more", "about:blank"); + is(grid.children.length, 7); + is(grid.itemCount, 7); + + // clearAll results in slots being emptied + grid.clearAll(); + is(grid.children.length, 6, "Extra slots are trimmed when we clearAll"); + ok(!Array.some(grid.children, (node) => node.hasAttribute("value")), "All slots have no value attribute after clearAll") + }, + tearDown: gridSlotsTearDown +}); + +gTests.push({ + desc: "richgrid slot management", + setUp: gridSlotsSetup, + run: function() { + let grid = this.grid; + // populate grid with some items + let numSlots = grid.getAttribute("minSlots"); + for (let idx of [0,1,2,3,4,5]) { + let item = grid.appendItem("item "+idx, "about:mozilla"); + } + + is(grid.itemCount, 6, "Grid setup with 6 items"); + is(grid.children.length, 6, "Full grid has the expected number of slots"); + + // removing an item creates a replacement slot *on the end of the stack* + let item = grid.removeItemAt(0); + is(item.getAttribute("label"), "item 0", "removeItemAt gives back the populated node"); + is(grid.children.length, 6); + is(grid.itemCount, 5); + is(grid.items[0].getAttribute("label"), "item 1", "removeItemAt removes the node so the nextSibling takes its place"); + ok(grid.children[5] && !grid.children[5].hasAttribute("value"), "empty slot is added at the end of the existing children"); + + let item1 = grid.removeItem(grid.items[0]); + is(grid.children.length, 6); + is(grid.itemCount, 4); + is(grid.items[0].getAttribute("label"), "item 2", "removeItem removes the node so the nextSibling takes its place"); + }, + tearDown: gridSlotsTearDown +}); diff --git a/browser/metro/theme/tiles.css b/browser/metro/theme/tiles.css index b78f174cc403..578b7dece641 100644 --- a/browser/metro/theme/tiles.css +++ b/browser/metro/theme/tiles.css @@ -275,6 +275,26 @@ richgriditem[bending] > .tile-content { transform-origin: center center; } +/* Empty/unused tiles */ +richgriditem:not([value]) { + visibility: hidden; +} +richgriditem[tiletype="thumbnail"]:not([value]) { + visibility: visible; +} +richgriditem:not([value]) > .tile-content { + padding: 10px 14px; +} +richgriditem[tiletype="thumbnail"]:not([value]) > .tile-content { + box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.05); + background-image: url("chrome://browser/skin/images/firefox-watermark.png"); + background-origin: content-box; + background-repeat: no-repeat; + background-color: rgba(255,255,255, 0.2); + background-position: center center; + background-size: @grid_row_height@; +} + /* Snapped-view variation We use the compact, single-column grid treatment for <=320px */ From 2432a3edab7df1b2e590ba86c521387076cfd820 Mon Sep 17 00:00:00 2001 From: Gaia Pushbot Date: Wed, 16 Oct 2013 17:00:25 -0700 Subject: [PATCH 15/26] Bumping gaia.json for 2 gaia-central revision(s) a=gaia-bump ======== https://hg.mozilla.org/integration/gaia-central/rev/312d5c0596e5 Author: Ben Kelly Desc: Merge pull request #12832 from wanderview/email-remove-onclick Bug 926524: Remove bound click handler in MessageListTopbar. r=gaye ======== https://hg.mozilla.org/integration/gaia-central/rev/b8c548bd56e9 Author: Ben Kelly Desc: Bug 926524: Remove bound click handler in MessageListTopbar. --- b2g/config/gaia.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index 2ed75f789326..fa27e43114c5 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -1,4 +1,4 @@ { - "revision": "5e851aef150b0c7d4236a89dd2e11e3320412ccc", + "revision": "312d5c0596e5f34cf8f1ff1ded8e31d072e3a44c", "repo_path": "/integration/gaia-central" } From bfa0ea9c7d4a02aed48110bd5374355668de3889 Mon Sep 17 00:00:00 2001 From: Gaia Pushbot Date: Wed, 16 Oct 2013 17:25:24 -0700 Subject: [PATCH 16/26] Bumping gaia.json for 2 gaia-central revision(s) a=gaia-bump ======== https://hg.mozilla.org/integration/gaia-central/rev/49e443173e45 Author: gasolin Desc: Merge pull request #12809 from gasolin/issue-902036 Bug 902036 - [B2G][Settings][Media Storage]Spelling error on Default Med...,r=arthur ======== https://hg.mozilla.org/integration/gaia-central/rev/a10f3481e3bb Author: gasolin Desc: Bug 902036 - [B2G][Settings][Media Storage]Spelling error on Default Media Location Information Screen --- b2g/config/gaia.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index fa27e43114c5..cb74943c9f16 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -1,4 +1,4 @@ { - "revision": "312d5c0596e5f34cf8f1ff1ded8e31d072e3a44c", + "revision": "49e443173e45e298e1c8e0152c4abd485748fc07", "repo_path": "/integration/gaia-central" } From a1fdb5198154ef05e628340310858ece3705ffab Mon Sep 17 00:00:00 2001 From: Richard Newman Date: Wed, 16 Oct 2013 18:56:26 -0700 Subject: [PATCH 17/26] Bug 922694 - Part 1: split Distribution class to allow for use from FHR, use buffer when expanding zip contents. r=margaret --- mobile/android/base/Distribution.java | 278 ++++++++++++++++---------- 1 file changed, 167 insertions(+), 111 deletions(-) diff --git a/mobile/android/base/Distribution.java b/mobile/android/base/Distribution.java index 9a6a485d9cfb..999d07897088 100644 --- a/mobile/android/base/Distribution.java +++ b/mobile/android/base/Distribution.java @@ -1,11 +1,7 @@ /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * ***** BEGIN LICENSE BLOCK ***** - * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - * - * ***** END LICENSE BLOCK ***** */ + * You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko; @@ -19,14 +15,12 @@ import android.content.Context; import android.content.SharedPreferences; import android.util.Log; -import java.io.BufferedReader; import java.io.File; -import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.io.OutputStream; +import java.util.Scanner; import java.util.Enumeration; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -39,87 +33,149 @@ public final class Distribution { private static final int STATE_SET = 2; /** - * Initializes distribution if it hasn't already been initalized. + * Initializes distribution if it hasn't already been initalized. Sends + * messages to Gecko as appropriate. * - * @param packagePath specifies where to look for the distribution directory. + * @param packagePath where to look for the distribution directory. */ public static void init(final Context context, final String packagePath) { // Read/write preferences and files on the background thread. ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { - // Bail if we've already initialized the distribution. - SharedPreferences settings = context.getSharedPreferences(GeckoApp.PREFS_NAME, Activity.MODE_PRIVATE); - String keyName = context.getPackageName() + ".distribution_state"; - int state = settings.getInt(keyName, STATE_UNKNOWN); - if (state == STATE_NONE) { - return; - } - - // Send a message to Gecko if we've set a distribution. - if (state == STATE_SET) { - GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Distribution:Set", "")); - return; - } - - boolean distributionSet = false; - try { - // First, try copying distribution files out of the APK. - distributionSet = copyFiles(context, packagePath); - } catch (IOException e) { - Log.e(LOGTAG, "Error copying distribution files", e); - } - - if (!distributionSet) { - // If there aren't any distribution files in the APK, look in the /system directory. - File distDir = new File("/system/" + context.getPackageName() + "/distribution"); - if (distDir.exists()) { - distributionSet = true; - } - } - + Distribution dist = new Distribution(context, packagePath); + boolean distributionSet = dist.doInit(); if (distributionSet) { GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Distribution:Set", "")); - settings.edit().putInt(keyName, STATE_SET).commit(); - } else { - settings.edit().putInt(keyName, STATE_NONE).commit(); } } }); } + /** + * Use Context.getPackageResourcePath to find an implicit + * package path. + */ + public static void init(final Context context) { + Distribution.init(context, context.getPackageResourcePath()); + } + + /** + * Returns parsed contents of bookmarks.json. + * This method should only be called from a background thread. + */ + public static JSONArray getBookmarks(final Context context) { + Distribution dist = new Distribution(context); + return dist.getBookmarks(); + } + + private final String packagePath; + private final Context context; + + private int state = STATE_UNKNOWN; + private File distributionDir = null; + + /** + * @param packagePath where to look for the distribution directory. + */ + public Distribution(final Context context, final String packagePath) { + this.context = context; + this.packagePath = packagePath; + } + + public Distribution(final Context context) { + this(context, context.getPackageResourcePath()); + } + + /** + * Don't call from the main thread. + * + * @return true if we've set a distribution. + */ + private boolean doInit() { + // Bail if we've already tried to initialize the distribution, and + // there wasn't one. + SharedPreferences settings = context.getSharedPreferences(GeckoApp.PREFS_NAME, Activity.MODE_PRIVATE); + String keyName = context.getPackageName() + ".distribution_state"; + this.state = settings.getInt(keyName, STATE_UNKNOWN); + if (this.state == STATE_NONE) { + return false; + } + + // We've done the work once; don't do it again. + if (this.state == STATE_SET) { + // Note that we don't compute the distribution directory. + // Call `ensureDistributionDir` if you need it. + return true; + } + + boolean distributionSet = false; + try { + // First, try copying distribution files out of the APK. + distributionSet = copyFiles(); + if (distributionSet) { + // We always copy to the data dir, and we only copy files from + // a 'distribution' subdirectory. Track our dist dir now that + // we know it. + this.distributionDir = new File(getDataDir(), "distribution/"); + } + } catch (IOException e) { + Log.e(LOGTAG, "Error copying distribution files", e); + } + + if (!distributionSet) { + // If there aren't any distribution files in the APK, look in the /system directory. + File distDir = getSystemDistributionDir(); + if (distDir.exists()) { + distributionSet = true; + this.distributionDir = distDir; + } + } + + this.state = distributionSet ? STATE_SET : STATE_NONE; + settings.edit().putInt(keyName, this.state).commit(); + return distributionSet; + } + /** * Copies the /distribution folder out of the APK and into the app's data directory. * Returns true if distribution files were found and copied. */ - private static boolean copyFiles(Context context, String packagePath) throws IOException { + private boolean copyFiles() throws IOException { File applicationPackage = new File(packagePath); ZipFile zip = new ZipFile(applicationPackage); boolean distributionSet = false; Enumeration zipEntries = zip.entries(); + + byte[] buffer = new byte[1024]; while (zipEntries.hasMoreElements()) { ZipEntry fileEntry = zipEntries.nextElement(); String name = fileEntry.getName(); - if (!name.startsWith("distribution/")) + if (!name.startsWith("distribution/")) { continue; + } distributionSet = true; - File dataDir = new File(context.getApplicationInfo().dataDir); - File outFile = new File(dataDir, name); - + File outFile = new File(getDataDir(), name); File dir = outFile.getParentFile(); - if (!dir.exists()) - dir.mkdirs(); + + if (!dir.exists()) { + if (!dir.mkdirs()) { + Log.e(LOGTAG, "Unable to create directories: " + dir.getAbsolutePath()); + continue; + } + } InputStream fileStream = zip.getInputStream(fileEntry); OutputStream outStream = new FileOutputStream(outFile); - int b; - while ((b = fileStream.read()) != -1) - outStream.write(b); + int count; + while ((count = fileStream.read(buffer)) != -1) { + outStream.write(buffer, 0, count); + } fileStream.close(); outStream.close(); @@ -132,77 +188,77 @@ public final class Distribution { } /** - * Returns parsed contents of bookmarks.json. - * This method should only be called from a background thread. + * After calling this method, either distributionDir + * will be set, or there is no distribution in use. + * + * Only call after init. */ - public static JSONArray getBookmarks(Context context) { - SharedPreferences settings = context.getSharedPreferences(GeckoApp.PREFS_NAME, Activity.MODE_PRIVATE); - String keyName = context.getPackageName() + ".distribution_state"; - int state = settings.getInt(keyName, STATE_UNKNOWN); - if (state == STATE_NONE) { + private File ensureDistributionDir() { + if (this.distributionDir != null) { + return this.distributionDir; + } + + if (this.state != STATE_SET) { return null; } - ZipFile zip = null; - InputStream inputStream = null; + // After init, we know that either we've copied a distribution out of + // the APK, or it exists in /system/. + // Look in each location in turn. + // (This could be optimized by caching the path in shared prefs.) + File copied = new File(getDataDir(), "distribution/"); + if (copied.exists()) { + return this.distributionDir = copied; + } + File system = getSystemDistributionDir(); + if (system.exists()) { + return this.distributionDir = system; + } + return null; + } + + public JSONArray getBookmarks() { + if (this.state == STATE_UNKNOWN) { + this.doInit(); + } + + File dist = ensureDistributionDir(); + if (dist == null) { + return null; + } + + File bookmarks = new File(dist, "bookmarks.json"); + if (!bookmarks.exists()) { + return null; + } + + // Shortcut to slurp a file without messing around with streams. try { - if (state == STATE_UNKNOWN) { - // If the distribution hasn't been set yet, first look for bookmarks.json in the APK. - File applicationPackage = new File(context.getPackageResourcePath()); - zip = new ZipFile(applicationPackage); - ZipEntry zipEntry = zip.getEntry("distribution/bookmarks.json"); - if (zipEntry != null) { - inputStream = zip.getInputStream(zipEntry); - } else { - // If there's no bookmarks.json in the APK, but there is a preferences.json, - // don't create any distribution bookmarks. - zipEntry = zip.getEntry("distribution/preferences.json"); - if (zipEntry != null) { - return null; - } - // Otherwise, look for bookmarks.json in the /system directory. - File systemFile = new File("/system/" + context.getPackageName() + "/distribution/bookmarks.json"); - if (!systemFile.exists()) { - return null; - } - inputStream = new FileInputStream(systemFile); + Scanner scanner = null; + try { + scanner = new Scanner(bookmarks, "UTF-8"); + final String contents = scanner.useDelimiter("\\A").next(); + return new JSONArray(contents); + } finally { + if (scanner != null) { + scanner.close(); } - } else { - // Otherwise, first look for the distribution in the data directory. - File distDir = new File(context.getApplicationInfo().dataDir, "distribution"); - if (!distDir.exists()) { - // If that doesn't exist, then we must be using a distribution from the system directory. - distDir = new File("/system/" + context.getPackageName() + "/distribution"); - } - - File file = new File(distDir, "bookmarks.json"); - inputStream = new FileInputStream(file); } - // Convert input stream to JSONArray - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); - StringBuilder stringBuilder = new StringBuilder(); - String s; - while ((s = reader.readLine()) != null) { - stringBuilder.append(s); - } - return new JSONArray(stringBuilder.toString()); } catch (IOException e) { Log.e(LOGTAG, "Error getting bookmarks", e); } catch (JSONException e) { Log.e(LOGTAG, "Error parsing bookmarks.json", e); - } finally { - try { - if (zip != null) { - zip.close(); - } - if (inputStream != null) { - inputStream.close(); - } - } catch (IOException e) { - Log.e(LOGTAG, "Error closing streams", e); - } } + return null; } + + private String getDataDir() { + return context.getApplicationInfo().dataDir; + } + + private File getSystemDistributionDir() { + return new File("/system/" + context.getPackageName() + "/distribution"); + } } From 87f9bb7a83fa19cf4c44f27af1d6c464c34ac602 Mon Sep 17 00:00:00 2001 From: Richard Newman Date: Wed, 16 Oct 2013 18:56:26 -0700 Subject: [PATCH 18/26] Bug 922694 - Part 2: expose distribution ID. r=margaret --- mobile/android/base/Distribution.java | 125 ++++++++++++++++++++++---- 1 file changed, 110 insertions(+), 15 deletions(-) diff --git a/mobile/android/base/Distribution.java b/mobile/android/base/Distribution.java index 999d07897088..a42d90332b0b 100644 --- a/mobile/android/base/Distribution.java +++ b/mobile/android/base/Distribution.java @@ -9,6 +9,7 @@ import org.mozilla.gecko.util.ThreadUtils; import org.json.JSONArray; import org.json.JSONException; +import org.json.JSONObject; import android.app.Activity; import android.content.Context; @@ -20,8 +21,12 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.Scanner; +import java.util.Collections; import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Scanner; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -32,6 +37,48 @@ public final class Distribution { private static final int STATE_NONE = 1; private static final int STATE_SET = 2; + public static class DistributionDescriptor { + public final boolean valid; + public final String id; + public final String version; // Example uses a float, but that's a crazy idea. + + // Default UI-visible description of the distribution. + public final String about; + + // Each distribution file can include multiple localized versions of + // the 'about' string. These are represented as, e.g., "about.en-US" + // keys in the Global object. + // Here we map locale to description. + public final Map localizedAbout; + + @SuppressWarnings("unchecked") + public DistributionDescriptor(JSONObject obj) { + this.id = obj.optString("id"); + this.version = obj.optString("version"); + this.about = obj.optString("about"); + Map loc = new HashMap(); + try { + Iterator keys = obj.keys(); + while (keys.hasNext()) { + String key = keys.next(); + if (key.startsWith("about.")) { + String locale = key.substring(6); + if (!obj.isNull(locale)) { + loc.put(locale, obj.getString(key)); + } + } + } + } catch (JSONException ex) { + Log.w(LOGTAG, "Unable to completely process distribution JSON.", ex); + } + + this.localizedAbout = Collections.unmodifiableMap(loc); + this.valid = (null != this.id) && + (null != this.version) && + (null != this.about); + } + } + /** * Initializes distribution if it hasn't already been initalized. Sends * messages to Gecko as appropriate. @@ -217,9 +264,18 @@ public final class Distribution { return null; } - public JSONArray getBookmarks() { + /** + * Helper to grab a file in the distribution directory. + * + * Returns null if there is no distribution directory or the file + * doesn't exist. Ensures init first. + */ + private File getDistributionFile(String name) { + Log.i(LOGTAG, "Getting file from distribution."); if (this.state == STATE_UNKNOWN) { - this.doInit(); + if (!this.doInit()) { + return null; + } } File dist = ensureDistributionDir(); @@ -227,24 +283,50 @@ public final class Distribution { return null; } - File bookmarks = new File(dist, "bookmarks.json"); - if (!bookmarks.exists()) { + File descFile = new File(dist, name); + if (!descFile.exists()) { + Log.e(LOGTAG, "Distribution directory exists, but no file named " + name); + return null; + } + + return descFile; + } + + public DistributionDescriptor getDescriptor() { + File descFile = getDistributionFile("preferences.json"); + if (descFile == null) { + // Logging and existence checks are handled in getDistributionFile. return null; } - // Shortcut to slurp a file without messing around with streams. try { - Scanner scanner = null; - try { - scanner = new Scanner(bookmarks, "UTF-8"); - final String contents = scanner.useDelimiter("\\A").next(); - return new JSONArray(contents); - } finally { - if (scanner != null) { - scanner.close(); - } + JSONObject all = new JSONObject(getFileContents(descFile)); + + if (!all.has("Global")) { + Log.e(LOGTAG, "Distribution preferences.json has no Global entry!"); + return null; } + return new DistributionDescriptor(all.getJSONObject("Global")); + + } catch (IOException e) { + Log.e(LOGTAG, "Error getting distribution descriptor file.", e); + return null; + } catch (JSONException e) { + Log.e(LOGTAG, "Error parsing preferences.json", e); + return null; + } + } + + public JSONArray getBookmarks() { + File bookmarks = getDistributionFile("bookmarks.json"); + if (bookmarks == null) { + // Logging and existence checks are handled in getDistributionFile. + return null; + } + + try { + return new JSONArray(getFileContents(bookmarks)); } catch (IOException e) { Log.e(LOGTAG, "Error getting bookmarks", e); } catch (JSONException e) { @@ -254,6 +336,19 @@ public final class Distribution { return null; } + // Shortcut to slurp a file without messing around with streams. + private String getFileContents(File file) throws IOException { + Scanner scanner = null; + try { + scanner = new Scanner(file, "UTF-8"); + return scanner.useDelimiter("\\A").next(); + } finally { + if (scanner != null) { + scanner.close(); + } + } + } + private String getDataDir() { return context.getApplicationInfo().dataDir; } From 8f6f03b2527af0fd1abdfdbbefd5642eef6565a6 Mon Sep 17 00:00:00 2001 From: Richard Newman Date: Wed, 16 Oct 2013 18:56:27 -0700 Subject: [PATCH 19/26] Bug 922694 - Part 3: FHR changes for distribution and locale. r=mcomella --- mobile/android/base/android-services-files.mk | 1 + .../background/healthreport/Environment.java | 261 ++--------------- .../healthreport/EnvironmentBuilder.java | 12 + .../healthreport/EnvironmentV1.java | 267 ++++++++++++++++++ .../HealthReportDatabaseStorage.java | 58 +++- .../healthreport/HealthReportGenerator.java | 111 +++++++- .../healthreport/ProfileInformationCache.java | 117 +++++++- mobile/android/services/java-sources.mn | 1 + .../healthreport/MockDatabaseEnvironment.java | 12 +- .../TestHealthReportGenerator.java | 112 +++++++- .../TestHealthReportProvider.java | 9 +- 11 files changed, 700 insertions(+), 261 deletions(-) create mode 100644 mobile/android/base/background/healthreport/EnvironmentV1.java diff --git a/mobile/android/base/android-services-files.mk b/mobile/android/base/android-services-files.mk index f107f3fd9cfb..cfd6216ce9af 100644 --- a/mobile/android/base/android-services-files.mk +++ b/mobile/android/base/android-services-files.mk @@ -40,6 +40,7 @@ SYNC_JAVA_FILES := \ background/db/Tab.java \ background/healthreport/Environment.java \ background/healthreport/EnvironmentBuilder.java \ + background/healthreport/EnvironmentV1.java \ background/healthreport/HealthReportBroadcastReceiver.java \ background/healthreport/HealthReportBroadcastService.java \ background/healthreport/HealthReportDatabases.java \ diff --git a/mobile/android/base/background/healthreport/Environment.java b/mobile/android/base/background/healthreport/Environment.java index 41b8a4117b94..0c30c965bb71 100644 --- a/mobile/android/base/background/healthreport/Environment.java +++ b/mobile/android/base/background/healthreport/Environment.java @@ -4,17 +4,6 @@ package org.mozilla.gecko.background.healthreport; -import java.io.UnsupportedEncodingException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Iterator; -import java.util.SortedSet; - -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.apache.commons.codec.binary.Base64; -import org.mozilla.gecko.background.common.log.Logger; - /** * This captures all of the details that define an 'environment' for FHR's purposes. * Whenever this format changes, it'll be changing with a build ID, so no migration @@ -29,246 +18,32 @@ import org.mozilla.gecko.background.common.log.Logger; * registered an Environment, don't do so again; start from scratch. * */ -public abstract class Environment { - private static final String LOG_TAG = "GeckoEnvironment"; +public abstract class Environment extends EnvironmentV1 { + // Version 2 adds osLocale, appLocale, acceptLangSet, and distribution. + public static final int CURRENT_VERSION = 2; - public static int VERSION = 1; - - protected final Class appenderClass; - - protected volatile String hash = null; - protected volatile int id = -1; - - // org.mozilla.profile.age. - public int profileCreation; - - // org.mozilla.sysinfo.sysinfo. - public int cpuCount; - public int memoryMB; - public String architecture; - public String sysName; - public String sysVersion; // Kernel. - - // geckoAppInfo. Not sure if we can/should provide this on Android. - public String vendor; - public String appName; - public String appID; - public String appVersion; - public String appBuildID; - public String platformVersion; - public String platformBuildID; - public String os; - public String xpcomabi; - public String updateChannel; - - // appInfo. - public int isBlocklistEnabled; - public int isTelemetryEnabled; - // public int isDefaultBrowser; // This is meaningless on Android. - - // org.mozilla.addons.active. - public JSONObject addons = null; - - // org.mozilla.addons.counts. - public int extensionCount; - public int pluginCount; - public int themeCount; + public String osLocale; // The Android OS "Locale" value. + public String appLocale; + public int acceptLangSet; + public String distribution; // ID + version. Typically empty. public Environment() { this(Environment.HashAppender.class); } public Environment(Class appenderClass) { - this.appenderClass = appenderClass; + super(appenderClass); + version = CURRENT_VERSION; } - public JSONObject getNonIgnoredAddons() { - if (addons == null) { - return null; - } - JSONObject out = new JSONObject(); - @SuppressWarnings("unchecked") - Iterator keys = addons.keys(); - while (keys.hasNext()) { - try { - final String key = keys.next(); - final Object obj = addons.get(key); - if (obj != null && obj instanceof JSONObject && ((JSONObject) obj).optBoolean("ignore", false)) { - continue; - } - out.put(key, obj); - } catch (JSONException ex) { - // Do nothing. - } - } - return out; + @Override + protected void appendHash(EnvironmentAppender appender) { + super.appendHash(appender); + + // v2. + appender.append(osLocale); + appender.append(appLocale); + appender.append(acceptLangSet); + appender.append(distribution); } - - /** - * We break out this interface in order to allow for testing -- pass in your - * own appender that just records strings, for example. - */ - public static abstract class EnvironmentAppender { - public abstract void append(String s); - public abstract void append(int v); - } - - public static class HashAppender extends EnvironmentAppender { - final MessageDigest hasher; - - public HashAppender() throws NoSuchAlgorithmException { - // Note to the security minded reader: we deliberately use SHA-1 here, not - // a stronger hash. These identifiers don't strictly need a cryptographic - // hash function, because there is negligible value in attacking the hash. - // We use SHA-1 because it's *shorter* -- the exact same reason that Git - // chose SHA-1. - hasher = MessageDigest.getInstance("SHA-1"); - } - - @Override - public void append(String s) { - try { - hasher.update(((s == null) ? "null" : s).getBytes("UTF-8")); - } catch (UnsupportedEncodingException e) { - // This can never occur. Thanks, Java. - } - } - - @Override - public void append(int profileCreation) { - append(Integer.toString(profileCreation, 10)); - } - - @Override - public String toString() { - // We *could* use ASCII85… but the savings would be negated by the - // inclusion of JSON-unsafe characters like double-quote. - return new Base64(-1, null, false).encodeAsString(hasher.digest()); - } - } - - /** - * Compute the stable hash of the configured environment. - * - * @return the hash in base34, or null if there was a problem. - */ - public String getHash() { - // It's never unset, so we only care about partial reads. volatile is enough. - if (hash != null) { - return hash; - } - - EnvironmentAppender appender; - try { - appender = appenderClass.newInstance(); - } catch (InstantiationException ex) { - // Should never happen, but... - Logger.warn(LOG_TAG, "Could not compute hash.", ex); - return null; - } catch (IllegalAccessException ex) { - // Should never happen, but... - Logger.warn(LOG_TAG, "Could not compute hash.", ex); - return null; - } - - appender.append(profileCreation); - appender.append(cpuCount); - appender.append(memoryMB); - appender.append(architecture); - appender.append(sysName); - appender.append(sysVersion); - appender.append(vendor); - appender.append(appName); - appender.append(appID); - appender.append(appVersion); - appender.append(appBuildID); - appender.append(platformVersion); - appender.append(platformBuildID); - appender.append(os); - appender.append(xpcomabi); - appender.append(updateChannel); - appender.append(isBlocklistEnabled); - appender.append(isTelemetryEnabled); - appender.append(extensionCount); - appender.append(pluginCount); - appender.append(themeCount); - - // We need sorted values. - if (addons != null) { - appendSortedAddons(getNonIgnoredAddons(), appender); - } - - return hash = appender.toString(); - } - - /** - * Take a collection of add-on descriptors, appending a consistent string - * to the provided builder. - */ - public static void appendSortedAddons(JSONObject addons, - final EnvironmentAppender builder) { - final SortedSet keys = HealthReportUtils.sortedKeySet(addons); - - // For each add-on, produce a consistent, sorted mapping of its descriptor. - for (String key : keys) { - try { - JSONObject addon = addons.getJSONObject(key); - - // Now produce the output for this add-on. - builder.append(key); - builder.append("={"); - - for (String addonKey : HealthReportUtils.sortedKeySet(addon)) { - builder.append(addonKey); - builder.append("=="); - try { - builder.append(addon.get(addonKey).toString()); - } catch (JSONException e) { - builder.append("_e_"); - } - } - - builder.append("}"); - } catch (Exception e) { - // Muffle. - Logger.warn(LOG_TAG, "Invalid add-on for ID " + key); - } - } - } - - public void setJSONForAddons(byte[] json) throws Exception { - setJSONForAddons(new String(json, "UTF-8")); - } - - public void setJSONForAddons(String json) throws Exception { - if (json == null || "null".equals(json)) { - addons = null; - return; - } - addons = new JSONObject(json); - } - - public void setJSONForAddons(JSONObject json) { - addons = json; - } - - /** - * Includes ignored add-ons. - */ - public String getNormalizedAddonsJSON() { - // We trust that our input will already be normalized. If that assumption - // is invalidated, then we'll be sorry. - return (addons == null) ? "null" : addons.toString(); - } - - /** - * Ensure that the {@link Environment} has been registered with its - * storage layer, and can be used to annotate events. - * - * It's safe to call this method more than once, and each time you'll - * get the same ID. - * - * @return the integer ID to use in subsequent DB insertions. - */ - public abstract int register(); } diff --git a/mobile/android/base/background/healthreport/EnvironmentBuilder.java b/mobile/android/base/background/healthreport/EnvironmentBuilder.java index 44586eeacb75..8d23bd7d25cd 100644 --- a/mobile/android/base/background/healthreport/EnvironmentBuilder.java +++ b/mobile/android/base/background/healthreport/EnvironmentBuilder.java @@ -58,7 +58,13 @@ public class EnvironmentBuilder { public static interface ProfileInformationProvider { public boolean isBlocklistEnabled(); public boolean isTelemetryEnabled(); + public boolean isAcceptLangUserSet(); public long getProfileCreationTime(); + + public String getDistributionString(); + public String getOSLocale(); + public String getAppLocale(); + public JSONObject getAddonsJSON(); } @@ -124,6 +130,12 @@ public class EnvironmentBuilder { } e.addons = addons; + + // v2 environment fields. + e.distribution = info.getDistributionString(); + e.osLocale = info.getOSLocale(); + e.appLocale = info.getAppLocale(); + e.acceptLangSet = info.isAcceptLangUserSet() ? 1 : 0; } /** diff --git a/mobile/android/base/background/healthreport/EnvironmentV1.java b/mobile/android/base/background/healthreport/EnvironmentV1.java new file mode 100644 index 000000000000..1582afa93a6b --- /dev/null +++ b/mobile/android/base/background/healthreport/EnvironmentV1.java @@ -0,0 +1,267 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.background.healthreport; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Iterator; +import java.util.SortedSet; + +import org.json.JSONException; +import org.json.JSONObject; +import org.mozilla.apache.commons.codec.binary.Base64; +import org.mozilla.gecko.background.common.log.Logger; + +public abstract class EnvironmentV1 { + private static final String LOG_TAG = "GeckoEnvironment"; + private static final int VERSION = 1; + + protected final Class appenderClass; + + protected volatile String hash = null; + protected volatile int id = -1; + + public int version = VERSION; + + // org.mozilla.profile.age. + public int profileCreation; + + // org.mozilla.sysinfo.sysinfo. + public int cpuCount; + public int memoryMB; + public String architecture; + public String sysName; + public String sysVersion; // Kernel. + + // geckoAppInfo. + public String vendor; + public String appName; + public String appID; + public String appVersion; + public String appBuildID; + public String platformVersion; + public String platformBuildID; + public String os; + public String xpcomabi; + public String updateChannel; + + // appinfo. + public int isBlocklistEnabled; + public int isTelemetryEnabled; + + // org.mozilla.addons.active. + public JSONObject addons = null; + + // org.mozilla.addons.counts. + public int extensionCount; + public int pluginCount; + public int themeCount; + + /** + * We break out this interface in order to allow for testing -- pass in your + * own appender that just records strings, for example. + */ + public static abstract class EnvironmentAppender { + public abstract void append(String s); + public abstract void append(int v); + } + + public static class HashAppender extends EnvironmentAppender { + final MessageDigest hasher; + + public HashAppender() throws NoSuchAlgorithmException { + // Note to the security-minded reader: we deliberately use SHA-1 here, not + // a stronger hash. These identifiers don't strictly need a cryptographic + // hash function, because there is negligible value in attacking the hash. + // We use SHA-1 because it's *shorter* -- the exact same reason that Git + // chose SHA-1. + hasher = MessageDigest.getInstance("SHA-1"); + } + + @Override + public void append(String s) { + try { + hasher.update(((s == null) ? "null" : s).getBytes("UTF-8")); + } catch (UnsupportedEncodingException e) { + // This can never occur. Thanks, Java. + } + } + + @Override + public void append(int profileCreation) { + append(Integer.toString(profileCreation, 10)); + } + + @Override + public String toString() { + // We *could* use ASCII85… but the savings would be negated by the + // inclusion of JSON-unsafe characters like double-quote. + return new Base64(-1, null, false).encodeAsString(hasher.digest()); + } + } + + /** + * Ensure that the {@link Environment} has been registered with its + * storage layer, and can be used to annotate events. + * + * It's safe to call this method more than once, and each time you'll + * get the same ID. + * + * @return the integer ID to use in subsequent DB insertions. + */ + public abstract int register(); + + protected EnvironmentAppender getAppender() { + EnvironmentAppender appender = null; + try { + appender = appenderClass.newInstance(); + } catch (InstantiationException ex) { + // Should never happen, but... + Logger.warn(LOG_TAG, "Could not compute hash.", ex); + } catch (IllegalAccessException ex) { + // Should never happen, but... + Logger.warn(LOG_TAG, "Could not compute hash.", ex); + } + return appender; + } + + protected void appendHash(EnvironmentAppender appender) { + appender.append(profileCreation); + appender.append(cpuCount); + appender.append(memoryMB); + appender.append(architecture); + appender.append(sysName); + appender.append(sysVersion); + appender.append(vendor); + appender.append(appName); + appender.append(appID); + appender.append(appVersion); + appender.append(appBuildID); + appender.append(platformVersion); + appender.append(platformBuildID); + appender.append(os); + appender.append(xpcomabi); + appender.append(updateChannel); + appender.append(isBlocklistEnabled); + appender.append(isTelemetryEnabled); + appender.append(extensionCount); + appender.append(pluginCount); + appender.append(themeCount); + + // We need sorted values. + if (addons != null) { + appendSortedAddons(getNonIgnoredAddons(), appender); + } + } + + /** + * Compute the stable hash of the configured environment. + * + * @return the hash in base34, or null if there was a problem. + */ + public String getHash() { + // It's never unset, so we only care about partial reads. volatile is enough. + if (hash != null) { + return hash; + } + + EnvironmentAppender appender = getAppender(); + if (appender == null) { + return null; + } + + appendHash(appender); + return hash = appender.toString(); + } + + public EnvironmentV1(Class appenderClass) { + super(); + this.appenderClass = appenderClass; + } + + public JSONObject getNonIgnoredAddons() { + if (addons == null) { + return null; + } + JSONObject out = new JSONObject(); + @SuppressWarnings("unchecked") + Iterator keys = addons.keys(); + while (keys.hasNext()) { + try { + final String key = keys.next(); + final Object obj = addons.get(key); + if (obj != null && + obj instanceof JSONObject && + ((JSONObject) obj).optBoolean("ignore", false)) { + continue; + } + out.put(key, obj); + } catch (JSONException ex) { + // Do nothing. + } + } + return out; + } + + /** + * Take a collection of add-on descriptors, appending a consistent string + * to the provided builder. + */ + public static void appendSortedAddons(JSONObject addons, final EnvironmentAppender builder) { + final SortedSet keys = HealthReportUtils.sortedKeySet(addons); + + // For each add-on, produce a consistent, sorted mapping of its descriptor. + for (String key : keys) { + try { + JSONObject addon = addons.getJSONObject(key); + + // Now produce the output for this add-on. + builder.append(key); + builder.append("={"); + + for (String addonKey : HealthReportUtils.sortedKeySet(addon)) { + builder.append(addonKey); + builder.append("=="); + try { + builder.append(addon.get(addonKey).toString()); + } catch (JSONException e) { + builder.append("_e_"); + } + } + + builder.append("}"); + } catch (Exception e) { + // Muffle. + Logger.warn(LOG_TAG, "Invalid add-on for ID " + key); + } + } + } + + public void setJSONForAddons(byte[] json) throws Exception { + setJSONForAddons(new String(json, "UTF-8")); + } + + public void setJSONForAddons(String json) throws Exception { + if (json == null || "null".equals(json)) { + addons = null; + return; + } + addons = new JSONObject(json); + } + + public void setJSONForAddons(JSONObject json) { + addons = json; + } + + /** + * Includes ignored add-ons. + */ + public String getNormalizedAddonsJSON() { + // We trust that our input will already be normalized. If that assumption + // is invalidated, then we'll be sorry. + return (addons == null) ? "null" : addons.toString(); + } +} diff --git a/mobile/android/base/background/healthreport/HealthReportDatabaseStorage.java b/mobile/android/base/background/healthreport/HealthReportDatabaseStorage.java index 83cda357b176..87af864f93b9 100644 --- a/mobile/android/base/background/healthreport/HealthReportDatabaseStorage.java +++ b/mobile/android/base/background/healthreport/HealthReportDatabaseStorage.java @@ -128,7 +128,7 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { }; private static final String[] COLUMNS_ENVIRONMENT_DETAILS = new String[] { - "id", "hash", + "id", "version", "hash", "profileCreation", "cpuCount", "memoryMB", "isBlocklistEnabled", "isTelemetryEnabled", "extensionCount", @@ -138,6 +138,8 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { "appVersion", "appBuildID", "platformVersion", "platformBuildID", "os", "xpcomabi", "updateChannel", + "distribution", "osLocale", "appLocale", "acceptLangSet", + // Joined to the add-ons table. "addonsBody" }; @@ -188,7 +190,7 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { protected final HealthReportSQLiteOpenHelper helper; public static class HealthReportSQLiteOpenHelper extends SQLiteOpenHelper { - public static final int CURRENT_VERSION = 5; + public static final int CURRENT_VERSION = 6; public static final String LOG_TAG = "HealthReportSQL"; /** @@ -252,7 +254,10 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { " UNIQUE (body) " + ")"); + // N.B., hash collisions can occur across versions. In that case, the system + // is likely to persist the original environment version. db.execSQL("CREATE TABLE environments (id INTEGER PRIMARY KEY AUTOINCREMENT, " + + " version INTEGER, " + " hash TEXT, " + " profileCreation INTEGER, " + " cpuCount INTEGER, " + @@ -275,6 +280,12 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { " os TEXT, " + " xpcomabi TEXT, " + " updateChannel TEXT, " + + + " distribution TEXT, " + + " osLocale TEXT, " + + " appLocale TEXT, " + + " acceptLangSet INTEGER, " + + " addonsID INTEGER, " + " FOREIGN KEY (addonsID) REFERENCES addons(id) ON DELETE RESTRICT, " + " UNIQUE (hash) " + @@ -357,6 +368,7 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { private void createAddonsEnvironmentsView(SQLiteDatabase db) { db.execSQL("CREATE VIEW environments_with_addons AS " + "SELECT e.id AS id, " + + " e.version AS version, " + " e.hash AS hash, " + " e.profileCreation AS profileCreation, " + " e.cpuCount AS cpuCount, " + @@ -379,6 +391,10 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { " e.os AS os, " + " e.xpcomabi AS xpcomabi, " + " e.updateChannel AS updateChannel, " + + " e.distribution AS distribution, " + + " e.osLocale AS osLocale, " + + " e.appLocale AS appLocale, " + + " e.acceptLangSet AS acceptLangSet, " + " addons.body AS addonsBody " + "FROM environments AS e, addons " + "WHERE e.addonsID = addons.id"); @@ -417,6 +433,22 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { db.delete(EVENTS_TEXTUAL, "field NOT IN (SELECT id FROM fields)", null); } + private void upgradeDatabaseFrom5to6(SQLiteDatabase db) { + db.execSQL("DROP VIEW environments_with_addons"); + + // Add version to environment (default to 1). + db.execSQL("ALTER TABLE environments ADD COLUMN version INTEGER DEFAULT 1"); + + // Add fields to environment (default to empty string). + db.execSQL("ALTER TABLE environments ADD COLUMN distribution TEXT DEFAULT ''"); + db.execSQL("ALTER TABLE environments ADD COLUMN osLocale TEXT DEFAULT ''"); + db.execSQL("ALTER TABLE environments ADD COLUMN appLocale TEXT DEFAULT ''"); + db.execSQL("ALTER TABLE environments ADD COLUMN acceptLangSet INTEGER DEFAULT 0"); + + // Recreate view. + createAddonsEnvironmentsView(db); + } + @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion >= newVersion) { @@ -432,6 +464,8 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { upgradeDatabaseFrom3To4(db); case 4: upgradeDatabaseFrom4to5(db); + case 5: + upgradeDatabaseFrom5to6(db); } } catch (Exception e) { Logger.error(LOG_TAG, "Failure in onUpgrade.", e); @@ -536,6 +570,7 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { // Otherwise, add data and hash to the DB. ContentValues v = new ContentValues(); + v.put("version", version); v.put("hash", h); v.put("profileCreation", profileCreation); v.put("cpuCount", cpuCount); @@ -558,6 +593,10 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { v.put("os", os); v.put("xpcomabi", xpcomabi); v.put("updateChannel", updateChannel); + v.put("distribution", distribution); + v.put("osLocale", osLocale); + v.put("appLocale", appLocale); + v.put("acceptLangSet", acceptLangSet); final SQLiteDatabase db = storage.helper.getWritableDatabase(); @@ -643,6 +682,7 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { } public void init(ContentValues v) { + version = v.containsKey("version") ? v.getAsInteger("version") : Environment.CURRENT_VERSION; profileCreation = v.getAsInteger("profileCreation"); cpuCount = v.getAsInteger("cpuCount"); memoryMB = v.getAsInteger("memoryMB"); @@ -667,6 +707,11 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { xpcomabi = v.getAsString("xpcomabi"); updateChannel = v.getAsString("updateChannel"); + distribution = v.getAsString("distribution"); + osLocale = v.getAsString("osLocale"); + appLocale = v.getAsString("appLocale"); + acceptLangSet = v.getAsInteger("acceptLangSet"); + try { setJSONForAddons(v.getAsString("addonsBody")); } catch (Exception e) { @@ -686,6 +731,7 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { public boolean init(Cursor cursor) { int i = 0; this.id = cursor.getInt(i++); + this.version = cursor.getInt(i++); this.hash = cursor.getString(i++); profileCreation = cursor.getInt(i++); @@ -712,6 +758,11 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { xpcomabi = cursor.getString(i++); updateChannel = cursor.getString(i++); + distribution = cursor.getString(i++); + osLocale = cursor.getString(i++); + appLocale = cursor.getString(i++); + acceptLangSet = cursor.getInt(i++); + try { setJSONForAddons(cursor.getBlob(i++)); } catch (Exception e) { @@ -1339,6 +1390,7 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { } // Called internally only to ensure the same db instance is used. + @SuppressWarnings("static-method") protected int deleteOrphanedEnv(final SQLiteDatabase db, final int curEnv) { final String whereClause = "id != ? AND " + @@ -1353,6 +1405,7 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { } // Called internally only to ensure the same db instance is used. + @SuppressWarnings("static-method") protected int deleteEventsBefore(final SQLiteDatabase db, final String dayString) { final String whereClause = "date < ?"; final String[] whereArgs = new String[] {dayString}; @@ -1377,6 +1430,7 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { } // Called internally only to ensure the same db instance is used. + @SuppressWarnings("static-method") protected int deleteOrphanedAddons(final SQLiteDatabase db) { final String whereClause = "id NOT IN (SELECT addonsID FROM environments)"; return db.delete("addons", whereClause, null); diff --git a/mobile/android/base/background/healthreport/HealthReportGenerator.java b/mobile/android/base/background/healthreport/HealthReportGenerator.java index 426a1c336a96..10559b62b34e 100644 --- a/mobile/android/base/background/healthreport/HealthReportGenerator.java +++ b/mobile/android/base/background/healthreport/HealthReportGenerator.java @@ -388,24 +388,117 @@ public class HealthReportGenerator { return gecko; } + // Null-safe string comparison. + private static boolean stringsDiffer(final String a, final String b) { + if (a == null) { + return b != null; + } + return !a.equals(b); + } + private static JSONObject getAppInfo(Environment e, Environment current) throws JSONException { JSONObject appinfo = new JSONObject(); - int changes = 0; - if (current == null || current.isBlocklistEnabled != e.isBlocklistEnabled) { - appinfo.put("isBlocklistEnabled", e.isBlocklistEnabled); - changes++; + + Logger.debug(LOG_TAG, "Generating appinfo for v" + e.version + " env " + e.hash); + + // Is the environment in question newer than the diff target, or is + // there no diff target? + final boolean outdated = current == null || + e.version > current.version; + + // Is the environment in question a different version (lower or higher), + // or is there no diff target? + final boolean differ = outdated || current.version > e.version; + + // Always produce an output object if there's a version mismatch or this + // isn't a diff. Otherwise, track as we go if there's any difference. + boolean changed = differ; + + switch (e.version) { + // There's a straightforward correspondence between environment versions + // and appinfo versions. + case 2: + appinfo.put("_v", 3); + break; + case 1: + appinfo.put("_v", 2); + break; + default: + Logger.warn(LOG_TAG, "Unknown environment version: " + e.version); + return appinfo; } - if (current == null || current.isTelemetryEnabled != e.isTelemetryEnabled) { - appinfo.put("isTelemetryEnabled", e.isTelemetryEnabled); - changes++; + + switch (e.version) { + case 2: + if (populateAppInfoV2(appinfo, e, current, outdated)) { + changed = true; + } + // Fall through. + + case 1: + // There is no older version than v1, so don't check outdated. + if (populateAppInfoV1(e, current, appinfo)) { + changed = true; + } } - if (current != null && changes == 0) { + + if (!changed) { return null; } - appinfo.put("_v", 2); + return appinfo; } + private static boolean populateAppInfoV1(Environment e, + Environment current, + JSONObject appinfo) + throws JSONException { + boolean changes = false; + if (current == null || current.isBlocklistEnabled != e.isBlocklistEnabled) { + appinfo.put("isBlocklistEnabled", e.isBlocklistEnabled); + changes = true; + } + + if (current == null || current.isTelemetryEnabled != e.isTelemetryEnabled) { + appinfo.put("isTelemetryEnabled", e.isTelemetryEnabled); + changes = true; + } + + return changes; + } + + private static boolean populateAppInfoV2(JSONObject appinfo, + Environment e, + Environment current, + final boolean outdated) + throws JSONException { + boolean changes = false; + if (outdated || + stringsDiffer(current.osLocale, e.osLocale)) { + appinfo.put("osLocale", e.osLocale); + changes = true; + } + + if (outdated || + stringsDiffer(current.appLocale, e.appLocale)) { + appinfo.put("appLocale", e.appLocale); + changes = true; + } + + if (outdated || + stringsDiffer(current.distribution, e.distribution)) { + appinfo.put("distribution", e.distribution); + changes = true; + } + + if (outdated || + current.acceptLangSet != e.acceptLangSet) { + appinfo.put("acceptLangIsUserSet", e.acceptLangSet); + changes = true; + } + return changes; + } + private static JSONObject getAddonCounts(Environment e, Environment current) throws JSONException { JSONObject counts = new JSONObject(); int changes = 0; diff --git a/mobile/android/base/background/healthreport/ProfileInformationCache.java b/mobile/android/base/background/healthreport/ProfileInformationCache.java index 42cb2f6a631f..7f1fca9cda13 100644 --- a/mobile/android/base/background/healthreport/ProfileInformationCache.java +++ b/mobile/android/base/background/healthreport/ProfileInformationCache.java @@ -10,6 +10,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.nio.charset.Charset; +import java.util.Locale; import java.util.Scanner; import org.json.JSONException; @@ -32,8 +33,9 @@ public class ProfileInformationCache implements ProfileInformationProvider { * -: No version number; implicit v1. * 1: Add versioning (Bug 878670). * 2: Bump to regenerate add-on set after landing Bug 900694 (Bug 901622). + * 3: Add distribution, osLocale, appLocale. */ - public static final int FORMAT_VERSION = 2; + public static final int FORMAT_VERSION = 3; protected boolean initialized = false; protected boolean needsWrite = false; @@ -42,7 +44,29 @@ public class ProfileInformationCache implements ProfileInformationProvider { private volatile boolean blocklistEnabled = true; private volatile boolean telemetryEnabled = false; + private volatile boolean isAcceptLangUserSet = false; + private volatile long profileCreationTime = 0; + private volatile String distribution = ""; + + // There are really four kinds of locale in play: + // + // * The OS + // * The Android environment of the app (setDefault) + // * The Gecko locale + // * The requested content locale (Accept-Language). + // + // We track only the first two, assuming that the Gecko locale will typically + // be the same as the app locale. + // + // The app locale is fetched from the PIC because it can be modified at + // runtime -- it won't necessarily be what Locale.getDefaultLocale() returns + // in a fresh non-browser profile. + // + // We also track the OS locale here for the same reason -- we need to store + // the default (OS) value before the locale-switching code takes effect! + private volatile String osLocale = ""; + private volatile String appLocale = ""; private volatile JSONObject addons = null; @@ -62,7 +86,11 @@ public class ProfileInformationCache implements ProfileInformationProvider { object.put("version", FORMAT_VERSION); object.put("blocklist", blocklistEnabled); object.put("telemetry", telemetryEnabled); + object.put("isAcceptLangUserSet", isAcceptLangUserSet); object.put("profileCreated", profileCreationTime); + object.put("osLocale", osLocale); + object.put("appLocale", appLocale); + object.put("distribution", distribution); object.put("addons", addons); } catch (JSONException e) { // There isn't much we can do about this. @@ -86,8 +114,12 @@ public class ProfileInformationCache implements ProfileInformationProvider { case FORMAT_VERSION: blocklistEnabled = object.getBoolean("blocklist"); telemetryEnabled = object.getBoolean("telemetry"); + isAcceptLangUserSet = object.getBoolean("isAcceptLangUserSet"); profileCreationTime = object.getLong("profileCreated"); addons = object.getJSONObject("addons"); + distribution = object.getString("distribution"); + osLocale = object.getString("osLocale"); + appLocale = object.getString("appLocale"); return true; default: Logger.warn(LOG_TAG, "Unable to restore from version " + version + " PIC file: expecting " + FORMAT_VERSION); @@ -206,6 +238,18 @@ public class ProfileInformationCache implements ProfileInformationProvider { needsWrite = true; } + @Override + public boolean isAcceptLangUserSet() { + ensureInitialized(); + return isAcceptLangUserSet; + } + + public void setAcceptLangUserSet(boolean value) { + Logger.debug(LOG_TAG, "Setting accept-lang as user-set: " + value); + isAcceptLangUserSet = value; + needsWrite = true; + } + @Override public long getProfileCreationTime() { ensureInitialized(); @@ -218,17 +262,83 @@ public class ProfileInformationCache implements ProfileInformationProvider { needsWrite = true; } + @Override + public String getDistributionString() { + ensureInitialized(); + return distribution; + } + + /** + * Ensure that your arguments are non-null. + */ + public void setDistributionString(String distributionID, String distributionVersion) { + Logger.debug(LOG_TAG, "Setting distribution: " + distributionID + ", " + distributionVersion); + distribution = distributionID + ":" + distributionVersion; + needsWrite = true; + } + + @Override + public String getAppLocale() { + ensureInitialized(); + return appLocale; + } + + public void setAppLocale(String value) { + if (value.equalsIgnoreCase(appLocale)) { + return; + } + Logger.debug(LOG_TAG, "Setting app locale: " + value); + appLocale = value.toLowerCase(Locale.US); + needsWrite = true; + } + + @Override + public String getOSLocale() { + ensureInitialized(); + return osLocale; + } + + public void setOSLocale(String value) { + if (value.equalsIgnoreCase(osLocale)) { + return; + } + Logger.debug(LOG_TAG, "Setting OS locale: " + value); + osLocale = value.toLowerCase(Locale.US); + needsWrite = true; + } + + /** + * Update the PIC, if necessary, to match the current locale environment. + * + * @return true if the PIC needed to be updated. + */ + public boolean updateLocales(String osLocale, String appLocale) { + if (this.osLocale.equalsIgnoreCase(osLocale) && + (appLocale == null || this.appLocale.equalsIgnoreCase(appLocale))) { + return false; + } + this.setOSLocale(osLocale); + if (appLocale != null) { + this.setAppLocale(appLocale); + } + return true; + } + @Override public JSONObject getAddonsJSON() { + ensureInitialized(); return addons; } public void updateJSONForAddon(String id, String json) throws Exception { addons.put(id, new JSONObject(json)); + needsWrite = true; } public void removeAddon(String id) { - addons.remove(id); + if (null != addons.remove(id)) { + needsWrite = true; + } } /** @@ -240,6 +350,7 @@ public class ProfileInformationCache implements ProfileInformationProvider { } try { addons.put(id, json); + needsWrite = true; } catch (Exception e) { // Why would this happen? Logger.warn(LOG_TAG, "Unexpected failure updating JSON for add-on.", e); @@ -253,9 +364,11 @@ public class ProfileInformationCache implements ProfileInformationProvider { */ public void setJSONForAddons(String json) throws Exception { addons = new JSONObject(json); + needsWrite = true; } public void setJSONForAddons(JSONObject json) { addons = json; + needsWrite = true; } } diff --git a/mobile/android/services/java-sources.mn b/mobile/android/services/java-sources.mn index 2fcd0cff9808..c37962420891 100644 --- a/mobile/android/services/java-sources.mn +++ b/mobile/android/services/java-sources.mn @@ -27,6 +27,7 @@ background/db/CursorDumper.java background/db/Tab.java background/healthreport/Environment.java background/healthreport/EnvironmentBuilder.java +background/healthreport/EnvironmentV1.java background/healthreport/HealthReportBroadcastReceiver.java background/healthreport/HealthReportBroadcastService.java background/healthreport/HealthReportDatabases.java diff --git a/mobile/android/tests/background/junit3/src/healthreport/MockDatabaseEnvironment.java b/mobile/android/tests/background/junit3/src/healthreport/MockDatabaseEnvironment.java index f9ea5e8b2c17..526784b4a2bf 100644 --- a/mobile/android/tests/background/junit3/src/healthreport/MockDatabaseEnvironment.java +++ b/mobile/android/tests/background/junit3/src/healthreport/MockDatabaseEnvironment.java @@ -38,7 +38,7 @@ public class MockDatabaseEnvironment extends DatabaseEnvironment { } } - public MockDatabaseEnvironment mockInit(String version) { + public MockDatabaseEnvironment mockInit(String appVersion) { profileCreation = 1234; cpuCount = 2; memoryMB = 512; @@ -55,7 +55,7 @@ public class MockDatabaseEnvironment extends DatabaseEnvironment { vendor = ""; appName = ""; appID = ""; - appVersion = version; + this.appVersion = appVersion; appBuildID = ""; platformVersion = ""; platformBuildID = ""; @@ -63,6 +63,14 @@ public class MockDatabaseEnvironment extends DatabaseEnvironment { xpcomabi = ""; updateChannel = ""; + // v2 fields. + distribution = ""; + appLocale = ""; + osLocale = ""; + acceptLangSet = 0; + + version = Environment.CURRENT_VERSION; + return this; } } diff --git a/mobile/android/tests/background/junit3/src/healthreport/TestHealthReportGenerator.java b/mobile/android/tests/background/junit3/src/healthreport/TestHealthReportGenerator.java index 0ac122dec441..d123284164a0 100644 --- a/mobile/android/tests/background/junit3/src/healthreport/TestHealthReportGenerator.java +++ b/mobile/android/tests/background/junit3/src/healthreport/TestHealthReportGenerator.java @@ -15,6 +15,10 @@ import org.mozilla.gecko.background.healthreport.HealthReportStorage.Field; import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields; import org.mozilla.gecko.background.helpers.FakeProfileTestCase; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.util.SparseArray; + public class TestHealthReportGenerator extends FakeProfileTestCase { @SuppressWarnings("static-method") public void testOptObject() throws JSONException { @@ -57,9 +61,14 @@ public class TestHealthReportGenerator extends FakeProfileTestCase { assertFalse(bar.has("b")); } + // We don't initialize the env in testHashing, so these are just the default + // values for the Java types, in order. private static final String EXPECTED_MOCK_BASE_HASH = "000nullnullnullnullnullnullnull" + "nullnullnullnullnullnull00000"; + // v2 fields. + private static final String EXPECTED_MOCK_BASE_HASH_SUFFIX = "null" + "null" + 0 + "null"; + public void testHashing() throws JSONException { MockHealthReportDatabaseStorage storage = new MockHealthReportDatabaseStorage(context, fakeProfileDirectory); MockDatabaseEnvironment env = new MockDatabaseEnvironment(storage, MockDatabaseEnvironment.MockEnvironmentAppender.class); @@ -96,10 +105,10 @@ public class TestHealthReportGenerator extends FakeProfileTestCase { "}"); env.addons.put("{addonA}", addonA1); - assertEquals(EXPECTED_MOCK_BASE_HASH + addonAHash, env.getHash()); + assertEquals(EXPECTED_MOCK_BASE_HASH + addonAHash + EXPECTED_MOCK_BASE_HASH_SUFFIX, env.getHash()); env.addons.put("{addonA}", addonA1rev); - assertEquals(EXPECTED_MOCK_BASE_HASH + addonAHash, env.getHash()); + assertEquals(EXPECTED_MOCK_BASE_HASH + addonAHash + EXPECTED_MOCK_BASE_HASH_SUFFIX, env.getHash()); } private void assertJSONDiff(JSONObject source, JSONObject diff) throws JSONException { @@ -406,4 +415,103 @@ public class TestHealthReportGenerator extends FakeProfileTestCase { protected String getCacheSuffix() { return File.separator + "health-" + System.currentTimeMillis() + ".profile"; } + + + public void testEnvironmentDiffing() throws JSONException { + // Manually insert a v1 environment. + final MockHealthReportDatabaseStorage storage = new MockHealthReportDatabaseStorage(context, fakeProfileDirectory); + final SQLiteDatabase db = storage.getDB(); + storage.deleteEverything(); + final MockDatabaseEnvironment v1env = storage.getEnvironment(); + v1env.mockInit("27.0a1"); + v1env.version = 1; + v1env.appLocale = ""; + v1env.osLocale = ""; + v1env.distribution = ""; + v1env.acceptLangSet = 0; + final int v1ID = v1env.register(); + + // Verify. + final String[] cols = new String[] { + "id", "version", "hash", + "osLocale", "acceptLangSet", "appLocale", "distribution" + }; + + final Cursor c1 = db.query("environments", cols, "id = " + v1ID, null, null, null, null); + String v1envHash; + try { + assertTrue(c1.moveToFirst()); + assertEquals(1, c1.getCount()); + + assertEquals(v1ID, c1.getInt(0)); + assertEquals(1, c1.getInt(1)); + + v1envHash = c1.getString(2); + assertNotNull(v1envHash); + assertEquals("", c1.getString(3)); + assertEquals(0, c1.getInt(4)); + assertEquals("", c1.getString(5)); + assertEquals("", c1.getString(6)); + } finally { + c1.close(); + } + + // Insert a v2 environment. + final MockDatabaseEnvironment v2env = storage.getEnvironment(); + v2env.mockInit("27.0a1"); + v2env.appLocale = v2env.osLocale = "en_us"; + v2env.acceptLangSet = 1; + + final int v2ID = v2env.register(); + assertFalse(v1ID == v2ID); + final Cursor c2 = db.query("environments", cols, "id = " + v2ID, null, null, null, null); + String v2envHash; + try { + assertTrue(c2.moveToFirst()); + assertEquals(1, c2.getCount()); + + assertEquals(v2ID, c2.getInt(0)); + assertEquals(2, c2.getInt(1)); + + v2envHash = c2.getString(2); + assertNotNull(v2envHash); + assertEquals("en_us", c2.getString(3)); + assertEquals(1, c2.getInt(4)); + assertEquals("en_us", c2.getString(5)); + assertEquals("", c2.getString(6)); + } finally { + c2.close(); + } + + assertFalse(v1envHash.equals(v2envHash)); + + // Now let's diff based on DB contents. + SparseArray envs = storage.getEnvironmentRecordsByID(); + + JSONObject oldEnv = HealthReportGenerator.jsonify(envs.get(v1ID), null).getJSONObject("org.mozilla.appInfo.appinfo"); + JSONObject newEnv = HealthReportGenerator.jsonify(envs.get(v2ID), null).getJSONObject("org.mozilla.appInfo.appinfo"); + + // Generate the new env as if the old were the current. This should rarely happen in practice. + // Fields supported by the new env but not the old will appear, even if the 'default' for the + // old implementation is equal to the new env's value. + JSONObject newVsOld = HealthReportGenerator.jsonify(envs.get(v2ID), envs.get(v1ID)).getJSONObject("org.mozilla.appInfo.appinfo"); + + // Generate the old env as if the new were the current. This is normal. Fields not supported by the old + // environment version should not appear in the output. + JSONObject oldVsNew = HealthReportGenerator.jsonify(envs.get(v1ID), envs.get(v2ID)).getJSONObject("org.mozilla.appInfo.appinfo"); + assertEquals(2, oldEnv.getInt("_v")); + assertEquals(3, newEnv.getInt("_v")); + assertEquals(2, oldVsNew.getInt("_v")); + assertEquals(3, newVsOld.getInt("_v")); + + assertFalse(oldVsNew.has("osLocale")); + assertFalse(oldVsNew.has("appLocale")); + assertFalse(oldVsNew.has("distribution")); + assertFalse(oldVsNew.has("acceptLangIsUserSet")); + + assertTrue(newVsOld.has("osLocale")); + assertTrue(newVsOld.has("appLocale")); + assertTrue(newVsOld.has("distribution")); + assertTrue(newVsOld.has("acceptLangIsUserSet")); + } } diff --git a/mobile/android/tests/background/junit3/src/healthreport/TestHealthReportProvider.java b/mobile/android/tests/background/junit3/src/healthreport/TestHealthReportProvider.java index 8d49d6fe8618..eacf5ef5f119 100644 --- a/mobile/android/tests/background/junit3/src/healthreport/TestHealthReportProvider.java +++ b/mobile/android/tests/background/junit3/src/healthreport/TestHealthReportProvider.java @@ -146,7 +146,7 @@ public class TestHealthReportProvider extends DBProviderTestCase Date: Wed, 16 Oct 2013 18:56:27 -0700 Subject: [PATCH 20/26] Bug 922694 - Part 4: grab Accept-Locale pref in FHR. r=mcomella --- mobile/android/base/GeckoApp.java | 23 +++- mobile/android/base/GeckoAppShell.java | 6 + .../base/health/BrowserHealthRecorder.java | 125 ++++++++++-------- mobile/android/chrome/content/browser.js | 88 +++++++++--- 4 files changed, 170 insertions(+), 72 deletions(-) diff --git a/mobile/android/base/GeckoApp.java b/mobile/android/base/GeckoApp.java index 322d0c5836ea..6a2a0552d2b8 100644 --- a/mobile/android/base/GeckoApp.java +++ b/mobile/android/base/GeckoApp.java @@ -117,6 +117,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; @@ -1291,7 +1292,16 @@ abstract public class GeckoApp final String profilePath = getProfile().getDir().getAbsolutePath(); final EventDispatcher dispatcher = GeckoAppShell.getEventDispatcher(); Log.i(LOGTAG, "Creating BrowserHealthRecorder."); - mHealthRecorder = new BrowserHealthRecorder(GeckoApp.this, profilePath, dispatcher, + final String osLocale = Locale.getDefault().toString(); + Log.d(LOGTAG, "Locale is " + osLocale); + + // Replace the duplicate `osLocale` argument when we support switchable + // application locales. + mHealthRecorder = new BrowserHealthRecorder(GeckoApp.this, + profilePath, + dispatcher, + osLocale, + osLocale, // Placeholder. previousSession); } }); @@ -1555,8 +1565,15 @@ abstract public class GeckoApp GeckoPreferences.broadcastHealthReportUploadPref(context); /* - XXXX see bug 635342 - We want to disable this code if possible. It is about 145ms in runtime + XXXX see Bug 635342. + We want to disable this code if possible. It is about 145ms in runtime. + + If this code ever becomes live again, you'll need to chain the + new locale into BrowserHealthRecorder correctly. See + GeckoAppShell.setSelectedLocale. + We pass the OS locale into the BHR constructor: we need to grab + that *before* we modify the current locale! + SharedPreferences settings = getPreferences(Activity.MODE_PRIVATE); String localeCode = settings.getString(getPackageName() + ".locale", ""); if (localeCode != null && localeCode.length() > 0) diff --git a/mobile/android/base/GeckoAppShell.java b/mobile/android/base/GeckoAppShell.java index 28d91f00cff6..4e5cc9fda45f 100644 --- a/mobile/android/base/GeckoAppShell.java +++ b/mobile/android/base/GeckoAppShell.java @@ -1525,6 +1525,12 @@ public class GeckoAppShell Gecko resets the locale to en-US by calling this function with an empty string. This affects GeckoPreferences activity in multi-locale builds. + N.B., if this code ever becomes live again, you need to hook it up to locale + recording in BrowserHealthRecorder: we track the current app and OS locales + as part of the recorded environment. + + See similar note in GeckoApp.java for the startup path. + //We're not using this, not need to save it (see bug 635342) SharedPreferences settings = getContext().getPreferences(Activity.MODE_PRIVATE); diff --git a/mobile/android/base/health/BrowserHealthRecorder.java b/mobile/android/base/health/BrowserHealthRecorder.java index c73cbcdfc76f..055abb56c8c4 100644 --- a/mobile/android/base/health/BrowserHealthRecorder.java +++ b/mobile/android/base/health/BrowserHealthRecorder.java @@ -16,8 +16,6 @@ import org.mozilla.gecko.AppConstants; import org.mozilla.gecko.GeckoApp; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoEvent; -import org.mozilla.gecko.PrefsHelper; -import org.mozilla.gecko.PrefsHelper.PrefHandler; import org.mozilla.gecko.background.healthreport.EnvironmentBuilder; import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage; @@ -38,6 +36,7 @@ import java.io.FileOutputStream; import java.io.OutputStreamWriter; import java.nio.charset.Charset; import java.util.ArrayList; +import java.util.Iterator; import java.util.Scanner; import java.util.concurrent.atomic.AtomicBoolean; @@ -50,8 +49,7 @@ import java.util.concurrent.atomic.AtomicBoolean; * Keep an instance of this class around. * * Tell it when an environment attribute has changed: call {@link - * #onBlocklistPrefChanged(boolean)} or {@link - * #onTelemetryPrefChanged(boolean)}, followed by {@link + * #onAppLocaleChanged(String)} followed by {@link * #onEnvironmentChanged()}. * * Use it to record events: {@link #recordSearch(String, String)}. @@ -60,8 +58,9 @@ import java.util.concurrent.atomic.AtomicBoolean; */ public class BrowserHealthRecorder implements GeckoEventListener { private static final String LOG_TAG = "GeckoHealthRec"; + private static final String PREF_ACCEPT_LANG = "intl.accept_languages"; private static final String PREF_BLOCKLIST_ENABLED = "extensions.blocklist.enabled"; - private static final String EVENT_ADDONS_ALL = "Addons:All"; + private static final String EVENT_SNAPSHOT = "HealthReport:Snapshot"; private static final String EVENT_ADDONS_CHANGE = "Addons:Change"; private static final String EVENT_ADDONS_UNINSTALLING = "Addons:Uninstalling"; private static final String EVENT_PREF_CHANGE = "Pref:Change"; @@ -242,8 +241,15 @@ public class BrowserHealthRecorder implements GeckoEventListener { /** * This constructor does IO. Run it on a background thread. + * + * appLocale can be null, which indicates that it will be provided later. */ - public BrowserHealthRecorder(final Context context, final String profilePath, final EventDispatcher dispatcher, SessionInformation previousSession) { + public BrowserHealthRecorder(final Context context, + final String profilePath, + final EventDispatcher dispatcher, + final String osLocale, + final String appLocale, + SessionInformation previousSession) { Log.d(LOG_TAG, "Initializing. Dispatcher is " + dispatcher); this.dispatcher = dispatcher; this.previousSession = previousSession; @@ -263,9 +269,12 @@ public class BrowserHealthRecorder implements GeckoEventListener { this.client = null; } + // Note that the PIC is not necessarily fully initialized at this point: + // we haven't set the app locale. This must be done before an environment + // is recorded. this.profileCache = new ProfileInformationCache(profilePath); try { - this.initialize(context, profilePath); + this.initialize(context, profilePath, osLocale, appLocale); } catch (Exception e) { Log.e(LOG_TAG, "Exception initializing.", e); } @@ -299,7 +308,7 @@ public class BrowserHealthRecorder implements GeckoEventListener { } private void unregisterEventListeners() { - this.dispatcher.unregisterEventListener(EVENT_ADDONS_ALL, this); + this.dispatcher.unregisterEventListener(EVENT_SNAPSHOT, this); this.dispatcher.unregisterEventListener(EVENT_ADDONS_CHANGE, this); this.dispatcher.unregisterEventListener(EVENT_ADDONS_UNINSTALLING, this); this.dispatcher.unregisterEventListener(EVENT_PREF_CHANGE, this); @@ -307,14 +316,9 @@ public class BrowserHealthRecorder implements GeckoEventListener { this.dispatcher.unregisterEventListener(EVENT_SEARCH, this); } - public void onBlocklistPrefChanged(boolean to) { + public void onAppLocaleChanged(String to) { this.profileCache.beginInitialization(); - this.profileCache.setBlocklistEnabled(to); - } - - public void onTelemetryPrefChanged(boolean to) { - this.profileCache.beginInitialization(); - this.profileCache.setTelemetryEnabled(to); + this.profileCache.setAppLocale(to); } public void onAddonChanged(String id, JSONObject json) { @@ -340,8 +344,7 @@ public class BrowserHealthRecorder implements GeckoEventListener { * environment, such that a new environment should be computed and prepared * for use in future events. * - * Invoke this method after calls that mutate the environment, such as - * {@link #onBlocklistPrefChanged(boolean)}. + * Invoke this method after calls that mutate the environment. * * If this change resulted in a transition between two environments, {@link * #onEnvironmentTransition(int, int)} will be invoked on the background @@ -491,14 +494,36 @@ public class BrowserHealthRecorder implements GeckoEventListener { return time; } - private void handlePrefValue(final String pref, final boolean value) { - Log.d(LOG_TAG, "Incorporating environment: " + pref + " = " + value); - if (AppConstants.TELEMETRY_PREF_NAME.equals(pref)) { - profileCache.setTelemetryEnabled(value); + private void onPrefMessage(final String pref, final JSONObject message) { + Log.d(LOG_TAG, "Incorporating environment: " + pref); + if (PREF_ACCEPT_LANG.equals(pref)) { + // We only record whether this is user-set. + try { + this.profileCache.beginInitialization(); + this.profileCache.setAcceptLangUserSet(message.getBoolean("isUserSet")); + } catch (JSONException ex) { + Log.w(LOG_TAG, "Unexpected JSONException fetching isUserSet for " + pref); + } return; } - if (PREF_BLOCKLIST_ENABLED.equals(pref)) { - profileCache.setBlocklistEnabled(value); + + // (We only handle boolean prefs right now.) + try { + boolean value = message.getBoolean("value"); + + if (AppConstants.TELEMETRY_PREF_NAME.equals(pref)) { + this.profileCache.beginInitialization(); + this.profileCache.setTelemetryEnabled(value); + return; + } + + if (PREF_BLOCKLIST_ENABLED.equals(pref)) { + this.profileCache.beginInitialization(); + this.profileCache.setBlocklistEnabled(value); + return; + } + } catch (JSONException ex) { + Log.w(LOG_TAG, "Unexpected JSONException fetching boolean value for " + pref); return; } Log.w(LOG_TAG, "Unexpected pref: " + pref); @@ -571,7 +596,9 @@ public class BrowserHealthRecorder implements GeckoEventListener { * Add provider-specific initialization in this method. */ private synchronized void initialize(final Context context, - final String profilePath) + final String profilePath, + final String osLocale, + final String appLocale) throws java.io.IOException { Log.d(LOG_TAG, "Initializing profile cache."); @@ -579,6 +606,9 @@ public class BrowserHealthRecorder implements GeckoEventListener { // If we can restore state from last time, great. if (this.profileCache.restoreUnlessInitialized()) { + this.profileCache.updateLocales(osLocale, appLocale); + this.profileCache.completeInitialization(); + Log.d(LOG_TAG, "Successfully restored state. Initializing storage."); initializeStorage(); return; @@ -587,31 +617,12 @@ public class BrowserHealthRecorder implements GeckoEventListener { // Otherwise, let's initialize it from scratch. this.profileCache.beginInitialization(); this.profileCache.setProfileCreationTime(getAndPersistProfileInitTime(context, profilePath)); + this.profileCache.setOSLocale(osLocale); + this.profileCache.setAppLocale(appLocale); - final BrowserHealthRecorder self = this; - - PrefHandler handler = new PrefsHelper.PrefHandlerBase() { - @Override - public void prefValue(String pref, boolean value) { - handlePrefValue(pref, value); - } - - @Override - public void finish() { - Log.d(LOG_TAG, "Requesting all add-ons from Gecko."); - dispatcher.registerEventListener(EVENT_ADDONS_ALL, self); - GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Addons:FetchAll", null)); - // Wait for the broadcast event which completes our initialization. - } - }; - - // Oh, singletons. - PrefsHelper.getPrefs(new String[] { - AppConstants.TELEMETRY_PREF_NAME, - PREF_BLOCKLIST_ENABLED - }, - handler); - Log.d(LOG_TAG, "Requested prefs."); + Log.d(LOG_TAG, "Requesting all add-ons and FHR prefs from Gecko."); + dispatcher.registerEventListener(EVENT_SNAPSHOT, this); + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HealthReport:RequestSnapshot", null)); } /** @@ -638,12 +649,22 @@ public class BrowserHealthRecorder implements GeckoEventListener { @Override public void handleMessage(String event, JSONObject message) { try { - if (EVENT_ADDONS_ALL.equals(event)) { - Log.d(LOG_TAG, "Got all add-ons."); + if (EVENT_SNAPSHOT.equals(event)) { + Log.d(LOG_TAG, "Got all add-ons and prefs."); try { - JSONObject addons = message.getJSONObject("json"); + JSONObject json = message.getJSONObject("json"); + JSONObject addons = json.getJSONObject("addons"); Log.i(LOG_TAG, "Persisting " + addons.length() + " add-ons."); profileCache.setJSONForAddons(addons); + + JSONObject prefs = json.getJSONObject("prefs"); + Log.i(LOG_TAG, "Persisting prefs."); + Iterator keys = prefs.keys(); + while (keys.hasNext()) { + String pref = (String) keys.next(); + this.onPrefMessage(pref, prefs.getJSONObject(pref)); + } + profileCache.completeInitialization(); } catch (java.io.IOException e) { Log.e(LOG_TAG, "Error completing profile cache initialization.", e); @@ -675,7 +696,7 @@ public class BrowserHealthRecorder implements GeckoEventListener { if (EVENT_PREF_CHANGE.equals(event)) { final String pref = message.getString("pref"); Log.d(LOG_TAG, "Pref changed: " + pref); - handlePrefValue(pref, message.getBoolean("value")); + this.onPrefMessage(pref, message); this.onEnvironmentChanged(); return; } diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index de052a3d3114..759b1f74495c 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -5316,7 +5316,10 @@ var FormAssistant = { * -- and reflect them back to Java. */ let HealthReportStatusListener = { - TELEMETRY_PREF: + PREF_ACCEPT_LANG: "intl.accept_languages", + PREF_BLOCKLIST_ENABLED: "extensions.blocklist.enabled", + + PREF_TELEMETRY_ENABLED: #ifdef MOZ_TELEMETRY_REPORTING // Telemetry pref differs based on build. #ifdef MOZ_TELEMETRY_ON_BY_DEFAULT @@ -5335,18 +5338,21 @@ let HealthReportStatusListener = { console.log("Failed to initialize add-on status listener. FHR cannot report add-on state. " + ex); } - Services.obs.addObserver(this, "Addons:FetchAll", false); - Services.prefs.addObserver("extensions.blocklist.enabled", this, false); - if (this.TELEMETRY_PREF) { - Services.prefs.addObserver(this.TELEMETRY_PREF, this, false); + console.log("Adding HealthReport:RequestSnapshot observer."); + Services.obs.addObserver(this, "HealthReport:RequestSnapshot", false); + Services.prefs.addObserver(this.PREF_ACCEPT_LANG, this, false); + Services.prefs.addObserver(this.PREF_BLOCKLIST_ENABLED, this, false); + if (this.PREF_TELEMETRY_ENABLED) { + Services.prefs.addObserver(this.PREF_TELEMETRY_ENABLED, this, false); } }, uninit: function () { - Services.obs.removeObserver(this, "Addons:FetchAll"); - Services.prefs.removeObserver("extensions.blocklist.enabled", this); - if (this.TELEMETRY_PREF) { - Services.prefs.removeObserver(this.TELEMETRY_PREF, this); + Services.obs.removeObserver(this, "HealthReport:RequestSnapshot"); + Services.prefs.removeObserver(this.PREF_ACCEPT_LANG, this); + Services.prefs.removeObserver(this.PREF_BLOCKLIST_ENABLED, this); + if (this.PREF_TELEMETRY_ENABLED) { + Services.prefs.removeObserver(this.PREF_TELEMETRY_ENABLED, this); } AddonManager.removeAddonListener(this); @@ -5354,11 +5360,30 @@ let HealthReportStatusListener = { observe: function (aSubject, aTopic, aData) { switch (aTopic) { - case "Addons:FetchAll": - HealthReportStatusListener.sendAllAddonsToJava(); + case "HealthReport:RequestSnapshot": + HealthReportStatusListener.sendSnapshotToJava(); break; case "nsPref:changed": - sendMessageToJava({ type: "Pref:Change", pref: aData, value: Services.prefs.getBoolPref(aData) }); + let response = { + type: "Pref:Change", + pref: aData, + isUserSet: Services.prefs.prefHasUserValue(aData), + }; + + switch (aData) { + case this.PREF_ACCEPT_LANG: + response.value = Services.prefs.getCharPref(aData); + break; + case this.PREF_TELEMETRY_ENABLED: + case this.PREF_BLOCKLIST_ENABLED: + response.value = Services.prefs.getBoolPref(aData); + break; + default: + console.log("Unexpected pref in HealthReportStatusListener: " + aData); + return; + } + + sendMessageToJava(response); break; } }, @@ -5440,9 +5465,9 @@ let HealthReportStatusListener = { this.notifyJava(aAddon); }, - sendAllAddonsToJava: function () { + sendSnapshotToJava: function () { AddonManager.getAllAddons(function (aAddons) { - let json = {}; + let jsonA = {}; if (aAddons) { for (let i = 0; i < aAddons.length; ++i) { let addon = aAddons[i]; @@ -5451,14 +5476,43 @@ let HealthReportStatusListener = { if (HealthReportStatusListener._shouldIgnore(addon)) { addonJSON.ignore = true; } - json[addon.id] = addonJSON; + jsonA[addon.id] = addonJSON; } catch (e) { // Just skip this add-on. } } } - sendMessageToJava({ type: "Addons:All", json: json }); - }); + + // Now add prefs. + let jsonP = {}; + for (let pref of [this.PREF_BLOCKLIST_ENABLED, this.PREF_TELEMETRY_ENABLED]) { + if (!pref) { + // This will be the case for PREF_TELEMETRY_ENABLED in developer builds. + continue; + } + jsonP[pref] = { + pref: pref, + value: Services.prefs.getBoolPref(pref), + isUserSet: Services.prefs.prefHasUserValue(pref), + }; + } + for (let pref of [this.PREF_ACCEPT_LANG]) { + jsonP[pref] = { + pref: pref, + value: Services.prefs.getCharPref(pref), + isUserSet: Services.prefs.prefHasUserValue(pref), + }; + } + + console.log("Sending snapshot message."); + sendMessageToJava({ + type: "HealthReport:Snapshot", + json: { + addons: jsonA, + prefs: jsonP, + }, + }); + }.bind(this)); }, }; From 95c9d0078116fdb5e7574d13546db5a6f237e6c8 Mon Sep 17 00:00:00 2001 From: Richard Newman Date: Wed, 16 Oct 2013 18:56:27 -0700 Subject: [PATCH 21/26] Bug 922694 - Part 5: grab distribution ID in FHR. r=mcomella --- .../base/health/BrowserHealthRecorder.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/mobile/android/base/health/BrowserHealthRecorder.java b/mobile/android/base/health/BrowserHealthRecorder.java index 055abb56c8c4..a35699c5b6be 100644 --- a/mobile/android/base/health/BrowserHealthRecorder.java +++ b/mobile/android/base/health/BrowserHealthRecorder.java @@ -13,6 +13,8 @@ import android.content.SharedPreferences; import android.util.Log; import org.mozilla.gecko.AppConstants; +import org.mozilla.gecko.Distribution; +import org.mozilla.gecko.Distribution.DistributionDescriptor; import org.mozilla.gecko.GeckoApp; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoEvent; @@ -620,9 +622,21 @@ public class BrowserHealthRecorder implements GeckoEventListener { this.profileCache.setOSLocale(osLocale); this.profileCache.setAppLocale(appLocale); - Log.d(LOG_TAG, "Requesting all add-ons and FHR prefs from Gecko."); - dispatcher.registerEventListener(EVENT_SNAPSHOT, this); - GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HealthReport:RequestSnapshot", null)); + // Because the distribution lookup can take some time, do it at the end of + // our background startup work, along with the Gecko snapshot fetch. + final GeckoEventListener self = this; + ThreadUtils.postToBackgroundThread(new Runnable() { + @Override + public void run() { + final DistributionDescriptor desc = new Distribution(context).getDescriptor(); + if (desc != null && desc.valid) { + profileCache.setDistributionString(desc.id, desc.version); + } + Log.d(LOG_TAG, "Requesting all add-ons and FHR prefs from Gecko."); + dispatcher.registerEventListener(EVENT_SNAPSHOT, self); + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HealthReport:RequestSnapshot", null)); + } + }); } /** From 1eb80875c5daf08b3716e7e65232e6f3017898e6 Mon Sep 17 00:00:00 2001 From: Richard Newman Date: Wed, 16 Oct 2013 18:56:27 -0700 Subject: [PATCH 22/26] Bug 922694 - Part 6: follow-up to fix test. r=nalexander --- .../base/tests/testDistribution.java.in | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/mobile/android/base/tests/testDistribution.java.in b/mobile/android/base/tests/testDistribution.java.in index 6b094c92d11b..7e407ae5830b 100644 --- a/mobile/android/base/tests/testDistribution.java.in +++ b/mobile/android/base/tests/testDistribution.java.in @@ -38,11 +38,50 @@ public class testDistribution extends ContentProviderTest { return TEST_MOCHITEST; } + /** + * This is a hack. + * + * Startup results in us writing prefs -- we fetch the Distribution, which + * caches its state. Our tests try to wipe those prefs, but apparently + * sometimes race with startup, which leads to us not getting one of our + * expected messages. The test fails. + * + * This hack waits for any existing background tasks -- such as the one that + * writes prefs -- to finish before we begin the test. + */ + private void waitForBackgroundHappiness() { + try { + ClassLoader classLoader = mActivity.getClassLoader(); + Class threadUtilsClass = classLoader.loadClass("org.mozilla.gecko.util.ThreadUtils"); + Method postToBackgroundThread = threadUtilsClass.getMethod("postToBackgroundThread", Runnable.class); + final Object signal = new Object(); + final Runnable done = new Runnable() { + @Override + public void run() { + synchronized (signal) { + signal.notify(); + } + } + }; + synchronized (signal) { + postToBackgroundThread.invoke(null, done); + signal.wait(); + } + } catch (Exception e) { + mAsserter.ok(false, "Exception waiting on background thread.", e.toString()); + } + mAsserter.dumpLog("Background task completed. Proceeding."); + } + public void testDistribution() { mActivity = getActivity(); String mockPackagePath = getMockPackagePath(); + // Wait for any startup-related background distribution shenanigans to + // finish. This reduces the chance of us racing with startup pref writes. + waitForBackgroundHappiness(); + // Pre-clear distribution pref, run basic preferences and en-US localized preferences Tests clearDistributionPref(); setTestLocale("en-US"); @@ -268,6 +307,7 @@ public class testDistribution extends ContentProviderTest { // Clears the distribution pref to return distribution state to STATE_UNKNOWN private void clearDistributionPref() { + mAsserter.dumpLog("Clearing distribution pref."); SharedPreferences settings = mActivity.getSharedPreferences("GeckoApp", Activity.MODE_PRIVATE); String keyName = mActivity.getPackageName() + ".distribution_state"; settings.edit().remove(keyName).commit(); From 2973051823a7c00209be01f13323423d5df548b9 Mon Sep 17 00:00:00 2001 From: Richard Newman Date: Wed, 16 Oct 2013 19:25:13 -0700 Subject: [PATCH 23/26] Bug 922694 - Part 7: use standalone prefs to avoid racing with startup. r=ckitching --- mobile/android/base/BrowserApp.java | 2 +- mobile/android/base/Distribution.java | 18 +++++++++++------- .../base/tests/testDistribution.java.in | 6 +++--- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/mobile/android/base/BrowserApp.java b/mobile/android/base/BrowserApp.java index 7e2068acb590..eda8fc2b7a4f 100644 --- a/mobile/android/base/BrowserApp.java +++ b/mobile/android/base/BrowserApp.java @@ -505,7 +505,7 @@ abstract public class BrowserApp extends GeckoApp registerEventListener("Updater:Launch"); registerEventListener("Reader:GoToReadingList"); - Distribution.init(this, getPackageResourcePath()); + Distribution.init(this); JavaAddonManager.getInstance().init(getApplicationContext()); mSharedPreferencesHelper = new SharedPreferencesHelper(getApplicationContext()); mOrderedBroadcastHelper = new OrderedBroadcastHelper(getApplicationContext()); diff --git a/mobile/android/base/Distribution.java b/mobile/android/base/Distribution.java index a42d90332b0b..d2104bec47c7 100644 --- a/mobile/android/base/Distribution.java +++ b/mobile/android/base/Distribution.java @@ -33,6 +33,8 @@ import java.util.zip.ZipFile; public final class Distribution { private static final String LOGTAG = "GeckoDistribution"; + private static final String DEFAULT_PREFS = GeckoApp.PREFS_NAME; + private static final int STATE_UNKNOWN = 0; private static final int STATE_NONE = 1; private static final int STATE_SET = 2; @@ -85,12 +87,12 @@ public final class Distribution { * * @param packagePath where to look for the distribution directory. */ - public static void init(final Context context, final String packagePath) { + public static void init(final Context context, final String packagePath, final String prefsPath) { // Read/write preferences and files on the background thread. ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { - Distribution dist = new Distribution(context, packagePath); + Distribution dist = new Distribution(context, packagePath, prefsPath); boolean distributionSet = dist.doInit(); if (distributionSet) { GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Distribution:Set", "")); @@ -104,7 +106,7 @@ public final class Distribution { * package path. */ public static void init(final Context context) { - Distribution.init(context, context.getPackageResourcePath()); + Distribution.init(context, context.getPackageResourcePath(), DEFAULT_PREFS); } /** @@ -116,8 +118,9 @@ public final class Distribution { return dist.getBookmarks(); } - private final String packagePath; private final Context context; + private final String packagePath; + private final String prefsBranch; private int state = STATE_UNKNOWN; private File distributionDir = null; @@ -125,13 +128,14 @@ public final class Distribution { /** * @param packagePath where to look for the distribution directory. */ - public Distribution(final Context context, final String packagePath) { + public Distribution(final Context context, final String packagePath, final String prefsBranch) { this.context = context; this.packagePath = packagePath; + this.prefsBranch = prefsBranch; } public Distribution(final Context context) { - this(context, context.getPackageResourcePath()); + this(context, context.getPackageResourcePath(), DEFAULT_PREFS); } /** @@ -142,7 +146,7 @@ public final class Distribution { private boolean doInit() { // Bail if we've already tried to initialize the distribution, and // there wasn't one. - SharedPreferences settings = context.getSharedPreferences(GeckoApp.PREFS_NAME, Activity.MODE_PRIVATE); + SharedPreferences settings = context.getSharedPreferences(prefsBranch, Activity.MODE_PRIVATE); String keyName = context.getPackageName() + ".distribution_state"; this.state = settings.getInt(keyName, STATE_UNKNOWN); if (this.state == STATE_NONE) { diff --git a/mobile/android/base/tests/testDistribution.java.in b/mobile/android/base/tests/testDistribution.java.in index 7e407ae5830b..1d5d6c525b05 100644 --- a/mobile/android/base/tests/testDistribution.java.in +++ b/mobile/android/base/tests/testDistribution.java.in @@ -4,6 +4,7 @@ package @ANDROID_PACKAGE_NAME@.tests; import @ANDROID_PACKAGE_NAME@.*; import android.app.Activity; +import android.content.Context; import android.content.SharedPreferences; import java.io.File; import java.io.FileOutputStream; @@ -103,11 +104,10 @@ public class testDistribution extends ContentProviderTest { // Call Distribution.init with the mock package. ClassLoader classLoader = mActivity.getClassLoader(); Class distributionClass = classLoader.loadClass("org.mozilla.gecko.Distribution"); - Class contextClass = classLoader.loadClass("android.content.Context"); - Method init = distributionClass.getMethod("init", contextClass, String.class); + Method init = distributionClass.getMethod("init", Context.class, String.class, String.class); Actions.EventExpecter distributionSetExpecter = mActions.expectGeckoEvent("Distribution:Set:OK"); - init.invoke(null, mActivity, aPackagePath); + init.invoke(null, mActivity, aPackagePath, "prefs-" + System.currentTimeMillis()); distributionSetExpecter.blockForEvent(); distributionSetExpecter.unregisterListener(); } catch (Exception e) { From b3686963478d62377efb67d4d9d2699d4602ea1c Mon Sep 17 00:00:00 2001 From: Vicamo Yang Date: Thu, 17 Oct 2013 10:26:27 +0800 Subject: [PATCH 24/26] Bug 927432: fix 'finish() not called' in chrome js Marionette test cases. r=jgriffin --- .../marionette/tests/unit/test_chrome_async_finish.js | 6 ++++++ .../marionette/client/marionette/tests/unit/unit-tests.ini | 3 ++- testing/marionette/marionette-server.js | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 testing/marionette/client/marionette/tests/unit/test_chrome_async_finish.js diff --git a/testing/marionette/client/marionette/tests/unit/test_chrome_async_finish.js b/testing/marionette/client/marionette/tests/unit/test_chrome_async_finish.js new file mode 100644 index 000000000000..8d2df3ac26ba --- /dev/null +++ b/testing/marionette/client/marionette/tests/unit/test_chrome_async_finish.js @@ -0,0 +1,6 @@ +MARIONETTE_TIMEOUT = 60000; +MARIONETTE_CONTEXT = "chrome"; +ok(true); +(function () { + finish(); +})(); diff --git a/testing/marionette/client/marionette/tests/unit/unit-tests.ini b/testing/marionette/client/marionette/tests/unit/unit-tests.ini index 545e4685cee0..30b48d11a7f1 100644 --- a/testing/marionette/client/marionette/tests/unit/unit-tests.ini +++ b/testing/marionette/client/marionette/tests/unit/unit-tests.ini @@ -93,4 +93,5 @@ b2g = false [test_implicit_waits.py] [test_date_time_value.py] [test_getactiveframe_oop.py] -[test_submit.py] \ No newline at end of file +[test_submit.py] +[test_chrome_async_finish.js] diff --git a/testing/marionette/marionette-server.js b/testing/marionette/marionette-server.js index 65b2a41ad563..091498775d00 100644 --- a/testing/marionette/marionette-server.js +++ b/testing/marionette/marionette-server.js @@ -844,7 +844,7 @@ MarionetteServerConnection.prototype = { aRequest.newSandbox = true; } if (this.context == "chrome") { - if (aRequest.async) { + if (aRequest.parameters.async) { this.executeWithCallback(aRequest, aRequest.parameters.async); } else { From 3ff0993a7309e37ff08b49a8d21c63ca22e7b257 Mon Sep 17 00:00:00 2001 From: Gaia Pushbot Date: Wed, 16 Oct 2013 20:45:23 -0700 Subject: [PATCH 25/26] Bumping gaia.json for 2 gaia-central revision(s) a=gaia-bump ======== https://hg.mozilla.org/integration/gaia-central/rev/dcae8edae13b Author: Luke Chang Desc: Merge pull request #12142 from luke-chang/900906_pinyin_in_worker Bug 900906 - [Keyboard] Wrap the pinyin IME engine with web worker, r=rudylu ======== https://hg.mozilla.org/integration/gaia-central/rev/af4356d09549 Author: Luke Chang Desc: Bug 900906 - [Keyboard] Wrap the pinyin IME engine with web worker --- b2g/config/gaia.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index cb74943c9f16..8c181a0cdfe8 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -1,4 +1,4 @@ { - "revision": "49e443173e45e298e1c8e0152c4abd485748fc07", + "revision": "dcae8edae13b8c8ac42a6c697c0211567218a53c", "repo_path": "/integration/gaia-central" } From 06e360bf3892dc55c433d24fdb4f89137a6616e3 Mon Sep 17 00:00:00 2001 From: Gaia Pushbot Date: Wed, 16 Oct 2013 21:20:23 -0700 Subject: [PATCH 26/26] Bumping gaia.json for 2 gaia-central revision(s) a=gaia-bump ======== https://hg.mozilla.org/integration/gaia-central/rev/563d1aa93586 Author: George Desc: Merge pull request #12904 from cctuan/927345 Bug 927345 - The sorting of video group in gallery view is not correct ======== https://hg.mozilla.org/integration/gaia-central/rev/33205118c316 Author: cctuan Desc: Bug 927345 - The sorting of video group in gallery view is not correct --- b2g/config/gaia.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index 8c181a0cdfe8..10ab5af68a49 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -1,4 +1,4 @@ { - "revision": "dcae8edae13b8c8ac42a6c697c0211567218a53c", + "revision": "563d1aa93586165246ab2ab9d40566a598f56387", "repo_path": "/integration/gaia-central" }