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