Merge fx-team to m-c.
@ -195,8 +195,12 @@ let gFxAccounts = {
|
||||
},
|
||||
|
||||
onMenuPanelCommand: function (event) {
|
||||
if (event.originalTarget.hasAttribute("signedin")) {
|
||||
let button = event.originalTarget;
|
||||
|
||||
if (button.hasAttribute("signedin")) {
|
||||
this.openPreferences();
|
||||
} else if (button.hasAttribute("failed")) {
|
||||
this.openSignInAgainPage();
|
||||
} else {
|
||||
this.openAccountsPage();
|
||||
}
|
||||
|
@ -174,13 +174,6 @@
|
||||
this.setAttribute("viewtype", "main");
|
||||
}
|
||||
|
||||
this._mainViewObserver.observe(this._mainView, {
|
||||
attributes: true,
|
||||
characterData: true,
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
this._shiftMainView();
|
||||
]]></body>
|
||||
</method>
|
||||
@ -296,6 +289,14 @@
|
||||
// every time the popup closes, which is why we have to set it each time.
|
||||
this._panel.autoPosition = false;
|
||||
this._syncContainerWithMainView();
|
||||
|
||||
this._mainViewObserver.observe(this._mainView, {
|
||||
attributes: true,
|
||||
characterData: true,
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
break;
|
||||
case "popupshown":
|
||||
this._setMaxHeight();
|
||||
@ -304,6 +305,7 @@
|
||||
this.removeAttribute("panelopen");
|
||||
this._mainView.style.removeProperty("height");
|
||||
this.showMainView();
|
||||
this._mainViewObserver.disconnect();
|
||||
break;
|
||||
}
|
||||
]]></body>
|
||||
|
@ -240,7 +240,11 @@ let DebuggerController = {
|
||||
} else {
|
||||
this._startDebuggingTab(startedDebugging.resolve);
|
||||
const startedTracing = promise.defer();
|
||||
this._startTracingTab(traceActor, startedTracing.resolve);
|
||||
if (Prefs.tracerEnabled && traceActor) {
|
||||
this._startTracingTab(traceActor, startedTracing.resolve);
|
||||
} else {
|
||||
startedTracing.resolve();
|
||||
}
|
||||
|
||||
return promise.all([startedDebugging.promise, startedTracing.promise]);
|
||||
}
|
||||
|
@ -214,6 +214,7 @@ support-files =
|
||||
[browser_dbg_tracing-03.js]
|
||||
[browser_dbg_tracing-04.js]
|
||||
[browser_dbg_tracing-05.js]
|
||||
[browser_dbg_tracing-06.js]
|
||||
[browser_dbg_variables-view-01.js]
|
||||
[browser_dbg_variables-view-02.js]
|
||||
[browser_dbg_variables-view-03.js]
|
||||
|
39
browser/devtools/debugger/test/browser_dbg_tracing-06.js
Normal file
@ -0,0 +1,39 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Test that the tracer doesn't connect to the backend when tracing is disabled.
|
||||
*/
|
||||
|
||||
const TAB_URL = EXAMPLE_URL + "doc_tracing-01.html";
|
||||
const TRACER_PREF = "devtools.debugger.tracer";
|
||||
|
||||
let gTab, gDebuggee, gPanel, gDebugger;
|
||||
let gOriginalPref = Services.prefs.getBoolPref(TRACER_PREF);
|
||||
Services.prefs.setBoolPref(TRACER_PREF, false);
|
||||
|
||||
function test() {
|
||||
initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
|
||||
gTab = aTab;
|
||||
gDebuggee = aDebuggee;
|
||||
gPanel = aPanel;
|
||||
gDebugger = gPanel.panelWin;
|
||||
|
||||
waitForSourceShown(gPanel, "code_tracing-01.js")
|
||||
.then(() => {
|
||||
ok(!gDebugger.DebuggerController.traceClient, "Should not have a trace client");
|
||||
closeDebuggerAndFinish(gPanel);
|
||||
})
|
||||
.then(null, aError => {
|
||||
ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
registerCleanupFunction(function() {
|
||||
gTab = null;
|
||||
gDebuggee = null;
|
||||
gPanel = null;
|
||||
gDebugger = null;
|
||||
Services.prefs.setBoolPref(TRACER_PREF, gOriginalPref);
|
||||
});
|
@ -640,12 +640,6 @@ tabmodalprompt:not([promptType="promptUserAndPass"]) .infoContainer {
|
||||
|
||||
.meta {
|
||||
background-color: @panel_light_color@;
|
||||
/* bug 969354
|
||||
background-image: url("chrome://browser/skin/images/firefox-watermark.png");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-attachment: fixed;
|
||||
*/
|
||||
}
|
||||
|
||||
/* needs to observe the viewstate */
|
||||
|
@ -38,3 +38,19 @@
|
||||
#start-container[viewstate="snapped"] .meta-section:not([expanded]) > richgrid {
|
||||
visibility: collapse;
|
||||
}
|
||||
|
||||
/* Watermark */
|
||||
#startui-body::after {
|
||||
content: '';
|
||||
width: 256px;
|
||||
height: 256px;
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
margin-top: -128px;
|
||||
margin-left: -128px;
|
||||
z-index: -1;
|
||||
background-image: url("chrome://browser/skin/images/firefox-watermark.png");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
}
|
||||
|
@ -697,9 +697,13 @@ toolbarbutton[sdk-button="true"][cui-areatype="toolbar"] > .toolbarbutton-icon {
|
||||
#sync-button {
|
||||
-moz-image-region: rect(0px 144px 24px 120px);
|
||||
}
|
||||
#sync-button[status="active"] {
|
||||
list-style-image: url("chrome://browser/skin/sync-24-throbber.png");
|
||||
-moz-image-region: rect(0px 24px 24px 0px);
|
||||
#sync-button[cui-areatype="toolbar"][status="active"] {
|
||||
list-style-image: url("chrome://browser/skin/syncProgress-toolbar.png");
|
||||
-moz-image-region: rect(0px 18px 18px 0px);
|
||||
}
|
||||
#sync-button[cui-areatype="menu-panel"][status="active"] {
|
||||
list-style-image: url("chrome://browser/skin/syncProgress-menuPanel.png");
|
||||
-moz-image-region: rect(0px 32px 32px 0px);
|
||||
}
|
||||
|
||||
#feed-button {
|
||||
|
@ -269,15 +269,15 @@ browser.jar:
|
||||
skin/classic/browser/devtools/app-manager/noise.png (../shared/devtools/app-manager/images/noise.png)
|
||||
skin/classic/browser/devtools/app-manager/default-app-icon.png (../shared/devtools/app-manager/images/default-app-icon.png)
|
||||
#ifdef MOZ_SERVICES_SYNC
|
||||
skin/classic/browser/sync-16-throbber.png
|
||||
skin/classic/browser/sync-16.png
|
||||
skin/classic/browser/sync-24-throbber.png
|
||||
skin/classic/browser/sync-32.png
|
||||
skin/classic/browser/sync-bg.png
|
||||
skin/classic/browser/sync-128.png
|
||||
skin/classic/browser/sync-desktopIcon.png
|
||||
skin/classic/browser/sync-mobileIcon.png
|
||||
skin/classic/browser/sync-notification-24.png
|
||||
skin/classic/browser/syncProgress-menuPanel.png
|
||||
skin/classic/browser/syncProgress-toolbar.png
|
||||
skin/classic/browser/syncSetup.css
|
||||
skin/classic/browser/syncCommon.css
|
||||
skin/classic/browser/syncQuota.css
|
||||
|
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 15 KiB |
BIN
browser/themes/linux/syncProgress-menuPanel.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
browser/themes/linux/syncProgress-toolbar.png
Normal file
After Width: | Height: | Size: 623 B |
@ -6,6 +6,7 @@
|
||||
package org.mozilla.gecko;
|
||||
|
||||
import org.mozilla.gecko.SiteIdentity.SecurityMode;
|
||||
import org.mozilla.gecko.db.BrowserContract.Bookmarks;
|
||||
import org.mozilla.gecko.db.BrowserDB;
|
||||
import org.mozilla.gecko.gfx.Layer;
|
||||
import org.mozilla.gecko.util.ThreadUtils;
|
||||
@ -418,9 +419,9 @@ public class Tab {
|
||||
return;
|
||||
}
|
||||
|
||||
mBookmark = BrowserDB.isBookmark(getContentResolver(), url);
|
||||
mReadingListItem = BrowserDB.isReadingListItem(getContentResolver(), url);
|
||||
|
||||
final int flags = BrowserDB.getItemFlags(getContentResolver(), url);
|
||||
mBookmark = (flags & Bookmarks.FLAG_BOOKMARK) > 0;
|
||||
mReadingListItem = (flags & Bookmarks.FLAG_READING) > 0;
|
||||
Tabs.getInstance().notifyListeners(Tab.this, Tabs.TabEvents.MENU_UPDATED);
|
||||
}
|
||||
});
|
||||
|
@ -56,6 +56,7 @@ public class Tabs implements GeckoEventListener {
|
||||
private static final int LOAD_PROGRESS_START = 20;
|
||||
private static final int LOAD_PROGRESS_LOCATION_CHANGE = 60;
|
||||
private static final int LOAD_PROGRESS_LOADED = 80;
|
||||
private static final int LOAD_PROGRESS_STOP = 100;
|
||||
|
||||
public static final int LOADURL_NONE = 0;
|
||||
public static final int LOADURL_NEW_TAB = 1 << 0;
|
||||
@ -459,6 +460,7 @@ public class Tabs implements GeckoEventListener {
|
||||
notifyListeners(tab, Tabs.TabEvents.START);
|
||||
} else if ((state & GeckoAppShell.WPL_STATE_STOP) != 0) {
|
||||
tab.handleDocumentStop(message.getBoolean("success"));
|
||||
tab.setLoadProgress(LOAD_PROGRESS_STOP);
|
||||
notifyListeners(tab, Tabs.TabEvents.STOP);
|
||||
}
|
||||
}
|
||||
|
@ -143,6 +143,17 @@ public class BrowserContract {
|
||||
public static final int TYPE_LIVEMARK = 3;
|
||||
public static final int TYPE_QUERY = 4;
|
||||
|
||||
/*
|
||||
* These values are returned by getItemFlags. They're not really
|
||||
* exclusive to bookmarks, but there's no better place to put them.
|
||||
*/
|
||||
public static final int FLAG_SUCCESS = 1 << 1; // The query succeeded.
|
||||
public static final int FLAG_BOOKMARK = 1 << 2;
|
||||
public static final int FLAG_PINNED = 1 << 3;
|
||||
public static final int FLAG_READING = 1 << 4;
|
||||
|
||||
public static final Uri FLAGS_URI = Uri.withAppendedPath(AUTHORITY_URI, "flags");
|
||||
|
||||
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "bookmarks");
|
||||
public static final Uri PARENTS_CONTENT_URI = Uri.withAppendedPath(CONTENT_URI, "parents");
|
||||
// Hacky API for bulk-updating positions. Bug 728783.
|
||||
|
@ -41,8 +41,8 @@ public class BrowserDB {
|
||||
@RobocopTarget
|
||||
public Cursor filter(ContentResolver cr, CharSequence constraint, int limit);
|
||||
|
||||
// This should onlyl return frecent sites, BrowserDB.getTopSites will do the
|
||||
// work to combine that list with the pinned sites list
|
||||
// This should only return frecent sites. BrowserDB.getTopSites will do the
|
||||
// work to combine that list with the pinned sites list.
|
||||
public Cursor getTopSites(ContentResolver cr, int limit);
|
||||
|
||||
public void updateVisitedHistory(ContentResolver cr, String uri);
|
||||
@ -78,6 +78,12 @@ public class BrowserDB {
|
||||
|
||||
public boolean isReadingListItem(ContentResolver cr, String uri);
|
||||
|
||||
/**
|
||||
* Return a combination of fields about the provided URI
|
||||
* in a single hit on the DB.
|
||||
*/
|
||||
public int getItemFlags(ContentResolver cr, String uri);
|
||||
|
||||
public String getUrlForKeyword(ContentResolver cr, String keyword);
|
||||
|
||||
@RobocopTarget
|
||||
@ -232,6 +238,13 @@ public class BrowserDB {
|
||||
return (sAreContentProvidersEnabled && sDb.isReadingListItem(cr, uri));
|
||||
}
|
||||
|
||||
public static int getItemFlags(ContentResolver cr, String uri) {
|
||||
if (!sAreContentProvidersEnabled) {
|
||||
return 0;
|
||||
}
|
||||
return sDb.getItemFlags(cr, uri);
|
||||
}
|
||||
|
||||
public static void addBookmark(ContentResolver cr, String title, String uri) {
|
||||
sDb.addBookmark(cr, title, uri);
|
||||
}
|
||||
|
@ -109,6 +109,8 @@ public class BrowserProvider extends ContentProvider {
|
||||
static final String VIEW_HISTORY_WITH_FAVICONS = "history_with_favicons";
|
||||
static final String VIEW_COMBINED_WITH_FAVICONS = "combined_with_favicons";
|
||||
|
||||
static final String VIEW_FLAGS = "flags";
|
||||
|
||||
// Bookmark matches
|
||||
static final int BOOKMARKS = 100;
|
||||
static final int BOOKMARKS_ID = 101;
|
||||
@ -141,6 +143,8 @@ public class BrowserProvider extends ContentProvider {
|
||||
static final int THUMBNAILS = 800;
|
||||
static final int THUMBNAIL_ID = 801;
|
||||
|
||||
static final int FLAGS = 900;
|
||||
|
||||
static final String DEFAULT_BOOKMARKS_SORT_ORDER = Bookmarks.TYPE
|
||||
+ " ASC, " + Bookmarks.POSITION + " ASC, " + Bookmarks._ID
|
||||
+ " ASC";
|
||||
@ -305,6 +309,8 @@ public class BrowserProvider extends ContentProvider {
|
||||
// Search Suggest
|
||||
URI_MATCHER.addURI(BrowserContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH_SUGGEST);
|
||||
|
||||
URI_MATCHER.addURI(BrowserContract.AUTHORITY, "flags", FLAGS);
|
||||
|
||||
map = new HashMap<String, String>();
|
||||
map.put(SearchManager.SUGGEST_COLUMN_TEXT_1,
|
||||
Combined.TITLE + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1);
|
||||
@ -2154,6 +2160,9 @@ public class BrowserProvider extends ContentProvider {
|
||||
case SEARCH_SUGGEST:
|
||||
trace("URI is SEARCH_SUGGEST: " + uri);
|
||||
return SearchManager.SUGGEST_MIME_TYPE;
|
||||
case FLAGS:
|
||||
trace("URI is FLAGS.");
|
||||
return Bookmarks.CONTENT_ITEM_TYPE;
|
||||
}
|
||||
|
||||
debug("URI has unrecognized type: " + uri);
|
||||
@ -2492,6 +2501,40 @@ public class BrowserProvider extends ContentProvider {
|
||||
SQLiteDatabase db = getReadableDatabase(uri);
|
||||
final int match = URI_MATCHER.match(uri);
|
||||
|
||||
// The first selectionArgs value is the URI for which to query.
|
||||
if (match == FLAGS) {
|
||||
// We don't need the QB below for this.
|
||||
//
|
||||
// There are three possible kinds of bookmarks:
|
||||
// * Regular bookmarks
|
||||
// * Bookmarks whose parent is FIXED_READING_LIST_ID (reading list items)
|
||||
// * Bookmarks whose parent is FIXED_PINNED_LIST_ID (pinned items).
|
||||
//
|
||||
// Although SQLite doesn't have an aggregate operator for bitwise-OR, we're
|
||||
// using disjoint flags, so we can simply use SUM and DISTINCT to get the
|
||||
// flags we need.
|
||||
// We turn parents into flags according to the three kinds, above.
|
||||
//
|
||||
// When this query is extended to support queries across multiple tables, simply
|
||||
// extend it to look like
|
||||
//
|
||||
// SELECT COALESCE((SELECT ...), 0) | COALESCE(...) | ...
|
||||
final String query = "SELECT COALESCE(SUM(flag), 0) AS flags " +
|
||||
"FROM ( SELECT DISTINCT CASE" +
|
||||
" WHEN " + Bookmarks.PARENT + " = " + Bookmarks.FIXED_READING_LIST_ID +
|
||||
" THEN " + Bookmarks.FLAG_READING +
|
||||
|
||||
" WHEN " + Bookmarks.PARENT + " = " + Bookmarks.FIXED_PINNED_LIST_ID +
|
||||
" THEN " + Bookmarks.FLAG_PINNED +
|
||||
|
||||
" ELSE " + Bookmarks.FLAG_BOOKMARK +
|
||||
" END flag " +
|
||||
"FROM " + TABLE_BOOKMARKS + " WHERE " + Bookmarks.URL + " = ? " +
|
||||
")";
|
||||
|
||||
return db.rawQuery(query, selectionArgs);
|
||||
}
|
||||
|
||||
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
|
||||
String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
|
||||
String groupBy = null;
|
||||
|
@ -59,6 +59,7 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface {
|
||||
|
||||
private final Uri mBookmarksUriWithProfile;
|
||||
private final Uri mParentsUriWithProfile;
|
||||
private final Uri mFlagsUriWithProfile;
|
||||
private final Uri mHistoryUriWithProfile;
|
||||
private final Uri mHistoryExpireUriWithProfile;
|
||||
private final Uri mCombinedUriWithProfile;
|
||||
@ -82,6 +83,7 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface {
|
||||
|
||||
mBookmarksUriWithProfile = appendProfile(Bookmarks.CONTENT_URI);
|
||||
mParentsUriWithProfile = appendProfile(Bookmarks.PARENTS_CONTENT_URI);
|
||||
mFlagsUriWithProfile = appendProfile(Bookmarks.FLAGS_URI);
|
||||
mHistoryUriWithProfile = appendProfile(History.CONTENT_URI);
|
||||
mHistoryExpireUriWithProfile = appendProfile(History.CONTENT_OLD_URI);
|
||||
mCombinedUriWithProfile = appendProfile(Combined.CONTENT_URI);
|
||||
@ -471,7 +473,7 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface {
|
||||
|
||||
@Override
|
||||
public boolean isBookmark(ContentResolver cr, String uri) {
|
||||
// This method is about normal bookmarks, not the Reading List
|
||||
// This method is about normal bookmarks, not the Reading List.
|
||||
Cursor c = null;
|
||||
try {
|
||||
c = cr.query(bookmarksUriWithLimit(1),
|
||||
@ -517,6 +519,34 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* For a given URI, we want to return a number of things:
|
||||
*
|
||||
* * Is this URI the URI of a bookmark?
|
||||
* * ... a reading list item?
|
||||
*
|
||||
* This will expand as necessary to eliminate multiple consecutive queries.
|
||||
*/
|
||||
@Override
|
||||
public int getItemFlags(ContentResolver cr, String uri) {
|
||||
final Cursor c = cr.query(mFlagsUriWithProfile,
|
||||
null,
|
||||
null,
|
||||
new String[] { uri },
|
||||
null);
|
||||
if (c == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
// This should never fail: it returns a single `flags` row.
|
||||
c.moveToFirst();
|
||||
return Bookmarks.FLAG_SUCCESS | c.getInt(0);
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrlForKeyword(ContentResolver cr, String keyword) {
|
||||
Cursor c = null;
|
||||
|
@ -130,8 +130,10 @@ class BookmarksListAdapter extends MultiTypeCursorAdapter {
|
||||
* @return Whether the adapter successfully moved to a parent folder.
|
||||
*/
|
||||
public boolean moveToParentFolder() {
|
||||
// If we're already at the root, we can't move to a parent folder
|
||||
if (mParentStack.size() == 1) {
|
||||
// If we're already at the root, we can't move to a parent folder.
|
||||
// An empty parent stack here means we're still waiting for the
|
||||
// initial list of bookmarks and can't go to a parent folder.
|
||||
if (mParentStack.size() <= 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -106,8 +106,7 @@ public final class HomeConfig {
|
||||
|
||||
public enum Flags {
|
||||
DEFAULT_PANEL,
|
||||
DISABLED_PANEL,
|
||||
DELETED_PANEL
|
||||
DISABLED_PANEL
|
||||
}
|
||||
|
||||
public PanelConfig(JSONObject json) throws JSONException, IllegalArgumentException {
|
||||
@ -286,18 +285,6 @@ public final class HomeConfig {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isDeleted() {
|
||||
return mFlags.contains(Flags.DELETED_PANEL);
|
||||
}
|
||||
|
||||
public void setIsDeleted(boolean isDeleted) {
|
||||
if (isDeleted) {
|
||||
mFlags.add(Flags.DELETED_PANEL);
|
||||
} else {
|
||||
mFlags.remove(Flags.DELETED_PANEL);
|
||||
}
|
||||
}
|
||||
|
||||
public JSONObject toJSON() throws JSONException {
|
||||
final JSONObject json = new JSONObject();
|
||||
|
||||
@ -596,12 +583,6 @@ public final class HomeConfig {
|
||||
}
|
||||
|
||||
public void save(List<PanelConfig> panelConfigs) {
|
||||
for (PanelConfig panelConfig : panelConfigs) {
|
||||
if (panelConfig.isDeleted()) {
|
||||
throw new IllegalArgumentException("Should never save a deleted PanelConfig: " + panelConfig.getId());
|
||||
}
|
||||
}
|
||||
|
||||
mBackend.save(panelConfigs);
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,7 @@ import org.json.JSONObject;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
@ -38,13 +39,30 @@ public class HomeConfigInvalidator implements GeckoEventListener {
|
||||
|
||||
private static final String EVENT_HOMEPANELS_INSTALL = "HomePanels:Install";
|
||||
private static final String EVENT_HOMEPANELS_REMOVE = "HomePanels:Remove";
|
||||
private static final String EVENT_HOMEPANELS_REFRESH = "HomePanels:Refresh";
|
||||
|
||||
private static final String JSON_KEY_PANEL = "panel";
|
||||
|
||||
private enum ChangeType {
|
||||
REMOVE,
|
||||
INSTALL,
|
||||
REFRESH
|
||||
}
|
||||
|
||||
private static class ConfigChange {
|
||||
private final ChangeType type;
|
||||
private final PanelConfig target;
|
||||
|
||||
public ConfigChange(ChangeType type, PanelConfig target) {
|
||||
this.type = type;
|
||||
this.target = target;
|
||||
}
|
||||
}
|
||||
|
||||
private Context mContext;
|
||||
private HomeConfig mHomeConfig;
|
||||
|
||||
private final ConcurrentLinkedQueue<PanelConfig> mPendingChanges = new ConcurrentLinkedQueue<PanelConfig>();
|
||||
private final Queue<ConfigChange> mPendingChanges = new ConcurrentLinkedQueue<ConfigChange>();
|
||||
private final Runnable mInvalidationRunnable = new InvalidationRunnable();
|
||||
|
||||
public static HomeConfigInvalidator getInstance() {
|
||||
@ -57,6 +75,11 @@ public class HomeConfigInvalidator implements GeckoEventListener {
|
||||
|
||||
GeckoAppShell.getEventDispatcher().registerEventListener(EVENT_HOMEPANELS_INSTALL, this);
|
||||
GeckoAppShell.getEventDispatcher().registerEventListener(EVENT_HOMEPANELS_REMOVE, this);
|
||||
GeckoAppShell.getEventDispatcher().registerEventListener(EVENT_HOMEPANELS_REFRESH, this);
|
||||
}
|
||||
|
||||
public void refreshAll() {
|
||||
handlePanelRefresh(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -71,6 +94,9 @@ public class HomeConfigInvalidator implements GeckoEventListener {
|
||||
} else if (event.equals(EVENT_HOMEPANELS_REMOVE)) {
|
||||
Log.d(LOGTAG, EVENT_HOMEPANELS_REMOVE);
|
||||
handlePanelRemove(panelConfig);
|
||||
} else if (event.equals(EVENT_HOMEPANELS_REFRESH)) {
|
||||
Log.d(LOGTAG, EVENT_HOMEPANELS_REFRESH);
|
||||
handlePanelRefresh(panelConfig);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOGTAG, "Failed to handle event " + event, e);
|
||||
@ -81,7 +107,7 @@ public class HomeConfigInvalidator implements GeckoEventListener {
|
||||
* Runs in the gecko thread.
|
||||
*/
|
||||
private void handlePanelInstall(PanelConfig panelConfig) {
|
||||
mPendingChanges.offer(panelConfig);
|
||||
mPendingChanges.offer(new ConfigChange(ChangeType.INSTALL, panelConfig));
|
||||
Log.d(LOGTAG, "handlePanelInstall: " + mPendingChanges.size());
|
||||
|
||||
scheduleInvalidation();
|
||||
@ -91,13 +117,25 @@ public class HomeConfigInvalidator implements GeckoEventListener {
|
||||
* Runs in the gecko thread.
|
||||
*/
|
||||
private void handlePanelRemove(PanelConfig panelConfig) {
|
||||
panelConfig.setIsDeleted(true);
|
||||
mPendingChanges.offer(panelConfig);
|
||||
mPendingChanges.offer(new ConfigChange(ChangeType.REMOVE, panelConfig));
|
||||
Log.d(LOGTAG, "handlePanelRemove: " + mPendingChanges.size());
|
||||
|
||||
scheduleInvalidation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules a panel refresh in HomeConfig. Runs in the gecko thread.
|
||||
*
|
||||
* @param panelConfig the target PanelConfig instance or NULL to refresh
|
||||
* all HomeConfig entries.
|
||||
*/
|
||||
private void handlePanelRefresh(PanelConfig panelConfig) {
|
||||
mPendingChanges.offer(new ConfigChange(ChangeType.REFRESH, panelConfig));
|
||||
Log.d(LOGTAG, "handlePanelRefresh: " + mPendingChanges.size());
|
||||
|
||||
scheduleInvalidation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs in the gecko or main thread.
|
||||
*/
|
||||
@ -110,31 +148,63 @@ public class HomeConfigInvalidator implements GeckoEventListener {
|
||||
Log.d(LOGTAG, "scheduleInvalidation: scheduled new invalidation");
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace an element if a matching PanelConfig is
|
||||
* present in the given list.
|
||||
*/
|
||||
private boolean replacePanelConfig(List<PanelConfig> panelConfigs, PanelConfig panelConfig) {
|
||||
final int index = panelConfigs.indexOf(panelConfig);
|
||||
if (index >= 0) {
|
||||
panelConfigs.set(index, panelConfig);
|
||||
Log.d(LOGTAG, "executePendingChanges: replaced position " + index + " with " + panelConfig.getId());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs in the background thread.
|
||||
*/
|
||||
private List<PanelConfig> executePendingChanges(List<PanelConfig> panelConfigs) {
|
||||
while (!mPendingChanges.isEmpty()) {
|
||||
final PanelConfig panelConfig = mPendingChanges.poll();
|
||||
final String id = panelConfig.getId();
|
||||
boolean shouldRefreshAll = false;
|
||||
|
||||
if (panelConfig.isDeleted()) {
|
||||
if (panelConfigs.remove(panelConfig)) {
|
||||
Log.d(LOGTAG, "executePendingChanges: removed panel " + id);
|
||||
}
|
||||
} else {
|
||||
final int index = panelConfigs.indexOf(panelConfig);
|
||||
if (index >= 0) {
|
||||
panelConfigs.set(index, panelConfig);
|
||||
Log.d(LOGTAG, "executePendingChanges: replaced position " + index + " with " + id);
|
||||
} else {
|
||||
panelConfigs.add(panelConfig);
|
||||
Log.d(LOGTAG, "executePendingChanges: added panel " + id);
|
||||
}
|
||||
while (!mPendingChanges.isEmpty()) {
|
||||
final ConfigChange pendingChange = mPendingChanges.poll();
|
||||
final PanelConfig panelConfig = pendingChange.target;
|
||||
|
||||
switch (pendingChange.type) {
|
||||
case REMOVE:
|
||||
if (panelConfigs.remove(panelConfig)) {
|
||||
Log.d(LOGTAG, "executePendingChanges: removed panel " + panelConfig.getId());
|
||||
}
|
||||
break;
|
||||
|
||||
case INSTALL:
|
||||
if (!replacePanelConfig(panelConfigs, panelConfig)) {
|
||||
panelConfigs.add(panelConfig);
|
||||
Log.d(LOGTAG, "executePendingChanges: added panel " + panelConfig.getId());
|
||||
}
|
||||
break;
|
||||
|
||||
case REFRESH:
|
||||
if (panelConfig != null) {
|
||||
if (!replacePanelConfig(panelConfigs, panelConfig)) {
|
||||
Log.w(LOGTAG, "Tried to refresh non-existing panel " + panelConfig.getId());
|
||||
}
|
||||
} else {
|
||||
shouldRefreshAll = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return executeRefresh(panelConfigs);
|
||||
if (shouldRefreshAll) {
|
||||
return executeRefresh(panelConfigs);
|
||||
} else {
|
||||
return panelConfigs;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -318,10 +318,11 @@ public class HomePager extends ViewPager {
|
||||
setAdapter(adapter);
|
||||
|
||||
// Use the default panel as defined in the HomePager's configuration
|
||||
// if the initial panel wasn't explicitly set by the show() caller.
|
||||
if (mInitialPanelId != null) {
|
||||
// XXX: Handle the case where the desired panel isn't currently in the adapter (bug 949178)
|
||||
setCurrentItem(adapter.getItemPosition(mInitialPanelId), false);
|
||||
// if the initial panel wasn't explicitly set by the show() caller,
|
||||
// or if the initial panel is not found in the adapter.
|
||||
final int itemPosition = (mInitialPanelId == null) ? -1 : adapter.getItemPosition(mInitialPanelId);
|
||||
if (itemPosition > -1) {
|
||||
setCurrentItem(itemPosition, false);
|
||||
mInitialPanelId = null;
|
||||
} else {
|
||||
for (int i = 0; i < count; i++) {
|
||||
|
@ -175,6 +175,15 @@ class PinSiteDialog extends DialogFragment {
|
||||
filter("");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
|
||||
// Discard any additional site selection as the dialog
|
||||
// is getting destroyed (see bug 935542).
|
||||
setOnSiteSelectedListener(null);
|
||||
}
|
||||
|
||||
public void setSearchTerm(String searchTerm) {
|
||||
mSearchTerm = searchTerm;
|
||||
}
|
||||
|
@ -449,7 +449,7 @@ public class TopSitesPanel extends HomeFragment {
|
||||
public void onEditPinnedSite(int position, String searchTerm) {
|
||||
mPosition = position;
|
||||
|
||||
final FragmentManager manager = getActivity().getSupportFragmentManager();
|
||||
final FragmentManager manager = getChildFragmentManager();
|
||||
PinSiteDialog dialog = (PinSiteDialog) manager.findFragmentByTag(TAG_PIN_SITE);
|
||||
if (dialog == null) {
|
||||
dialog = PinSiteDialog.newInstance();
|
||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 601 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 283 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 283 B |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 569 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 213 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 213 B |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 901 B |
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 603 B After Width: | Height: | Size: 421 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 206 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 213 B |
@ -475,13 +475,13 @@ public class BrowserToolbar extends GeckoRelativeLayout
|
||||
case LOCATION_CHANGE:
|
||||
case LOAD_ERROR:
|
||||
case LOADED:
|
||||
case STOP:
|
||||
flags.add(UpdateFlags.PROGRESS);
|
||||
if (mProgressBar.getVisibility() == View.VISIBLE) {
|
||||
mProgressBar.animateProgress(tab.getLoadProgress());
|
||||
}
|
||||
break;
|
||||
|
||||
case STOP:
|
||||
case SELECTED:
|
||||
flags.add(UpdateFlags.PROGRESS);
|
||||
updateProgressVisibility();
|
||||
|
@ -35,8 +35,9 @@ import android.view.View;
|
||||
* perceived performance.
|
||||
*/
|
||||
public class ToolbarProgressView extends ImageView {
|
||||
public static final int MAX_PROGRESS = 10000;
|
||||
private static final int MSG_UPDATE = 42;
|
||||
private static final int MAX_PROGRESS = 10000;
|
||||
private static final int MSG_UPDATE = 0;
|
||||
private static final int MSG_HIDE = 1;
|
||||
private static final int STEPS = 10;
|
||||
private static final int DELAY = 40;
|
||||
|
||||
@ -68,15 +69,23 @@ public class ToolbarProgressView extends ImageView {
|
||||
mHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
if (msg.what == MSG_UPDATE) {
|
||||
final int progress = Math.min(mTargetProgress, mCurrentProgress + mIncrement);
|
||||
mCurrentProgress = progress;
|
||||
switch (msg.what) {
|
||||
case MSG_UPDATE:
|
||||
mCurrentProgress = Math.min(mTargetProgress, mCurrentProgress + mIncrement);
|
||||
|
||||
updateBounds();
|
||||
updateBounds();
|
||||
|
||||
if (progress < mTargetProgress) {
|
||||
sendMessageDelayed(mHandler.obtainMessage(MSG_UPDATE), DELAY);
|
||||
}
|
||||
if (mCurrentProgress < mTargetProgress) {
|
||||
final int delay = (mTargetProgress < MAX_PROGRESS) ? DELAY : DELAY / 4;
|
||||
sendMessageDelayed(mHandler.obtainMessage(msg.what), delay);
|
||||
} else if (mCurrentProgress == MAX_PROGRESS) {
|
||||
sendMessageDelayed(mHandler.obtainMessage(MSG_HIDE), DELAY);
|
||||
}
|
||||
break;
|
||||
|
||||
case MSG_HIDE:
|
||||
setVisibility(View.GONE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,7 +116,7 @@ public class ToolbarProgressView extends ImageView {
|
||||
mCurrentProgress = mTargetProgress = getAbsoluteProgress(progressPercentage);
|
||||
updateBounds();
|
||||
|
||||
mHandler.removeMessages(MSG_UPDATE);
|
||||
clearMessages();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -118,18 +127,26 @@ public class ToolbarProgressView extends ImageView {
|
||||
*/
|
||||
void animateProgress(int progressPercentage) {
|
||||
final int absoluteProgress = getAbsoluteProgress(progressPercentage);
|
||||
if (absoluteProgress == mTargetProgress) {
|
||||
if (absoluteProgress <= mTargetProgress) {
|
||||
// After we manually click stop, we can still receive page load
|
||||
// events (e.g., DOMContentLoaded). Updating for other updates
|
||||
// after a STOP event can freeze the progress bar, so guard against
|
||||
// that here.
|
||||
return;
|
||||
}
|
||||
|
||||
mCurrentProgress = mTargetProgress;
|
||||
mTargetProgress = absoluteProgress;
|
||||
mIncrement = (mTargetProgress - mCurrentProgress) / STEPS;
|
||||
|
||||
mHandler.removeMessages(MSG_UPDATE);
|
||||
clearMessages();
|
||||
mHandler.sendEmptyMessage(MSG_UPDATE);
|
||||
}
|
||||
|
||||
private void clearMessages() {
|
||||
mHandler.removeMessages(MSG_UPDATE);
|
||||
mHandler.removeMessages(MSG_HIDE);
|
||||
}
|
||||
|
||||
private int getAbsoluteProgress(int progressPercentage) {
|
||||
if (progressPercentage < 0) {
|
||||
return 0;
|
||||
|
@ -7,6 +7,9 @@ let Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils;
|
||||
Cu.import("resource://gre/modules/Services.jsm")
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
// Panel ID defined in HomeConfig.java.
|
||||
const READING_LIST_PANEL_ID = "20f4549a-64ad-4c32-93e4-1dcef792733b";
|
||||
|
||||
XPCOMUtils.defineLazyGetter(window, "gChromeWin", function ()
|
||||
window.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebNavigation)
|
||||
@ -366,7 +369,7 @@ AboutReader.prototype = {
|
||||
if (!this._article || this._readingListCount < 1)
|
||||
return;
|
||||
|
||||
gChromeWin.BrowserApp.loadURI("about:home?page=reading_list");
|
||||
gChromeWin.BrowserApp.loadURI("about:home?page=" + READING_LIST_PANEL_ID);
|
||||
},
|
||||
|
||||
_onShare: function Reader_onShare() {
|
||||
|
@ -163,6 +163,12 @@ let HomePanels = Object.freeze({
|
||||
GRID: "grid"
|
||||
}),
|
||||
|
||||
// Valid actions for a panel.
|
||||
Action: Object.freeze({
|
||||
INSTALL: "install",
|
||||
REFRESH: "refresh"
|
||||
}),
|
||||
|
||||
// Holds the currrent set of registered panels.
|
||||
_panels: {},
|
||||
|
||||
@ -202,8 +208,11 @@ let HomePanels = Object.freeze({
|
||||
throw "Home.panels: Can't create a home panel without an id and title!";
|
||||
}
|
||||
|
||||
// Bail if the panel already exists
|
||||
if (panel.id in this._panels) {
|
||||
let action = options.action;
|
||||
|
||||
// Bail if the panel already exists, except when we're refreshing
|
||||
// an existing panel instance.
|
||||
if (panel.id in this._panels && action != this.Action.REFRESH) {
|
||||
throw "Home.panels: Panel already exists: id = " + panel.id;
|
||||
}
|
||||
|
||||
@ -223,9 +232,24 @@ let HomePanels = Object.freeze({
|
||||
|
||||
this._panels[panel.id] = panel;
|
||||
|
||||
if (options.autoInstall) {
|
||||
if (action) {
|
||||
let messageType;
|
||||
|
||||
switch(action) {
|
||||
case this.Action.INSTALL:
|
||||
messageType = "HomePanels:Install";
|
||||
break;
|
||||
|
||||
case this.Action.REFRESH:
|
||||
messageType = "HomePanels:Refresh";
|
||||
break;
|
||||
|
||||
default:
|
||||
throw "Home.panels: Invalid action for panel: panel.id = " + panel.id + ", action = " + action;
|
||||
}
|
||||
|
||||
sendMessageToJava({
|
||||
type: "HomePanels:Install",
|
||||
type: messageType,
|
||||
panel: this._panelToJSON(panel)
|
||||
});
|
||||
}
|
||||
|
@ -717,8 +717,14 @@ add_test(function test_timeout() {
|
||||
do_check_eq(error.result, Cr.NS_ERROR_NET_TIMEOUT);
|
||||
do_check_eq(this.status, this.ABORTED);
|
||||
|
||||
_("Closing connection.");
|
||||
server_connection.close();
|
||||
// server_connection is undefined on the Android emulator for reasons
|
||||
// unknown. Yet, we still get here. If this test is refactored, we should
|
||||
// investigate the reason why the above callback is behaving differently.
|
||||
if (server_connection) {
|
||||
_("Closing connection.");
|
||||
server_connection.close();
|
||||
}
|
||||
|
||||
_("Shutting down server.");
|
||||
server.stop(run_next_test);
|
||||
});
|
||||
|
@ -54,6 +54,11 @@ HMAC_EVENT_INTERVAL: 600000,
|
||||
// How long to wait between sync attempts if the Master Password is locked.
|
||||
MASTER_PASSWORD_LOCKED_RETRY_INTERVAL: 15 * 60 * 1000, // 15 minutes
|
||||
|
||||
// How long to initially wait between sync attempts if the identity manager is
|
||||
// not ready. As we expect this to become ready relatively quickly, we retry
|
||||
// in (IDENTITY_NOT_READY_RETRY_INTERVAL * num_failures) seconds.
|
||||
IDENTITY_NOT_READY_RETRY_INTERVAL: 5 * 1000, // 5 seconds
|
||||
|
||||
// Separate from the ID fetch batch size to allow tuning for mobile.
|
||||
MOBILE_BATCH_SIZE: 50,
|
||||
|
||||
|
@ -30,6 +30,8 @@ SyncScheduler.prototype = {
|
||||
LOGIN_FAILED_INVALID_PASSPHRASE,
|
||||
LOGIN_FAILED_LOGIN_REJECTED],
|
||||
|
||||
_loginNotReadyCounter: 0,
|
||||
|
||||
/**
|
||||
* The nsITimer object that schedules the next sync. See scheduleNextSync().
|
||||
*/
|
||||
@ -113,6 +115,10 @@ SyncScheduler.prototype = {
|
||||
// we'll handle that later
|
||||
Status.resetBackoff();
|
||||
|
||||
// Reset the loginNotReady counter, just in-case the user signs in
|
||||
// as another user and re-hits the not-ready state.
|
||||
this._loginNotReadyCounter = 0;
|
||||
|
||||
this.globalScore = 0;
|
||||
break;
|
||||
case "weave:service:sync:finish":
|
||||
@ -155,6 +161,13 @@ SyncScheduler.prototype = {
|
||||
this._log.debug("Couldn't log in: master password is locked.");
|
||||
this._log.trace("Scheduling a sync at MASTER_PASSWORD_LOCKED_RETRY_INTERVAL");
|
||||
this.scheduleAtInterval(MASTER_PASSWORD_LOCKED_RETRY_INTERVAL);
|
||||
} else if (Status.login == LOGIN_FAILED_NOT_READY) {
|
||||
this._loginNotReadyCounter++;
|
||||
this._log.debug("Couldn't log in: identity not ready.");
|
||||
this._log.trace("Scheduling a sync at IDENTITY_NOT_READY_RETRY_INTERVAL * " +
|
||||
this._loginNotReadyCounter);
|
||||
this.scheduleAtInterval(IDENTITY_NOT_READY_RETRY_INTERVAL *
|
||||
this._loginNotReadyCounter);
|
||||
} else if (this._fatalLoginStatus.indexOf(Status.login) == -1) {
|
||||
// Not a fatal login error, just an intermittent network or server
|
||||
// issue. Keep on syncin'.
|
||||
|
@ -30,6 +30,13 @@ const AGGREGATE_STARTUP_DELAY_MS = 57000;
|
||||
|
||||
const MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
// Converts Date to days since UNIX epoch.
|
||||
// This was copied from /services/metrics.storage.jsm. The implementation
|
||||
// does not account for leap seconds.
|
||||
function dateToDays(date) {
|
||||
return Math.floor(date.getTime() / MILLISECONDS_IN_DAY);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A gateway to crash-related data.
|
||||
@ -520,6 +527,17 @@ let gCrashManager;
|
||||
* When metadata is updated, the caller must explicitly persist the changes
|
||||
* to disk. This prevents excessive I/O during updates.
|
||||
*
|
||||
* The store has a mechanism for ensuring it doesn't grow too large. A ceiling
|
||||
* is placed on the number of daily events that can occur for events that can
|
||||
* occur with relatively high frequency, notably plugin crashes and hangs
|
||||
* (plugins can enter cycles where they repeatedly crash). If we've reached
|
||||
* the high water mark and new data arrives, it's silently dropped.
|
||||
* However, the count of actual events is always preserved. This allows
|
||||
* us to report on the severity of problems beyond the storage threshold.
|
||||
*
|
||||
* Main process crashes are excluded from limits because they are both
|
||||
* important and should be rare.
|
||||
*
|
||||
* @param storeDir (string)
|
||||
* Directory the store should be located in.
|
||||
* @param telemetrySizeKey (string)
|
||||
@ -534,25 +552,41 @@ function CrashStore(storeDir, telemetrySizeKey) {
|
||||
|
||||
// Holds the read data from disk.
|
||||
this._data = null;
|
||||
|
||||
// Maps days since UNIX epoch to a Map of event types to counts.
|
||||
// This data structure is populated when the JSON file is loaded
|
||||
// and is also updated when new events are added.
|
||||
this._countsByDay = new Map();
|
||||
}
|
||||
|
||||
CrashStore.prototype = Object.freeze({
|
||||
// A crash that occurred in the main process.
|
||||
TYPE_MAIN_CRASH: "main-crash",
|
||||
|
||||
// A crash in a plugin process.
|
||||
TYPE_PLUGIN_CRASH: "plugin-crash",
|
||||
|
||||
// A hang in a plugin process.
|
||||
TYPE_PLUGIN_HANG: "plugin-hang",
|
||||
|
||||
// Maximum number of events to store per day. This establishes a
|
||||
// ceiling on the per-type/per-day records that will be stored.
|
||||
HIGH_WATER_DAILY_THRESHOLD: 100,
|
||||
|
||||
/**
|
||||
* Load data from disk.
|
||||
*
|
||||
* @return Promise<null>
|
||||
* @return Promise
|
||||
*/
|
||||
load: function () {
|
||||
return Task.spawn(function* () {
|
||||
// Loading replaces data. So reset data structures.
|
||||
this._data = {
|
||||
v: 1,
|
||||
crashes: new Map(),
|
||||
corruptDate: null,
|
||||
};
|
||||
this._countsByDay = new Map();
|
||||
|
||||
try {
|
||||
let decoder = new TextDecoder();
|
||||
@ -563,10 +597,42 @@ CrashStore.prototype = Object.freeze({
|
||||
this._data.corruptDate = new Date(data.corruptDate);
|
||||
}
|
||||
|
||||
// actualCounts is used to validate that the derived counts by
|
||||
// days stored in the payload matches up to actual data.
|
||||
let actualCounts = new Map();
|
||||
|
||||
for (let id in data.crashes) {
|
||||
let crash = data.crashes[id];
|
||||
let denormalized = this._denormalize(crash);
|
||||
|
||||
this._data.crashes.set(id, denormalized);
|
||||
|
||||
let key = dateToDays(denormalized.crashDate) + "-" + denormalized.type;
|
||||
actualCounts.set(key, (actualCounts.get(key) || 0) + 1);
|
||||
}
|
||||
|
||||
// The validation in this loop is arguably not necessary. We perform
|
||||
// it as a defense against unknown bugs.
|
||||
for (let dayKey in data.countsByDay) {
|
||||
let day = parseInt(dayKey, 10);
|
||||
for (let type in data.countsByDay[day]) {
|
||||
this._ensureCountsForDay(day);
|
||||
|
||||
let count = data.countsByDay[day][type];
|
||||
let key = day + "-" + type;
|
||||
|
||||
// If the payload says we have data for a given day but we
|
||||
// don't, the payload is wrong. Ignore it.
|
||||
if (!actualCounts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we encountered more data in the payload than what the
|
||||
// data structure says, use the proper value.
|
||||
count = Math.max(count, actualCounts.get(key));
|
||||
|
||||
this._countsByDay.get(day).set(type, count);
|
||||
}
|
||||
}
|
||||
} catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
|
||||
// Missing files (first use) are allowed.
|
||||
@ -594,8 +660,22 @@ CrashStore.prototype = Object.freeze({
|
||||
}
|
||||
|
||||
let normalized = {
|
||||
// The version should be incremented whenever the format
|
||||
// changes.
|
||||
v: 1,
|
||||
// Maps crash IDs to objects defining the crash.
|
||||
crashes: {},
|
||||
// Maps days since UNIX epoch to objects mapping event types to
|
||||
// counts. This is a mirror of this._countsByDay. e.g.
|
||||
// {
|
||||
// 15000: {
|
||||
// "main-crash": 2,
|
||||
// "plugin-crash": 1
|
||||
// }
|
||||
// }
|
||||
countsByDay: {},
|
||||
|
||||
// When the store was last corrupted.
|
||||
corruptDate: null,
|
||||
};
|
||||
|
||||
@ -608,6 +688,13 @@ CrashStore.prototype = Object.freeze({
|
||||
normalized.crashes[id] = c;
|
||||
}
|
||||
|
||||
for (let [day, m] of this._countsByDay) {
|
||||
normalized.countsByDay[day] = {};
|
||||
for (let [type, count] of m) {
|
||||
normalized.countsByDay[day][type] = count;
|
||||
}
|
||||
}
|
||||
|
||||
let encoder = new TextEncoder();
|
||||
let data = encoder.encode(JSON.stringify(normalized));
|
||||
let size = yield OS.File.writeAtomic(this._storePath, data, {
|
||||
@ -729,16 +816,51 @@ CrashStore.prototype = Object.freeze({
|
||||
return null;
|
||||
},
|
||||
|
||||
_ensureCrashRecord: function (id) {
|
||||
_ensureCountsForDay: function (day) {
|
||||
if (!this._countsByDay.has(day)) {
|
||||
this._countsByDay.set(day, new Map());
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensure the crash record is present in storage.
|
||||
*
|
||||
* Returns the crash record if we're allowed to store it or null
|
||||
* if we've hit the high water mark.
|
||||
*
|
||||
* @param id
|
||||
* (string) The crash ID.
|
||||
* @param type
|
||||
* (string) One of the this.TYPE_* constants describing the crash type.
|
||||
* @param date
|
||||
* (Date) When this crash occurred.
|
||||
*
|
||||
* @return null | object crash record
|
||||
*/
|
||||
_ensureCrashRecord: function (id, type, date) {
|
||||
let day = dateToDays(date);
|
||||
this._ensureCountsForDay(day);
|
||||
|
||||
let count = (this._countsByDay.get(day).get(type) || 0) + 1;
|
||||
this._countsByDay.get(day).set(type, count);
|
||||
|
||||
if (count > this.HIGH_WATER_DAILY_THRESHOLD && type != this.TYPE_MAIN_CRASH) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this._data.crashes.has(id)) {
|
||||
this._data.crashes.set(id, {
|
||||
id: id,
|
||||
type: null,
|
||||
crashDate: null,
|
||||
type: type,
|
||||
crashDate: date,
|
||||
});
|
||||
}
|
||||
|
||||
return this._data.crashes.get(id);
|
||||
let crash = this._data.crashes.get(id);
|
||||
crash.type = type;
|
||||
crash.date = date;
|
||||
|
||||
return crash;
|
||||
},
|
||||
|
||||
/**
|
||||
@ -748,9 +870,7 @@ CrashStore.prototype = Object.freeze({
|
||||
* @param date (Date) When the crash occurred.
|
||||
*/
|
||||
addMainProcessCrash: function (id, date) {
|
||||
let r = this._ensureCrashRecord(id);
|
||||
r.type = this.TYPE_MAIN_CRASH;
|
||||
r.crashDate = date;
|
||||
this._ensureCrashRecord(id, this.TYPE_MAIN_CRASH, date);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -760,9 +880,7 @@ CrashStore.prototype = Object.freeze({
|
||||
* @param date (Date) When the crash occurred.
|
||||
*/
|
||||
addPluginCrash: function (id, date) {
|
||||
let r = this._ensureCrashRecord(id);
|
||||
r.type = this.TYPE_PLUGIN_CRASH;
|
||||
r.crashDate = date;
|
||||
this._ensureCrashRecord(id, this.TYPE_PLUGIN_CRASH, date);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -772,9 +890,7 @@ CrashStore.prototype = Object.freeze({
|
||||
* @param date (Date) When the hang was reported.
|
||||
*/
|
||||
addPluginHang: function (id, date) {
|
||||
let r = this._ensureCrashRecord(id);
|
||||
r.type = this.TYPE_PLUGIN_HANG;
|
||||
r.crashDate = date;
|
||||
this._ensureCrashRecord(id, this.TYPE_PLUGIN_HANG, date);
|
||||
},
|
||||
|
||||
get mainProcessCrashes() {
|
||||
@ -847,6 +963,10 @@ CrashRecord.prototype = Object.freeze({
|
||||
return this._o.crashDate;
|
||||
},
|
||||
|
||||
get oldestDate() {
|
||||
return this._o.crashDate;
|
||||
},
|
||||
|
||||
get type() {
|
||||
return this._o.type;
|
||||
},
|
||||
|
@ -155,9 +155,8 @@ crash data grows. As new data is accumulated, we need to read and write
|
||||
an entire file to make small updates. LZ4 compression helps reduce I/O.
|
||||
But, there is a potential for unbounded file growth. We establish a
|
||||
limit for the max age of records. Anything older than that limit is
|
||||
pruned. Future patches will also limit the maximum number of records. This
|
||||
will establish a hard limit on the size of the file, at least in terms of
|
||||
crashes.
|
||||
|
||||
Care must be taken when new crash data is recorded, as this will increase
|
||||
the size of the file and make I/O a larger concern.
|
||||
pruned. We also establish a daily limit on the number of crashes we will
|
||||
store. All crashes beyond the first N in a day have no payload and are
|
||||
only recorded by the presence of a count. This count ensures we can
|
||||
distinguish between ``N`` and ``100 * N``, which are very different
|
||||
values!
|
||||
|
@ -258,3 +258,24 @@ add_task(function* test_plugin_hang_event_file() {
|
||||
count = yield m.aggregateEventsFiles();
|
||||
Assert.equal(count, 0);
|
||||
});
|
||||
|
||||
// Excessive amounts of files should be processed properly.
|
||||
add_task(function* test_high_water_mark() {
|
||||
let m = yield getManager();
|
||||
|
||||
let store = yield m._getStore();
|
||||
|
||||
for (let i = 0; i < store.HIGH_WATER_DAILY_THRESHOLD + 1; i++) {
|
||||
yield m.createEventsFile("m" + i, "crash.main.1", DUMMY_DATE, "m" + i);
|
||||
yield m.createEventsFile("pc" + i, "crash.plugin.1", DUMMY_DATE, "pc" + i);
|
||||
yield m.createEventsFile("ph" + i, "hang.plugin.1", DUMMY_DATE, "ph" + i);
|
||||
}
|
||||
|
||||
let count = yield m.aggregateEventsFiles();
|
||||
Assert.equal(count, 3 * bsp.CrashStore.prototype.HIGH_WATER_DAILY_THRESHOLD + 3);
|
||||
|
||||
// Need to fetch again in case the first one was garbage collected.
|
||||
store = yield m._getStore();
|
||||
// +1 is for preserved main process crash.
|
||||
Assert.equal(store.crashesCount, 3 * store.HIGH_WATER_DAILY_THRESHOLD + 1);
|
||||
});
|
||||
|
@ -181,3 +181,51 @@ add_task(function* test_add_mixed_types() {
|
||||
Assert.equal(s.pluginCrashes.length, 1);
|
||||
Assert.equal(s.pluginHangs.length, 1);
|
||||
});
|
||||
|
||||
// Crashes added beyond the high water mark behave properly.
|
||||
add_task(function* test_high_water() {
|
||||
let s = yield getStore();
|
||||
|
||||
let d1 = new Date(2014, 0, 1, 0, 0, 0);
|
||||
let d2 = new Date(2014, 0, 2, 0, 0, 0);
|
||||
|
||||
for (let i = 0; i < s.HIGH_WATER_DAILY_THRESHOLD + 1; i++) {
|
||||
s.addMainProcessCrash("m1" + i, d1);
|
||||
s.addMainProcessCrash("m2" + i, d2);
|
||||
s.addPluginCrash("pc1" + i, d1);
|
||||
s.addPluginCrash("pc2" + i, d2);
|
||||
s.addPluginHang("ph1" + i, d1);
|
||||
s.addPluginHang("ph2" + i, d2);
|
||||
}
|
||||
|
||||
// We preserve main process crashes. Plugin crashes and hangs beyond should
|
||||
// be discarded.
|
||||
Assert.equal(s.crashesCount, 6 * s.HIGH_WATER_DAILY_THRESHOLD + 2);
|
||||
Assert.equal(s.mainProcessCrashes.length, 2 * s.HIGH_WATER_DAILY_THRESHOLD + 2);
|
||||
Assert.equal(s.pluginCrashes.length, 2 * s.HIGH_WATER_DAILY_THRESHOLD);
|
||||
Assert.equal(s.pluginHangs.length, 2 * s.HIGH_WATER_DAILY_THRESHOLD);
|
||||
|
||||
// But raw counts should be preserved.
|
||||
let day1 = bsp.dateToDays(d1);
|
||||
let day2 = bsp.dateToDays(d2);
|
||||
Assert.ok(s._countsByDay.has(day1));
|
||||
Assert.ok(s._countsByDay.has(day2));
|
||||
Assert.equal(s._countsByDay.get(day1).get(s.TYPE_MAIN_CRASH),
|
||||
s.HIGH_WATER_DAILY_THRESHOLD + 1);
|
||||
Assert.equal(s._countsByDay.get(day1).get(s.TYPE_PLUGIN_CRASH),
|
||||
s.HIGH_WATER_DAILY_THRESHOLD + 1);
|
||||
Assert.equal(s._countsByDay.get(day1).get(s.TYPE_PLUGIN_HANG),
|
||||
s.HIGH_WATER_DAILY_THRESHOLD + 1);
|
||||
|
||||
yield s.save();
|
||||
yield s.load();
|
||||
|
||||
Assert.ok(s._countsByDay.has(day1));
|
||||
Assert.ok(s._countsByDay.has(day2));
|
||||
Assert.equal(s._countsByDay.get(day1).get(s.TYPE_MAIN_CRASH),
|
||||
s.HIGH_WATER_DAILY_THRESHOLD + 1);
|
||||
Assert.equal(s._countsByDay.get(day1).get(s.TYPE_PLUGIN_CRASH),
|
||||
s.HIGH_WATER_DAILY_THRESHOLD + 1);
|
||||
Assert.equal(s._countsByDay.get(day1).get(s.TYPE_PLUGIN_HANG),
|
||||
s.HIGH_WATER_DAILY_THRESHOLD + 1);
|
||||
});
|
||||
|