diff --git a/build/mach_bootstrap.py b/build/mach_bootstrap.py index dd81a7ded69b..c67637c9412a 100644 --- a/build/mach_bootstrap.py +++ b/build/mach_bootstrap.py @@ -128,7 +128,7 @@ def bootstrap(topsrcdir, mozilla_dir=None): if not os.path.exists(state_env_dir): print('Creating global state directory from environment variable: %s' % state_env_dir) - os.makedirs(state_env_dir, mode=0777) + os.makedirs(state_env_dir, mode=0o770) print('Please re-run mach.') sys.exit(1) else: diff --git a/configure.in b/configure.in index fd08abffd450..c17c131c5d0f 100644 --- a/configure.in +++ b/configure.in @@ -7844,7 +7844,15 @@ else fi if test "$MOZ_WIDGET_TOOLKIT" = "android"; then - dnl On Android, static resources live in the assets/ folder of the APK. + dnl Fennec's static resources live in the assets/ folder of the + dnl APK. Adding a path to the name here works because we only + dnl have one omnijar file in the final package (which is not the + dnl case on desktop), and necessitates some contortions during + dnl packaging so that the resources in the omnijar are considered + dnl as rooted at / and not as rooted at assets/ (which again is + dnl not the case on desktop: there are omnijars rooted at webrtc/, + dnl etc). packager.mk handles changing the rooting of the single + dnl omnijar. OMNIJAR_NAME=assets/omni.ja else OMNIJAR_NAME=omni.ja diff --git a/mobile/android/base/GeckoApp.java b/mobile/android/base/GeckoApp.java index 6ed814276793..b6cbf998cc0e 100644 --- a/mobile/android/base/GeckoApp.java +++ b/mobile/android/base/GeckoApp.java @@ -556,9 +556,13 @@ abstract public class GeckoApp geckoConnected(); // This method is already running on the background thread, so we - // know that mHealthRecorder will exist. This method is cheap, so - // don't spawn a new runnable. - mHealthRecorder.recordGeckoStartupTime(mGeckoReadyStartupTimer.getElapsed()); + // know that mHealthRecorder will exist. That doesn't stop us being + // paranoid. + // This method is cheap, so don't spawn a new runnable. + final BrowserHealthRecorder rec = mHealthRecorder; + if (rec != null) { + rec.recordGeckoStartupTime(mGeckoReadyStartupTimer.getElapsed()); + } } else if (event.equals("ToggleChrome:Hide")) { toggleChrome(false); } else if (event.equals("ToggleChrome:Show")) { @@ -1532,7 +1536,10 @@ abstract public class GeckoApp ThreadUtils.getBackgroundHandler().postDelayed(new Runnable() { @Override public void run() { - mHealthRecorder.recordJavaStartupTime(javaDuration); + final BrowserHealthRecorder rec = mHealthRecorder; + if (rec != null) { + rec.recordJavaStartupTime(javaDuration); + } // Sync settings need Gecko to be loaded, so // no hurry in starting this. @@ -1879,7 +1886,6 @@ abstract public class GeckoApp // track the duration of the session. final long now = System.currentTimeMillis(); final long realTime = android.os.SystemClock.elapsedRealtime(); - final BrowserHealthRecorder rec = mHealthRecorder; ThreadUtils.postToBackgroundThread(new Runnable() { @Override @@ -1894,8 +1900,11 @@ abstract public class GeckoApp currentSession.recordBegin(editor); editor.commit(); + final BrowserHealthRecorder rec = mHealthRecorder; if (rec != null) { rec.setCurrentSession(currentSession); + } else { + Log.w(LOGTAG, "Can't record session: rec is null."); } } }); diff --git a/mobile/android/base/background/healthreport/HealthReportGenerator.java b/mobile/android/base/background/healthreport/HealthReportGenerator.java index 0b791768834d..d57e150522f7 100644 --- a/mobile/android/base/background/healthreport/HealthReportGenerator.java +++ b/mobile/android/base/background/healthreport/HealthReportGenerator.java @@ -427,7 +427,16 @@ public class HealthReportGenerator { /** * Compute the *tree* difference set between the two objects. If the two - * objects are identical, returns null. + * objects are identical, returns null. If from is + * null, returns to. If to is + * null, behaves as if to were an empty object. + * + * (Note that this method does not check for {@link JSONObject#NULL}, because + * by definition it can't be provided as input to this method.) + * + * This behavior is intended to simplify life for callers: a missing object + * can be viewed as (and behaves as) an empty map, to a useful extent, rather + * than throwing an exception. * * @param from * a JSONObject. @@ -445,10 +454,14 @@ public class HealthReportGenerator { public static JSONObject diff(JSONObject from, JSONObject to, boolean includeNull) throws JSONException { - if (from == null || from == JSONObject.NULL) { + if (from == null) { return to; } + if (to == null) { + return diff(from, new JSONObject(), includeNull); + } + JSONObject out = new JSONObject(); HashSet toKeys = includeNull ? new HashSet(to.length()) diff --git a/mobile/android/base/health/BrowserHealthRecorder.java b/mobile/android/base/health/BrowserHealthRecorder.java index 225d0b277d03..51a4e1b2b0f0 100644 --- a/mobile/android/base/health/BrowserHealthRecorder.java +++ b/mobile/android/base/health/BrowserHealthRecorder.java @@ -777,9 +777,17 @@ public class BrowserHealthRecorder implements GeckoEventListener { final int day = storage.getDay(); final int env = this.env; final String key = getEngineKey(engine); + final BrowserHealthRecorder self = this; + ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { + final HealthReportDatabaseStorage storage = self.storage; + if (storage == null) { + Log.d(LOG_TAG, "No storage: not recording search. Shutting down?"); + return; + } + Log.d(LOG_TAG, "Recording search: " + key + ", " + location + " (" + day + ", " + env + ")."); final int searchField = storage.getField(MEASUREMENT_NAME_SEARCH_COUNTS, @@ -829,8 +837,12 @@ public class BrowserHealthRecorder implements GeckoEventListener { * * "r": reason. Values are "P" (activity paused), "A" (abnormal termination) * "d": duration. Value in seconds. - * "sg": Gecko startup time. Present if this is a clean launch. - * "sj": Java startup time. Present if this is a clean launch. + * "sg": Gecko startup time. Present if this is a clean launch. This + * corresponds to the telemetry timer FENNEC_STARTUP_TIME_GECKOREADY. + * "sj": Java activity init time. Present if this is a clean launch. This + * corresponds to the telemetry timer FENNEC_STARTUP_TIME_JAVAUI, + * and includes initialization tasks beyond initial + * onWindowFocusChanged. * * Abnormal terminations will be missing a duration and will feature these keys: * diff --git a/mobile/android/base/health/BrowserHealthReporter.java b/mobile/android/base/health/BrowserHealthReporter.java index fbf64d43621a..4b2c754fbb28 100644 --- a/mobile/android/base/health/BrowserHealthReporter.java +++ b/mobile/android/base/health/BrowserHealthReporter.java @@ -14,6 +14,7 @@ import org.mozilla.gecko.GeckoEvent; import org.mozilla.gecko.GeckoProfile; import org.mozilla.gecko.background.healthreport.EnvironmentBuilder; +import org.mozilla.gecko.background.healthreport.HealthReportConstants; import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage; import org.mozilla.gecko.background.healthreport.HealthReportGenerator; @@ -34,16 +35,15 @@ import org.json.JSONObject; public class BrowserHealthReporter implements GeckoEventListener { private static final String LOGTAG = "GeckoHealthRep"; - public static final long MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000; - public static final long MILLISECONDS_PER_SIX_MONTHS = 180 * MILLISECONDS_PER_DAY; - public static final String EVENT_REQUEST = "HealthReport:Request"; public static final String EVENT_RESPONSE = "HealthReport:Response"; + protected final Context context; + public BrowserHealthReporter() { GeckoAppShell.registerEventListener(EVENT_REQUEST, this); - final Context context = GeckoAppShell.getContext(); + context = GeckoAppShell.getContext(); if (context == null) { throw new IllegalStateException("Null Gecko context"); } @@ -62,14 +62,11 @@ public class BrowserHealthReporter implements GeckoEventListener { * @param lastPingTime timestamp when last health report was uploaded * (milliseconds since epoch). * @param profilePath path of the profile to generate report for. + * @throws JSONException if JSON generation fails. + * @throws IllegalStateException if the environment does not allow to generate a report. + * @return non-null report. */ public JSONObject generateReport(long since, long lastPingTime, String profilePath) throws JSONException { - final Context context = GeckoAppShell.getContext(); - if (context == null) { - Log.e(LOGTAG, "Null Gecko context; returning null report.", new RuntimeException()); - return null; - } - // We abuse the life-cycle of an Android ContentProvider slightly by holding // onto a ContentProviderClient while we generate a payload. This keeps // our database storage alive, while also allowing us to share a database @@ -86,29 +83,50 @@ public class BrowserHealthReporter implements GeckoEventListener { // to close it. HealthReportDatabaseStorage storage = EnvironmentBuilder.getStorage(client, profilePath); if (storage == null) { - Log.e(LOGTAG, "No storage in health reporter; returning null report.", new RuntimeException()); - return null; + throw new IllegalStateException("No storage in Health Reporter."); } HealthReportGenerator generator = new HealthReportGenerator(storage); - return generator.generateDocument(since, lastPingTime, profilePath); + JSONObject report = generator.generateDocument(since, lastPingTime, profilePath); + if (report == null) { + throw new IllegalStateException("Not enough profile information to generate report."); + } + return report; } finally { client.release(); } } + /** + * Get last time a health report was successfully uploaded. + * + * This is read from shared preferences, so call it from a background + * thread. Bug 882182 tracks making this work with multiple profiles. + * + * @return milliseconds since the epoch, or 0 if never uploaded. + */ + protected long getLastUploadLocalTime() { + return context + .getSharedPreferences(HealthReportConstants.PREFS_BRANCH, 0) + .getLong(HealthReportConstants.PREF_LAST_UPLOAD_LOCAL_TIME, 0L); + } + /** * Generate a new Health Report for the current Gecko profile. * * This method performs IO, so call it from a background thread. + * + * @throws JSONException if JSON generation fails. + * @throws IllegalStateException if the environment does not allow to generate a report. + * @return non-null Health Report. */ public JSONObject generateReport() throws JSONException { GeckoProfile profile = GeckoAppShell.getGeckoInterface().getProfile(); String profilePath = profile.getDir().getAbsolutePath(); - long since = System.currentTimeMillis() - MILLISECONDS_PER_SIX_MONTHS; - // TODO: read this from per-profile SharedPreference owned by background uploader. - long lastPingTime = since; + long since = System.currentTimeMillis() - HealthReportConstants.MILLISECONDS_PER_SIX_MONTHS; + long lastPingTime = Math.max(getLastUploadLocalTime(), HealthReportConstants.EARLIEST_LAST_PING); + return generateReport(since, lastPingTime, profilePath); } @@ -118,11 +136,12 @@ public class BrowserHealthReporter implements GeckoEventListener { ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { - JSONObject report = new JSONObject(); + JSONObject report = null; try { - report = generateReport(); + report = generateReport(); // non-null if it returns. } catch (Exception e) { - Log.e(LOGTAG, "Generating report failed; responding with null.", e); + Log.e(LOGTAG, "Generating report failed; responding with empty report.", e); + report = new JSONObject(); } GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(EVENT_RESPONSE, report.toString())); diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index a3d33b6a0539..df0e31f6be23 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -204,7 +204,6 @@ function FormTracker(name, engine) { Tracker.call(this, name, engine); Svc.Obs.add("weave:engine:start-tracking", this); Svc.Obs.add("weave:engine:stop-tracking", this); - Svc.Obs.add("profile-change-teardown", this); } FormTracker.prototype = { __proto__: Tracker.prototype, @@ -243,16 +242,17 @@ FormTracker.prototype = { case "satchel-storage-changed": if (data == "formhistory-add" || data == "formhistory-remove") { let guid = subject.QueryInterface(Ci.nsISupportsString).toString(); - this.addChangedID(guid); - this.score += SCORE_INCREMENT_MEDIUM; + this.trackEntry(guid); } break; - case "profile-change-teardown": - FormWrapper._finalize(); - break; } }, + trackEntry: function (guid) { + this.addChangedID(guid); + this.score += SCORE_INCREMENT_MEDIUM; + }, + notify: function (formElement, aWindow, actionURI) { if (this.ignoreAll) { return; @@ -320,7 +320,10 @@ FormTracker.prototype = { // Get the GUID on a delay so that it can be added to the DB first... Utils.nextTick(function() { this._log.trace("Logging form element: " + [name, el.value]); - this.trackEntry(name, el.value); + let guid = FormWrapper.getGUID(name, el.value); + if (guid) { + this.trackEntry(guid); + } }, this); } } diff --git a/toolkit/mozapps/installer/packager.mk b/toolkit/mozapps/installer/packager.mk index ee0335cc078b..3670df0bb3ba 100644 --- a/toolkit/mozapps/installer/packager.mk +++ b/toolkit/mozapps/installer/packager.mk @@ -357,6 +357,19 @@ ifdef MOZ_ENABLE_SZIP SZIP_LIBRARIES := $(ASSET_SO_LIBRARIES) endif +# Fennec's OMNIJAR_NAME can include a directory; for example, it might +# be "assets/omni.ja". This path specifies where the omni.ja file +# lives in the APK, but should not root the resources it contains +# under assets/ (i.e., resources should not live at chrome://assets/). +# packager.py writes /omni.ja in order to be consistent with the +# layout expected by language repacks. Therefore, we move it to the +# correct path here, in INNER_MAKE_PACKAGE. See comment about +# OMNIJAR_NAME in configure.in. + +# OMNIJAR_DIR is './' for "omni.ja", 'assets/' for "assets/omni.ja". +OMNIJAR_DIR := $(dir $(OMNIJAR_NAME)) +OMNIJAR_NAME := $(notdir $(OMNIJAR_NAME)) + PKG_SUFFIX = .apk INNER_MAKE_PACKAGE = \ $(if $(ALREADY_SZIPPED),,$(foreach lib,$(SZIP_LIBRARIES),host/bin/szip $(MOZ_SZIP_FLAGS) $(STAGEPATH)$(MOZ_PKG_DIR)$(_BINPATH)/$(lib) && )) \ @@ -369,7 +382,9 @@ INNER_MAKE_PACKAGE = \ rm $(_ABS_DIST)/gecko.ap_ && \ $(ZIP) -0 $(_ABS_DIST)/gecko.ap_ $(ASSET_SO_LIBRARIES) && \ $(ZIP) -r9D $(_ABS_DIST)/gecko.ap_ $(DIST_FILES) -x $(NON_DIST_FILES) $(SZIP_LIBRARIES) && \ - $(ZIP) -0 $(_ABS_DIST)/gecko.ap_ $(OMNIJAR_NAME)) && \ + $(if $(filter-out ./,$(OMNIJAR_DIR)), \ + mkdir -p $(OMNIJAR_DIR) && mv $(OMNIJAR_NAME) $(OMNIJAR_DIR) && ) \ + $(ZIP) -0 $(_ABS_DIST)/gecko.ap_ $(OMNIJAR_DIR)$(OMNIJAR_NAME)) && \ rm -f $(_ABS_DIST)/gecko.apk && \ cp $(_ABS_DIST)/gecko.ap_ $(_ABS_DIST)/gecko.apk && \ $(ZIP) -j0 $(_ABS_DIST)/gecko.apk $(STAGEPATH)$(MOZ_PKG_DIR)$(_BINPATH)/classes.dex && \ @@ -378,13 +393,21 @@ INNER_MAKE_PACKAGE = \ $(ZIPALIGN) -f -v 4 $(_ABS_DIST)/gecko.apk $(PACKAGE) && \ $(INNER_ROBOCOP_PACKAGE) +# Language repacks root the resources contained in assets/omni.ja +# under assets/, but the repacks expect them to be rooted at /. +# Therefore, we we move the omnijar back to / so the resources are +# under the root here, in INNER_UNMAKE_PACKAGE. See comments about +# OMNIJAR_NAME earlier in this file and in configure.in. + INNER_UNMAKE_PACKAGE = \ mkdir $(MOZ_PKG_DIR) && \ ( cd $(MOZ_PKG_DIR) && \ $(UNZIP) $(UNPACKAGE) && \ mv lib/$(ABI_DIR)/libmozglue.so . && \ mv lib/$(ABI_DIR)/*plugin-container* $(MOZ_CHILD_PROCESS_NAME) && \ - rm -rf lib/$(ABI_DIR) ) + rm -rf lib/$(ABI_DIR) \ + $(if $(filter-out ./,$(OMNIJAR_DIR)), \ + && mv $(OMNIJAR_DIR)$(OMNIJAR_NAME) $(OMNIJAR_NAME)) ) endif ifeq ($(MOZ_PKG_FORMAT),DMG) @@ -584,8 +607,12 @@ endif export NO_PKG_FILES USE_ELF_HACK ELF_HACK_FLAGS +# Override the value of OMNIJAR_NAME from config.status with the value +# set earlier in this file. + stage-package: $(MOZ_PKG_MANIFEST) @rm -rf $(DIST)/$(PKG_PATH)$(PKG_BASENAME).tar $(DIST)/$(PKG_PATH)$(PKG_BASENAME).dmg $@ $(EXCLUDE_LIST) + OMNIJAR_NAME=$(OMNIJAR_NAME) \ $(PYTHON) $(MOZILLA_DIR)/toolkit/mozapps/installer/packager.py $(DEFINES) \ --format $(MOZ_PACKAGER_FORMAT) \ $(addprefix --removals ,$(MOZ_PKG_REMOVALS)) \