Merge m-c to inbound

This commit is contained in:
Wes Kocher 2014-02-25 20:24:27 -08:00
commit cd6c2201e2
42 changed files with 1373 additions and 289 deletions

View File

@ -12,7 +12,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="8e21d0a5cdac94f05e9c3623fa3b9ad579ba2967"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="80d6405725788327102cab36e8d8c017cf25fb23"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="d11f524d00cacf5ba0dfbf25e4aa2158b1c3a036"/>

View File

@ -11,7 +11,7 @@
</project>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="8e21d0a5cdac94f05e9c3623fa3b9ad579ba2967"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="80d6405725788327102cab36e8d8c017cf25fb23"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="8c449b53328059e9b55bb34baec9b27a15055a7e"/>

View File

@ -12,7 +12,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="8e21d0a5cdac94f05e9c3623fa3b9ad579ba2967"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="80d6405725788327102cab36e8d8c017cf25fb23"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="d11f524d00cacf5ba0dfbf25e4aa2158b1c3a036"/>

View File

@ -1,4 +1,4 @@
{
"revision": "823616f0af83eca32bceb0367a7a221f8b187110",
"revision": "b2b0a8234336f7004812bf27e4053957cecad492",
"repo_path": "/integration/gaia-central"
}

View File

@ -11,7 +11,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="8e21d0a5cdac94f05e9c3623fa3b9ad579ba2967"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="80d6405725788327102cab36e8d8c017cf25fb23"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>

View File

@ -10,7 +10,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="8e21d0a5cdac94f05e9c3623fa3b9ad579ba2967"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="80d6405725788327102cab36e8d8c017cf25fb23"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>

View File

@ -12,7 +12,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="8e21d0a5cdac94f05e9c3623fa3b9ad579ba2967"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="80d6405725788327102cab36e8d8c017cf25fb23"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>

View File

@ -11,7 +11,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="8e21d0a5cdac94f05e9c3623fa3b9ad579ba2967"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="80d6405725788327102cab36e8d8c017cf25fb23"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>

View File

@ -11,7 +11,7 @@
</project>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="8e21d0a5cdac94f05e9c3623fa3b9ad579ba2967"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="80d6405725788327102cab36e8d8c017cf25fb23"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="8c449b53328059e9b55bb34baec9b27a15055a7e"/>

View File

@ -11,7 +11,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="8e21d0a5cdac94f05e9c3623fa3b9ad579ba2967"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="80d6405725788327102cab36e8d8c017cf25fb23"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>

View File

@ -592,6 +592,9 @@
cui-areatype="toolbar"
tooltiptext="&closeTab.label;"/>
#ifdef XP_WIN
<hbox id="private-browsing-indicator" skipintoolbarset="true" ordinal="1000"/>
#endif
#ifdef CAN_DRAW_IN_TITLEBAR
<hbox class="titlebar-placeholder" type="caption-buttons"
id="titlebar-placeholder-on-TabsToolbar-for-captions-buttons" persist="width"

View File

@ -191,6 +191,19 @@ const CustomizableWidgets = [{
}
recentlyClosedWindows.appendChild(windowsFragment);
},
onCreated: function(aNode) {
// Middle clicking recently closed items won't close the panel - cope:
let onRecentlyClosedClick = function(aEvent) {
if (aEvent.button == 1) {
CustomizableUI.hidePanelForNode(this);
}
};
let doc = aNode.ownerDocument;
let recentlyClosedTabs = doc.getElementById("PanelUI-recentlyClosedTabs");
let recentlyClosedWindows = doc.getElementById("PanelUI-recentlyClosedWindows");
recentlyClosedTabs.addEventListener("click", onRecentlyClosedClick);
recentlyClosedWindows.addEventListener("click", onRecentlyClosedClick);
},
onViewHiding: function(aEvent) {
LOG("History view is being hidden!");
}

View File

@ -107,6 +107,8 @@ support-files =
test-bug_923281_test2.js
test-bug_939783_console_trace_duplicates.html
test-bug-952277-highlight-nodes-in-vview.html
test-bug-609872-cd-iframe-parent.html
test-bug-609872-cd-iframe-child.html
[browser_bug664688_sandbox_update_after_navigation.js]
[browser_bug_638949_copy_link_location.js]
@ -267,3 +269,4 @@ run-if = os == "mac"
[browser_webconsole_output_events.js]
[browser_console_variables_view_highlighter.js]
[browser_webconsole_console_trace_duplicates.js]
[browser_webconsole_cd_iframe.js]

View File

@ -0,0 +1,110 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test that the cd() jsterm helper function works as expected. See bug 609872.
function test() {
let hud;
const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-609872-cd-iframe-parent.html";
const parentMessages = [{
name: "document.title in parent iframe",
text: "bug 609872 - iframe parent",
category: CATEGORY_OUTPUT,
}, {
name: "paragraph content",
text: "p: test for bug 609872 - iframe parent",
category: CATEGORY_OUTPUT,
}, {
name: "object content",
text: "obj: parent!",
category: CATEGORY_OUTPUT,
}];
const childMessages = [{
name: "document.title in child iframe",
text: "bug 609872 - iframe child",
category: CATEGORY_OUTPUT,
}, {
name: "paragraph content",
text: "p: test for bug 609872 - iframe child",
category: CATEGORY_OUTPUT,
}, {
name: "object content",
text: "obj: child!",
category: CATEGORY_OUTPUT,
}];
Task.spawn(runner).then(finishTest);
function* runner() {
const {tab} = yield loadTab(TEST_URI);
hud = yield openConsole(tab);
executeWindowTest();
yield waitForMessages({ webconsole: hud, messages: parentMessages });
info("cd() into the iframe using a selector");
hud.jsterm.clearOutput();
hud.jsterm.execute("cd('iframe')");
executeWindowTest();
yield waitForMessages({ webconsole: hud, messages: childMessages });
info("cd() out of the iframe, reset to default window");
hud.jsterm.clearOutput();
hud.jsterm.execute("cd()");
executeWindowTest();
yield waitForMessages({ webconsole: hud, messages: parentMessages });
info("call cd() with unexpected arguments");
hud.jsterm.clearOutput();
hud.jsterm.execute("cd(document)");
yield waitForMessages({
webconsole: hud,
messages: [{
text: "Cannot cd()",
category: CATEGORY_OUTPUT,
severity: SEVERITY_ERROR,
}],
});
hud.jsterm.clearOutput();
hud.jsterm.execute("cd('p')");
yield waitForMessages({
webconsole: hud,
messages: [{
text: "Cannot cd()",
category: CATEGORY_OUTPUT,
severity: SEVERITY_ERROR,
}],
});
info("cd() into the iframe using an iframe DOM element");
hud.jsterm.clearOutput();
hud.jsterm.execute("cd($('iframe'))");
executeWindowTest();
yield waitForMessages({ webconsole: hud, messages: childMessages });
info("cd(window.parent)");
hud.jsterm.clearOutput();
hud.jsterm.execute("cd(window.parent)");
executeWindowTest();
yield waitForMessages({ webconsole: hud, messages: parentMessages });
yield closeConsole(tab);
}
function executeWindowTest() {
hud.jsterm.execute("document.title");
hud.jsterm.execute("'p: ' + document.querySelector('p').textContent");
hud.jsterm.execute("'obj: ' + window.foobarBug609872");
}
}

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>test for bug 609872 - iframe child</title>
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
</head>
<body>
<p>test for bug 609872 - iframe child</p>
<script>window.foobarBug609872 = 'child!';</script>
</body>
</html>

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>test for bug 609872 - iframe parent</title>
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
</head>
<body>
<p>test for bug 609872 - iframe parent</p>
<script>window.foobarBug609872 = 'parent!';</script>
<iframe src="test-bug-609872-cd-iframe-child.html"></iframe>
</body>
</html>

View File

@ -213,3 +213,7 @@ emptyPropertiesList=No properties to display
# #1 number of message repeats
# example: 3 repeats
messageRepeats.tooltip2=#1 repeat;#1 repeats
# LOCALIZATION NOTE (cdFunctionInvalidArgument): the text that is displayed when
# cd() is invoked with an invalid argument.
cdFunctionInvalidArgument=Cannot cd() to the given window. Invalid argument.

View File

@ -64,6 +64,7 @@ var Browser = {
messageManager.loadFrameScript("chrome://browser/content/contenthandlers/SelectionHandler.js", true);
messageManager.loadFrameScript("chrome://browser/content/contenthandlers/ContextMenuHandler.js", true);
messageManager.loadFrameScript("chrome://browser/content/contenthandlers/ConsoleAPIObserver.js", true);
messageManager.loadFrameScript("chrome://browser/content/contenthandlers/PluginHelper.js", true);
} catch (e) {
// XXX whatever is calling startup needs to dump errors!
dump("###########" + e + "\n");

View File

@ -1120,6 +1120,9 @@ Desktop browser's sync prefs.
<setting pref="signon.rememberSignons"
title="&optionsHeader.privacy.passwords.label;"
type="bool"/>
<setting pref="browser.display.overlaynavbuttons"
title="&optionsHeader.displayOverlayButtons.label;"
type="bool"/>
<settings id="prefs-reporting"
label="&optionsHeader.reporting.title;">
<setting pref="app.crashreporter.autosubmit"

View File

@ -0,0 +1,95 @@
/* 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/. */
"use strict";
dump("### PluginHelper.js loaded\n");
/**
* Handle events generated by plugin click-to-play code.
*
* This "PluginBindingAttached" fires when a "pluginProblem" XBL binding is
* created. This binding overlays plugin content when the plugin is missing,
* blocked, click-to-play, or replaced by a "preview" extension plugin like
* Shumway or PDF.js.
*/
var PluginHelper = {
init: function () {
addEventListener("PluginBindingAttached", this, true, true);
},
handleEvent: function handleEvent(aEvent) {
switch (aEvent.type) {
case "PluginBindingAttached":
this.handlePluginBindingAttached(aEvent);
break;
}
},
getPluginMimeType: function (plugin) {
var tagMimetype = plugin.actualType;
if (tagMimetype == "") {
tagMimetype = plugin.type;
}
return tagMimetype;
},
handlePluginBindingAttached: function (aEvent) {
let plugin = aEvent.target;
let doc = plugin.ownerDocument;
let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
if (!overlay || overlay._bindingHandled) {
return;
}
overlay._bindingHandled = true;
let eventType = PluginHelper._getBindingType(plugin);
if (!eventType) {
return;
}
switch (eventType) {
case "PluginPlayPreview": {
// Load the "preview" handler (an extension plugin like Shumway or PDF.js).
let previewContent = doc.getAnonymousElementByAttribute(plugin, "class", "previewPluginContent");
let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
let mimeType = PluginHelper.getPluginMimeType(plugin);
let playPreviewInfo = pluginHost.getPlayPreviewInfo(mimeType);
let iframe = previewContent.getElementsByClassName("previewPluginContentFrame")[0];
if (!iframe) {
// lazy initialization of the iframe
iframe = doc.createElementNS("http://www.w3.org/1999/xhtml", "iframe");
iframe.className = "previewPluginContentFrame";
previewContent.appendChild(iframe);
}
iframe.src = playPreviewInfo.redirectURL;
break;
}
case "PluginNotFound": {
// TODO: Display a message about missing plugins (bug 936907)
break;
}
}
},
// Helper to get the binding handler type from a plugin object
_getBindingType: function(plugin) {
if (!(plugin instanceof Ci.nsIObjectLoadingContent))
return null;
switch (plugin.pluginFallbackType) {
case Ci.nsIObjectLoadingContent.PLUGIN_UNSUPPORTED:
return "PluginNotFound";
case Ci.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW:
return "PluginPlayPreview";
default:
// Metro Firefox does not yet support other fallback types.
return null;
}
},
};
PluginHelper.init();

View File

@ -59,6 +59,7 @@ chrome.jar:
content/contenthandlers/FormHelper.js (content/contenthandlers/FormHelper.js)
content/contenthandlers/ConsoleAPIObserver.js (content/contenthandlers/ConsoleAPIObserver.js)
content/contenthandlers/Content.js (content/contenthandlers/Content.js)
content/contenthandlers/PluginHelper.js (content/contenthandlers/PluginHelper.js)
content/library/SelectionPrototype.js (content/library/SelectionPrototype.js)

View File

@ -27,6 +27,7 @@
<!ENTITY clearPrivateData.logins "Active logins">
<!ENTITY optionsHeader.privacy.passwords.label "Remember Passwords">
<!ENTITY optionsHeader.displayOverlayButtons.label "Show Navigation Buttons">
<!ENTITY doNotTrack.title "Do Not Track">
<!ENTITY doNotTrack.options.doNotTrack "Tell websites that I do not want to be tracked">
<!ENTITY doNotTrack.options.doTrack "Tell websites that I want to be tracked">

View File

@ -573,7 +573,12 @@ pref("pdfjs.previousHandler.alwaysAskBeforeHandling", false);
#endif
#ifdef NIGHTLY_BUILD
// Shumay is currently experimental. Toggle this pref to enable Shumway for
// testing and development.
pref("shumway.disabled", true);
// When Shumway is enabled, use it all the time, not only when Flash is set to
// click-to-play (because Metro doesn't even load the native Flash plugin).
pref("shumway.ignoreCTP", true);
#endif
// The maximum amount of decoded image data we'll willingly keep around (we

View File

@ -2509,9 +2509,7 @@ chatbox {
/* End customization mode */
#main-window[privatebrowsingmode=temporary] #TabsToolbar::after {
content: "";
display: -moz-box;
#main-window[privatebrowsingmode=temporary] #private-browsing-indicator {
width: 40px;
background: url("chrome://browser/skin/privatebrowsing-indicator.png") no-repeat center center;
}

View File

@ -952,12 +952,6 @@ nsDOMCameraControl::Shutdown()
mCameraControl->Shutdown();
}
nsRefPtr<ICameraControl>
nsDOMCameraControl::GetNativeCameraControl()
{
return mCameraControl;
}
nsresult
nsDOMCameraControl::NotifyRecordingStatusChange(const nsString& aMsg)
{
@ -1139,21 +1133,31 @@ void
nsDOMCameraControl::OnAutoFocusComplete(bool aAutoFocusSucceeded)
{
MOZ_ASSERT(NS_IsMainThread());
ErrorResult ignored;
nsCOMPtr<CameraAutoFocusCallback> cb = mAutoFocusOnSuccessCb.forget();
mAutoFocusOnErrorCb = nullptr;
cb->Call(aAutoFocusSucceeded, ignored);
if (cb) {
ErrorResult ignored;
cb->Call(aAutoFocusSucceeded, ignored);
}
}
void
nsDOMCameraControl::OnTakePictureComplete(nsIDOMBlob* aPicture)
{
MOZ_ASSERT(NS_IsMainThread());
ErrorResult ignored;
nsCOMPtr<CameraTakePictureCallback> cb = mTakePictureOnSuccessCb.forget();
mTakePictureOnErrorCb = nullptr;
if (!cb) {
// Warn because it shouldn't be possible to get here without
// having passed a success callback into takePicture(), even
// though we guard against a nullptr dereference.
NS_WARNING("DOM Null success callback in OnTakePictureComplete()");
return;
}
ErrorResult ignored;
cb->Call(aPicture, ignored);
}

View File

@ -45,7 +45,6 @@ public:
dom::GetCameraCallback* aOnSuccess,
dom::CameraErrorCallback* aOnError,
nsPIDOMWindow* aWindow);
nsRefPtr<ICameraControl> GetNativeCameraControl();
void Shutdown();

View File

@ -272,53 +272,86 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
}
}
/*
* This utility is replicated from RepoUtils, which is managed by android-sync.
*/
private static String computeSQLInClause(int items, String field) {
final StringBuilder builder = new StringBuilder(field);
builder.append(" IN (");
int i = 0;
for (; i < items - 1; ++i) {
builder.append("?, ");
}
if (i < items) {
builder.append("?");
}
builder.append(")");
return builder.toString();
}
/**
* Turn a single-column cursor of longs into a single SQL "IN" clause.
* We can do this without using selection arguments because Long isn't
* vulnerable to injection.
*/
private static String computeSQLInClauseFromLongs(final Cursor cursor, String field) {
final StringBuilder builder = new StringBuilder(field);
builder.append(" IN (");
final int commaLimit = cursor.getCount() - 1;
int i = 0;
while (cursor.moveToNext()) {
builder.append(cursor.getLong(0));
if (i++ < commaLimit) {
builder.append(", ");
}
}
builder.append(")");
return builder.toString();
}
/**
* Clean up some deleted records from the specified table.
*
* If called in an existing transaction, it is the caller's responsibility
* to ensure that the transaction is already upgraded to a writer, because
* this method issues a read followed by a write, and thus is potentially
* vulnerable to an unhandled SQLITE_BUSY failure during the upgrade.
*
* If not called in an existing transaction, no new explicit transaction
* will be begun.
*/
private void cleanupSomeDeletedRecords(Uri fromUri, Uri targetUri, String tableName) {
Log.d(LOGTAG, "Cleaning up deleted records from " + tableName);
// we cleanup records marked as deleted that are older than a
// We clean up records marked as deleted that are older than a
// predefined max age. It's important not be too greedy here and
// remove only a few old deleted records at a time.
// The PARAM_SHOW_DELETED argument is necessary to return the records
// that were marked as deleted. We use PARAM_IS_SYNC here to ensure
// that we'll be actually deleting records instead of flagging them.
Uri.Builder uriBuilder = targetUri.buildUpon()
.appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(DELETED_RECORDS_PURGE_LIMIT))
.appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1")
.appendQueryParameter(BrowserContract.PARAM_IS_SYNC, "1");
String profile = fromUri.getQueryParameter(BrowserContract.PARAM_PROFILE);
if (!TextUtils.isEmpty(profile))
uriBuilder = uriBuilder.appendQueryParameter(BrowserContract.PARAM_PROFILE, profile);
if (isTest(fromUri))
uriBuilder = uriBuilder.appendQueryParameter(BrowserContract.PARAM_IS_TEST, "1");
Uri uriWithArgs = uriBuilder.build();
Cursor cursor = null;
// Android SQLite doesn't have LIMIT on DELETE. Instead, query for the
// IDs of matching rows, then delete them in one go.
final long now = System.currentTimeMillis();
final String selection = SyncColumns.IS_DELETED + " = 1 AND " +
SyncColumns.DATE_MODIFIED + " <= " +
(now - MAX_AGE_OF_DELETED_RECORDS);
final String profile = fromUri.getQueryParameter(BrowserContract.PARAM_PROFILE);
final SQLiteDatabase db = getWritableDatabaseForProfile(profile, isTest(fromUri));
final String[] ids;
final String limit = Long.toString(DELETED_RECORDS_PURGE_LIMIT, 10);
final Cursor cursor = db.query(tableName, new String[] { CommonColumns._ID }, selection, null, null, null, null, limit);
try {
long now = System.currentTimeMillis();
String selection = SyncColumns.IS_DELETED + " = 1 AND " +
SyncColumns.DATE_MODIFIED + " <= " + (now - MAX_AGE_OF_DELETED_RECORDS);
cursor = query(uriWithArgs,
new String[] { CommonColumns._ID },
selection,
null,
null);
ids = new String[cursor.getCount()];
int i = 0;
while (cursor.moveToNext()) {
Uri uriWithId = ContentUris.withAppendedId(uriWithArgs, cursor.getLong(0));
delete(uriWithId, null, null);
debug("Removed old deleted item with URI: " + uriWithId);
ids[i++] = Long.toString(cursor.getLong(0), 10);
}
} finally {
if (cursor != null)
cursor.close();
cursor.close();
}
final String inClause = computeSQLInClause(ids.length,
CommonColumns._ID);
db.delete(tableName, inClause, ids);
}
/**
@ -328,8 +361,6 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
* Provide <code>keepAfter</code> less than or equal to zero to skip that check.
*
* Items will be removed according to an approximate frecency calculation.
*
* Call this method within a transaction.
*/
private void expireHistory(final SQLiteDatabase db, final int retain, final long keepAfter) {
Log.d(LOGTAG, "Expiring history.");
@ -358,6 +389,8 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
"ORDER BY " + sortOrder + " LIMIT " + toRemove + ")";
}
trace("Deleting using query: " + sql);
beginWrite(db);
db.execSQL(sql);
}
@ -442,6 +475,7 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
trace("Calling delete in transaction on URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
final int match = URI_MATCHER.match(uri);
int deleted = 0;
@ -469,6 +503,7 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
// fall through
case HISTORY: {
trace("Deleting history: " + uri);
beginWrite(db);
deleted = deleteHistory(uri, selection, selectionArgs);
deleteUnusedImages(uri);
break;
@ -498,6 +533,7 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
// fall through
case FAVICONS: {
trace("Deleting favicons: " + uri);
beginWrite(db);
deleted = deleteFavicons(uri, selection, selectionArgs);
break;
}
@ -511,6 +547,7 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
// fall through
case THUMBNAILS: {
trace("Deleting thumbnails: " + uri);
beginWrite(db);
deleted = deleteThumbnails(uri, selection, selectionArgs);
break;
}
@ -577,6 +614,7 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
int match = URI_MATCHER.match(uri);
int updated = 0;
final SQLiteDatabase db = getWritableDatabase(uri);
switch (match) {
// We provide a dedicated (hacky) API for callers to bulk-update the positions of
// folder children by passing an array of GUID strings as `selectionArgs`.
@ -589,13 +627,16 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
// `values` and `selection` are ignored.
case BOOKMARKS_POSITIONS: {
debug("Update on BOOKMARKS_POSITIONS: " + uri);
// This already starts and finishes its own transaction.
updated = updateBookmarkPositions(uri, selectionArgs);
break;
}
case BOOKMARKS_PARENT: {
debug("Update on BOOKMARKS_PARENT: " + uri);
updated = updateBookmarkParents(uri, values, selection, selectionArgs);
beginWrite(db);
updated = updateBookmarkParents(db, values, selection, selectionArgs);
break;
}
@ -608,10 +649,11 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
// fall through
case BOOKMARKS: {
debug("Updating bookmark: " + uri);
if (shouldUpdateOrInsert(uri))
if (shouldUpdateOrInsert(uri)) {
updated = updateOrInsertBookmark(uri, values, selection, selectionArgs);
else
} else {
updated = updateBookmarks(uri, values, selection, selectionArgs);
}
break;
}
@ -624,10 +666,11 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
// fall through
case HISTORY: {
debug("Updating history: " + uri);
if (shouldUpdateOrInsert(uri))
if (shouldUpdateOrInsert(uri)) {
updated = updateOrInsertHistory(uri, values, selection, selectionArgs);
else
} else {
updated = updateHistory(uri, values, selection, selectionArgs);
}
break;
}
@ -643,11 +686,11 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
faviconSelectionArgs = new String[] { url };
}
if (shouldUpdateOrInsert(uri))
if (shouldUpdateOrInsert(uri)) {
updated = updateOrInsertFavicon(uri, values, faviconSelection, faviconSelectionArgs);
else
} else {
updated = updateExistingFavicon(uri, values, faviconSelection, faviconSelectionArgs);
}
break;
}
@ -657,15 +700,15 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
String url = values.getAsString(Thumbnails.URL);
// if no URL is provided, update all of the entries
if (TextUtils.isEmpty(values.getAsString(Thumbnails.URL)))
if (TextUtils.isEmpty(values.getAsString(Thumbnails.URL))) {
updated = updateExistingThumbnail(uri, values, null, null);
else if (shouldUpdateOrInsert(uri))
} else if (shouldUpdateOrInsert(uri)) {
updated = updateOrInsertThumbnail(uri, values, Thumbnails.URL + " = ?",
new String[] { url });
else
} else {
updated = updateExistingThumbnail(uri, values, Thumbnails.URL + " = ?",
new String[] { url });
}
break;
}
@ -674,7 +717,6 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
}
debug("Updated " + updated + " rows for URI: " + uri);
return updated;
}
@ -876,40 +918,42 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
return cursor;
}
int getUrlCount(SQLiteDatabase db, String table, String url) {
Cursor c = db.query(table, new String[] { "COUNT(*)" },
URLColumns.URL + " = ?", new String[] { url }, null, null,
null);
int count = 0;
private static int getUrlCount(SQLiteDatabase db, String table, String url) {
final Cursor c = db.query(table, new String[] { "COUNT(*)" },
URLColumns.URL + " = ?", new String[] { url },
null, null, null);
try {
if (c.moveToFirst())
count = c.getInt(0);
if (c.moveToFirst()) {
return c.getInt(0);
}
} finally {
c.close();
}
return count;
return 0;
}
/**
* Update the positions of bookmarks in batches.
*
* Begins and ends its own transactions.
*
* @see #updateBookmarkPositionsInTransaction(SQLiteDatabase, String[], int, int)
*/
int updateBookmarkPositions(Uri uri, String[] guids) {
if (guids == null)
if (guids == null) {
return 0;
}
int guidsCount = guids.length;
if (guidsCount == 0)
if (guidsCount == 0) {
return 0;
}
final SQLiteDatabase db = getWritableDatabase(uri);
int offset = 0;
int updated = 0;
final SQLiteDatabase db = getWritableDatabase(uri);
db.beginTransaction();
while (offset < guidsCount) {
@ -942,8 +986,8 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
* Construct and execute an update expression that will modify the positions
* of records in-place.
*/
int updateBookmarkPositionsInTransaction(final SQLiteDatabase db, final String[] guids,
final int offset, final int max) {
private static int updateBookmarkPositionsInTransaction(final SQLiteDatabase db, final String[] guids,
final int offset, final int max) {
int guidsCount = guids.length;
int processCount = Math.min(max, guidsCount - offset);
@ -969,6 +1013,7 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
b.append(" WHEN ? THEN " + i);
}
// TODO: use computeSQLInClause
b.append(" END WHERE " + Bookmarks.GUID + " IN (");
i = 1;
while (i++ < processCount) {
@ -985,13 +1030,13 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
* Construct an update expression that will modify the parents of any records
* that match.
*/
int updateBookmarkParents(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
private int updateBookmarkParents(SQLiteDatabase db, ContentValues values, String selection, String[] selectionArgs) {
trace("Updating bookmark parents of " + selection + " (" + selectionArgs[0] + ")");
String where = Bookmarks._ID + " IN (" +
" SELECT DISTINCT " + Bookmarks.PARENT +
" FROM " + TABLE_BOOKMARKS +
" WHERE " + selection + " )";
return getWritableDatabase(uri).update(TABLE_BOOKMARKS, values, where, selectionArgs);
return db.update(TABLE_BOOKMARKS, values, where, selectionArgs);
}
long insertBookmark(Uri uri, ContentValues values) {
@ -1017,10 +1062,10 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
}
String url = values.getAsString(Bookmarks.URL);
Integer type = values.getAsInteger(Bookmarks.TYPE);
debug("Inserting bookmark in database with URL: " + url);
final SQLiteDatabase db = getWritableDatabase(uri);
beginWrite(db);
return db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.TITLE, values);
}
@ -1028,9 +1073,11 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
int updateOrInsertBookmark(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
int updated = updateBookmarks(uri, values, selection, selectionArgs);
if (updated > 0)
if (updated > 0) {
return updated;
}
// Transaction already begun by updateBookmarks.
if (0 <= insertBookmark(uri, values)) {
// We 'updated' one row.
return 1;
@ -1044,49 +1091,35 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
String[] selectionArgs) {
trace("Updating bookmarks on URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
int updated = 0;
final String[] bookmarksProjection = new String[] {
Bookmarks._ID, // 0
Bookmarks.URL, // 1
};
trace("Quering bookmarks to update on URI: " + uri);
Cursor cursor = db.query(TABLE_BOOKMARKS, bookmarksProjection,
selection, selectionArgs, null, null, null);
try {
if (!values.containsKey(Bookmarks.DATE_MODIFIED))
values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
boolean updatingUrl = values.containsKey(Bookmarks.URL);
String url = null;
if (updatingUrl)
url = values.getAsString(Bookmarks.URL);
while (cursor.moveToNext()) {
long id = cursor.getLong(0);
trace("Updating bookmark with ID: " + id);
updated += db.update(TABLE_BOOKMARKS, values, "_id = ?",
new String[] { Long.toString(id) });
}
} finally {
if (cursor != null)
cursor.close();
if (!values.containsKey(Bookmarks.DATE_MODIFIED)) {
values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
}
return updated;
trace("Querying bookmarks to update on URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
// Compute matching IDs.
final Cursor cursor = db.query(TABLE_BOOKMARKS, bookmarksProjection,
selection, selectionArgs, null, null, null);
// Now that we're done reading, open a transaction.
final String inClause;
try {
inClause = computeSQLInClauseFromLongs(cursor, Bookmarks._ID);
} finally {
cursor.close();
}
beginWrite(db);
return db.update(TABLE_BOOKMARKS, values, inClause, null);
}
long insertHistory(Uri uri, ContentValues values) {
final SQLiteDatabase db = getWritableDatabase(uri);
long now = System.currentTimeMillis();
final long now = System.currentTimeMillis();
values.put(History.DATE_CREATED, now);
values.put(History.DATE_MODIFIED, now);
@ -1098,20 +1131,25 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
String url = values.getAsString(History.URL);
debug("Inserting history in database with URL: " + url);
final SQLiteDatabase db = getWritableDatabase(uri);
beginWrite(db);
return db.insertOrThrow(TABLE_HISTORY, History.VISITS, values);
}
int updateOrInsertHistory(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
int updated = updateHistory(uri, values, selection, selectionArgs);
if (updated > 0)
final int updated = updateHistory(uri, values, selection, selectionArgs);
if (updated > 0) {
return updated;
}
// Insert a new entry if necessary
if (!values.containsKey(History.VISITS))
if (!values.containsKey(History.VISITS)) {
values.put(History.VISITS, 1);
if (!values.containsKey(History.TITLE))
}
if (!values.containsKey(History.TITLE)) {
values.put(History.TITLE, values.getAsString(History.URL));
}
if (0 <= insertHistory(uri, values)) {
return 1;
@ -1124,7 +1162,6 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
String[] selectionArgs) {
trace("Updating history on URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
int updated = 0;
final String[] historyProjection = new String[] {
@ -1133,20 +1170,15 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
History.VISITS // 2
};
Cursor cursor = db.query(TABLE_HISTORY, historyProjection, selection,
selectionArgs, null, null, null);
final SQLiteDatabase db = getWritableDatabase(uri);
final Cursor cursor = db.query(TABLE_HISTORY, historyProjection, selection,
selectionArgs, null, null, null);
try {
if (!values.containsKey(Bookmarks.DATE_MODIFIED)) {
values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
}
boolean updatingUrl = values.containsKey(History.URL);
String url = null;
if (updatingUrl)
url = values.getAsString(History.URL);
while (cursor.moveToNext()) {
long id = cursor.getLong(0);
@ -1161,11 +1193,10 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
}
updated += db.update(TABLE_HISTORY, values, "_id = ?",
new String[] { Long.toString(id) });
new String[] { Long.toString(id) });
}
} finally {
if (cursor != null)
cursor.close();
cursor.close();
}
return updated;
@ -1193,7 +1224,6 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
// If changes are needed, please update both
String faviconUrl = values.getAsString(Favicons.URL);
String pageUrl = null;
long faviconId;
trace("Inserting favicon for URL: " + faviconUrl);
@ -1210,15 +1240,16 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
values.put(Favicons.URL, org.mozilla.gecko.favicons.Favicons.guessDefaultFaviconURL(pageUrl));
}
long now = System.currentTimeMillis();
final long now = System.currentTimeMillis();
values.put(Favicons.DATE_CREATED, now);
values.put(Favicons.DATE_MODIFIED, now);
faviconId = db.insertOrThrow(TABLE_FAVICONS, null, values);
beginWrite(db);
final long faviconId = db.insertOrThrow(TABLE_FAVICONS, null, values);
if (pageUrl != null) {
updateFaviconIdsForUrl(db, pageUrl, faviconId);
}
return faviconId;
}
@ -1239,8 +1270,6 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
String faviconUrl = values.getAsString(Favicons.URL);
String pageUrl = null;
int updated = 0;
final SQLiteDatabase db = getWritableDatabase(uri);
Cursor cursor = null;
Long faviconId = null;
long now = System.currentTimeMillis();
@ -1256,33 +1285,38 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
values.put(Favicons.DATE_MODIFIED, now);
final SQLiteDatabase db = getWritableDatabase(uri);
// If there's no favicon URL given and we're inserting if needed, skip
// the update and only do an insert (otherwise all rows would be
// updated)
// updated).
if (!(insertIfNeeded && (faviconUrl == null))) {
updated = db.update(TABLE_FAVICONS, values, selection, selectionArgs);
}
if (updated > 0) {
if ((faviconUrl != null) && (pageUrl != null)) {
final Cursor cursor = db.query(TABLE_FAVICONS,
new String[] { Favicons._ID },
Favicons.URL + " = ?",
new String[] { faviconUrl },
null, null, null);
try {
cursor = db.query(TABLE_FAVICONS,
new String[] { Favicons._ID },
Favicons.URL + " = ?",
new String[] { faviconUrl },
null, null, null);
if (cursor.moveToFirst()) {
faviconId = cursor.getLong(cursor.getColumnIndexOrThrow(Favicons._ID));
}
} finally {
if (cursor != null)
cursor.close();
cursor.close();
}
}
if (pageUrl != null) {
beginWrite(db);
}
} else if (insertIfNeeded) {
values.put(Favicons.DATE_CREATED, now);
trace("No update, inserting favicon for URL: " + faviconUrl);
beginWrite(db);
faviconId = db.insert(TABLE_FAVICONS, null, values);
updated = 1;
}
@ -1294,40 +1328,40 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
return updated;
}
long insertThumbnail(Uri uri, ContentValues values) {
String url = values.getAsString(Thumbnails.URL);
final SQLiteDatabase db = getWritableDatabase(uri);
private long insertThumbnail(Uri uri, ContentValues values) {
final String url = values.getAsString(Thumbnails.URL);
trace("Inserting thumbnail for URL: " + url);
DBUtils.stripEmptyByteArray(values, Thumbnails.DATA);
final SQLiteDatabase db = getWritableDatabase(uri);
beginWrite(db);
return db.insertOrThrow(TABLE_THUMBNAILS, null, values);
}
int updateOrInsertThumbnail(Uri uri, ContentValues values, String selection,
private int updateOrInsertThumbnail(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
return updateThumbnail(uri, values, selection, selectionArgs,
true /* insert if needed */);
}
int updateExistingThumbnail(Uri uri, ContentValues values, String selection,
private int updateExistingThumbnail(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
return updateThumbnail(uri, values, selection, selectionArgs,
false /* only update, no insert */);
}
int updateThumbnail(Uri uri, ContentValues values, String selection,
private int updateThumbnail(Uri uri, ContentValues values, String selection,
String[] selectionArgs, boolean insertIfNeeded) {
String url = values.getAsString(Thumbnails.URL);
int updated = 0;
final SQLiteDatabase db = getWritableDatabase(uri);
final String url = values.getAsString(Thumbnails.URL);
DBUtils.stripEmptyByteArray(values, Thumbnails.DATA);
trace("Updating thumbnail for URL: " + url);
updated = db.update(TABLE_THUMBNAILS, values, selection, selectionArgs);
final SQLiteDatabase db = getWritableDatabase(uri);
beginWrite(db);
int updated = db.update(TABLE_THUMBNAILS, values, selection, selectionArgs);
if (updated == 0 && insertIfNeeded) {
trace("No update, inserting thumbnail for URL: " + url);
@ -1338,6 +1372,12 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
return updated;
}
/**
* This method does not create a new transaction. Its first operation is
* guaranteed to be a write, which in the case of a new enclosing
* transaction will guarantee that a read does not need to be upgraded to
* a write.
*/
int deleteHistory(Uri uri, String selection, String[] selectionArgs) {
debug("Deleting history entry for URI: " + uri);
@ -1360,8 +1400,20 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
values.put(History.VISITS, 0);
values.put(History.DATE_MODIFIED, System.currentTimeMillis());
cleanupSomeDeletedRecords(uri, History.CONTENT_URI, TABLE_HISTORY);
return db.update(TABLE_HISTORY, values, selection, selectionArgs);
// Doing this UPDATE (or the DELETE above) first ensures that the
// first operation within a new enclosing transaction is a write.
// The cleanup call below will do a SELECT first, and thus would
// require the transaction to be upgraded from a reader to a writer.
// In some cases that upgrade can fail (SQLITE_BUSY), so we avoid
// it if we can.
final int updated = db.update(TABLE_HISTORY, values, selection, selectionArgs);
try {
cleanupSomeDeletedRecords(uri, History.CONTENT_URI, TABLE_HISTORY);
} catch (Exception e) {
// We don't care.
Log.e(LOGTAG, "Unable to clean up deleted history records: ", e);
}
return updated;
}
int deleteBookmarks(Uri uri, String selection, String[] selectionArgs) {
@ -1370,6 +1422,7 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
final SQLiteDatabase db = getWritableDatabase(uri);
if (isCallerSync(uri)) {
beginWrite(db);
return db.delete(TABLE_BOOKMARKS, selection, selectionArgs);
}
@ -1378,8 +1431,18 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
ContentValues values = new ContentValues();
values.put(Bookmarks.IS_DELETED, 1);
cleanupSomeDeletedRecords(uri, Bookmarks.CONTENT_URI, TABLE_BOOKMARKS);
return updateBookmarks(uri, values, selection, selectionArgs);
// Doing this UPDATE (or the DELETE above) first ensures that the
// first operation within this transaction is a write.
// The cleanup call below will do a SELECT first, and thus would
// require the transaction to be upgraded from a reader to a writer.
final int updated = updateBookmarks(uri, values, selection, selectionArgs);
try {
cleanupSomeDeletedRecords(uri, Bookmarks.CONTENT_URI, TABLE_BOOKMARKS);
} catch (Exception e) {
// We don't care.
Log.e(LOGTAG, "Unable to clean up deleted bookmark records: ", e);
}
return updated;
}
int deleteFavicons(Uri uri, String selection, String[] selectionArgs) {
@ -1430,29 +1493,36 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
throws OperationApplicationException {
final int numOperations = operations.size();
final ContentProviderResult[] results = new ContentProviderResult[numOperations];
boolean failures = false;
SQLiteDatabase db = null;
if (numOperations >= 1) {
// We only have 1 database for all Uri's that we can get
db = getWritableDatabase(operations.get(0).getUri());
} else {
if (numOperations < 1) {
debug("applyBatch: no operations; returning immediately.");
// The original Android implementation returns a zero-length
// array in this case, we do the same.
// array in this case. We do the same.
return results;
}
boolean failures = false;
// We only have 1 database for all Uris that we can get.
SQLiteDatabase db = getWritableDatabase(operations.get(0).getUri());
// Note that the apply() call may cause us to generate
// additional transactions for the invidual operations.
// additional transactions for the individual operations.
// But Android's wrapper for SQLite supports nested transactions,
// so this will do the right thing.
db.beginTransaction();
//
// Note further that in some circumstances this can result in
// exceptions: if this transaction is first involved in reading,
// and then (naturally) tries to perform writes, SQLITE_BUSY can
// be raised. See Bug 947939 and friends.
beginBatch(db);
for (int i = 0; i < numOperations; i++) {
try {
results[i] = operations.get(i).apply(this, results, i);
final ContentProviderOperation operation = operations.get(i);
results[i] = operation.apply(this, results, i);
} catch (SQLException e) {
Log.w(LOGTAG, "SQLite Exception during applyBatch: ", e);
Log.w(LOGTAG, "SQLite Exception during applyBatch.", e);
// The Android API makes it implementation-defined whether
// the failure of a single operation makes all others abort
// or not. For our use cases, best-effort operation makes
@ -1485,8 +1555,8 @@ public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper
}
trace("Flushing DB applyBatch...");
db.setTransactionSuccessful();
db.endTransaction();
markBatchSuccessful(db);
endBatch(db);
if (failures) {
throw new OperationApplicationException();

View File

@ -57,21 +57,23 @@ public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends
/*
* Deletes items from the database within a DB transaction.
*
* @param uri query URI
* @param selection An optional filter to match rows to update.
* @param selectionArgs arguments for the selection
* @return number of rows impacted by the deletion
* @param uri Query URI.
* @param selection An optional filter to match rows to delete.
* @param selectionArgs An array of arguments to substitute into the selection.
*
* @return number of rows impacted by the deletion.
*/
abstract protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs);
/*
* Updates the database within a DB transaction.
*
* @param uri Query URI
* @param uri Query URI.
* @param values A set of column_name/value pairs to add to the database.
* @param selection An optional filter to match rows to update.
* @param selectionArgs Arguments for the selection
* @return number of rows impacted by the update
* @param selectionArgs An array of arguments to substitute into the selection.
*
* @return number of rows impacted by the update.
*/
abstract protected int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs);
@ -109,6 +111,10 @@ public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends
return mDatabases.getDatabaseHelperForProfile(profile, isTest(uri)).getWritableDatabase();
}
protected SQLiteDatabase getWritableDatabaseForProfile(String profile, boolean isTest) {
return mDatabases.getDatabaseHelperForProfile(profile, isTest).getWritableDatabase();
}
@Override
public boolean onCreate() {
synchronized (this) {
@ -125,25 +131,133 @@ public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends
return true;
}
/**
* Return true if OS version and database parallelism support indicates
* that this provider should bundle writes into transactions.
*/
@SuppressWarnings("static-method")
protected boolean shouldUseTransactions() {
return Build.VERSION.SDK_INT >= 11;
}
/**
* Track whether we're in a batch operation.
*
* When we're in a batch operation, individual write steps won't even try
* to start a transaction... and neither will they attempt to finish one.
*
* Set this to <code>Boolean.TRUE</code> when you're entering a batch --
* a section of code in which {@link ContentProvider} methods will be
* called, but nested transactions should not be started. Callers are
* responsible for beginning and ending the enclosing transaction, and
* for setting this to <code>Boolean.FALSE</code> when done.
*
* This is a ThreadLocal separate from `db.inTransaction` because batched
* operations start transactions independent of individual ContentProvider
* operations. This doesn't work well with the entire concept of this
* abstract class -- that is, automatically beginning and ending transactions
* for each insert/delete/update operation -- and doing so without
* causing arbitrary nesting requires external tracking.
*
* Note that beginWrite takes a DB argument, but we don't differentiate
* between databases in this tracking flag. If your ContentProvider manages
* multiple database transactions within the same thread, you'll need to
* amend this scheme -- but then, you're already doing some serious wizardry,
* so rock on.
*/
final ThreadLocal<Boolean> isInBatchOperation = new ThreadLocal<Boolean>();
private boolean isInBatch() {
final Boolean isInBatch = isInBatchOperation.get();
if (isInBatch == null) {
return false;
}
return isInBatch.booleanValue();
}
/**
* If we're not currently in a transaction, and we should be, start one.
*/
protected void beginWrite(final SQLiteDatabase db) {
if (isInBatch()) {
trace("Not bothering with an intermediate write transaction: inside batch operation.");
return;
}
if (shouldUseTransactions() && !db.inTransaction()) {
trace("beginWrite: beginning transaction.");
db.beginTransaction();
}
}
/**
* If we're not in a batch, but we are in a write transaction, mark it as
* successful.
*/
protected void markWriteSuccessful(final SQLiteDatabase db) {
if (isInBatch()) {
trace("Not marking write successful: inside batch operation.");
return;
}
if (shouldUseTransactions() && db.inTransaction()) {
trace("Marking write transaction successful.");
db.setTransactionSuccessful();
}
}
/**
* If we're not in a batch, but we are in a write transaction,
* end it.
*
* @see TransactionalProvider#markWriteSuccessful(SQLiteDatabase)
*/
protected void endWrite(final SQLiteDatabase db) {
if (isInBatch()) {
trace("Not ending write: inside batch operation.");
return;
}
if (shouldUseTransactions() && db.inTransaction()) {
trace("endWrite: ending transaction.");
db.endTransaction();
}
}
protected void beginBatch(final SQLiteDatabase db) {
trace("Beginning batch.");
isInBatchOperation.set(Boolean.TRUE);
db.beginTransaction();
}
protected void markBatchSuccessful(final SQLiteDatabase db) {
if (isInBatch()) {
trace("Marking batch successful.");
db.setTransactionSuccessful();
return;
}
Log.w(LOGTAG, "Unexpectedly asked to mark batch successful, but not in batch!");
throw new IllegalStateException("Not in batch.");
}
protected void endBatch(final SQLiteDatabase db) {
trace("Ending batch.");
db.endTransaction();
isInBatchOperation.set(Boolean.FALSE);
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
trace("Calling delete on URI: " + uri);
trace("Calling delete on URI: " + uri + ", " + selection + ", " + selectionArgs);
final SQLiteDatabase db = getWritableDatabase(uri);
int deleted = 0;
if (Build.VERSION.SDK_INT >= 11) {
trace("Beginning delete transaction: " + uri);
db.beginTransaction();
try {
deleted = deleteInTransaction(uri, selection, selectionArgs);
db.setTransactionSuccessful();
trace("Successful delete transaction: " + uri);
} finally {
db.endTransaction();
}
} else {
try {
deleted = deleteInTransaction(uri, selection, selectionArgs);
markWriteSuccessful(db);
} finally {
endWrite(db);
}
if (deleted > 0) {
@ -160,23 +274,14 @@ public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends
final SQLiteDatabase db = getWritableDatabase(uri);
Uri result = null;
try {
if (Build.VERSION.SDK_INT >= 11) {
trace("Beginning insert transaction: " + uri);
db.beginTransaction();
try {
result = insertInTransaction(uri, values);
db.setTransactionSuccessful();
trace("Successful insert transaction: " + uri);
} finally {
db.endTransaction();
}
} else {
result = insertInTransaction(uri, values);
}
result = insertInTransaction(uri, values);
markWriteSuccessful(db);
} catch (SQLException sqle) {
Log.e(LOGTAG, "exception in DB operation", sqle);
} catch (UnsupportedOperationException uoe) {
Log.e(LOGTAG, "don't know how to perform that insert", uoe);
} finally {
endWrite(db);
}
if (result != null) {
@ -190,23 +295,17 @@ public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
trace("Calling update on URI: " + uri);
trace("Calling update on URI: " + uri + ", " + selection + ", " + selectionArgs);
final SQLiteDatabase db = getWritableDatabase(uri);
int updated = 0;
if (Build.VERSION.SDK_INT >= 11) {
trace("Beginning update transaction: " + uri);
db.beginTransaction();
try {
updated = updateInTransaction(uri, values, selection, selectionArgs);
db.setTransactionSuccessful();
trace("Successful update transaction: " + uri);
} finally {
db.endTransaction();
}
} else {
updated = updateInTransaction(uri, values, selection, selectionArgs);
try {
updated = updateInTransaction(uri, values, selection,
selectionArgs);
markWriteSuccessful(db);
} finally {
endWrite(db);
}
if (updated > 0) {
@ -227,7 +326,8 @@ public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends
final SQLiteDatabase db = getWritableDatabase(uri);
db.beginTransaction();
debug("bulkInsert: explicitly starting transaction.");
beginBatch(db);
try {
for (int i = 0; i < numValues; i++) {
@ -235,9 +335,10 @@ public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends
successes++;
}
trace("Flushing DB bulkinsert...");
db.setTransactionSuccessful();
markBatchSuccessful(db);
} finally {
db.endTransaction();
debug("bulkInsert: explicitly ending transaction.");
endBatch(db);
}
if (successes > 0) {

View File

@ -297,11 +297,14 @@ gbjar.sources += [
'PrivateTab.java',
'prompts/ColorPickerInput.java',
'prompts/IconGridInput.java',
'prompts/IntentChooserPrompt.java',
'prompts/IntentHandler.java',
'prompts/Prompt.java',
'prompts/PromptInput.java',
'prompts/PromptListAdapter.java',
'prompts/PromptListItem.java',
'prompts/PromptService.java',
'prompts/TabInput.java',
'ReaderModeUtils.java',
'ReferrerReceiver.java',
'RemoteTabs.java',

View File

@ -0,0 +1,157 @@
/* 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.prompts;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.widget.GeckoActionProvider;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.widget.ListView;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
/**
* Shows a prompt letting the user pick from a list of intent handlers for a set of Intents or
* for a GeckoActionProvider. Basic usage:
* IntentChooserPrompt prompt = new IntentChooserPrompt(context, new Intent[] {
* ... // some intents
* });
* prompt.show("Title", context, new IntentHandler() {
* public void onIntentSelected(Intent intent, int position) { }
* public void onCancelled() { }
* });
**/
public class IntentChooserPrompt {
private static final String LOGTAG = "GeckoIntentChooser";
private final ArrayList<PromptListItem> mItems;
public IntentChooserPrompt(Context context, Intent[] intents) {
mItems = getItems(context, intents);
}
public IntentChooserPrompt(Context context, GeckoActionProvider provider) {
mItems = getItems(context, provider);
}
/* If an IntentHandler is passed in, will asynchronously call the handler when the dialog is closed
* Otherwise, will return the Intent that was chosen by the user. Must be called on the UI thread.
*/
public void show(final String title, final Context context, final IntentHandler handler) {
ThreadUtils.assertOnUiThread();
if (mItems.isEmpty()) {
Log.i(LOGTAG, "No activities for the intent chooser!");
handler.onCancelled();
return;
}
// If there's only one item in the intent list, just return it
if (mItems.size() == 1) {
handler.onIntentSelected(mItems.get(0).intent, 0);
return;
}
final Prompt prompt = new Prompt(context, new Prompt.PromptCallback() {
@Override
public void onPromptFinished(String promptServiceResult) {
if (handler == null) {
return;
}
int itemId = -1;
try {
itemId = new JSONObject(promptServiceResult).getInt("button");
} catch (JSONException e) {
Log.e(LOGTAG, "result from promptservice was invalid: ", e);
}
if (itemId == -1) {
handler.onCancelled();
} else {
handler.onIntentSelected(mItems.get(itemId).intent, itemId);
}
}
});
PromptListItem[] arrays = new PromptListItem[mItems.size()];
mItems.toArray(arrays);
prompt.show(title, "", arrays, ListView.CHOICE_MODE_NONE);
return;
}
// Whether or not any activities were found. Useful for checking if you should try a different Intent set
public boolean hasActivities(Context context) {
return mItems.isEmpty();
}
// Gets a list of PromptListItems for an Intent array
private ArrayList<PromptListItem> getItems(final Context context, Intent[] intents) {
final ArrayList<PromptListItem> items = new ArrayList<PromptListItem>();
// If we have intents, use them to build the initial list
for (final Intent intent : intents) {
items.addAll(getItemsForIntent(context, intent));
}
return items;
}
// Gets a list of PromptListItems for a GeckoActionProvider
private ArrayList<PromptListItem> getItems(final Context context, final GeckoActionProvider provider) {
final ArrayList<PromptListItem> items = new ArrayList<PromptListItem>();
// Add any intents from the provider.
final PackageManager packageManager = context.getPackageManager();
final ArrayList<ResolveInfo> infos = provider.getSortedActivites();
for (final ResolveInfo info : infos) {
items.add(getItemForResolveInfo(info, packageManager, provider.getIntent()));
}
return items;
}
private PromptListItem getItemForResolveInfo(ResolveInfo info, PackageManager pm, Intent intent) {
PromptListItem item = new PromptListItem(info.loadLabel(pm).toString());
item.icon = info.loadIcon(pm);
item.intent = new Intent(intent);
// These intents should be implicit.
item.intent.setComponent(new ComponentName(info.activityInfo.applicationInfo.packageName,
info.activityInfo.name));
return item;
}
private ArrayList<PromptListItem> getItemsForIntent(Context context, Intent intent) {
ArrayList<PromptListItem> items = new ArrayList<PromptListItem>();
PackageManager pm = context.getPackageManager();
List<ResolveInfo> lri = pm.queryIntentActivityOptions(GeckoAppShell.getGeckoInterface().getActivity().getComponentName(), null, intent, 0);
// If we didn't find any activities, just return the empty list
if (lri == null) {
return items;
}
// Otherwise, convert the ResolveInfo. Note we don't currently check for duplicates here.
for (ResolveInfo ri : lri) {
items.add(getItemForResolveInfo(ri, pm, intent));
}
return items;
}
}

View File

@ -0,0 +1,12 @@
/* 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.prompts;
import android.content.Intent;
public interface IntentHandler {
public void onIntentSelected(Intent intent, int position);
public void onCancelled();
}

View File

@ -76,6 +76,7 @@ public class PromptInput {
mView = (View)input;
return mView;
}
@Override
public Object getValue() {
EditText edit = (EditText)mView;
@ -111,6 +112,7 @@ public class PromptInput {
InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
return input;
}
@Override
public Object getValue() {
EditText edit = (EditText)mView;
@ -135,6 +137,7 @@ public class PromptInput {
mView = (View)checkbox;
return mView;
}
@Override
public Object getValue() {
CheckBox checkbox = (CheckBox)mView;
@ -213,6 +216,7 @@ public class PromptInput {
private static String formatDateString(String dateFormat, Calendar calendar) {
return new SimpleDateFormat(dateFormat).format(calendar.getTime());
}
@Override
public Object getValue() {
if (Build.VERSION.SDK_INT < 11 && mType.equals("date")) {
@ -293,6 +297,7 @@ public class PromptInput {
return spinner;
}
@Override
public Object getValue() {
return new Integer(spinner.getSelectedItemPosition());
@ -312,10 +317,6 @@ public class PromptInput {
mView = view;
return mView;
}
@Override
public Object getValue() {
return "";
}
}
public PromptInput(JSONObject obj) {
@ -344,6 +345,8 @@ public class PromptInput {
return new IconGridInput(obj);
} else if (ColorPickerInput.INPUT_TYPE.equals(type)) {
return new ColorPickerInput(obj);
} else if (TabInput.INPUT_TYPE.equals(type)) {
return new TabInput(obj);
} else {
for (String dtType : DateTimeInput.INPUT_TYPES) {
if (dtType.equals(type)) {
@ -363,7 +366,7 @@ public class PromptInput {
}
public Object getValue() {
return "";
return null;
}
public boolean getScrollable() {
@ -371,6 +374,6 @@ public class PromptInput {
}
public boolean canApplyInputStyle() {
return true;
return true;
}
}

View File

@ -4,7 +4,9 @@ import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONException;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import java.util.List;
import java.util.ArrayList;
@ -17,6 +19,7 @@ public class PromptListItem {
public final boolean disabled;
public final int id;
public boolean selected;
public Intent intent;
public boolean isParent;
public Drawable icon;

View File

@ -0,0 +1,109 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* 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.prompts;
import org.mozilla.gecko.R;
import org.mozilla.gecko.util.ThreadUtils;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONException;
import android.content.Context;
import android.view.View;
import android.view.LayoutInflater;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TabHost;
import android.widget.TabWidget;
import android.widget.FrameLayout;
import android.widget.ListView;
import android.widget.LinearLayout.LayoutParams;
import java.util.LinkedHashMap;
import android.util.Log;
import android.widget.AdapterView;
public class TabInput extends PromptInput implements AdapterView.OnItemClickListener {
public static final String INPUT_TYPE = "tabs";
public static final String LOGTAG = "GeckoTabInput";
/* Keeping the order of this in sync with the JSON is important. */
final private LinkedHashMap<String, PromptListItem[]> mTabs;
private TabHost mHost;
private int mPosition;
public TabInput(JSONObject obj) {
super(obj);
mTabs = new LinkedHashMap<String, PromptListItem[]>();
try {
JSONArray tabs = obj.getJSONArray("items");
for (int i = 0; i < tabs.length(); i++) {
JSONObject tab = tabs.getJSONObject(i);
String title = tab.getString("label");
JSONArray items = tab.getJSONArray("items");
mTabs.put(title, PromptListItem.getArray(items));
}
} catch(JSONException ex) {
Log.e(LOGTAG, "Exception", ex);
}
}
@Override
public View getView(final Context context) throws UnsupportedOperationException {
LayoutInflater inflater = LayoutInflater.from(context);
mHost = (TabHost) inflater.inflate(R.layout.tab_prompt_input, null);
mHost.setup();
for (String title : mTabs.keySet()) {
final TabHost.TabSpec spec = mHost.newTabSpec(title);
spec.setContent(new TabHost.TabContentFactory() {
@Override
public View createTabContent(final String tag) {
PromptListAdapter adapter = new PromptListAdapter(context, android.R.layout.simple_list_item_1, mTabs.get(tag));
ListView listView = new ListView(context);
listView.setOnItemClickListener(TabInput.this);
listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
listView.setAdapter(adapter);
return listView;
}
});
spec.setIndicator(title);
mHost.addTab(spec);
}
mView = mHost;
return mHost;
}
@Override
public Object getValue() {
JSONObject obj = new JSONObject();
try {
obj.put("tab", mHost.getCurrentTab());
obj.put("item", mPosition);
} catch(JSONException ex) { }
return obj;
}
@Override
public boolean getScrollable() {
return true;
}
@Override
public boolean canApplyInputStyle() {
return false;
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
ThreadUtils.assertOnUiThread();
mPosition = position;
}
}

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<TabHost xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/tabhost"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:minHeight="100dp">
<TabWidget
android:id="@android:id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
</TabWidget>
<FrameLayout
android:id="@android:id/tabcontent"
android:layout_width="match_parent"
android:layout_height="match_parent" >
</FrameLayout>
</LinearLayout>
</TabHost>

View File

@ -65,17 +65,41 @@ public class testBrowserProvider extends ContentProviderTest {
BrowserContract.Bookmarks.READING_LIST_FOLDER_GUID });
c = mProvider.query(appendUriParam(BrowserContract.Bookmarks.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), null, null, null, null);
mAsserter.is(c.getCount(), 7, "All non-special bookmarks and folders were deleted");
assertCountIsAndClose(c, 7, "All non-special bookmarks and folders were deleted");
mProvider.delete(appendUriParam(BrowserContract.History.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"), null, null);
c = mProvider.query(appendUriParam(BrowserContract.History.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), null, null, null, null);
mAsserter.is(c.getCount(), 0, "All history entries were deleted");
assertCountIsAndClose(c, 0, "All history entries were deleted");
mProvider.delete(BrowserContract.Favicons.CONTENT_URI, null, null);
mAsserter.is(c.getCount(), 0, "All favicons were deleted");
/**
* There's no reason why the following two parts should fail.
* But sometimes they do, and I'm not going to spend the time
* to figure out why in an unrelated bug.
*/
mProvider.delete(BrowserContract.Thumbnails.CONTENT_URI, null, null);
mAsserter.is(c.getCount(), 0, "All thumbnails were deleted");
mProvider.delete(appendUriParam(BrowserContract.Favicons.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"), null, null);
c = mProvider.query(appendUriParam(BrowserContract.Favicons.CONTENT_URI,
BrowserContract.PARAM_SHOW_DELETED, "1"),
null, null, null, null);
if (c.getCount() > 0) {
mAsserter.dumpLog("Unexpected favicons in ensureEmptyDatabase.");
}
c.close();
// assertCountIsAndClose(c, 0, "All favicons were deleted");
mProvider.delete(appendUriParam(BrowserContract.Thumbnails.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"), null, null);
c = mProvider.query(appendUriParam(BrowserContract.Thumbnails.CONTENT_URI,
BrowserContract.PARAM_SHOW_DELETED, "1"),
null, null, null, null);
if (c.getCount() > 0) {
mAsserter.dumpLog("Unexpected thumbnails in ensureEmptyDatabase.");
}
c.close();
// assertCountIsAndClose(c, 0, "All thumbnails were deleted");
}
private ContentValues createBookmark(String title, String url, long parentId,
@ -326,7 +350,7 @@ public class testBrowserProvider extends ContentProviderTest {
mAsserter.is(seenException, false, "Batch updating succeded");
mOperations.clear();
// Delte all visits
// Delete all visits
for (int i = 0; i < TESTCOUNT; i++) {
builder = ContentProviderOperation.newDelete(BrowserContract.History.CONTENT_URI);
builder.withSelection(BrowserContract.History.URL + " = ?",
@ -340,7 +364,7 @@ public class testBrowserProvider extends ContentProviderTest {
} catch (OperationApplicationException ex) {
seenException = true;
}
mAsserter.is(seenException, false, "Batch deletion succeded");
mAsserter.is(seenException, false, "Batch deletion succeeded");
}
// Force a Constraint error, see if later operations still apply correctly
@ -475,6 +499,8 @@ public class testBrowserProvider extends ContentProviderTest {
mAsserter.is(new Integer(parentId), new Integer(rootId),
"The PARENT of the " + guid + " special folder is correct");
}
c.close();
}
}
@ -549,6 +575,7 @@ public class testBrowserProvider extends ContentProviderTest {
mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TYPE)), String.valueOf(BrowserContract.Bookmarks.TYPE_BOOKMARK),
"Inserted bookmark has correct default type");
c.close();
}
}
@ -571,12 +598,14 @@ public class testBrowserProvider extends ContentProviderTest {
mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Bookmarks.FAVICON)), "UTF8"),
favicon, "Inserted bookmark has corresponding favicon image");
c.close();
c = getFaviconsByUrl(pageUrl);
mAsserter.is(c.moveToFirst(), true, "Inserted favicon found");
mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"),
favicon, "Inserted favicon has corresponding favicon image");
c.close();
}
}
@ -587,6 +616,7 @@ public class testBrowserProvider extends ContentProviderTest {
Cursor c = getBookmarkById(id);
mAsserter.is(c.moveToFirst(), true, "Inserted bookmark found");
c.close();
return id;
}
@ -603,6 +633,7 @@ public class testBrowserProvider extends ContentProviderTest {
Cursor c = getBookmarkById(appendUriParam(BrowserContract.Bookmarks.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), id);
mAsserter.is(c.moveToFirst(), true, "Deleted bookmark was only marked as deleted");
c.close();
deleted = mProvider.delete(appendUriParam(BrowserContract.Bookmarks.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"),
BrowserContract.Bookmarks._ID + " = ?",
@ -612,6 +643,7 @@ public class testBrowserProvider extends ContentProviderTest {
c = getBookmarkById(appendUriParam(BrowserContract.Bookmarks.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), id);
mAsserter.is(c.moveToFirst(), false, "Inserted bookmark is now actually deleted");
c.close();
id = insertOneBookmark();
@ -622,6 +654,7 @@ public class testBrowserProvider extends ContentProviderTest {
c = getBookmarkById(id);
mAsserter.is(c.moveToFirst(), false,
"Inserted bookmark can't be found after deletion using URI with ID");
c.close();
if (Build.VERSION.SDK_INT >= 8 &&
Build.VERSION.SDK_INT < 16) {
@ -631,6 +664,7 @@ public class testBrowserProvider extends ContentProviderTest {
long parentId = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
c = getBookmarkById(parentId);
mAsserter.is(c.moveToFirst(), true, "Inserted bookmarks folder found");
c.close();
b = createBookmark("Example", "http://example.com", parentId,
BrowserContract.Bookmarks.TYPE_BOOKMARK, 0, "tags", "description", "keyword");
@ -638,6 +672,7 @@ public class testBrowserProvider extends ContentProviderTest {
id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
c = getBookmarkById(id);
mAsserter.is(c.moveToFirst(), true, "Inserted bookmark found");
c.close();
deleted = 0;
try {
@ -664,11 +699,13 @@ public class testBrowserProvider extends ContentProviderTest {
Cursor c = getFaviconsByUrl(pageUrl);
mAsserter.is(c.moveToFirst(), true, "Inserted favicon found");
c.close();
mProvider.delete(ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI, id), null, null);
c = getFaviconsByUrl(pageUrl);
mAsserter.is(c.moveToFirst(), false, "Favicon is deleted with last reference to it");
c.close();
}
}
@ -713,6 +750,7 @@ public class testBrowserProvider extends ContentProviderTest {
new String[] { String.valueOf(id) });
mAsserter.is((updated == 1), true, "Inserted bookmark was updated");
c.close();
c = getBookmarkById(id);
mAsserter.is(c.moveToFirst(), true, "Updated bookmark found");
@ -752,12 +790,14 @@ public class testBrowserProvider extends ContentProviderTest {
u.put(BrowserContract.Bookmarks.URL, "http://examples2.com");
updated = mProvider.update(ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI, id), u, null, null);
c.close();
c = getBookmarkById(id);
mAsserter.is(c.moveToFirst(), true, "Updated bookmark found");
mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.URL)), u.getAsString(BrowserContract.Bookmarks.URL),
"Updated bookmark has correct URL using URI with id");
c.close();
}
}
@ -784,12 +824,14 @@ public class testBrowserProvider extends ContentProviderTest {
ContentValues u = createFaviconEntry(pageUrl, newFavicon);
mProvider.update(BrowserContract.Favicons.CONTENT_URI, u, null, null);
c.close();
c = getFaviconsByUrl(pageUrl);
mAsserter.is(c.moveToFirst(), true, "Updated favicon found");
mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"),
newFavicon, "Updated favicon has corresponding favicon image");
c.close();
}
}
@ -825,6 +867,7 @@ public class testBrowserProvider extends ContentProviderTest {
}
mAsserter.is(i, count, "Folder has the right number of children.");
c.close();
}
public static final int NUMBER_OF_CHILDREN = 1001;
@ -921,6 +964,7 @@ public class testBrowserProvider extends ContentProviderTest {
id = insertWithNullCol(BrowserContract.History.VISITS);
mAsserter.is(new Long(id), new Long(-1),
"Should not be able to insert history with null number of visits");
c.close();
}
}
@ -943,12 +987,14 @@ public class testBrowserProvider extends ContentProviderTest {
mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.History.FAVICON)), "UTF8"),
favicon, "Inserted history entry has corresponding favicon image");
c.close();
c = getFaviconsByUrl(pageUrl);
mAsserter.is(c.moveToFirst(), true, "Inserted favicon found");
mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"),
favicon, "Inserted favicon has corresponding favicon image");
c.close();
}
}
@ -959,6 +1005,7 @@ public class testBrowserProvider extends ContentProviderTest {
Cursor c = getHistoryEntryById(id);
mAsserter.is(c.moveToFirst(), true, "Inserted history entry found");
c.close();
return id;
}
@ -981,6 +1028,7 @@ public class testBrowserProvider extends ContentProviderTest {
new String[] { String.valueOf(id) });
mAsserter.is((deleted == 1), true, "Inserted history entry was deleted");
c.close();
c = getHistoryEntryById(appendUriParam(BrowserContract.History.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), id);
mAsserter.is(c.moveToFirst(), false, "Inserted history is now actually deleted");
@ -990,10 +1038,12 @@ public class testBrowserProvider extends ContentProviderTest {
deleted = mProvider.delete(ContentUris.withAppendedId(BrowserContract.History.CONTENT_URI, id), null, null);
mAsserter.is((deleted == 1), true,
"Inserted history entry was deleted using URI with id");
c.close();
c = getHistoryEntryById(id);
mAsserter.is(c.moveToFirst(), false,
"Inserted history entry can't be found after deletion using URI with ID");
c.close();
}
}
@ -1012,9 +1062,11 @@ public class testBrowserProvider extends ContentProviderTest {
mAsserter.is(c.moveToFirst(), true, "Inserted favicon found");
mProvider.delete(ContentUris.withAppendedId(BrowserContract.History.CONTENT_URI, id), null, null);
c.close();
c = getFaviconsByUrl(pageUrl);
mAsserter.is(c.moveToFirst(), false, "Favicon is deleted with last reference to it");
c.close();
}
}
@ -1056,6 +1108,7 @@ public class testBrowserProvider extends ContentProviderTest {
new String[] { String.valueOf(id) });
mAsserter.is((updated == 1), true, "Inserted history entry was updated");
c.close();
c = getHistoryEntryById(id);
mAsserter.is(c.moveToFirst(), true, "Updated history entry found");
@ -1089,12 +1142,14 @@ public class testBrowserProvider extends ContentProviderTest {
u.put(BrowserContract.History.URL, "http://examples2.com");
updated = mProvider.update(ContentUris.withAppendedId(BrowserContract.History.CONTENT_URI, id), u, null, null);
c.close();
c = getHistoryEntryById(id);
mAsserter.is(c.moveToFirst(), true, "Updated history entry found");
mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.URL)), u.getAsString(BrowserContract.History.URL),
"Updated history entry has correct URL using URI with id");
c.close();
}
}
@ -1121,12 +1176,14 @@ public class testBrowserProvider extends ContentProviderTest {
ContentValues u = createFaviconEntry(pageUrl, newFavicon);
mProvider.update(BrowserContract.Favicons.CONTENT_URI, u, null, null);
c.close();
c = getFaviconsByUrl(pageUrl);
mAsserter.is(c.moveToFirst(), true, "Updated favicon found");
mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"),
newFavicon, "Updated favicon has corresponding favicon image");
c.close();
}
}
@ -1195,6 +1252,7 @@ public class testBrowserProvider extends ContentProviderTest {
BrowserContract.History._ID + " = ?",
new String[] { String.valueOf(id) });
mAsserter.is((updated == 1), true, "Inserted history entry was updated");
c.close();
c = getHistoryEntryById(id);
mAsserter.is(c.moveToFirst(), true, "Updated history entry found");
@ -1220,6 +1278,8 @@ public class testBrowserProvider extends ContentProviderTest {
mAsserter.is((updated == 1), true, "History entry was inserted");
id = getHistoryEntryIdByUrl(TEST_URL_2);
c.close();
c = getHistoryEntryById(id);
mAsserter.is(c.moveToFirst(), true, "History entry was inserted");
@ -1239,6 +1299,7 @@ public class testBrowserProvider extends ContentProviderTest {
BrowserContract.History._ID + " = ?",
new String[] { String.valueOf(id) });
mAsserter.is((updated == 1), true, "Inserted history entry was updated");
c.close();
c = getHistoryEntryById(id);
mAsserter.is(c.moveToFirst(), true, "Updated history entry found");
@ -1253,6 +1314,7 @@ public class testBrowserProvider extends ContentProviderTest {
"Updated history entry has same creation date");
mAsserter.isnot(new Long(c.getLong(c.getColumnIndex(BrowserContract.History.DATE_MODIFIED))), new Long(dateModified),
"Updated history entry has new modification date");
c.close();
}
}
@ -1275,6 +1337,7 @@ public class testBrowserProvider extends ContentProviderTest {
mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Thumbnails.DATA)), "UTF8"),
thumbnail, "Inserted thumbnail has corresponding thumbnail image");
c.close();
}
}
@ -1301,12 +1364,14 @@ public class testBrowserProvider extends ContentProviderTest {
ContentValues u = createThumbnailEntry(pageUrl, newThumbnail);
mProvider.update(BrowserContract.Thumbnails.CONTENT_URI, u, null, null);
c.close();
c = getThumbnailByUrl(pageUrl);
mAsserter.is(c.moveToFirst(), true, "Updated thumbnail found");
mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Thumbnails.DATA)), "UTF8"),
newThumbnail, "Updated thumbnail has corresponding thumbnail image");
c.close();
}
}
@ -1325,9 +1390,11 @@ public class testBrowserProvider extends ContentProviderTest {
mAsserter.is(c.moveToFirst(), true, "Inserted thumbnail found");
mProvider.delete(ContentUris.withAppendedId(BrowserContract.History.CONTENT_URI, id), null, null);
c.close();
c = getThumbnailByUrl(pageUrl);
mAsserter.is(c.moveToFirst(), false, "Thumbnail is deleted with last reference to it");
c.close();
}
}
@ -1435,6 +1502,7 @@ public class testBrowserProvider extends ContentProviderTest {
"Combined entry has correct number of visits");
mAsserter.is(new Long(c.getLong(c.getColumnIndex(BrowserContract.Combined.DATE_LAST_VISITED))), new Long(LAST_VISITED),
"Combined entry has correct last visit time");
c.close();
}
}
@ -1495,6 +1563,7 @@ public class testBrowserProvider extends ContentProviderTest {
mAsserter.is(new Integer(display), new Integer(expectedDisplay),
"Combined display column should always be DISPLAY_READER for the reading list item");
}
c.close();
}
}
@ -1527,6 +1596,7 @@ public class testBrowserProvider extends ContentProviderTest {
new String[] { String.valueOf(combinedBookmarkId) });
mAsserter.is((deleted == 1), true, "Inserted combined bookmark was deleted");
c.close();
c = mProvider.query(BrowserContract.Combined.CONTENT_URI, null, "", null, null);
mAsserter.is(c.getCount(), 1, "1 combined entry found");
@ -1534,6 +1604,7 @@ public class testBrowserProvider extends ContentProviderTest {
mAsserter.is(c.moveToFirst(), true, "Found combined entry without bookmark id");
mAsserter.is(new Long(c.getLong(c.getColumnIndex(BrowserContract.Combined.BOOKMARK_ID))), new Long(0),
"Bookmark id should not be set to removed bookmark id");
c.close();
}
}
@ -1569,6 +1640,7 @@ public class testBrowserProvider extends ContentProviderTest {
new String[] { String.valueOf(combinedReadingListItemId) });
mAsserter.is((deleted == 1), true, "Inserted combined reading list item was deleted");
c.close();
c = mProvider.query(BrowserContract.Combined.CONTENT_URI, null, "", null, null);
mAsserter.is(c.getCount(), 1, "1 combined entry found");
@ -1578,6 +1650,7 @@ public class testBrowserProvider extends ContentProviderTest {
"Bookmark id should not be set to removed bookmark id");
mAsserter.is(new Long(c.getLong(c.getColumnIndex(BrowserContract.Combined.DISPLAY))), new Long(BrowserContract.Combined.DISPLAY_NORMAL),
"Combined entry should have reader display type");
c.close();
}
}
@ -1608,7 +1681,8 @@ public class testBrowserProvider extends ContentProviderTest {
}
Cursor c = mProvider.query(BrowserContract.History.CONTENT_URI, null, "", null, null);
mAsserter.is(c.getCount(), count, count + " history entries found");
assertCountIsAndClose(c, count, count + " history entries found");
// add thumbnails for each entry
allVals = new ContentValues[count];
@ -1622,7 +1696,7 @@ public class testBrowserProvider extends ContentProviderTest {
mAsserter.is(inserts, count, "Expected number of inserts matches");
c = mProvider.query(BrowserContract.Thumbnails.CONTENT_URI, null, null, null, null);
mAsserter.is(c.getCount(), count, count + " thumbnails entries found");
assertCountIsAndClose(c, count, count + " thumbnails entries found");
}
@Override
@ -1637,57 +1711,61 @@ public class testBrowserProvider extends ContentProviderTest {
Uri url = appendUriParam(BrowserContract.History.CONTENT_OLD_URI, BrowserContract.PARAM_EXPIRE_PRIORITY, "NORMAL");
mProvider.delete(url, null, null);
Cursor c = mProvider.query(BrowserContract.History.CONTENT_URI, null, "", null, null);
mAsserter.is(c.getCount(), count, count + " history entries found");
assertCountIsAndClose(c, count, count + " history entries found");
// expiring with a normal priority should delete all but 10 thumbnails
c = mProvider.query(BrowserContract.Thumbnails.CONTENT_URI, null, null, null, null);
mAsserter.is(c.getCount(), thumbCount, thumbCount + " thumbnails found");
assertCountIsAndClose(c, thumbCount, thumbCount + " thumbnails found");
ensureEmptyDatabase();
// insert a bunch of new entries
// Insert a bunch of new entries.
createFakeHistory(0, count);
// expiring with a aggressive priority should leave 500 entries
// Expiring with a aggressive priority should leave 500 entries.
url = appendUriParam(BrowserContract.History.CONTENT_OLD_URI, BrowserContract.PARAM_EXPIRE_PRIORITY, "AGGRESSIVE");
mProvider.delete(url, null, null);
c = mProvider.query(BrowserContract.History.CONTENT_URI, null, "", null, null);
mAsserter.is(c.getCount(), 500, "500 history entries found");
// expiring with a aggressive priority should delete all but 10 thumbnails
c = mProvider.query(BrowserContract.History.CONTENT_URI, null, "", null, null);
assertCountIsAndClose(c, 500, "500 history entries found");
// Expiring with a aggressive priority should delete all but 10 thumbnails.
c = mProvider.query(BrowserContract.Thumbnails.CONTENT_URI, null, null, null, null);
mAsserter.is(c.getCount(), thumbCount, thumbCount + " thumbnails found");
assertCountIsAndClose(c, thumbCount, thumbCount + " thumbnails found");
ensureEmptyDatabase();
// insert a bunch of entries with an old time created/modified
// Insert a bunch of entries with an old time created/modified.
long time = 1000L * 60L * 60L * 24L * 30L * 3L;
createFakeHistory(time, count);
// expiring with an normal priority should remove at most 1000 entries
// entries leaving at least 2000
// Expiring with an normal priority should remove at most 1000 entries,
// entries leaving at least 2000.
url = appendUriParam(BrowserContract.History.CONTENT_OLD_URI, BrowserContract.PARAM_EXPIRE_PRIORITY, "NORMAL");
mProvider.delete(url, null, null);
c = mProvider.query(BrowserContract.History.CONTENT_URI, null, "", null, null);
mAsserter.is(c.getCount(), 2000, "2000 history entries found");
// expiring with a normal priority should delete all but 10 thumbnails
c = mProvider.query(BrowserContract.History.CONTENT_URI, null, "", null, null);
assertCountIsAndClose(c, 2000, "2000 history entries found");
// Expiring with a normal priority should delete all but 10 thumbnails.
c = mProvider.query(BrowserContract.Thumbnails.CONTENT_URI, null, null, null, null);
mAsserter.is(c.getCount(), thumbCount, thumbCount + " thumbnails found");
assertCountIsAndClose(c, thumbCount, thumbCount + " thumbnails found");
ensureEmptyDatabase();
// insert a bunch of entries with an old time created/modified
time = 1000L * 60L * 60L * 24L * 30L * 3L;
createFakeHistory(time, count);
// expiring with an agressive priority should remove old
// entries leaving at least 500
// Expiring with an aggressive priority should remove old
// entries, leaving at least 500.
url = appendUriParam(BrowserContract.History.CONTENT_OLD_URI, BrowserContract.PARAM_EXPIRE_PRIORITY, "AGGRESSIVE");
mProvider.delete(url, null, null);
c = mProvider.query(BrowserContract.History.CONTENT_URI, null, "", null, null);
mAsserter.is(c.getCount(), 500, "500 history entries found");
assertCountIsAndClose(c, 500, "500 history entries found");
// expiring with an aggressive priority should delete all but 10 thumbnails
c = mProvider.query(BrowserContract.Thumbnails.CONTENT_URI, null, null, null, null);
mAsserter.is(c.getCount(), thumbCount, thumbCount + " thumbnails found");
assertCountIsAndClose(c, thumbCount, thumbCount + " thumbnails found");
}
}
@ -1776,4 +1854,16 @@ public class testBrowserProvider extends ContentProviderTest {
ensureOnlyChangeNotifiedStartsWith(BrowserContract.History.CONTENT_URI, "bulkInsert");
}
}
/**
* Assert that the provided cursor has the expected number of rows,
* closing the cursor afterwards.
*/
private void assertCountIsAndClose(Cursor c, int expectedCount, String message) {
try {
mAsserter.is(c.getCount(), expectedCount, message);
} finally {
c.close();
}
}
}

View File

@ -19,6 +19,8 @@ import android.view.SubMenu;
import android.view.View;
import android.view.View.OnClickListener;
import java.util.ArrayList;
public class GeckoActionProvider extends ActionProvider {
private static int MAX_HISTORY_SIZE = 2;
@ -125,6 +127,20 @@ public class GeckoActionProvider extends ActionProvider {
mOnTargetListener = listener;
}
public ArrayList<ResolveInfo> getSortedActivites() {
ArrayList<ResolveInfo> infos = new ArrayList<ResolveInfo>();
ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
PackageManager packageManager = mContext.getPackageManager();
// Populate the sub-menu with a sub set of the activities.
final int count = dataModel.getActivityCount();
for (int i = 0; i < count; i++) {
infos.add(dataModel.getActivity(i));
}
return infos;
}
/**
* Listener for handling default activity / menu item clicks.
*/

View File

@ -1045,6 +1045,8 @@ BrowserAddonActor.prototype = {
function DebuggerProgressListener(aBrowserTabActor) {
this._tabActor = aBrowserTabActor;
this._tabActor._tabbrowser.addProgressListener(this);
let EventEmitter = devtools.require("devtools/shared/event-emitter");
EventEmitter.decorate(this);
}
DebuggerProgressListener.prototype = {
@ -1072,28 +1074,32 @@ DebuggerProgressListener.prototype = {
this._tabActor._pendingNavigation = aRequest;
}
this._tabActor.threadActor.disableAllBreakpoints();
this._tabActor.conn.send({
let packet = {
from: this._tabActor.actorID,
type: "tabNavigated",
url: aRequest.URI.spec,
nativeConsoleAPI: true,
state: "start"
});
};
this._tabActor.threadActor.disableAllBreakpoints();
this._tabActor.conn.send(packet);
this.emit("will-navigate", packet);
} else if (isStop) {
if (this._tabActor.threadActor.state == "running") {
this._tabActor.threadActor.dbg.enabled = true;
}
let window = this._tabActor.window;
this._tabActor.conn.send({
let packet = {
from: this._tabActor.actorID,
type: "tabNavigated",
url: this._tabActor.url,
title: this._tabActor.title,
nativeConsoleAPI: this._tabActor.hasNativeConsoleAPI(window),
state: "stop"
});
};
this._tabActor.conn.send(packet);
this.emit("navigate", packet);
}
}, "DebuggerProgressListener.prototype.onStateChange"),

View File

@ -59,10 +59,10 @@ function WebConsoleActor(aConnection, aParentActor)
this.dbg = new Debugger();
this._protoChains = new Map();
this._netEvents = new Map();
this._gripDepth = 0;
this._onWillNavigate = this._onWillNavigate.bind(this);
this._onObserverNotification = this._onObserverNotification.bind(this);
if (this.parentActor.isRootActor) {
Services.obs.addObserver(this._onObserverNotification,
@ -112,16 +112,6 @@ WebConsoleActor.prototype =
*/
_netEvents: null,
/**
* A cache of prototype chains for objects that have received a
* prototypeAndProperties request.
*
* @private
* @type Map
* @see dbg-script-actors.js, ThreadActor._protoChains
*/
_protoChains: null,
/**
* The debugger server connection instance.
* @type object
@ -209,6 +199,31 @@ WebConsoleActor.prototype =
*/
_lastChromeWindow: null,
// The evalWindow is used at the scope for JS evaluation.
_evalWindow: null,
get evalWindow() {
return this._evalWindow || this.window;
},
set evalWindow(aWindow) {
this._evalWindow = aWindow;
if (!this._progressListenerActive && this.parentActor._progressListener) {
this.parentActor._progressListener.once("will-navigate", this._onWillNavigate);
this._progressListenerActive = true;
}
},
/**
* Flag used to track if we are listening for events from the progress
* listener of the tab actor. We use the progress listener to clear
* this.evalWindow on page navigation.
*
* @private
* @type boolean
*/
_progressListenerActive: false,
/**
* The ConsoleServiceListener instance.
* @type object
@ -295,8 +310,9 @@ WebConsoleActor.prototype =
}
this._actorPool = null;
this._jstermHelpersCache = null;
this._evalWindow = null;
this._netEvents.clear();
this._protoChains.clear();
this.dbg.enabled = false;
this.dbg = null;
this.conn = null;
@ -725,7 +741,7 @@ WebConsoleActor.prototype =
}
// This is the general case (non-paused debugger)
else {
dbgObject = this.dbg.makeGlobalObjectReference(this.window);
dbgObject = this.dbg.makeGlobalObjectReference(this.evalWindow);
}
let result = JSPropertyProvider(dbgObject, environment, aRequest.text,
@ -822,12 +838,13 @@ WebConsoleActor.prototype =
_getJSTermHelpers: function WCA__getJSTermHelpers(aDebuggerGlobal)
{
let helpers = {
window: this.window,
window: this.evalWindow,
chromeWindow: this.chromeWindow.bind(this),
makeDebuggeeValue: aDebuggerGlobal.makeDebuggeeValue.bind(aDebuggerGlobal),
createValueGrip: this.createValueGrip.bind(this),
sandbox: Object.create(null),
helperResult: null,
consoleActor: this,
};
JSTermHelpers(helpers);
@ -924,12 +941,12 @@ WebConsoleActor.prototype =
// as ordinary objects, not as references to be followed, so mixing
// debuggers causes strange behaviors.)
let dbg = frame ? frameActor.threadActor.dbg : this.dbg;
let dbgWindow = dbg.makeGlobalObjectReference(this.window);
let dbgWindow = dbg.makeGlobalObjectReference(this.evalWindow);
// If we have an object to bind to |_self|, create a Debugger.Object
// referring to that object, belonging to dbg.
let bindSelf = null;
let dbgWindow = dbg.makeGlobalObjectReference(this.window);
let dbgWindow = dbg.makeGlobalObjectReference(this.evalWindow);
if (aOptions.bindObjectActor) {
let objActor = this.getActorByID(aOptions.bindObjectActor);
if (objActor) {
@ -1289,7 +1306,17 @@ WebConsoleActor.prototype =
});
break;
}
}
},
/**
* The "will-navigate" progress listener. This is used to clear the current
* eval scope.
*/
_onWillNavigate: function WCA__onWillNavigate()
{
this._evalWindow = null;
this._progressListenerActive = false;
},
};
WebConsoleActor.prototype.requestTypes =

View File

@ -21,3 +21,4 @@ support-files =
[test_object_actor_native_getters_lenient_this.html]
[test_page_errors.html]
[test_throw.html]
[test_jsterm_cd_iframe.html]

View File

@ -0,0 +1,154 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf8">
<title>Test for the cd() function</title>
<script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript;version=1.8" src="common.js"></script>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
</head>
<body>
<p>Test for the cd() function</p>
<script class="testbody" type="text/javascript;version=1.8">
SimpleTest.waitForExplicitFinish();
let gState;
function startTest()
{
removeEventListener("load", startTest);
attachConsole([], onAttach, true);
}
function onAttach(aState, aResponse)
{
top.foobarObject = Object.create(null);
top.foobarObject.bug609872 = "parent";
window.foobarObject = Object.create(null);
window.foobarObject.bug609872 = "child";
gState = aState;
let tests = [doCheckParent, doCdIframe, doCheckIframe, doCdParent,
doCheckParent2];
runTests(tests, testEnd);
}
function doCheckParent()
{
info("check parent window");
gState.client.evaluateJS("window.foobarObject.bug609872",
onFooObjectFromParent);
}
function onFooObjectFromParent(aResponse)
{
checkObject(aResponse, {
from: gState.actor,
input: "window.foobarObject.bug609872",
result: "parent",
});
ok(!aResponse.exception, "no eval exception");
ok(!aResponse.helperResult, "no helper result");
nextTest();
}
function doCdIframe()
{
info("test cd('iframe')");
gState.client.evaluateJS("cd('iframe')", onCdIframe);
}
function onCdIframe(aResponse)
{
checkObject(aResponse, {
from: gState.actor,
input: "cd('iframe')",
result: { type: "undefined" },
helperResult: { type: "cd" },
});
ok(!aResponse.exception, "no eval exception");
nextTest();
}
function doCheckIframe()
{
info("check foobarObject from the iframe");
gState.client.evaluateJS("window.foobarObject.bug609872",
onFooObjectFromIframe);
}
function onFooObjectFromIframe(aResponse)
{
checkObject(aResponse, {
from: gState.actor,
input: "window.foobarObject.bug609872",
result: "child",
});
ok(!aResponse.exception, "no js eval exception");
ok(!aResponse.helperResult, "no helper result");
nextTest();
}
function doCdParent()
{
info("test cd() back to parent");
gState.client.evaluateJS("cd()", onCdParent);
}
function onCdParent(aResponse)
{
checkObject(aResponse, {
from: gState.actor,
input: "cd()",
result: { type: "undefined" },
helperResult: { type: "cd" },
});
ok(!aResponse.exception, "no eval exception");
nextTest();
}
function doCheckParent2()
{
gState.client.evaluateJS("window.foobarObject.bug609872",
onFooObjectFromParent2);
}
function onFooObjectFromParent2(aResponse)
{
checkObject(aResponse, {
from: gState.actor,
input: "window.foobarObject.bug609872",
result: "parent",
});
ok(!aResponse.exception, "no eval exception");
ok(!aResponse.helperResult, "no helper result");
nextTest();
}
function testEnd()
{
closeDebugger(gState, function() {
gState = null;
SimpleTest.finish();
});
}
addEventListener("load", startTest);
</script>
</body>
</html>

View File

@ -1512,6 +1512,40 @@ function JSTermHelpers(aOwner)
aOwner.helperResult = { type: "help" };
};
/**
* Change the JS evaluation scope.
*
* @param DOMElement|string|window aWindow
* The window object to use for eval scope. This can be a string that
* is used to perform document.querySelector(), to find the iframe that
* you want to cd() to. A DOMElement can be given as well, the
* .contentWindow property is used. Lastly, you can directly pass
* a window object. If you call cd() with no arguments, the current
* eval scope is cleared back to its default (the top window).
*/
aOwner.sandbox.cd = function JSTH_cd(aWindow)
{
if (!aWindow) {
aOwner.consoleActor.evalWindow = null;
aOwner.helperResult = { type: "cd" };
return;
}
if (typeof aWindow == "string") {
aWindow = aOwner.window.document.querySelector(aWindow);
}
if (aWindow instanceof Ci.nsIDOMElement && aWindow.contentWindow) {
aWindow = aWindow.contentWindow;
}
if (!(aWindow instanceof Ci.nsIDOMWindow)) {
aOwner.helperResult = { type: "error", message: "cdFunctionInvalidArgument" };
return;
}
aOwner.consoleActor.evalWindow = aWindow;
aOwner.helperResult = { type: "cd" };
};
/**
* Inspects the passed aObject. This is done by opening the PropertyPanel.
*