зеркало из https://github.com/mozilla/gecko-dev.git
Merge autoland to mozilla-central. a=merge
This commit is contained in:
Коммит
4ef183d00f
|
@ -85,12 +85,12 @@ rev = "3484d3e3ebdc8931493aa5df4d7ee9360a90e76b"
|
|||
[source."https://github.com/gfx-rs/wgpu"]
|
||||
git = "https://github.com/gfx-rs/wgpu"
|
||||
replace-with = "vendored-sources"
|
||||
rev = "0b61a191"
|
||||
rev = "b51fd851"
|
||||
|
||||
[source."https://github.com/gfx-rs/naga"]
|
||||
git = "https://github.com/gfx-rs/naga"
|
||||
replace-with = "vendored-sources"
|
||||
rev = "85056524"
|
||||
rev = "1aa91549"
|
||||
|
||||
[source."https://github.com/gfx-rs/metal-rs"]
|
||||
git = "https://github.com/gfx-rs/metal-rs"
|
||||
|
|
|
@ -332,7 +332,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.0.1"
|
||||
version = "1.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "baldrdash"
|
||||
|
@ -3432,7 +3432,7 @@ checksum = "a2983372caf4480544083767bf2d27defafe32af49ab4df3a0b7fc90793a3664"
|
|||
[[package]]
|
||||
name = "naga"
|
||||
version = "0.8.0"
|
||||
source = "git+https://github.com/gfx-rs/naga?rev=85056524#850565243d1d0d03215f13246c94d63e1d4c51cd"
|
||||
source = "git+https://github.com/gfx-rs/naga?rev=1aa91549#1aa9154964238af8c692cf521ff90e1f2395e147"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"bitflags",
|
||||
|
@ -5876,7 +5876,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "wgpu-core"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=0b61a191#0b61a191244da0f0d987d53614a6698097a7622f"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=b51fd851#b51fd851be51cfe40c937ef789a44244e7dc2971"
|
||||
dependencies = [
|
||||
"arrayvec 0.7.2",
|
||||
"bitflags",
|
||||
|
@ -5899,7 +5899,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "wgpu-hal"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=0b61a191#0b61a191244da0f0d987d53614a6698097a7622f"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=b51fd851#b51fd851be51cfe40c937ef789a44244e7dc2971"
|
||||
dependencies = [
|
||||
"arrayvec 0.7.2",
|
||||
"ash",
|
||||
|
@ -5936,7 +5936,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "wgpu-types"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=0b61a191#0b61a191244da0f0d987d53614a6698097a7622f"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=b51fd851#b51fd851be51cfe40c937ef789a44244e7dc2971"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags_serde_shim",
|
||||
|
|
|
@ -276,7 +276,7 @@ var StarUI = {
|
|||
{ capture: true, once: true }
|
||||
);
|
||||
};
|
||||
gEditItemOverlay.initPanel({
|
||||
await gEditItemOverlay.initPanel({
|
||||
node: aNode,
|
||||
onPanelReady,
|
||||
hiddenRows: ["location", "keyword"],
|
||||
|
|
|
@ -67,10 +67,6 @@ var AboutModuleFactory = {
|
|||
return new AboutModule().QueryInterface(aIID);
|
||||
},
|
||||
|
||||
lockFactory(aLock) {
|
||||
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
||||
},
|
||||
|
||||
QueryInterface: ChromeUtils.generateQI(["nsIFactory"]),
|
||||
};
|
||||
|
||||
|
|
|
@ -1905,7 +1905,7 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
|
|||
*/
|
||||
getHardcodedLayout = ({
|
||||
items = 21,
|
||||
spocPositions = [2, 4, 11, 20],
|
||||
spocPositions = [1, 5, 7, 11, 18, 20],
|
||||
sponsoredCollectionsEnabled = false,
|
||||
compactLayout = false,
|
||||
hybridLayout = false,
|
||||
|
|
|
@ -288,6 +288,12 @@ var gEditItemOverlay = {
|
|||
onPanelReady,
|
||||
} = this._setPaneInfo(aInfo);
|
||||
|
||||
// initPanel can be called multiple times in a row,
|
||||
// and awaits Promises. If the reference to `instance`
|
||||
// changes, it must mean another caller has called
|
||||
// initPanel again, so bail out of the initialization.
|
||||
let instance = (this._instance = {});
|
||||
|
||||
// If we're creating a new item on the toolbar, show it:
|
||||
if (
|
||||
aInfo.isNewBookmark &&
|
||||
|
@ -326,6 +332,11 @@ var gEditItemOverlay = {
|
|||
|
||||
if (showOrCollapse("keywordRow", isBookmark, "keyword")) {
|
||||
await this._initKeywordField().catch(Cu.reportError);
|
||||
// paneInfo can be null if paneInfo is uninitialized while
|
||||
// the process above is awaiting initialization
|
||||
if (instance != this._instance || this._paneInfo == null) {
|
||||
return;
|
||||
}
|
||||
this._keywordField.readOnly = this.readOnly;
|
||||
}
|
||||
|
||||
|
@ -342,6 +353,9 @@ var gEditItemOverlay = {
|
|||
// this (it's only the Star UI that shows the folderPicker)
|
||||
if (showOrCollapse("folderRow", isItem, "folderPicker")) {
|
||||
await this._initFolderMenuList(parentGuid).catch(Cu.reportError);
|
||||
if (instance != this._instance || this._paneInfo == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Selection count.
|
||||
|
|
|
@ -241,7 +241,7 @@ var gEditItemOverlay = {
|
|||
* List of rows to be hidden regardless of the item edited. Possible values:
|
||||
* "title", "location", "keyword", "folderPicker".
|
||||
*/
|
||||
initPanel(aInfo) {
|
||||
async initPanel(aInfo) {
|
||||
if (typeof aInfo != "object" || aInfo === null) {
|
||||
throw new Error("aInfo must be an object.");
|
||||
}
|
||||
|
@ -279,6 +279,12 @@ var gEditItemOverlay = {
|
|||
onPanelReady,
|
||||
} = this._setPaneInfo(aInfo);
|
||||
|
||||
// initPanel can be called multiple times in a row,
|
||||
// and awaits Promises. If the reference to `instance`
|
||||
// changes, it must mean another caller has called
|
||||
// initPanel again, so bail out of the initialization.
|
||||
let instance = (this._instance = {});
|
||||
|
||||
// If we're creating a new item on the toolbar, show it:
|
||||
if (
|
||||
aInfo.isNewBookmark &&
|
||||
|
@ -316,7 +322,12 @@ var gEditItemOverlay = {
|
|||
}
|
||||
|
||||
if (showOrCollapse("keywordRow", isBookmark, "keyword")) {
|
||||
this._initKeywordField().catch(Cu.reportError);
|
||||
await this._initKeywordField().catch(Cu.reportError);
|
||||
// paneInfo can be null if paneInfo is uninitialized while
|
||||
// the process above is awaiting initialization
|
||||
if (instance != this._instance || this._paneInfo == null) {
|
||||
return;
|
||||
}
|
||||
this._keywordField.readOnly = this.readOnly;
|
||||
}
|
||||
|
||||
|
@ -332,7 +343,10 @@ var gEditItemOverlay = {
|
|||
// not cheap (we don't always have the parent), and there's no use case for
|
||||
// this (it's only the Star UI that shows the folderPicker)
|
||||
if (showOrCollapse("folderRow", isItem, "folderPicker")) {
|
||||
this._initFolderMenuList(parentGuid).catch(Cu.reportError);
|
||||
await this._initFolderMenuList(parentGuid).catch(Cu.reportError);
|
||||
if (instance != this._instance || this._paneInfo == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Selection count.
|
||||
|
|
|
@ -777,17 +777,21 @@ var PlacesOrganizer = {
|
|||
gEditItemOverlay.uninitPanel(false);
|
||||
|
||||
if (selectedNode && !PlacesUtils.nodeIsSeparator(selectedNode)) {
|
||||
gEditItemOverlay.initPanel({
|
||||
node: selectedNode,
|
||||
hiddenRows: ["folderPicker"],
|
||||
});
|
||||
gEditItemOverlay
|
||||
.initPanel({
|
||||
node: selectedNode,
|
||||
hiddenRows: ["folderPicker"],
|
||||
})
|
||||
.catch(ex => Cu.reportError(ex));
|
||||
} else if (!selectedNode && aNodeList[0]) {
|
||||
if (aNodeList.every(PlacesUtils.nodeIsURI)) {
|
||||
let uris = aNodeList.map(node => Services.io.newURI(node.uri));
|
||||
gEditItemOverlay.initPanel({
|
||||
uris,
|
||||
hiddenRows: ["folderPicker", "location", "keyword", "name"],
|
||||
});
|
||||
gEditItemOverlay
|
||||
.initPanel({
|
||||
uris,
|
||||
hiddenRows: ["folderPicker", "location", "keyword", "name"],
|
||||
})
|
||||
.catch(ex => Cu.reportError(ex));
|
||||
} else {
|
||||
let selectItemDesc = document.getElementById("selectItemDescription");
|
||||
let itemsCountLabel = document.getElementById("itemsCountText");
|
||||
|
|
|
@ -54,7 +54,7 @@ add_task(async function() {
|
|||
|
||||
// Init panel.
|
||||
let node = await PlacesUIUtils.promiseNodeLikeFromFetchInfo(bm);
|
||||
gEditItemOverlay.initPanel({ node });
|
||||
await gEditItemOverlay.initPanel({ node });
|
||||
|
||||
// Add a tag.
|
||||
await promiseTagSelectorUpdated(() =>
|
||||
|
@ -99,7 +99,7 @@ add_task(async function() {
|
|||
});
|
||||
|
||||
// Init panel with multiple uris.
|
||||
gEditItemOverlay.initPanel({ uris: [TEST_URI, TEST_URI2] });
|
||||
await gEditItemOverlay.initPanel({ uris: [TEST_URI, TEST_URI2] });
|
||||
|
||||
// Add a tag to the first uri.
|
||||
await promiseTagSelectorUpdated(() =>
|
||||
|
@ -166,7 +166,7 @@ add_task(async function() {
|
|||
await checkTagsSelector([], []);
|
||||
|
||||
// Init panel with a nsIURI entry.
|
||||
gEditItemOverlay.initPanel({ uris: [TEST_URI] });
|
||||
await gEditItemOverlay.initPanel({ uris: [TEST_URI] });
|
||||
|
||||
// Add a tag.
|
||||
await promiseTagSelectorUpdated(() =>
|
||||
|
@ -201,7 +201,7 @@ add_task(async function() {
|
|||
await checkTagsSelector([], []);
|
||||
|
||||
// Init panel with multiple nsIURI entries.
|
||||
gEditItemOverlay.initPanel({ uris: [TEST_URI, TEST_URI2] });
|
||||
await gEditItemOverlay.initPanel({ uris: [TEST_URI, TEST_URI2] });
|
||||
|
||||
// Add a tag to the first entry.
|
||||
await promiseTagSelectorUpdated(() =>
|
||||
|
|
|
@ -2438,6 +2438,13 @@ bool JsepSessionImpl::CheckNegotiationNeeded() const {
|
|||
}
|
||||
|
||||
size_t level = transceiver->GetLevel();
|
||||
if (NS_WARN_IF(mCurrentLocalDescription->GetMediaSectionCount() <= level) ||
|
||||
NS_WARN_IF(mCurrentRemoteDescription->GetMediaSectionCount() <=
|
||||
level)) {
|
||||
MOZ_ASSERT(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
const SdpMediaSection& local =
|
||||
mCurrentLocalDescription->GetMediaSection(level);
|
||||
const SdpMediaSection& remote =
|
||||
|
|
|
@ -16,9 +16,6 @@ var provider = {
|
|||
}
|
||||
return this.QueryInterface(iid);
|
||||
},
|
||||
lockFactory: function eventsink_lockf(lock) {
|
||||
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
||||
},
|
||||
startup() {},
|
||||
watch() {},
|
||||
shutdown() {},
|
||||
|
|
|
@ -16,9 +16,6 @@ var provider = {
|
|||
}
|
||||
return this.QueryInterface(iid);
|
||||
},
|
||||
lockFactory: function eventsink_lockf(lock) {
|
||||
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
||||
},
|
||||
startup() {},
|
||||
watch() {},
|
||||
shutdown() {},
|
||||
|
|
|
@ -72,6 +72,8 @@ class CommandEncoder final : public ObjectBase, public ChildOf<Device> {
|
|||
nsTArray<WeakPtr<CanvasContext>> mTargetContexts;
|
||||
|
||||
public:
|
||||
const auto& GetDevice() const { return mParent; };
|
||||
|
||||
void EndComputePass(ffi::WGPUComputePass& aPass, ErrorResult& aRv);
|
||||
void EndRenderPass(ffi::WGPURenderPass& aPass, ErrorResult& aRv);
|
||||
|
||||
|
|
|
@ -63,15 +63,15 @@ void ComputePassEncoder::SetPipeline(const ComputePipeline& aPipeline) {
|
|||
void ComputePassEncoder::DispatchWorkgroups(uint32_t x, uint32_t y,
|
||||
uint32_t z) {
|
||||
if (mValid) {
|
||||
ffi::wgpu_compute_pass_dispatch(mPass, x, y, z);
|
||||
ffi::wgpu_compute_pass_dispatch_workgroups(mPass, x, y, z);
|
||||
}
|
||||
}
|
||||
|
||||
void ComputePassEncoder::DispatchWorkgroupsIndirect(
|
||||
const Buffer& aIndirectBuffer, uint64_t aIndirectOffset) {
|
||||
if (mValid) {
|
||||
ffi::wgpu_compute_pass_dispatch_indirect(mPass, aIndirectBuffer.mId,
|
||||
aIndirectOffset);
|
||||
ffi::wgpu_compute_pass_dispatch_workgroups_indirect(
|
||||
mPass, aIndirectBuffer.mId, aIndirectOffset);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -94,6 +94,12 @@ void Device::CleanupUnregisteredInParent() {
|
|||
mValid = false;
|
||||
}
|
||||
|
||||
void Device::GenerateError(const nsCString& aMessage) {
|
||||
if (mBridge->CanSend()) {
|
||||
mBridge->SendGenerateError(mId, aMessage);
|
||||
}
|
||||
}
|
||||
|
||||
void Device::GetLabel(nsAString& aValue) const { aValue = mLabel; }
|
||||
void Device::SetLabel(const nsAString& aLabel) { mLabel = aLabel; }
|
||||
|
||||
|
|
|
@ -111,6 +111,8 @@ class Device final : public DOMEventTargetHelper, public SupportsWeakPtr {
|
|||
|
||||
void CleanupUnregisteredInParent();
|
||||
|
||||
void GenerateError(const nsCString& aMessage);
|
||||
|
||||
private:
|
||||
~Device();
|
||||
void Cleanup();
|
||||
|
|
|
@ -52,7 +52,7 @@ ffi::WGPUColor ConvertColor(const dom::GPUColorDict& aColor) {
|
|||
}
|
||||
|
||||
ffi::WGPURenderPass* BeginRenderPass(
|
||||
RawId aEncoderId, const dom::GPURenderPassDescriptor& aDesc) {
|
||||
CommandEncoder* const aParent, const dom::GPURenderPassDescriptor& aDesc) {
|
||||
ffi::WGPURenderPassDescriptor desc = {};
|
||||
|
||||
ffi::WGPURenderPassDepthStencilAttachment dsDesc = {};
|
||||
|
@ -84,6 +84,12 @@ ffi::WGPURenderPass* BeginRenderPass(
|
|||
desc.depth_stencil_attachment = &dsDesc;
|
||||
}
|
||||
|
||||
if (aDesc.mColorAttachments.Length() > WGPUMAX_COLOR_TARGETS) {
|
||||
aParent->GetDevice()->GenerateError(nsLiteralCString(
|
||||
"Too many color attachments in GPURenderPassDescriptor"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::array<ffi::WGPURenderPassColorAttachment, WGPUMAX_COLOR_TARGETS>
|
||||
colorDescs = {};
|
||||
desc.color_attachments = colorDescs.data();
|
||||
|
@ -124,12 +130,17 @@ ffi::WGPURenderPass* BeginRenderPass(
|
|||
}
|
||||
}
|
||||
|
||||
return ffi::wgpu_command_encoder_begin_render_pass(aEncoderId, &desc);
|
||||
return ffi::wgpu_command_encoder_begin_render_pass(aParent->mId, &desc);
|
||||
}
|
||||
|
||||
RenderPassEncoder::RenderPassEncoder(CommandEncoder* const aParent,
|
||||
const dom::GPURenderPassDescriptor& aDesc)
|
||||
: ChildOf(aParent), mPass(BeginRenderPass(aParent->mId, aDesc)) {
|
||||
: ChildOf(aParent), mPass(BeginRenderPass(aParent, aDesc)) {
|
||||
if (!mPass) {
|
||||
mValid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& at : aDesc.mColorAttachments) {
|
||||
mUsedTextureViews.AppendElement(at.mView);
|
||||
}
|
||||
|
|
|
@ -71,6 +71,7 @@ parent:
|
|||
async DevicePushErrorScope(RawId selfId);
|
||||
async DevicePopErrorScope(RawId selfId) returns (MaybeScopedError maybeError);
|
||||
|
||||
async GenerateError(RawId deviceId, nsCString message);
|
||||
child:
|
||||
async DeviceUncapturedError(RawId aDeviceId, nsCString message);
|
||||
async DropAction(ByteBuf buf);
|
||||
|
|
|
@ -239,21 +239,25 @@ bool WebGPUParent::ForwardError(RawId aDeviceId, ErrorBuffer& aError) {
|
|||
return false;
|
||||
}
|
||||
|
||||
ReportError(aDeviceId, cString.value());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void WebGPUParent::ReportError(RawId aDeviceId, const nsCString& aMessage) {
|
||||
// find the appropriate error scope
|
||||
const auto& lookup = mErrorScopeMap.find(aDeviceId);
|
||||
if (lookup != mErrorScopeMap.end() && !lookup->second.mStack.IsEmpty()) {
|
||||
auto& last = lookup->second.mStack.LastElement();
|
||||
if (last.isNothing()) {
|
||||
last.emplace(ScopedError{false, cString.value()});
|
||||
last.emplace(ScopedError{false, aMessage});
|
||||
}
|
||||
} else {
|
||||
// fall back to the uncaptured error handler
|
||||
if (!SendDeviceUncapturedError(aDeviceId, cString.value())) {
|
||||
if (!SendDeviceUncapturedError(aDeviceId, aMessage)) {
|
||||
NS_ERROR("Unable to SendError");
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
ipc::IPCResult WebGPUParent::RecvInstanceRequestAdapter(
|
||||
|
@ -927,4 +931,10 @@ ipc::IPCResult WebGPUParent::RecvDevicePopErrorScope(
|
|||
return IPC_OK();
|
||||
}
|
||||
|
||||
ipc::IPCResult WebGPUParent::RecvGenerateError(RawId aDeviceId,
|
||||
const nsCString& aMessage) {
|
||||
ReportError(aDeviceId, aMessage);
|
||||
return IPC_OK();
|
||||
}
|
||||
|
||||
} // namespace mozilla::webgpu
|
||||
|
|
|
@ -88,6 +88,7 @@ class WebGPUParent final : public PWebGPUParent {
|
|||
ipc::IPCResult RecvDevicePushErrorScope(RawId aSelfId);
|
||||
ipc::IPCResult RecvDevicePopErrorScope(
|
||||
RawId aSelfId, DevicePopErrorScopeResolver&& aResolver);
|
||||
ipc::IPCResult RecvGenerateError(RawId aDeviceId, const nsCString& message);
|
||||
|
||||
ipc::IPCResult GetFrontBufferSnapshot(IProtocol* aProtocol,
|
||||
const CompositableHandle& aHandle,
|
||||
|
@ -100,6 +101,7 @@ class WebGPUParent final : public PWebGPUParent {
|
|||
virtual ~WebGPUParent();
|
||||
void MaintainDevices();
|
||||
bool ForwardError(RawId aDeviceID, ErrorBuffer& aError);
|
||||
void ReportError(RawId aDeviceId, const nsCString& message);
|
||||
|
||||
UniquePtr<ffi::WGPUGlobal> mContext;
|
||||
base::RepeatingTimer<WebGPUParent> mTimer;
|
||||
|
|
|
@ -197,8 +197,7 @@ HTMLEditor::HTMLEditor()
|
|||
mDefaultParagraphSeparator(
|
||||
StaticPrefs::editor_use_div_for_default_newlines()
|
||||
? ParagraphSeparator::div
|
||||
: ParagraphSeparator::br) {
|
||||
}
|
||||
: ParagraphSeparator::br) {}
|
||||
|
||||
HTMLEditor::~HTMLEditor() {
|
||||
// Collect the data of `beforeinput` event only when it's enabled because
|
||||
|
|
|
@ -12,9 +12,6 @@ var factory = {
|
|||
createInstance() {
|
||||
throw new Error("There is no history service");
|
||||
},
|
||||
lockFactory() {
|
||||
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
||||
},
|
||||
QueryInterface: ChromeUtils.generateQI(["nsIFactory"]),
|
||||
};
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ default = []
|
|||
[dependencies.wgc]
|
||||
package = "wgpu-core"
|
||||
git = "https://github.com/gfx-rs/wgpu"
|
||||
rev = "0b61a191"
|
||||
rev = "b51fd851"
|
||||
#Note: "replay" shouldn't ideally be needed,
|
||||
# but it allows us to serialize everything across IPC.
|
||||
features = ["replay", "trace", "serial-pass"]
|
||||
|
@ -25,12 +25,12 @@ features = ["replay", "trace", "serial-pass"]
|
|||
[dependencies.wgt]
|
||||
package = "wgpu-types"
|
||||
git = "https://github.com/gfx-rs/wgpu"
|
||||
rev = "0b61a191"
|
||||
rev = "b51fd851"
|
||||
|
||||
[dependencies.wgh]
|
||||
package = "wgpu-hal"
|
||||
git = "https://github.com/gfx-rs/wgpu"
|
||||
rev = "0b61a191"
|
||||
rev = "b51fd851"
|
||||
|
||||
[dependencies]
|
||||
bincode = "1"
|
||||
|
|
|
@ -1092,28 +1092,6 @@ pub unsafe extern "C" fn wgpu_command_encoder_insert_debug_marker(
|
|||
*bb = make_byte_buf(&action);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn wgpu_render_pass_set_index_buffer(
|
||||
pass: &mut wgc::command::RenderPass,
|
||||
buffer: wgc::id::BufferId,
|
||||
index_format: wgt::IndexFormat,
|
||||
offset: wgt::BufferAddress,
|
||||
size: Option<wgt::BufferSize>,
|
||||
) {
|
||||
pass.set_index_buffer(buffer, index_format, offset, size);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn wgpu_render_bundle_set_index_buffer(
|
||||
encoder: &mut wgc::command::RenderBundleEncoder,
|
||||
buffer: wgc::id::BufferId,
|
||||
index_format: wgt::IndexFormat,
|
||||
offset: wgt::BufferAddress,
|
||||
size: Option<wgt::BufferSize>,
|
||||
) {
|
||||
encoder.set_index_buffer(buffer, index_format, offset, size);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn wgpu_queue_write_buffer(
|
||||
dst: id::BufferId,
|
||||
|
|
|
@ -3827,6 +3827,10 @@ pub struct CommandBufferBuilder {
|
|||
|
||||
/// List of render tasks that depend on the task that will be created for this builder
|
||||
pub extra_dependencies: Vec<RenderTaskId>,
|
||||
|
||||
/// If true, this represents a surface that wraps a sub-graph, and we need to look
|
||||
/// higher in the surface hierarchy for the resolve surface.
|
||||
pub wraps_sub_graph: bool,
|
||||
}
|
||||
|
||||
impl CommandBufferBuilder {
|
||||
|
@ -3836,6 +3840,7 @@ impl CommandBufferBuilder {
|
|||
establishes_sub_graph: false,
|
||||
resolve_source: None,
|
||||
extra_dependencies: Vec::new(),
|
||||
wraps_sub_graph: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3850,6 +3855,7 @@ impl CommandBufferBuilder {
|
|||
establishes_sub_graph: false,
|
||||
resolve_source: None,
|
||||
extra_dependencies: Vec::new(),
|
||||
wraps_sub_graph: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3858,6 +3864,7 @@ impl CommandBufferBuilder {
|
|||
render_task_id: RenderTaskId,
|
||||
establishes_sub_graph: bool,
|
||||
root_task_id: Option<RenderTaskId>,
|
||||
wraps_sub_graph: bool,
|
||||
) -> Self {
|
||||
CommandBufferBuilder {
|
||||
kind: CommandBufferBuilderKind::Simple {
|
||||
|
@ -3867,6 +3874,7 @@ impl CommandBufferBuilder {
|
|||
establishes_sub_graph,
|
||||
resolve_source: None,
|
||||
extra_dependencies: Vec::new(),
|
||||
wraps_sub_graph,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -457,42 +457,30 @@ struct TilePreUpdateContext {
|
|||
frame_id: FrameId,
|
||||
}
|
||||
|
||||
// Immutable context passed to picture cache tiles during post_update
|
||||
struct TilePostUpdateContext<'a> {
|
||||
// Immutable context passed to picture cache tiles during update_dirty_and_valid_rects
|
||||
struct TileUpdateDirtyContext<'a> {
|
||||
/// Maps from picture cache coords -> world space coords.
|
||||
pic_to_world_mapper: SpaceMapper<PicturePixel, WorldPixel>,
|
||||
|
||||
/// Global scale factor from world -> device pixels.
|
||||
global_device_pixel_scale: DevicePixelScale,
|
||||
|
||||
/// The local clip rect (in picture space) of the entire picture cache
|
||||
local_clip_rect: PictureRect,
|
||||
|
||||
/// The calculated backdrop information for this cache instance.
|
||||
backdrop: Option<BackdropInfo>,
|
||||
|
||||
/// Information about opacity bindings from the picture cache.
|
||||
opacity_bindings: &'a FastHashMap<PropertyBindingId, OpacityBindingInfo>,
|
||||
|
||||
/// Information about color bindings from the picture cache.
|
||||
color_bindings: &'a FastHashMap<PropertyBindingId, ColorBindingInfo>,
|
||||
|
||||
/// Current size in device pixels of tiles for this cache
|
||||
current_tile_size: DeviceIntSize,
|
||||
|
||||
/// The local rect of the overall picture cache
|
||||
local_rect: PictureRect,
|
||||
|
||||
/// Pre-allocated z-id to assign to tiles during post_update.
|
||||
z_id: ZBufferId,
|
||||
|
||||
/// If true, the scale factor of the root transform for this picture
|
||||
/// cache changed, so we need to invalidate the tile and re-render.
|
||||
invalidate_all: bool,
|
||||
}
|
||||
|
||||
// Mutable state passed to picture cache tiles during post_update
|
||||
struct TilePostUpdateState<'a> {
|
||||
// Mutable state passed to picture cache tiles during update_dirty_and_valid_rects
|
||||
struct TileUpdateDirtyState<'a> {
|
||||
/// Allow access to the texture cache for requesting tiles
|
||||
resource_cache: &'a mut ResourceCache,
|
||||
|
||||
|
@ -506,6 +494,30 @@ struct TilePostUpdateState<'a> {
|
|||
spatial_node_comparer: &'a mut SpatialNodeComparer,
|
||||
}
|
||||
|
||||
// Immutable context passed to picture cache tiles during post_update
|
||||
struct TilePostUpdateContext {
|
||||
/// The local clip rect (in picture space) of the entire picture cache
|
||||
local_clip_rect: PictureRect,
|
||||
|
||||
/// The calculated backdrop information for this cache instance.
|
||||
backdrop: Option<BackdropInfo>,
|
||||
|
||||
/// Current size in device pixels of tiles for this cache
|
||||
current_tile_size: DeviceIntSize,
|
||||
|
||||
/// Pre-allocated z-id to assign to tiles during post_update.
|
||||
z_id: ZBufferId,
|
||||
}
|
||||
|
||||
// Mutable state passed to picture cache tiles during post_update
|
||||
struct TilePostUpdateState<'a> {
|
||||
/// Allow access to the texture cache for requesting tiles
|
||||
resource_cache: &'a mut ResourceCache,
|
||||
|
||||
/// Current configuration and setup for compositing all the picture cache tiles in renderer.
|
||||
composite_state: &'a mut CompositeState,
|
||||
}
|
||||
|
||||
/// Information about the dependencies of a single primitive instance.
|
||||
struct PrimitiveDependencyInfo {
|
||||
/// Unique content identifier of the primitive.
|
||||
|
@ -723,6 +735,8 @@ pub enum InvalidationReason {
|
|||
ValidRectChanged,
|
||||
// The overall scale of the picture cache changed
|
||||
ScaleChanged,
|
||||
// The content of the sampling surface changed
|
||||
SurfaceContentChanged,
|
||||
}
|
||||
|
||||
/// Information about a cached tile.
|
||||
|
@ -823,8 +837,8 @@ impl Tile {
|
|||
/// Check if the content of the previous and current tile descriptors match
|
||||
fn update_dirty_rects(
|
||||
&mut self,
|
||||
ctx: &TilePostUpdateContext,
|
||||
state: &mut TilePostUpdateState,
|
||||
ctx: &TileUpdateDirtyContext,
|
||||
state: &mut TileUpdateDirtyState,
|
||||
invalidation_reason: &mut Option<InvalidationReason>,
|
||||
frame_context: &FrameVisibilityContext,
|
||||
) -> PictureRect {
|
||||
|
@ -857,8 +871,8 @@ impl Tile {
|
|||
/// later by changing how ComparableVec is used.
|
||||
fn update_content_validity(
|
||||
&mut self,
|
||||
ctx: &TilePostUpdateContext,
|
||||
state: &mut TilePostUpdateState,
|
||||
ctx: &TileUpdateDirtyContext,
|
||||
state: &mut TileUpdateDirtyState,
|
||||
frame_context: &FrameVisibilityContext,
|
||||
) {
|
||||
// Check if the contents of the primitives, clips, and
|
||||
|
@ -1061,12 +1075,12 @@ impl Tile {
|
|||
|
||||
/// Called during tile cache instance post_update. Allows invalidation and dirty
|
||||
/// rect calculation after primitive dependencies have been updated.
|
||||
fn post_update(
|
||||
fn update_dirty_and_valid_rects(
|
||||
&mut self,
|
||||
ctx: &TilePostUpdateContext,
|
||||
state: &mut TilePostUpdateState,
|
||||
ctx: &TileUpdateDirtyContext,
|
||||
state: &mut TileUpdateDirtyState,
|
||||
frame_context: &FrameVisibilityContext,
|
||||
) -> bool {
|
||||
) {
|
||||
// Register the frame id of this tile with the spatial node comparer, to ensure
|
||||
// that it doesn't GC any spatial nodes from the comparer that are referenced
|
||||
// by this tile. Must be done before we early exit below, so that we retain
|
||||
|
@ -1077,7 +1091,7 @@ impl Tile {
|
|||
// so don't want to invalidate, merge, split etc. The tile won't need to be drawn
|
||||
// (and thus updated / invalidated) until it is on screen again.
|
||||
if !self.is_visible {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the overall valid rect for this tile.
|
||||
|
@ -1116,6 +1130,22 @@ impl Tile {
|
|||
|
||||
// Invalidate the tile based on the content changing.
|
||||
self.update_content_validity(ctx, state, frame_context);
|
||||
}
|
||||
|
||||
/// Called during tile cache instance post_update. Allows invalidation and dirty
|
||||
/// rect calculation after primitive dependencies have been updated.
|
||||
fn post_update(
|
||||
&mut self,
|
||||
ctx: &TilePostUpdateContext,
|
||||
state: &mut TilePostUpdateState,
|
||||
frame_context: &FrameVisibilityContext,
|
||||
) {
|
||||
// If tile is not visible, just early out from here - we don't update dependencies
|
||||
// so don't want to invalidate, merge, split etc. The tile won't need to be drawn
|
||||
// (and thus updated / invalidated) until it is on screen again.
|
||||
if !self.is_visible {
|
||||
return;
|
||||
}
|
||||
|
||||
// If there are no primitives there is no need to draw or cache it.
|
||||
// Bug 1719232 - The final device valid rect does not always describe a non-empty
|
||||
|
@ -1132,7 +1162,7 @@ impl Tile {
|
|||
}
|
||||
|
||||
self.is_visible = false;
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this tile can be considered opaque. Opacity state must be updated only
|
||||
|
@ -1278,8 +1308,6 @@ impl Tile {
|
|||
|
||||
// Store the current surface backing info for use during batching.
|
||||
self.surface = Some(surface);
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1892,6 +1920,9 @@ pub struct TileCacheInstance {
|
|||
current_raster_scale: f32,
|
||||
/// Depth of off-screen surfaces that are currently pushed during dependency updates
|
||||
current_surface_traversal_depth: usize,
|
||||
/// A list of extra dirty invalidation tests that can only be checked once we
|
||||
/// know the dirty rect of all tiles
|
||||
deferred_dirty_tests: Vec<DeferredDirtyTest>,
|
||||
}
|
||||
|
||||
enum SurfacePromotionResult {
|
||||
|
@ -1950,6 +1981,7 @@ impl TileCacheInstance {
|
|||
invalidate_all_tiles: true,
|
||||
current_raster_scale: 1.0,
|
||||
current_surface_traversal_depth: 0,
|
||||
deferred_dirty_tests: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2068,6 +2100,7 @@ impl TileCacheInstance {
|
|||
self.surface_index = surface_index;
|
||||
self.local_rect = pic_rect;
|
||||
self.local_clip_rect = PictureRect::max_rect();
|
||||
self.deferred_dirty_tests.clear();
|
||||
|
||||
for sub_slice in &mut self.sub_slices {
|
||||
sub_slice.reset();
|
||||
|
@ -3297,6 +3330,13 @@ impl TileCacheInstance {
|
|||
tile.sub_graphs.push((pic_coverage_rect, surface_info.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
// For backdrop-filter, we need to check if any of the dirty rects
|
||||
// in tiles that are affected by the filter primitive are dirty.
|
||||
self.deferred_dirty_tests.push(DeferredDirtyTest {
|
||||
tile_rect: TileRect::new(p0, p1),
|
||||
prim_rect: pic_coverage_rect,
|
||||
});
|
||||
}
|
||||
PrimitiveInstanceKind::LineDecoration { .. } |
|
||||
PrimitiveInstanceKind::NormalBorder { .. } |
|
||||
|
@ -3549,20 +3589,16 @@ impl TileCacheInstance {
|
|||
frame_context.spatial_tree,
|
||||
);
|
||||
|
||||
let mut ctx = TilePostUpdateContext {
|
||||
let ctx = TileUpdateDirtyContext {
|
||||
pic_to_world_mapper,
|
||||
global_device_pixel_scale: frame_context.global_device_pixel_scale,
|
||||
local_clip_rect: self.local_clip_rect,
|
||||
backdrop: None,
|
||||
opacity_bindings: &self.opacity_bindings,
|
||||
color_bindings: &self.color_bindings,
|
||||
current_tile_size: self.current_tile_size,
|
||||
local_rect: self.local_rect,
|
||||
z_id: ZBufferId::invalid(),
|
||||
invalidate_all: self.invalidate_all_tiles,
|
||||
};
|
||||
|
||||
let mut state = TilePostUpdateState {
|
||||
let mut state = TileUpdateDirtyState {
|
||||
resource_cache: frame_state.resource_cache,
|
||||
composite_state: frame_state.composite_state,
|
||||
compare_cache: &mut self.compare_cache,
|
||||
|
@ -3571,6 +3607,60 @@ impl TileCacheInstance {
|
|||
|
||||
// Step through each tile and invalidate if the dependencies have changed. Determine
|
||||
// the current opacity setting and whether it's changed.
|
||||
for sub_slice in &mut self.sub_slices {
|
||||
for tile in sub_slice.tiles.values_mut() {
|
||||
tile.update_dirty_and_valid_rects(&ctx, &mut state, frame_context);
|
||||
}
|
||||
}
|
||||
|
||||
// Process any deferred dirty checks
|
||||
for sub_slice in &mut self.sub_slices {
|
||||
for dirty_test in self.deferred_dirty_tests.drain(..) {
|
||||
// Calculate the total dirty rect from all tiles that this primitive affects
|
||||
let mut total_dirty_rect = PictureRect::zero();
|
||||
|
||||
for y in dirty_test.tile_rect.min.y .. dirty_test.tile_rect.max.y {
|
||||
for x in dirty_test.tile_rect.min.x .. dirty_test.tile_rect.max.x {
|
||||
let key = TileOffset::new(x, y);
|
||||
let tile = sub_slice.tiles.get_mut(&key).expect("bug: no tile");
|
||||
total_dirty_rect = total_dirty_rect.union(&tile.local_dirty_rect);
|
||||
}
|
||||
}
|
||||
|
||||
// If that dirty rect intersects with the local rect of the primitive
|
||||
// being checked, invalidate that region in all of the affected tiles.
|
||||
// TODO(gw): This is somewhat conservative, we could be more clever
|
||||
// here and avoid invalidating every tile when this changes.
|
||||
// We could also store the dirty rect only when the prim
|
||||
// is encountered, so that we don't invalidate if something
|
||||
// *after* the query in the rendering order affects invalidation.
|
||||
if total_dirty_rect.intersects(&dirty_test.prim_rect) {
|
||||
for y in dirty_test.tile_rect.min.y .. dirty_test.tile_rect.max.y {
|
||||
for x in dirty_test.tile_rect.min.x .. dirty_test.tile_rect.max.x {
|
||||
let key = TileOffset::new(x, y);
|
||||
let tile = sub_slice.tiles.get_mut(&key).expect("bug: no tile");
|
||||
tile.invalidate(
|
||||
Some(dirty_test.prim_rect),
|
||||
InvalidationReason::SurfaceContentChanged,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut ctx = TilePostUpdateContext {
|
||||
local_clip_rect: self.local_clip_rect,
|
||||
backdrop: None,
|
||||
current_tile_size: self.current_tile_size,
|
||||
z_id: ZBufferId::invalid(),
|
||||
};
|
||||
|
||||
let mut state = TilePostUpdateState {
|
||||
resource_cache: frame_state.resource_cache,
|
||||
composite_state: frame_state.composite_state,
|
||||
};
|
||||
|
||||
for (i, sub_slice) in self.sub_slices.iter_mut().enumerate().rev() {
|
||||
// The backdrop is only relevant for the first sub-slice
|
||||
if i == 0 {
|
||||
|
@ -4302,6 +4392,8 @@ bitflags! {
|
|||
/// This picture establishes a sub-graph, which affects how SurfaceBuilder will
|
||||
/// set up dependencies in the render task graph
|
||||
const IS_SUB_GRAPH = 1 << 1;
|
||||
/// This picture wraps a sub-graph, but is not the resolve source itself
|
||||
const WRAPS_SUB_GRAPH = 1 << 2;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5037,6 +5129,7 @@ impl PicturePrimitive {
|
|||
frame_state.surface_builder.push_surface(
|
||||
surface_index,
|
||||
false,
|
||||
false,
|
||||
surface_local_dirty_rect,
|
||||
descriptor,
|
||||
frame_state.surfaces,
|
||||
|
@ -5472,10 +5565,12 @@ impl PicturePrimitive {
|
|||
}
|
||||
|
||||
let is_sub_graph = self.flags.contains(PictureFlags::IS_SUB_GRAPH);
|
||||
let wraps_sub_graph = self.flags.contains(PictureFlags::WRAPS_SUB_GRAPH);
|
||||
|
||||
frame_state.surface_builder.push_surface(
|
||||
raster_config.surface_index,
|
||||
is_sub_graph,
|
||||
wraps_sub_graph,
|
||||
surface_rects.clipped_local,
|
||||
surface_descriptor,
|
||||
frame_state.surfaces,
|
||||
|
@ -5834,7 +5929,8 @@ impl PicturePrimitive {
|
|||
// context.
|
||||
let allow_snapping =
|
||||
parent_allows_snapping &&
|
||||
!self.flags.contains(PictureFlags::IS_SUB_GRAPH);
|
||||
!self.flags.contains(PictureFlags::IS_SUB_GRAPH) &&
|
||||
!self.flags.contains(PictureFlags::WRAPS_SUB_GRAPH);
|
||||
|
||||
// Check if there is perspective or if an SVG filter is applied, and thus whether a new
|
||||
// rasterization root should be established.
|
||||
|
@ -6157,6 +6253,16 @@ impl ImageDependency {
|
|||
};
|
||||
}
|
||||
|
||||
/// In some cases, we need to know the dirty rect of all tiles in order
|
||||
/// to correctly invalidate a primitive.
|
||||
#[derive(Debug)]
|
||||
struct DeferredDirtyTest {
|
||||
/// The tile rect that the primitive being checked affects
|
||||
tile_rect: TileRect,
|
||||
/// The picture-cache local rect of the primitive being checked
|
||||
prim_rect: PictureRect,
|
||||
}
|
||||
|
||||
/// A helper struct to compare a primitive and all its sub-dependencies.
|
||||
struct PrimitiveComparer<'a> {
|
||||
clip_comparer: CompareHelper<'a, ItemUid>,
|
||||
|
|
|
@ -637,8 +637,8 @@ impl<'a> SceneBuilder<'a> {
|
|||
|
||||
// If we're a surface, use that spatial node, otherwise the parent
|
||||
let spatial_node_index = match pic.composite_mode {
|
||||
Some(_) => pic.spatial_node_index,
|
||||
None => parent_spatial_node_index.expect("bug: no parent"),
|
||||
Some(_) if !pic.flags.contains(PictureFlags::WRAPS_SUB_GRAPH) => pic.spatial_node_index,
|
||||
Some(_) | None => parent_spatial_node_index.expect("bug: no parent"),
|
||||
};
|
||||
|
||||
(
|
||||
|
@ -2271,6 +2271,13 @@ impl<'a> SceneBuilder<'a> {
|
|||
None => true,
|
||||
};
|
||||
|
||||
let pic_flags = if stacking_context.flags.contains(StackingContextFlags::WRAPS_BACKDROP_FILTER) {
|
||||
assert!(stacking_context.blit_reason.contains(BlitReason::CLIP));
|
||||
PictureFlags::WRAPS_SUB_GRAPH
|
||||
} else {
|
||||
PictureFlags::empty()
|
||||
};
|
||||
|
||||
let mut source = match stacking_context.context_3d {
|
||||
// TODO(gw): For now, as soon as this picture is in
|
||||
// a 3D context, we draw it to an intermediate
|
||||
|
@ -2295,7 +2302,7 @@ impl<'a> SceneBuilder<'a> {
|
|||
stacking_context.prim_list,
|
||||
stacking_context.spatial_node_index,
|
||||
stacking_context.raster_space,
|
||||
PictureFlags::empty(),
|
||||
pic_flags,
|
||||
))
|
||||
);
|
||||
|
||||
|
@ -2339,7 +2346,7 @@ impl<'a> SceneBuilder<'a> {
|
|||
stacking_context.prim_list,
|
||||
stacking_context.spatial_node_index,
|
||||
stacking_context.raster_space,
|
||||
PictureFlags::empty(),
|
||||
pic_flags,
|
||||
))
|
||||
);
|
||||
|
||||
|
|
|
@ -245,6 +245,7 @@ impl SurfaceBuilder {
|
|||
&mut self,
|
||||
surface_index: SurfaceIndex,
|
||||
is_sub_graph: bool,
|
||||
wraps_sub_graph: bool,
|
||||
clipping_rect: PictureRect,
|
||||
descriptor: SurfaceDescriptor,
|
||||
surfaces: &mut [SurfaceInfo],
|
||||
|
@ -266,6 +267,7 @@ impl SurfaceBuilder {
|
|||
render_task_id,
|
||||
is_sub_graph,
|
||||
None,
|
||||
wraps_sub_graph,
|
||||
)
|
||||
}
|
||||
SurfaceDescriptorKind::Chained { render_task_id, root_task_id } => {
|
||||
|
@ -273,6 +275,7 @@ impl SurfaceBuilder {
|
|||
render_task_id,
|
||||
is_sub_graph,
|
||||
Some(root_task_id),
|
||||
wraps_sub_graph,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
@ -407,7 +410,26 @@ impl SurfaceBuilder {
|
|||
// (c) Make the old parent surface tasks input dependencies of the resolve target
|
||||
// (d) Make the sub-graph output an input dependency of the new task(s).
|
||||
|
||||
match self.builder_stack.last_mut().unwrap().kind {
|
||||
// If this surface wraps a sub-graph, we need to look up in the hierarchy to find where
|
||||
// the resource source actually is. This may happen in cases where a clip-mask applies to
|
||||
// a backdrop-filter and child content in the associated stacking context.
|
||||
let sub_graph_source_index = self.builder_stack
|
||||
.iter()
|
||||
.rposition(|builder| {
|
||||
!builder.wraps_sub_graph
|
||||
})
|
||||
.expect("bug: no parent that is not a sub graph wrapper");
|
||||
|
||||
let sub_graph_parent = if sub_graph_source_index == self.builder_stack.len() - 1 {
|
||||
None
|
||||
} else {
|
||||
match self.builder_stack.last().unwrap().kind {
|
||||
CommandBufferBuilderKind::Simple { render_task_id, .. } => Some(render_task_id),
|
||||
_ => panic!("bug: should not occur on tiled surfaces"),
|
||||
}
|
||||
};
|
||||
|
||||
match self.builder_stack[sub_graph_source_index].kind {
|
||||
CommandBufferBuilderKind::Tiled { ref mut tiles } => {
|
||||
let keys: Vec<TileKey> = tiles.keys().cloned().collect();
|
||||
|
||||
|
@ -454,7 +476,7 @@ impl SurfaceBuilder {
|
|||
|
||||
// Make the output of the sub-graph a dependency of the new replacement tile task
|
||||
rg_builder.add_dependency(
|
||||
new_task_id,
|
||||
sub_graph_parent.unwrap_or(new_task_id),
|
||||
child_root_task_id.unwrap_or(child_render_task_id),
|
||||
);
|
||||
|
||||
|
@ -518,7 +540,7 @@ impl SurfaceBuilder {
|
|||
|
||||
// Make the output of the sub-graph a dependency of the new replacement tile task
|
||||
rg_builder.add_dependency(
|
||||
new_task_id,
|
||||
sub_graph_parent.unwrap_or(new_task_id),
|
||||
child_root_task_id.unwrap_or(child_render_task_id),
|
||||
);
|
||||
|
||||
|
|
|
@ -947,6 +947,10 @@ bitflags! {
|
|||
/// If true, this stacking context is a blend container than contains
|
||||
/// mix-blend-mode children (and should thus be isolated).
|
||||
const IS_BLEND_CONTAINER = 1 << 0;
|
||||
/// If true, this stacking context is a wrapper around a backdrop-filter (e.g. for
|
||||
/// a clip-mask). This is needed to allow the correct selection of a backdrop root
|
||||
/// since a clip-mask stacking context creates a parent surface.
|
||||
const WRAPS_BACKDROP_FILTER = 1 << 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
root:
|
||||
items:
|
||||
- type: rect
|
||||
bounds: 0 0 256 256
|
||||
color: red
|
||||
- type: rect
|
||||
bounds: 64 64 128 128
|
||||
color: cyan
|
|
@ -0,0 +1,25 @@
|
|||
# Ensure that a backdrop-filter enclosed by a stacking context with
|
||||
# a clip-mask can select the correct backdrop root
|
||||
---
|
||||
root:
|
||||
items:
|
||||
- type: clip
|
||||
bounds: [64, 64, 128, 128]
|
||||
id: 2
|
||||
image-mask:
|
||||
image: solid-color(255,255,255,255,128,128)
|
||||
rect: [64, 64, 128, 128]
|
||||
repeat: false
|
||||
- type: stacking-context
|
||||
backdrop-root: true
|
||||
items:
|
||||
- type: rect
|
||||
bounds: 0 0 256 256
|
||||
color: red
|
||||
- type: stacking-context
|
||||
clip-node: 2
|
||||
wraps-backdrop-filter: true
|
||||
items:
|
||||
- type: backdrop-filter
|
||||
bounds: 0 0 256 256
|
||||
filters: invert(1)
|
|
@ -64,6 +64,7 @@ fuzzy(1,1) platform(linux,mac) == svg-filter-drop-shadow-perspective.yaml svg-fi
|
|||
#== backdrop-filter-basic.yaml backdrop-filter-basic-ref.yaml
|
||||
#platform(linux,mac) == backdrop-filter-perspective.yaml backdrop-filter-perspective.png
|
||||
#== backdrop-filter-on-child-surface.yaml backdrop-filter-on-child-surface-ref.yaml
|
||||
#== backdrop-filter-clip-mask.yaml backdrop-filter-clip-mask-ref.yaml
|
||||
#platform(linux,mac) == backdrop-filter-across-tiles.yaml backdrop-filter-across-tiles.png
|
||||
#platform(linux,mac) == backdrop-filter-chain.yaml backdrop-filter-chain.png
|
||||
#platform(linux,mac) == backdrop-filter-overlap.yaml backdrop-filter-overlap.png
|
||||
|
|
|
@ -2025,6 +2025,7 @@ impl YamlFrameReader {
|
|||
.as_raster_space()
|
||||
.unwrap_or(RasterSpace::Screen);
|
||||
let is_blend_container = yaml["blend-container"].as_bool().unwrap_or(false);
|
||||
let wraps_backdrop_filter = yaml["wraps-backdrop-filter"].as_bool().unwrap_or(false);
|
||||
|
||||
if is_root {
|
||||
if let Some(vector) = yaml["scroll-offset"].as_vector() {
|
||||
|
@ -2045,6 +2046,7 @@ impl YamlFrameReader {
|
|||
|
||||
let mut flags = StackingContextFlags::empty();
|
||||
flags.set(StackingContextFlags::IS_BLEND_CONTAINER, is_blend_container);
|
||||
flags.set(StackingContextFlags::WRAPS_BACKDROP_FILTER, wraps_backdrop_filter);
|
||||
|
||||
dl.push_stacking_context(
|
||||
bounds.min,
|
||||
|
|
|
@ -3442,8 +3442,7 @@ void nsIFrame::BuildDisplayListForStackingContext(
|
|||
// to remove any existing content that isn't wrapped in the blend container,
|
||||
// and then we need to build content infront/behind the blend container
|
||||
// to get correct positioning during merging.
|
||||
if ((aBuilder->ContainsBlendMode()) &&
|
||||
aBuilder->IsRetainingDisplayList()) {
|
||||
if ((aBuilder->ContainsBlendMode()) && aBuilder->IsRetainingDisplayList()) {
|
||||
if (aBuilder->IsPartialUpdate()) {
|
||||
aBuilder->SetPartialBuildFailed(true);
|
||||
} else {
|
||||
|
@ -3551,7 +3550,7 @@ void nsIFrame::BuildDisplayListForStackingContext(
|
|||
: containerItemASR;
|
||||
/* List now emptied, so add the new list to the top. */
|
||||
resultList.AppendNewToTop<nsDisplayMasksAndClipPaths>(
|
||||
aBuilder, this, &resultList, maskASR);
|
||||
aBuilder, this, &resultList, maskASR, usingBackdropFilter);
|
||||
createdContainer = true;
|
||||
}
|
||||
|
||||
|
|
|
@ -7857,8 +7857,9 @@ static void ComputeMaskGeometry(PaintFramesParams& aParams) {
|
|||
|
||||
nsDisplayMasksAndClipPaths::nsDisplayMasksAndClipPaths(
|
||||
nsDisplayListBuilder* aBuilder, nsIFrame* aFrame, nsDisplayList* aList,
|
||||
const ActiveScrolledRoot* aActiveScrolledRoot)
|
||||
: nsDisplayEffectsBase(aBuilder, aFrame, aList, aActiveScrolledRoot, true) {
|
||||
const ActiveScrolledRoot* aActiveScrolledRoot, bool aWrapsBackdropFilter)
|
||||
: nsDisplayEffectsBase(aBuilder, aFrame, aList, aActiveScrolledRoot, true),
|
||||
mWrapsBackdropFilter(aWrapsBackdropFilter) {
|
||||
MOZ_COUNT_CTOR(nsDisplayMasksAndClipPaths);
|
||||
|
||||
nsPresContext* presContext = mFrame->PresContext();
|
||||
|
@ -8199,6 +8200,9 @@ bool nsDisplayMasksAndClipPaths::CreateWebRenderCommands(
|
|||
wr::StackingContextParams params;
|
||||
params.clip = wr::WrStackingContextClip::ClipId(*clip);
|
||||
params.opacity = opacity.ptrOr(nullptr);
|
||||
if (mWrapsBackdropFilter) {
|
||||
params.flags |= wr::StackingContextFlags::WRAPS_BACKDROP_FILTER;
|
||||
}
|
||||
layer.emplace(aSc, GetActiveScrolledRoot(), mFrame, this, aBuilder, params,
|
||||
bounds);
|
||||
sc = layer.ptr();
|
||||
|
@ -8327,6 +8331,15 @@ void nsDisplayBackdropFilters::Paint(nsDisplayListBuilder* aBuilder,
|
|||
mFrame->PresContext()->AppUnitsPerDevPixel());
|
||||
}
|
||||
|
||||
nsRect nsDisplayBackdropFilters::GetBounds(nsDisplayListBuilder* aBuilder,
|
||||
bool* aSnap) const {
|
||||
nsRect childBounds = nsDisplayWrapList::GetBounds(aBuilder, aSnap);
|
||||
|
||||
*aSnap = false;
|
||||
|
||||
return mBackdropRect.Union(childBounds);
|
||||
}
|
||||
|
||||
/* static */
|
||||
nsDisplayFilters::nsDisplayFilters(nsDisplayListBuilder* aBuilder,
|
||||
nsIFrame* aFrame, nsDisplayList* aList,
|
||||
|
|
|
@ -5863,11 +5863,13 @@ class nsDisplayMasksAndClipPaths : public nsDisplayEffectsBase {
|
|||
public:
|
||||
nsDisplayMasksAndClipPaths(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame,
|
||||
nsDisplayList* aList,
|
||||
const ActiveScrolledRoot* aActiveScrolledRoot);
|
||||
const ActiveScrolledRoot* aActiveScrolledRoot,
|
||||
bool aWrapsBackdropFilter);
|
||||
nsDisplayMasksAndClipPaths(nsDisplayListBuilder* aBuilder,
|
||||
const nsDisplayMasksAndClipPaths& aOther)
|
||||
: nsDisplayEffectsBase(aBuilder, aOther),
|
||||
mDestRects(aOther.mDestRects.Clone()) {
|
||||
mDestRects(aOther.mDestRects.Clone()),
|
||||
mWrapsBackdropFilter(aOther.mWrapsBackdropFilter) {
|
||||
MOZ_COUNT_CTOR(nsDisplayMasksAndClipPaths);
|
||||
}
|
||||
|
||||
|
@ -5932,6 +5934,7 @@ class nsDisplayMasksAndClipPaths : public nsDisplayEffectsBase {
|
|||
NS_DISPLAY_ALLOW_CLONING()
|
||||
|
||||
nsTArray<nsRect> mDestRects;
|
||||
bool mWrapsBackdropFilter;
|
||||
};
|
||||
|
||||
class nsDisplayBackdropFilters : public nsDisplayWrapList {
|
||||
|
@ -5962,6 +5965,8 @@ class nsDisplayBackdropFilters : public nsDisplayWrapList {
|
|||
|
||||
bool CreatesStackingContextHelper() override { return true; }
|
||||
|
||||
nsRect GetBounds(nsDisplayListBuilder* aBuilder, bool* aSnap) const override;
|
||||
|
||||
private:
|
||||
RefPtr<ComputedStyle> mStyle;
|
||||
nsRect mBackdropRect;
|
||||
|
|
|
@ -113,7 +113,7 @@ class SPSCRingBufferBase {
|
|||
|
||||
mData = std::make_unique<T[]>(StorageCapacity());
|
||||
|
||||
std::atomic_thread_fence(std::memory_order::memory_order_seq_cst);
|
||||
std::atomic_thread_fence(std::memory_order_seq_cst);
|
||||
}
|
||||
/**
|
||||
* Push `aCount` zero or default constructed elements in the array.
|
||||
|
@ -152,8 +152,8 @@ class SPSCRingBufferBase {
|
|||
AssertCorrectThread(mProducerId);
|
||||
#endif
|
||||
|
||||
int rdIdx = mReadIndex.load(std::memory_order::memory_order_acquire);
|
||||
int wrIdx = mWriteIndex.load(std::memory_order::memory_order_relaxed);
|
||||
int rdIdx = mReadIndex.load(std::memory_order_acquire);
|
||||
int wrIdx = mWriteIndex.load(std::memory_order_relaxed);
|
||||
|
||||
if (IsFull(rdIdx, wrIdx)) {
|
||||
return 0;
|
||||
|
@ -178,7 +178,7 @@ class SPSCRingBufferBase {
|
|||
}
|
||||
|
||||
mWriteIndex.store(IncrementIndex(wrIdx, toWrite),
|
||||
std::memory_order::memory_order_release);
|
||||
std::memory_order_release);
|
||||
|
||||
return toWrite;
|
||||
}
|
||||
|
@ -198,8 +198,8 @@ class SPSCRingBufferBase {
|
|||
AssertCorrectThread(mConsumerId);
|
||||
#endif
|
||||
|
||||
int wrIdx = mWriteIndex.load(std::memory_order::memory_order_acquire);
|
||||
int rdIdx = mReadIndex.load(std::memory_order::memory_order_relaxed);
|
||||
int wrIdx = mWriteIndex.load(std::memory_order_acquire);
|
||||
int rdIdx = mReadIndex.load(std::memory_order_relaxed);
|
||||
|
||||
if (IsEmpty(rdIdx, wrIdx)) {
|
||||
return 0;
|
||||
|
@ -217,8 +217,7 @@ class SPSCRingBufferBase {
|
|||
secondPart);
|
||||
}
|
||||
|
||||
mReadIndex.store(IncrementIndex(rdIdx, toRead),
|
||||
std::memory_order::memory_order_release);
|
||||
mReadIndex.store(IncrementIndex(rdIdx, toRead), std::memory_order_release);
|
||||
|
||||
return toRead;
|
||||
}
|
||||
|
@ -233,9 +232,8 @@ class SPSCRingBufferBase {
|
|||
* @return The number of available elements for reading.
|
||||
*/
|
||||
int AvailableRead() const {
|
||||
return AvailableReadInternal(
|
||||
mReadIndex.load(std::memory_order::memory_order_relaxed),
|
||||
mWriteIndex.load(std::memory_order::memory_order_relaxed));
|
||||
return AvailableReadInternal(mReadIndex.load(std::memory_order_relaxed),
|
||||
mWriteIndex.load(std::memory_order_relaxed));
|
||||
}
|
||||
/**
|
||||
* Get the number of available elements for writing.
|
||||
|
@ -248,9 +246,8 @@ class SPSCRingBufferBase {
|
|||
* @return The number of empty slots in the buffer, available for writing.
|
||||
*/
|
||||
int AvailableWrite() const {
|
||||
return AvailableWriteInternal(
|
||||
mReadIndex.load(std::memory_order::memory_order_relaxed),
|
||||
mWriteIndex.load(std::memory_order::memory_order_relaxed));
|
||||
return AvailableWriteInternal(mReadIndex.load(std::memory_order_relaxed),
|
||||
mWriteIndex.load(std::memory_order_relaxed));
|
||||
}
|
||||
/**
|
||||
* Get the total Capacity, for this ring buffer.
|
||||
|
|
|
@ -17,18 +17,20 @@ const { debug, warn } = GeckoViewUtils.initLogging("ContentDelegateParent");
|
|||
|
||||
class ContentDelegateParent extends GeckoViewActorParent {
|
||||
async receiveMessage(aMsg) {
|
||||
debug`receiveMessage: ${aMsg.name} ${aMsg}`;
|
||||
debug`receiveMessage: ${aMsg.name}`;
|
||||
|
||||
switch (aMsg.name) {
|
||||
case "GeckoView:DOMFullscreenExit": {
|
||||
this.window.windowUtils.remoteFrameFullscreenReverted();
|
||||
break;
|
||||
return null;
|
||||
}
|
||||
|
||||
case "GeckoView:DOMFullscreenRequest": {
|
||||
this.window.windowUtils.remoteFrameFullscreenChanged(this.browser);
|
||||
break;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return super.receiveMessage(aMsg);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,11 +11,13 @@ const { XPCOMUtils } = ChromeUtils.import(
|
|||
const { GeckoViewUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/GeckoViewUtils.jsm"
|
||||
);
|
||||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
FormLikeFactory: "resource://gre/modules/FormLikeFactory.jsm",
|
||||
GeckoViewAutofill: "resource://gre/modules/GeckoViewAutofill.jsm",
|
||||
WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.jsm",
|
||||
LoginManagerChild: "resource://gre/modules/LoginManagerChild.jsm",
|
||||
});
|
||||
|
||||
const EXPORTED_SYMBOLS = ["GeckoViewAutoFillChild"];
|
||||
|
@ -24,61 +26,352 @@ class GeckoViewAutoFillChild extends GeckoViewActorChild {
|
|||
constructor() {
|
||||
super();
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "_autofill", function() {
|
||||
return new GeckoViewAutofill(this.eventDispatcher);
|
||||
});
|
||||
this._autofillElements = undefined;
|
||||
this._autofillInfos = undefined;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
handleEvent(aEvent) {
|
||||
debug`handleEvent: ${aEvent.type}`;
|
||||
const { contentWindow } = this;
|
||||
|
||||
switch (aEvent.type) {
|
||||
case "DOMFormHasPassword": {
|
||||
this._autofill.addElement(
|
||||
FormLikeFactory.createFromForm(aEvent.composedTarget)
|
||||
);
|
||||
this.addElement(FormLikeFactory.createFromForm(aEvent.composedTarget));
|
||||
break;
|
||||
}
|
||||
case "DOMInputPasswordAdded": {
|
||||
const input = aEvent.composedTarget;
|
||||
if (!input.form) {
|
||||
this._autofill.addElement(FormLikeFactory.createFromField(input));
|
||||
this.addElement(FormLikeFactory.createFromField(input));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "focusin": {
|
||||
if (contentWindow.HTMLInputElement.isInstance(aEvent.composedTarget)) {
|
||||
this._autofill.onFocus(aEvent.composedTarget);
|
||||
if (
|
||||
this.contentWindow.HTMLInputElement.isInstance(aEvent.composedTarget)
|
||||
) {
|
||||
this.onFocus(aEvent.composedTarget);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "focusout": {
|
||||
if (contentWindow.HTMLInputElement.isInstance(aEvent.composedTarget)) {
|
||||
this._autofill.onFocus(null);
|
||||
if (
|
||||
this.contentWindow.HTMLInputElement.isInstance(aEvent.composedTarget)
|
||||
) {
|
||||
this.onFocus(null);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "pagehide": {
|
||||
if (aEvent.target === contentWindow.top.document) {
|
||||
this._autofill.clearElements();
|
||||
if (aEvent.target === this.document) {
|
||||
this.clearElements(this.browsingContext);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "pageshow": {
|
||||
if (aEvent.target === contentWindow.top.document && aEvent.persisted) {
|
||||
this._autofill.scanDocument(aEvent.target);
|
||||
if (aEvent.target === this.document) {
|
||||
this.scanDocument(this.document);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "PasswordManager:ShowDoorhanger": {
|
||||
const { form: formLike } = aEvent.detail;
|
||||
this._autofill.commitAutofill(formLike);
|
||||
this.commitAutofill(formLike);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an auto-fillable form and send the relevant details of the form
|
||||
* to Java. Multiple calls within a short time period for the same form are
|
||||
* coalesced, so that, e.g., if multiple inputs are added to a form in
|
||||
* succession, we will only perform one processing pass. Note that for inputs
|
||||
* without forms, FormLikeFactory treats the document as the "form", but
|
||||
* there is no difference in how we process them.
|
||||
*
|
||||
* @param aFormLike A FormLike object produced by FormLikeFactory.
|
||||
*/
|
||||
async addElement(aFormLike) {
|
||||
debug`Adding auto-fill ${aFormLike.rootElement.tagName}`;
|
||||
|
||||
const window = aFormLike.rootElement.ownerGlobal;
|
||||
// Get password field to get better form data via LoginManagerChild.
|
||||
let passwordField;
|
||||
for (const field of aFormLike.elements) {
|
||||
if (
|
||||
ChromeUtils.getClassName(field) === "HTMLInputElement" &&
|
||||
field.type == "password"
|
||||
) {
|
||||
passwordField = field;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const [usernameField] = LoginManagerChild.forWindow(
|
||||
window
|
||||
).getUserNameAndPasswordFields(passwordField || aFormLike.elements[0]);
|
||||
|
||||
const focusedElement = aFormLike.rootElement.ownerDocument.activeElement;
|
||||
let sendFocusEvent = aFormLike.rootElement === focusedElement;
|
||||
|
||||
const rootInfo = this._getInfo(
|
||||
aFormLike.rootElement,
|
||||
null,
|
||||
undefined,
|
||||
null
|
||||
);
|
||||
|
||||
rootInfo.rootUuid = rootInfo.uuid;
|
||||
rootInfo.children = aFormLike.elements
|
||||
.filter(
|
||||
element =>
|
||||
element.type != "hidden" &&
|
||||
(!usernameField ||
|
||||
element.type != "text" ||
|
||||
element == usernameField ||
|
||||
(element.getAutocompleteInfo() &&
|
||||
element.getAutocompleteInfo().fieldName == "email"))
|
||||
)
|
||||
.map(element => {
|
||||
sendFocusEvent |= element === focusedElement;
|
||||
return this._getInfo(
|
||||
element,
|
||||
rootInfo.uuid,
|
||||
rootInfo.uuid,
|
||||
usernameField
|
||||
);
|
||||
});
|
||||
|
||||
try {
|
||||
// We don't await here so that we can send a focus event immediately
|
||||
// after this as the app might not know which element is focused.
|
||||
const responsePromise = this.sendQuery("Add", {
|
||||
node: rootInfo,
|
||||
});
|
||||
|
||||
if (sendFocusEvent) {
|
||||
// We might have missed sending a focus event for the active element.
|
||||
this.onFocus(aFormLike.ownerDocument.activeElement);
|
||||
}
|
||||
|
||||
const responses = await responsePromise;
|
||||
// `responses` is an object with global IDs as keys.
|
||||
debug`Performing auto-fill ${Object.keys(responses)}`;
|
||||
|
||||
const AUTOFILL_STATE = "autofill";
|
||||
const winUtils = window.windowUtils;
|
||||
|
||||
for (const uuid in responses) {
|
||||
const entry =
|
||||
this._autofillElements && this._autofillElements.get(uuid);
|
||||
const element = entry && entry.get();
|
||||
const value = responses[uuid] || "";
|
||||
|
||||
if (
|
||||
window.HTMLInputElement.isInstance(element) &&
|
||||
!element.disabled &&
|
||||
element.parentElement
|
||||
) {
|
||||
element.setUserInput(value);
|
||||
if (winUtils && element.value === value) {
|
||||
// Add highlighting for autofilled fields.
|
||||
winUtils.addManuallyManagedState(element, AUTOFILL_STATE);
|
||||
|
||||
// Remove highlighting when the field is changed.
|
||||
element.addEventListener(
|
||||
"input",
|
||||
_ => winUtils.removeManuallyManagedState(element, AUTOFILL_STATE),
|
||||
{ mozSystemGroup: true, once: true }
|
||||
);
|
||||
}
|
||||
} else if (element) {
|
||||
warn`Don't know how to auto-fill ${element.tagName}`;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
warn`Cannot perform autofill ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
_getInfo(aElement, aParent, aRoot, aUsernameField) {
|
||||
if (!this._autofillInfos) {
|
||||
this._autofillInfos = new WeakMap();
|
||||
this._autofillElements = new Map();
|
||||
}
|
||||
|
||||
let info = this._autofillInfos.get(aElement);
|
||||
if (info) {
|
||||
return info;
|
||||
}
|
||||
|
||||
const window = aElement.ownerGlobal;
|
||||
const bounds = aElement.getBoundingClientRect();
|
||||
const isInputElement = window.HTMLInputElement.isInstance(aElement);
|
||||
|
||||
info = {
|
||||
isInputElement,
|
||||
uuid: Services.uuid
|
||||
.generateUUID()
|
||||
.toString()
|
||||
.slice(1, -1), // discard the surrounding curly braces
|
||||
parentUuid: aParent,
|
||||
rootUuid: aRoot,
|
||||
tag: aElement.tagName,
|
||||
type: isInputElement ? aElement.type : null,
|
||||
value: isInputElement ? aElement.value : null,
|
||||
editable:
|
||||
isInputElement &&
|
||||
[
|
||||
"color",
|
||||
"date",
|
||||
"datetime-local",
|
||||
"email",
|
||||
"month",
|
||||
"number",
|
||||
"password",
|
||||
"range",
|
||||
"search",
|
||||
"tel",
|
||||
"text",
|
||||
"time",
|
||||
"url",
|
||||
"week",
|
||||
].includes(aElement.type),
|
||||
disabled: isInputElement ? aElement.disabled : null,
|
||||
attributes: Object.assign(
|
||||
{},
|
||||
...Array.from(aElement.attributes)
|
||||
.filter(attr => attr.localName !== "value")
|
||||
.map(attr => ({ [attr.localName]: attr.value }))
|
||||
),
|
||||
origin: aElement.ownerDocument.location.origin,
|
||||
autofillhint: "",
|
||||
bounds: {
|
||||
left: bounds.left,
|
||||
top: bounds.top,
|
||||
right: bounds.right,
|
||||
bottom: bounds.bottom,
|
||||
},
|
||||
};
|
||||
|
||||
if (aElement === aUsernameField) {
|
||||
info.autofillhint = "username"; // AUTOFILL.HINT.USERNAME
|
||||
} else if (isInputElement) {
|
||||
// Using autocomplete attribute if it is email.
|
||||
const autocompleteInfo = aElement.getAutocompleteInfo();
|
||||
if (autocompleteInfo) {
|
||||
const autocompleteAttr = autocompleteInfo.fieldName;
|
||||
if (autocompleteAttr == "email") {
|
||||
info.type = "email";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._autofillInfos.set(aElement, info);
|
||||
this._autofillElements.set(info.uuid, Cu.getWeakReference(aElement));
|
||||
return info;
|
||||
}
|
||||
|
||||
_updateInfoValues(aElements) {
|
||||
if (!this._autofillInfos) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const updated = [];
|
||||
for (const element of aElements) {
|
||||
const info = this._autofillInfos.get(element);
|
||||
|
||||
if (!info?.isInputElement || info.value === element.value) {
|
||||
continue;
|
||||
}
|
||||
debug`Updating value ${info.value} to ${element.value}`;
|
||||
|
||||
info.value = element.value;
|
||||
this._autofillInfos.set(element, info);
|
||||
updated.push(info);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an auto-fillable field is focused or blurred.
|
||||
*
|
||||
* @param aTarget Focused element, or null if an element has lost focus.
|
||||
*/
|
||||
onFocus(aTarget) {
|
||||
debug`Auto-fill focus on ${aTarget && aTarget.tagName}`;
|
||||
|
||||
const info =
|
||||
aTarget && this._autofillInfos && this._autofillInfos.get(aTarget);
|
||||
if (!aTarget || info) {
|
||||
this.sendAsyncMessage("Focus", {
|
||||
node: info,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
commitAutofill(aFormLike) {
|
||||
if (!aFormLike) {
|
||||
throw new Error("null-form on autofill commit");
|
||||
}
|
||||
|
||||
debug`Committing auto-fill for ${aFormLike.rootElement.tagName}`;
|
||||
|
||||
const updatedNodeInfos = this._updateInfoValues([
|
||||
aFormLike.rootElement,
|
||||
...aFormLike.elements,
|
||||
]);
|
||||
|
||||
for (const updatedInfo of updatedNodeInfos) {
|
||||
debug`Updating node ${updatedInfo}`;
|
||||
this.sendAsyncMessage("Update", {
|
||||
node: updatedInfo,
|
||||
});
|
||||
}
|
||||
|
||||
const info = this._getInfo(aFormLike.rootElement);
|
||||
if (info) {
|
||||
debug`Committing node ${info}`;
|
||||
this.sendAsyncMessage("Commit", {
|
||||
node: info,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all tracked auto-fill forms and notify Java.
|
||||
*/
|
||||
clearElements(browsingContext) {
|
||||
this._autofillInfos = undefined;
|
||||
this._autofillElements = undefined;
|
||||
|
||||
if (browsingContext === browsingContext.top) {
|
||||
this.sendAsyncMessage("Clear");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan for auto-fillable forms and add them if necessary. Called when a page
|
||||
* is navigated to through history, in which case we don't get our typical
|
||||
* "input added" notifications.
|
||||
*
|
||||
* @param aDoc Document to scan.
|
||||
*/
|
||||
scanDocument(aDoc) {
|
||||
// Add forms first; only check forms with password inputs.
|
||||
const inputs = aDoc.querySelectorAll("input[type=password]");
|
||||
let inputAdded = false;
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
if (inputs[i].form) {
|
||||
// Let addElement coalesce multiple calls for the same form.
|
||||
this.addElement(FormLikeFactory.createFromForm(inputs[i].form));
|
||||
} else if (!inputAdded) {
|
||||
// Treat inputs without forms as one unit, and process them only once.
|
||||
inputAdded = true;
|
||||
this.addElement(FormLikeFactory.createFromField(inputs[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { debug, warn } = GeckoViewAutoFillChild.initLogging("GeckoViewAutoFill");
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["GeckoViewAutoFillParent"];
|
||||
|
||||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
const { GeckoViewActorParent } = ChromeUtils.import(
|
||||
"resource://gre/modules/GeckoViewActorParent.jsm"
|
||||
);
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
gAutofillManager: "resource://gre/modules/GeckoViewAutofill.jsm",
|
||||
});
|
||||
|
||||
class GeckoViewAutoFillParent extends GeckoViewActorParent {
|
||||
constructor() {
|
||||
super();
|
||||
this.sessionId = Services.uuid
|
||||
.generateUUID()
|
||||
.toString()
|
||||
.slice(1, -1); // discard the surrounding curly braces
|
||||
}
|
||||
|
||||
get rootActor() {
|
||||
return this.browsingContext.top.currentWindowGlobal.getActor(
|
||||
"GeckoViewAutoFill"
|
||||
);
|
||||
}
|
||||
|
||||
get autofill() {
|
||||
return gAutofillManager.get(this.sessionId);
|
||||
}
|
||||
|
||||
add(node) {
|
||||
// We will start a new session if the current one does not exist.
|
||||
const autofill = gAutofillManager.ensure(
|
||||
this.sessionId,
|
||||
this.eventDispatcher
|
||||
);
|
||||
return autofill?.add(node);
|
||||
}
|
||||
|
||||
focus(node) {
|
||||
this.autofill?.focus(node);
|
||||
}
|
||||
|
||||
commit(node) {
|
||||
this.autofill?.commit(node);
|
||||
}
|
||||
|
||||
update(node) {
|
||||
this.autofill?.update(node);
|
||||
}
|
||||
|
||||
clear() {
|
||||
gAutofillManager.delete(this.sessionId);
|
||||
}
|
||||
|
||||
async receiveMessage(aMessage) {
|
||||
const { name } = aMessage;
|
||||
debug`receiveMessage ${name}`;
|
||||
|
||||
// We need to re-route all messages through the root actor to ensure that we
|
||||
// have a consistent sessionId for the entire browsingContext tree.
|
||||
switch (name) {
|
||||
case "Add": {
|
||||
return this.rootActor.add(aMessage.data.node);
|
||||
}
|
||||
case "Focus": {
|
||||
this.rootActor.focus(aMessage.data.node);
|
||||
break;
|
||||
}
|
||||
case "Update": {
|
||||
this.rootActor.update(aMessage.data.node);
|
||||
break;
|
||||
}
|
||||
case "Commit": {
|
||||
this.rootActor.commit(aMessage.data.node);
|
||||
break;
|
||||
}
|
||||
case "Clear": {
|
||||
if (this.browsingContext === this.browsingContext.top) {
|
||||
this.clear();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const { debug, warn } = GeckoViewAutoFillParent.initLogging(
|
||||
"GeckoViewAutoFill"
|
||||
);
|
|
@ -0,0 +1,125 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["GeckoViewPermissionChild"];
|
||||
|
||||
const { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
const { GeckoViewActorChild } = ChromeUtils.import(
|
||||
"resource://gre/modules/GeckoViewActorChild.jsm"
|
||||
);
|
||||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
E10SUtils: "resource://gre/modules/E10SUtils.jsm",
|
||||
});
|
||||
|
||||
const PERM_ACCESS_FINE_LOCATION = "android.permission.ACCESS_FINE_LOCATION";
|
||||
|
||||
class GeckoViewPermissionChild extends GeckoViewActorChild {
|
||||
getMediaPermission(aPermission) {
|
||||
return this.eventDispatcher.sendRequestForResult({
|
||||
type: "GeckoView:MediaPermission",
|
||||
...aPermission,
|
||||
});
|
||||
}
|
||||
|
||||
addCameraPermission() {
|
||||
return this.sendQuery("AddCameraPermission");
|
||||
}
|
||||
|
||||
getAppPermissions(aPermissions) {
|
||||
return this.sendQuery("GetAppPermissions", aPermissions);
|
||||
}
|
||||
|
||||
mediaRecordingStatusChanged(aDevices) {
|
||||
return this.eventDispatcher.sendRequest({
|
||||
type: "GeckoView:MediaRecordingStatusChanged",
|
||||
devices: aDevices,
|
||||
});
|
||||
}
|
||||
|
||||
async promptPermission(aRequest) {
|
||||
// Only allow exactly one permission request here.
|
||||
const types = aRequest.types.QueryInterface(Ci.nsIArray);
|
||||
if (types.length !== 1) {
|
||||
return { allow: false };
|
||||
}
|
||||
|
||||
const perm = types.queryElementAt(0, Ci.nsIContentPermissionType);
|
||||
if (
|
||||
perm.type === "desktop-notification" &&
|
||||
!aRequest.hasValidTransientUserGestureActivation &&
|
||||
Services.prefs.getBoolPref(
|
||||
"dom.webnotifications.requireuserinteraction",
|
||||
true
|
||||
)
|
||||
) {
|
||||
// We need user interaction and don't have it.
|
||||
return { allow: false };
|
||||
}
|
||||
|
||||
const principal =
|
||||
perm.type === "storage-access"
|
||||
? aRequest.principal
|
||||
: aRequest.topLevelPrincipal;
|
||||
|
||||
let allowOrDeny;
|
||||
try {
|
||||
allowOrDeny = await this.eventDispatcher.sendRequestForResult({
|
||||
type: "GeckoView:ContentPermission",
|
||||
uri: principal.URI.displaySpec,
|
||||
thirdPartyOrigin: aRequest.principal.origin,
|
||||
principal: E10SUtils.serializePrincipal(principal),
|
||||
perm: perm.type,
|
||||
value: perm.capability,
|
||||
contextId: principal.originAttributes.geckoViewSessionContextId ?? null,
|
||||
privateMode: principal.privateBrowsingId != 0,
|
||||
});
|
||||
|
||||
if (allowOrDeny === Services.perms.ALLOW_ACTION) {
|
||||
// Ask for app permission after asking for content permission.
|
||||
if (perm.type === "geolocation") {
|
||||
const granted = await this.getAppPermissions([
|
||||
PERM_ACCESS_FINE_LOCATION,
|
||||
]);
|
||||
allowOrDeny = granted
|
||||
? Services.perms.ALLOW_ACTION
|
||||
: Services.perms.DENY_ACTION;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Cu.reportError("Permission error: " + error);
|
||||
allowOrDeny = Services.perms.DENY_ACTION;
|
||||
}
|
||||
|
||||
// Manually release the target request here to facilitate garbage collection.
|
||||
aRequest = undefined;
|
||||
|
||||
const allow = allowOrDeny === Services.perms.ALLOW_ACTION;
|
||||
|
||||
// The storage access code adds itself to the perm manager; no need for us to do it.
|
||||
if (perm.type === "storage-access") {
|
||||
if (allow) {
|
||||
return { allow, permission: { "storage-access": "allow" } };
|
||||
}
|
||||
return { allow };
|
||||
}
|
||||
|
||||
Services.perms.addFromPrincipal(
|
||||
principal,
|
||||
perm.type,
|
||||
allowOrDeny,
|
||||
Services.perms.EXPIRE_NEVER
|
||||
);
|
||||
|
||||
return { allow };
|
||||
}
|
||||
}
|
||||
|
||||
const { debug, warn } = GeckoViewPermissionChild.initLogging(
|
||||
"GeckoViewPermissionChild"
|
||||
);
|
|
@ -0,0 +1,75 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["GeckoViewPermissionParent"];
|
||||
|
||||
const { GeckoViewUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/GeckoViewUtils.jsm"
|
||||
);
|
||||
const { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const { GeckoViewActorParent } = ChromeUtils.import(
|
||||
"resource://gre/modules/GeckoViewActorParent.jsm"
|
||||
);
|
||||
|
||||
class GeckoViewPermissionParent extends GeckoViewActorParent {
|
||||
_appPermissions = {};
|
||||
|
||||
async getAppPermissions(aPermissions) {
|
||||
const perms = aPermissions.filter(perm => !this._appPermissions[perm]);
|
||||
if (!perms.length) {
|
||||
return Promise.resolve(/* granted */ true);
|
||||
}
|
||||
|
||||
const granted = await this.eventDispatcher.sendRequestForResult({
|
||||
type: "GeckoView:AndroidPermission",
|
||||
perms,
|
||||
});
|
||||
|
||||
if (granted) {
|
||||
for (const perm of perms) {
|
||||
this._appPermissions[perm] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return granted;
|
||||
}
|
||||
|
||||
addCameraPermission() {
|
||||
const principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
|
||||
this.browsingContext.top.currentWindowGlobal.documentPrincipal.origin
|
||||
);
|
||||
|
||||
// Although the lifetime is "session" it will be removed upon
|
||||
// use so it's more of a one-shot.
|
||||
Services.perms.addFromPrincipal(
|
||||
principal,
|
||||
"MediaManagerVideo",
|
||||
Services.perms.ALLOW_ACTION,
|
||||
Services.perms.EXPIRE_SESSION
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
receiveMessage(aMessage) {
|
||||
debug`receiveMessage ${aMessage.name}`;
|
||||
|
||||
switch (aMessage.name) {
|
||||
case "GetAppPermissions": {
|
||||
return this.getAppPermissions(aMessage.data);
|
||||
}
|
||||
case "AddCameraPermission": {
|
||||
return this.addCameraPermission();
|
||||
}
|
||||
}
|
||||
|
||||
return super.receiveMessage(aMessage);
|
||||
}
|
||||
}
|
||||
|
||||
const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewPermissionParent");
|
|
@ -0,0 +1,200 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["GeckoViewPermissionProcessChild"];
|
||||
|
||||
const { GeckoViewActorChild } = ChromeUtils.import(
|
||||
"resource://gre/modules/GeckoViewActorChild.jsm"
|
||||
);
|
||||
const { GeckoViewUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/GeckoViewUtils.jsm"
|
||||
);
|
||||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(
|
||||
this,
|
||||
"MediaManagerService",
|
||||
"@mozilla.org/mediaManagerService;1",
|
||||
"nsIMediaManagerService"
|
||||
);
|
||||
|
||||
const STATUS_RECORDING = "recording";
|
||||
const STATUS_INACTIVE = "inactive";
|
||||
const TYPE_CAMERA = "camera";
|
||||
const TYPE_MICROPHONE = "microphone";
|
||||
|
||||
class GeckoViewPermissionProcessChild extends JSProcessActorChild {
|
||||
getActor(window) {
|
||||
return window.windowGlobalChild.getActor("GeckoViewPermission");
|
||||
}
|
||||
|
||||
/* ---------- nsIObserver ---------- */
|
||||
async observe(aSubject, aTopic, aData) {
|
||||
switch (aTopic) {
|
||||
case "getUserMedia:ask-device-permission": {
|
||||
await this.sendQuery("AskDevicePermission", aData);
|
||||
Services.obs.notifyObservers(
|
||||
aSubject,
|
||||
"getUserMedia:got-device-permission"
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "getUserMedia:request": {
|
||||
const { callID } = aSubject;
|
||||
const allowedDevices = await this.handleMediaRequest(aSubject);
|
||||
Services.obs.notifyObservers(
|
||||
allowedDevices,
|
||||
allowedDevices
|
||||
? "getUserMedia:response:allow"
|
||||
: "getUserMedia:response:deny",
|
||||
callID
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "PeerConnection:request": {
|
||||
Services.obs.notifyObservers(
|
||||
null,
|
||||
"PeerConnection:response:allow",
|
||||
aSubject.callID
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "recording-device-events": {
|
||||
this.handleRecordingDeviceEvents(aSubject);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleRecordingDeviceEvents(aRequest) {
|
||||
aRequest.QueryInterface(Ci.nsIPropertyBag2);
|
||||
const contentWindow = aRequest.getProperty("window");
|
||||
const devices = [];
|
||||
|
||||
const getStatusString = function(activityStatus) {
|
||||
switch (activityStatus) {
|
||||
case MediaManagerService.STATE_CAPTURE_ENABLED:
|
||||
case MediaManagerService.STATE_CAPTURE_DISABLED:
|
||||
return STATUS_RECORDING;
|
||||
case MediaManagerService.STATE_NOCAPTURE:
|
||||
return STATUS_INACTIVE;
|
||||
default:
|
||||
throw new Error("Unexpected activityStatus value");
|
||||
}
|
||||
};
|
||||
|
||||
const hasCamera = {};
|
||||
const hasMicrophone = {};
|
||||
const screen = {};
|
||||
const window = {};
|
||||
const browser = {};
|
||||
const mediaDevices = {};
|
||||
MediaManagerService.mediaCaptureWindowState(
|
||||
contentWindow,
|
||||
hasCamera,
|
||||
hasMicrophone,
|
||||
screen,
|
||||
window,
|
||||
browser,
|
||||
mediaDevices
|
||||
);
|
||||
var cameraStatus = getStatusString(hasCamera.value);
|
||||
var microphoneStatus = getStatusString(hasMicrophone.value);
|
||||
if (hasCamera.value != MediaManagerService.STATE_NOCAPTURE) {
|
||||
devices.push({
|
||||
type: TYPE_CAMERA,
|
||||
status: cameraStatus,
|
||||
});
|
||||
}
|
||||
if (hasMicrophone.value != MediaManagerService.STATE_NOCAPTURE) {
|
||||
devices.push({
|
||||
type: TYPE_MICROPHONE,
|
||||
status: microphoneStatus,
|
||||
});
|
||||
}
|
||||
this.getActor(contentWindow).mediaRecordingStatusChanged(devices);
|
||||
}
|
||||
|
||||
async handleMediaRequest(aRequest) {
|
||||
const constraints = aRequest.getConstraints();
|
||||
const { devices, windowID } = aRequest;
|
||||
const window = Services.wm.getOuterWindowWithId(windowID);
|
||||
if (window.closed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Release the request first
|
||||
aRequest = undefined;
|
||||
|
||||
const sources = devices.map(device => {
|
||||
device = device.QueryInterface(Ci.nsIMediaDevice);
|
||||
return {
|
||||
type: device.type,
|
||||
id: device.rawId,
|
||||
rawId: device.rawId,
|
||||
name: device.rawName, // unfiltered device name to show to the user
|
||||
mediaSource: device.mediaSource,
|
||||
};
|
||||
});
|
||||
|
||||
if (
|
||||
constraints.video &&
|
||||
!sources.some(source => source.type === "videoinput")
|
||||
) {
|
||||
Cu.reportError("Media device error: no video source");
|
||||
return null;
|
||||
} else if (
|
||||
constraints.audio &&
|
||||
!sources.some(source => source.type === "audioinput")
|
||||
) {
|
||||
Cu.reportError("Media device error: no audio source");
|
||||
return null;
|
||||
}
|
||||
|
||||
const response = await this.getActor(window).getMediaPermission({
|
||||
uri: window.document.documentURI,
|
||||
video: constraints.video
|
||||
? sources.filter(source => source.type === "videoinput")
|
||||
: null,
|
||||
audio: constraints.audio
|
||||
? sources.filter(source => source.type === "audioinput")
|
||||
: null,
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
// Rejected.
|
||||
return null;
|
||||
}
|
||||
|
||||
const allowedDevices = Cc["@mozilla.org/array;1"].createInstance(
|
||||
Ci.nsIMutableArray
|
||||
);
|
||||
if (constraints.video) {
|
||||
const video = devices.find(device => response.video === device.rawId);
|
||||
if (!video) {
|
||||
Cu.reportError("Media device error: invalid video id");
|
||||
return null;
|
||||
}
|
||||
await this.getActor(window).addCameraPermission();
|
||||
allowedDevices.appendElement(video);
|
||||
}
|
||||
if (constraints.audio) {
|
||||
const audio = devices.find(device => response.audio === device.rawId);
|
||||
if (!audio) {
|
||||
Cu.reportError("Media device error: invalid audio id");
|
||||
return null;
|
||||
}
|
||||
allowedDevices.appendElement(audio);
|
||||
}
|
||||
return allowedDevices;
|
||||
}
|
||||
}
|
||||
|
||||
const { debug, warn } = GeckoViewUtils.initLogging(
|
||||
"GeckoViewPermissionProcessChild"
|
||||
);
|
|
@ -0,0 +1,62 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["GeckoViewPermissionProcessParent"];
|
||||
|
||||
const { GeckoViewUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/GeckoViewUtils.jsm"
|
||||
);
|
||||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
// See: http://developer.android.com/reference/android/Manifest.permission.html
|
||||
const PERM_CAMERA = "android.permission.CAMERA";
|
||||
const PERM_RECORD_AUDIO = "android.permission.RECORD_AUDIO";
|
||||
|
||||
class GeckoViewPermissionProcessParent extends JSProcessActorParent {
|
||||
async askDevicePermission(aType) {
|
||||
const perms = [];
|
||||
if (aType === "video" || aType === "all") {
|
||||
perms.push(PERM_CAMERA);
|
||||
}
|
||||
if (aType === "audio" || aType === "all") {
|
||||
perms.push(PERM_RECORD_AUDIO);
|
||||
}
|
||||
|
||||
try {
|
||||
// This looks sketchy but it's fine: Android needs the audio/video
|
||||
// permission to enumerate devices, which Gecko wants to do even before
|
||||
// we expose the list to web pages.
|
||||
// So really it doesn't matter what the request source is, because we
|
||||
// will, separately, issue a windowId-specific request to let the webpage
|
||||
// actually have access to the list of devices. So even if the source of
|
||||
// *this* request is incorrect, no actual harm will be done, as the user
|
||||
// will always receive the request with the right origin after this.
|
||||
const window = Services.wm.getMostRecentWindow("navigator:geckoview");
|
||||
const windowActor = window.browsingContext.currentWindowGlobal.getActor(
|
||||
"GeckoViewPermission"
|
||||
);
|
||||
await windowActor.getAppPermissions(perms);
|
||||
} catch (error) {
|
||||
// We can't really do anything here so we pretend we got the permission.
|
||||
warn`Error getting app permission: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
receiveMessage(aMessage) {
|
||||
debug`receiveMessage ${aMessage.name}`;
|
||||
|
||||
switch (aMessage.name) {
|
||||
case "AskDevicePermission": {
|
||||
return this.askDevicePermission(aMessage.data);
|
||||
}
|
||||
}
|
||||
|
||||
return super.receiveMessage(aMessage);
|
||||
}
|
||||
}
|
||||
|
||||
const { debug, warn } = GeckoViewUtils.initLogging(
|
||||
"GeckoViewPermissionProcess"
|
||||
);
|
|
@ -16,12 +16,12 @@ class GeckoViewPrompterChild extends GeckoViewActorChild {
|
|||
this._prompts = new Map();
|
||||
}
|
||||
|
||||
registerPrompt(prompt) {
|
||||
this._prompts.set(prompt.id, prompt);
|
||||
this.sendAsyncMessage("RegisterPrompt", {
|
||||
dismissPrompt(prompt) {
|
||||
this.eventDispatcher.sendRequest({
|
||||
type: "GeckoView:Prompt:Dismiss",
|
||||
id: prompt.id,
|
||||
promptType: prompt.getPromptType(),
|
||||
});
|
||||
this.unregisterPrompt(prompt);
|
||||
}
|
||||
|
||||
unregisterPrompt(prompt) {
|
||||
|
@ -31,10 +31,23 @@ class GeckoViewPrompterChild extends GeckoViewActorChild {
|
|||
});
|
||||
}
|
||||
|
||||
notifyPromptShow(prompt) {
|
||||
prompt(prompt, message) {
|
||||
this._prompts.set(prompt.id, prompt);
|
||||
this.sendAsyncMessage("RegisterPrompt", {
|
||||
id: prompt.id,
|
||||
promptType: prompt.getPromptType(),
|
||||
});
|
||||
// We intentionally do not await here as we want to fire NotifyPromptShow
|
||||
// immediately rather than waiting until the user accepts/dismisses the
|
||||
// prompt.
|
||||
const result = this.eventDispatcher.sendRequestForResult({
|
||||
type: "GeckoView:Prompt",
|
||||
prompt: message,
|
||||
});
|
||||
this.sendAsyncMessage("NotifyPromptShow", {
|
||||
id: prompt.id,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -76,3 +89,5 @@ class GeckoViewPrompterChild extends GeckoViewActorChild {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { debug, warn } = GeckoViewPrompterChild.initLogging("Prompter");
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["LoadURIDelegateParent"];
|
||||
|
||||
const { GeckoViewActorParent } = ChromeUtils.import(
|
||||
"resource://gre/modules/GeckoViewActorParent.jsm"
|
||||
);
|
||||
|
||||
// For this.eventDispatcher in the child
|
||||
class LoadURIDelegateParent extends GeckoViewActorParent {}
|
|
@ -0,0 +1,13 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["MediaControlDelegateParent"];
|
||||
|
||||
const { GeckoViewActorParent } = ChromeUtils.import(
|
||||
"resource://gre/modules/GeckoViewActorParent.jsm"
|
||||
);
|
||||
|
||||
// For this.eventDispatcher in the child
|
||||
class MediaControlDelegateParent extends GeckoViewActorParent {}
|
|
@ -0,0 +1,13 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["ScrollDelegateParent"];
|
||||
|
||||
const { GeckoViewActorParent } = ChromeUtils.import(
|
||||
"resource://gre/modules/GeckoViewActorParent.jsm"
|
||||
);
|
||||
|
||||
// For this.eventDispatcher in the child
|
||||
class ScrollDelegateParent extends GeckoViewActorParent {}
|
|
@ -21,7 +21,7 @@ class SelectionActionDelegateChild extends GeckoViewActorChild {
|
|||
constructor(aModuleName, aMessageManager) {
|
||||
super(aModuleName, aMessageManager);
|
||||
|
||||
this._seqNo = 0;
|
||||
this._actionCallback = () => {};
|
||||
this._isActive = false;
|
||||
this._previousMessage = "";
|
||||
|
||||
|
@ -133,6 +133,16 @@ class SelectionActionDelegateChild extends GeckoViewActorChild {
|
|||
},
|
||||
];
|
||||
|
||||
receiveMessage({ name, data }) {
|
||||
debug`receiveMessage ${name}`;
|
||||
|
||||
switch (name) {
|
||||
case "ExecuteSelectionAction": {
|
||||
this._actionCallback(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_performPaste() {
|
||||
this.handleEvent({ type: "pagehide" });
|
||||
this.docShell.doCommand("cmd_paste");
|
||||
|
@ -307,8 +317,6 @@ class SelectionActionDelegateChild extends GeckoViewActorChild {
|
|||
const password = this._isPasswordField(aEvent);
|
||||
|
||||
const msg = {
|
||||
type: "GeckoView:ShowSelectionAction",
|
||||
seqNo: this._seqNo,
|
||||
collapsed: aEvent.collapsed,
|
||||
editable: aEvent.selectionEditable,
|
||||
password,
|
||||
|
@ -333,29 +341,21 @@ class SelectionActionDelegateChild extends GeckoViewActorChild {
|
|||
return;
|
||||
}
|
||||
|
||||
msg.seqNo = ++this._seqNo;
|
||||
this._isActive = true;
|
||||
this._previousMessage = JSON.stringify(msg);
|
||||
|
||||
this.eventDispatcher.sendRequest(msg, {
|
||||
onSuccess: response => {
|
||||
if (response.seqNo !== this._seqNo) {
|
||||
// Stale action.
|
||||
warn`Stale response ${response.id}`;
|
||||
return;
|
||||
}
|
||||
const action = actions.find(action => action.id === response.id);
|
||||
if (action) {
|
||||
debug`Performing ${response.id}`;
|
||||
action.perform.call(this, aEvent, response);
|
||||
} else {
|
||||
warn`Invalid action ${response.id}`;
|
||||
}
|
||||
},
|
||||
onError: _ => {
|
||||
// Do nothing; we can get here if the delegate was just unregistered.
|
||||
},
|
||||
});
|
||||
// We can't just listen to the response of the message because we accept
|
||||
// multiple callbacks.
|
||||
this._actionCallback = data => {
|
||||
const action = actions.find(action => action.id === data.id);
|
||||
if (action) {
|
||||
debug`Performing ${data.id}`;
|
||||
action.perform.call(this, aEvent);
|
||||
} else {
|
||||
warn`Invalid action ${data.id}`;
|
||||
}
|
||||
};
|
||||
this.sendAsyncMessage("ShowSelectionAction", msg);
|
||||
} else if (
|
||||
[
|
||||
"invisibleselection",
|
||||
|
@ -367,7 +367,6 @@ class SelectionActionDelegateChild extends GeckoViewActorChild {
|
|||
if (!this._isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._isActive = false;
|
||||
|
||||
// Mark previous actions as stale. Don't do this for "invisibleselection"
|
||||
|
@ -377,10 +376,7 @@ class SelectionActionDelegateChild extends GeckoViewActorChild {
|
|||
this._seqNo++;
|
||||
}
|
||||
|
||||
this.eventDispatcher.sendRequest({
|
||||
type: "GeckoView:HideSelectionAction",
|
||||
reason,
|
||||
});
|
||||
this.sendAsyncMessage("HideSelectionAction", { reason });
|
||||
} else if (reason == "dragcaret") {
|
||||
// nothing for selection action
|
||||
} else {
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["SelectionActionDelegateParent"];
|
||||
|
||||
const { GeckoViewActorParent } = ChromeUtils.import(
|
||||
"resource://gre/modules/GeckoViewActorParent.jsm"
|
||||
);
|
||||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
class SelectionActionDelegateParent extends GeckoViewActorParent {
|
||||
respondTo = null;
|
||||
actionId = null;
|
||||
|
||||
get rootActor() {
|
||||
return this.browsingContext.top.currentWindowGlobal.getActor(
|
||||
"SelectionActionDelegate"
|
||||
);
|
||||
}
|
||||
|
||||
receiveMessage(aMessage) {
|
||||
const { data, name } = aMessage;
|
||||
switch (name) {
|
||||
case "ShowSelectionAction": {
|
||||
this.rootActor.showSelectionAction(this, data);
|
||||
break;
|
||||
}
|
||||
|
||||
case "HideSelectionAction": {
|
||||
this.rootActor.hideSelectionAction(this, data.reason);
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
super.receiveMessage(aMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hideSelectionAction(aRespondTo, reason) {
|
||||
// Mark previous actions as stale. Don't do this for "invisibleselection"
|
||||
// or "scroll" because previous actions should still be valid even after
|
||||
// these events occur.
|
||||
if (reason !== "invisibleselection" && reason !== "scroll") {
|
||||
this.actionId = null;
|
||||
}
|
||||
|
||||
this.eventDispatcher?.sendRequest({
|
||||
type: "GeckoView:HideSelectionAction",
|
||||
reason,
|
||||
});
|
||||
}
|
||||
|
||||
showSelectionAction(aRespondTo, aData) {
|
||||
this.actionId = Services.uuid.generateUUID().toString();
|
||||
this.respondTo = aRespondTo;
|
||||
|
||||
this.eventDispatcher?.sendRequest({
|
||||
type: "GeckoView:ShowSelectionAction",
|
||||
actionId: this.actionId,
|
||||
...aData,
|
||||
});
|
||||
}
|
||||
|
||||
executeSelectionAction(aData) {
|
||||
if (this.actionId === null || aData.actionId != this.actionId) {
|
||||
warn`Stale response ${aData.id} ${aData.actionId}`;
|
||||
return;
|
||||
}
|
||||
this.respondTo.sendAsyncMessage("ExecuteSelectionAction", aData);
|
||||
}
|
||||
}
|
||||
|
||||
const { debug, warn } = SelectionActionDelegateParent.initLogging(
|
||||
"SelectionActionDelegate"
|
||||
);
|
|
@ -9,17 +9,26 @@ FINAL_TARGET_FILES.actors += [
|
|||
"ContentDelegateChild.jsm",
|
||||
"ContentDelegateParent.jsm",
|
||||
"GeckoViewAutoFillChild.jsm",
|
||||
"GeckoViewAutoFillParent.jsm",
|
||||
"GeckoViewContentChild.jsm",
|
||||
"GeckoViewContentParent.jsm",
|
||||
"GeckoViewFormValidationChild.jsm",
|
||||
"GeckoViewPermissionChild.jsm",
|
||||
"GeckoViewPermissionParent.jsm",
|
||||
"GeckoViewPermissionProcessChild.jsm",
|
||||
"GeckoViewPermissionProcessParent.jsm",
|
||||
"GeckoViewPromptChild.jsm",
|
||||
"GeckoViewPrompterChild.jsm",
|
||||
"GeckoViewPrompterParent.jsm",
|
||||
"GeckoViewSettingsChild.jsm",
|
||||
"LoadURIDelegateChild.jsm",
|
||||
"LoadURIDelegateParent.jsm",
|
||||
"MediaControlDelegateChild.jsm",
|
||||
"MediaControlDelegateParent.jsm",
|
||||
"ProgressDelegateChild.jsm",
|
||||
"ProgressDelegateParent.jsm",
|
||||
"ScrollDelegateChild.jsm",
|
||||
"ScrollDelegateParent.jsm",
|
||||
"SelectionActionDelegateChild.jsm",
|
||||
"SelectionActionDelegateParent.jsm",
|
||||
]
|
||||
|
|
|
@ -468,7 +468,12 @@ class ModuleInfo {
|
|||
throw new Error(`No impl for message: ${aMessage.name}.`);
|
||||
}
|
||||
|
||||
this._impl.receiveMessage(aMessage);
|
||||
try {
|
||||
this._impl.receiveMessage(aMessage);
|
||||
} catch (error) {
|
||||
warn`this._impl.receiveMessage failed ${aMessage.name}`;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
onContentModuleLoaded() {
|
||||
|
@ -613,6 +618,9 @@ function startup() {
|
|||
onEnable: {
|
||||
actors: {
|
||||
ScrollDelegate: {
|
||||
parent: {
|
||||
moduleURI: "resource:///actors/ScrollDelegateParent.jsm",
|
||||
},
|
||||
child: {
|
||||
moduleURI: "resource:///actors/ScrollDelegateChild.jsm",
|
||||
events: {
|
||||
|
@ -627,8 +635,12 @@ function startup() {
|
|||
{
|
||||
name: "GeckoViewSelectionAction",
|
||||
onEnable: {
|
||||
resource: "resource://gre/modules/GeckoViewSelectionAction.jsm",
|
||||
actors: {
|
||||
SelectionActionDelegate: {
|
||||
parent: {
|
||||
moduleURI: "resource:///actors/SelectionActionDelegateParent.jsm",
|
||||
},
|
||||
child: {
|
||||
moduleURI: "resource:///actors/SelectionActionDelegateChild.jsm",
|
||||
events: {
|
||||
|
@ -679,6 +691,9 @@ function startup() {
|
|||
onInit: {
|
||||
actors: {
|
||||
GeckoViewAutoFill: {
|
||||
parent: {
|
||||
moduleURI: "resource:///actors/GeckoViewAutoFillParent.jsm",
|
||||
},
|
||||
child: {
|
||||
moduleURI: "resource:///actors/GeckoViewAutoFillChild.jsm",
|
||||
events: {
|
||||
|
@ -721,6 +736,9 @@ function startup() {
|
|||
resource: "resource://gre/modules/GeckoViewMediaControl.jsm",
|
||||
actors: {
|
||||
MediaControlDelegate: {
|
||||
parent: {
|
||||
moduleURI: "resource:///actors/MediaControlDelegateParent.jsm",
|
||||
},
|
||||
child: {
|
||||
moduleURI: "resource:///actors/MediaControlDelegateChild.jsm",
|
||||
events: {
|
||||
|
|
|
@ -8,301 +8,38 @@ var EXPORTED_SYMBOLS = ["GeckoViewPermission"];
|
|||
const { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
E10SUtils: "resource://gre/modules/E10SUtils.jsm",
|
||||
GeckoViewUtils: "resource://gre/modules/GeckoViewUtils.jsm",
|
||||
Services: "resource://gre/modules/Services.jsm",
|
||||
});
|
||||
|
||||
// See: http://developer.android.com/reference/android/Manifest.permission.html
|
||||
const PERM_ACCESS_FINE_LOCATION = "android.permission.ACCESS_FINE_LOCATION";
|
||||
const PERM_CAMERA = "android.permission.CAMERA";
|
||||
const PERM_RECORD_AUDIO = "android.permission.RECORD_AUDIO";
|
||||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const { GeckoViewUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/GeckoViewUtils.jsm"
|
||||
);
|
||||
|
||||
class GeckoViewPermission {
|
||||
constructor() {
|
||||
this.wrappedJSObject = this;
|
||||
}
|
||||
|
||||
_appPermissions = {};
|
||||
async prompt(aRequest) {
|
||||
const window = aRequest.window
|
||||
? aRequest.window
|
||||
: aRequest.element.ownerGlobal;
|
||||
|
||||
/* ---------- nsIObserver ---------- */
|
||||
observe(aSubject, aTopic, aData) {
|
||||
switch (aTopic) {
|
||||
case "getUserMedia:ask-device-permission": {
|
||||
this.handleMediaAskDevicePermission(aData, aSubject);
|
||||
break;
|
||||
}
|
||||
case "getUserMedia:request": {
|
||||
this.handleMediaRequest(aSubject);
|
||||
break;
|
||||
}
|
||||
case "PeerConnection:request": {
|
||||
this.handlePeerConnectionRequest(aSubject);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
receiveMessage(aMsg) {
|
||||
switch (aMsg.name) {
|
||||
case "GeckoView:AddCameraPermission": {
|
||||
const principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
|
||||
aMsg.data.origin
|
||||
);
|
||||
|
||||
// Although the lifetime is "session" it will be removed upon
|
||||
// use so it's more of a one-shot.
|
||||
Services.perms.addFromPrincipal(
|
||||
principal,
|
||||
"MediaManagerVideo",
|
||||
Services.perms.ALLOW_ACTION,
|
||||
Services.perms.EXPIRE_SESSION
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleMediaAskDevicePermission(aType, aCallback) {
|
||||
const perms = [];
|
||||
if (aType === "video" || aType === "all") {
|
||||
perms.push(PERM_CAMERA);
|
||||
}
|
||||
if (aType === "audio" || aType === "all") {
|
||||
perms.push(PERM_RECORD_AUDIO);
|
||||
}
|
||||
|
||||
const [dispatcher] = GeckoViewUtils.getActiveDispatcherAndWindow();
|
||||
const callback = _ => {
|
||||
Services.obs.notifyObservers(
|
||||
aCallback,
|
||||
"getUserMedia:got-device-permission"
|
||||
);
|
||||
};
|
||||
|
||||
if (dispatcher) {
|
||||
this.getAppPermissions(dispatcher, perms).then(callback, callback);
|
||||
const actor = window.windowGlobalChild.getActor("GeckoViewPermission");
|
||||
const result = await actor.promptPermission(aRequest);
|
||||
if (!result.allow) {
|
||||
aRequest.cancel();
|
||||
} else {
|
||||
// No dispatcher; just bail.
|
||||
callback();
|
||||
// Note: permission could be undefined, that's what aRequest expects.
|
||||
const { permission } = result;
|
||||
aRequest.allow(permission);
|
||||
}
|
||||
}
|
||||
|
||||
handleMediaRequest(aRequest) {
|
||||
const constraints = aRequest.getConstraints();
|
||||
const { callID, devices } = aRequest;
|
||||
const denyRequest = _ => {
|
||||
Services.obs.notifyObservers(null, "getUserMedia:response:deny", callID);
|
||||
};
|
||||
|
||||
const win = Services.wm.getOuterWindowWithId(aRequest.windowID);
|
||||
// Release the request first.
|
||||
aRequest = undefined;
|
||||
Promise.resolve()
|
||||
.then(() => {
|
||||
if (win.closed) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const sources = devices.map(device => {
|
||||
device = device.QueryInterface(Ci.nsIMediaDevice);
|
||||
return {
|
||||
type: device.type,
|
||||
id: device.rawId,
|
||||
rawId: device.rawId,
|
||||
name: device.rawName, // unfiltered device name to show to the user
|
||||
mediaSource: device.mediaSource,
|
||||
};
|
||||
});
|
||||
|
||||
if (
|
||||
constraints.video &&
|
||||
!sources.some(source => source.type === "videoinput")
|
||||
) {
|
||||
throw new Error("no video source");
|
||||
} else if (
|
||||
constraints.audio &&
|
||||
!sources.some(source => source.type === "audioinput")
|
||||
) {
|
||||
throw new Error("no audio source");
|
||||
}
|
||||
|
||||
const dispatcher = GeckoViewUtils.getDispatcherForWindow(win);
|
||||
const uri = win.top.document.documentURIObject;
|
||||
return dispatcher
|
||||
.sendRequestForResult({
|
||||
type: "GeckoView:MediaPermission",
|
||||
uri: uri.displaySpec,
|
||||
video: constraints.video
|
||||
? sources.filter(source => source.type === "videoinput")
|
||||
: null,
|
||||
audio: constraints.audio
|
||||
? sources.filter(source => source.type === "audioinput")
|
||||
: null,
|
||||
})
|
||||
.then(response => {
|
||||
if (!response) {
|
||||
// Rejected.
|
||||
denyRequest();
|
||||
return;
|
||||
}
|
||||
const allowedDevices = Cc["@mozilla.org/array;1"].createInstance(
|
||||
Ci.nsIMutableArray
|
||||
);
|
||||
if (constraints.video) {
|
||||
const video = devices.find(
|
||||
device => response.video === device.rawId
|
||||
);
|
||||
if (!video) {
|
||||
throw new Error("invalid video id");
|
||||
}
|
||||
Services.cpmm.sendAsyncMessage("GeckoView:AddCameraPermission", {
|
||||
origin: win.top.document.nodePrincipal.origin,
|
||||
});
|
||||
allowedDevices.appendElement(video);
|
||||
}
|
||||
if (constraints.audio) {
|
||||
const audio = devices.find(
|
||||
device => response.audio === device.rawId
|
||||
);
|
||||
if (!audio) {
|
||||
throw new Error("invalid audio id");
|
||||
}
|
||||
allowedDevices.appendElement(audio);
|
||||
}
|
||||
Services.obs.notifyObservers(
|
||||
allowedDevices,
|
||||
"getUserMedia:response:allow",
|
||||
callID
|
||||
);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
Cu.reportError("Media device error: " + error);
|
||||
denyRequest();
|
||||
});
|
||||
}
|
||||
|
||||
handlePeerConnectionRequest(aRequest) {
|
||||
Services.obs.notifyObservers(
|
||||
null,
|
||||
"PeerConnection:response:allow",
|
||||
aRequest.callID
|
||||
);
|
||||
}
|
||||
|
||||
checkAppPermissions(aPerms) {
|
||||
return aPerms.every(perm => this._appPermissions[perm]);
|
||||
}
|
||||
|
||||
getAppPermissions(aDispatcher, aPerms) {
|
||||
const perms = aPerms.filter(perm => !this._appPermissions[perm]);
|
||||
if (!perms.length) {
|
||||
return Promise.resolve(/* granted */ true);
|
||||
}
|
||||
return aDispatcher
|
||||
.sendRequestForResult({
|
||||
type: "GeckoView:AndroidPermission",
|
||||
perms,
|
||||
})
|
||||
.then(granted => {
|
||||
if (granted) {
|
||||
for (const perm of perms) {
|
||||
this._appPermissions[perm] = true;
|
||||
}
|
||||
}
|
||||
return granted;
|
||||
});
|
||||
}
|
||||
|
||||
prompt(aRequest) {
|
||||
// Only allow exactly one permission request here.
|
||||
const types = aRequest.types.QueryInterface(Ci.nsIArray);
|
||||
if (types.length !== 1) {
|
||||
aRequest.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
const perm = types.queryElementAt(0, Ci.nsIContentPermissionType);
|
||||
if (
|
||||
perm.type === "desktop-notification" &&
|
||||
!aRequest.hasValidTransientUserGestureActivation &&
|
||||
Services.prefs.getBoolPref(
|
||||
"dom.webnotifications.requireuserinteraction",
|
||||
true
|
||||
)
|
||||
) {
|
||||
// We need user interaction and don't have it.
|
||||
aRequest.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
const dispatcher = GeckoViewUtils.getDispatcherForWindow(
|
||||
aRequest.window ? aRequest.window : aRequest.element.ownerGlobal
|
||||
);
|
||||
const principal =
|
||||
perm.type == "storage-access"
|
||||
? aRequest.principal
|
||||
: aRequest.topLevelPrincipal;
|
||||
dispatcher
|
||||
.sendRequestForResult({
|
||||
type: "GeckoView:ContentPermission",
|
||||
uri: principal.URI.displaySpec,
|
||||
thirdPartyOrigin: aRequest.principal.origin,
|
||||
principal: E10SUtils.serializePrincipal(principal),
|
||||
perm: perm.type,
|
||||
value: perm.capability,
|
||||
contextId: principal.originAttributes.geckoViewSessionContextId ?? null,
|
||||
privateMode: principal.privateBrowsingId != 0,
|
||||
})
|
||||
.then(value => {
|
||||
if (value == Services.perms.ALLOW_ACTION) {
|
||||
// Ask for app permission after asking for content permission.
|
||||
if (perm.type === "geolocation") {
|
||||
return this.getAppPermissions(dispatcher, [
|
||||
PERM_ACCESS_FINE_LOCATION,
|
||||
]);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
})
|
||||
.catch(error => {
|
||||
Cu.reportError("Permission error: " + error);
|
||||
return /* value */ Services.perms.DENY_ACTION;
|
||||
})
|
||||
.then(value => {
|
||||
// The storage access code adds itself to the perm manager; no need for us to do it.
|
||||
if (perm.type == "storage-access") {
|
||||
if (value == Services.perms.ALLOW_ACTION) {
|
||||
aRequest.allow({ "storage-access": "allow" });
|
||||
} else {
|
||||
aRequest.cancel();
|
||||
}
|
||||
aRequest = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
(value == Services.perms.ALLOW_ACTION
|
||||
? aRequest.allow
|
||||
: aRequest.cancel)();
|
||||
Services.perms.addFromPrincipal(
|
||||
principal,
|
||||
perm.type,
|
||||
value,
|
||||
Services.perms.EXPIRE_NEVER
|
||||
);
|
||||
// Manually release the target request here to facilitate garbage collection.
|
||||
aRequest = undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
GeckoViewPermission.prototype.classID = Components.ID(
|
||||
"{42f3c238-e8e8-4015-9ca2-148723a8afcf}"
|
||||
);
|
||||
GeckoViewPermission.prototype.QueryInterface = ChromeUtils.generateQI([
|
||||
"nsIObserver",
|
||||
"nsIContentPermissionPrompt",
|
||||
]);
|
||||
|
||||
const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewPermission");
|
||||
|
|
|
@ -34,15 +34,8 @@ class GeckoViewPrompter {
|
|||
}
|
||||
}
|
||||
|
||||
if (this._domWin) {
|
||||
this._dispatcher = GeckoViewUtils.getDispatcherForWindow(this._domWin);
|
||||
}
|
||||
|
||||
if (!this._dispatcher) {
|
||||
[
|
||||
this._dispatcher,
|
||||
this._domWin,
|
||||
] = GeckoViewUtils.getActiveDispatcherAndWindow();
|
||||
if (!this._domWin) {
|
||||
this._domWin = Services.wm.getMostRecentWindow("navigator:geckoview");
|
||||
}
|
||||
|
||||
this._innerWindowId = this._domWin?.browsingContext.currentWindowContext.innerWindowId;
|
||||
|
@ -88,8 +81,7 @@ class GeckoViewPrompter {
|
|||
}
|
||||
|
||||
_dismissUi() {
|
||||
this.prompterActor?.unregisterPrompt(this);
|
||||
this._dispatcher.dispatch("GeckoView:Prompt:Dismiss", { id: this.id });
|
||||
this.prompterActor?.dismissPrompt(this);
|
||||
}
|
||||
|
||||
accept(aInputText = this.inputText) {
|
||||
|
@ -186,46 +178,35 @@ class GeckoViewPrompter {
|
|||
});
|
||||
}
|
||||
|
||||
asyncShowPrompt(aMsg, aCallback) {
|
||||
let handled = false;
|
||||
async asyncShowPrompt(aMsg, aCallback) {
|
||||
this.message = aMsg;
|
||||
this.inputText = aMsg.value;
|
||||
this.callback = aCallback;
|
||||
this.prompterActor?.registerPrompt(this);
|
||||
|
||||
const onResponse = response => {
|
||||
if (handled) {
|
||||
return;
|
||||
}
|
||||
if (!this.checkInnerWindow()) {
|
||||
// Page has navigated away, let's dismiss the prompt
|
||||
aCallback(null);
|
||||
} else {
|
||||
aCallback(response);
|
||||
}
|
||||
// This callback object is tied to the Java garbage collector because
|
||||
// it is invoked from Java. Manually release the target callback
|
||||
// here; otherwise we may hold onto resources for too long, because
|
||||
// we would be relying on both the Java and the JS garbage collectors
|
||||
// to run.
|
||||
aMsg = undefined;
|
||||
aCallback = undefined;
|
||||
handled = true;
|
||||
};
|
||||
|
||||
if (!this._dispatcher || !this.checkInnerWindow()) {
|
||||
onResponse(null);
|
||||
return;
|
||||
}
|
||||
|
||||
aMsg.id = this.id;
|
||||
this._dispatcher.dispatch("GeckoView:Prompt", aMsg, {
|
||||
onSuccess: onResponse,
|
||||
onError: error => {
|
||||
Cu.reportError("Prompt error: " + error);
|
||||
onResponse(null);
|
||||
},
|
||||
});
|
||||
this.prompterActor?.notifyPromptShow(this);
|
||||
|
||||
let response = null;
|
||||
try {
|
||||
if (this.checkInnerWindow()) {
|
||||
response = await this.prompterActor.prompt(this, aMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
// Nothing we can do really, we will treat this as a dismiss.
|
||||
warn`Error while prompting: ${error}`;
|
||||
}
|
||||
|
||||
if (!this.checkInnerWindow()) {
|
||||
// Page has navigated away, let's dismiss the prompt
|
||||
aCallback(null);
|
||||
} else {
|
||||
aCallback(response);
|
||||
}
|
||||
// This callback object is tied to the Java garbage collector because
|
||||
// it is invoked from Java. Manually release the target callback
|
||||
// here; otherwise we may hold onto resources for too long, because
|
||||
// we would be relying on both the Java and the JS garbage collectors
|
||||
// to run.
|
||||
aMsg = undefined;
|
||||
aCallback = undefined;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,13 +29,43 @@ function InitLater(fn, object, name) {
|
|||
return DelayedInit.schedule(fn, object, name, 15000 /* 15s max wait */);
|
||||
}
|
||||
|
||||
const JSPROCESSACTORS = {
|
||||
GeckoViewPermissionProcess: {
|
||||
parent: {
|
||||
moduleURI: "resource:///actors/GeckoViewPermissionProcessParent.jsm",
|
||||
},
|
||||
child: {
|
||||
moduleURI: "resource:///actors/GeckoViewPermissionProcessChild.jsm",
|
||||
observers: [
|
||||
"getUserMedia:ask-device-permission",
|
||||
"getUserMedia:request",
|
||||
"recording-device-events",
|
||||
"PeerConnection:request",
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const JSWINDOWACTORS = {
|
||||
LoadURIDelegate: {
|
||||
parent: {
|
||||
moduleURI: "resource:///actors/LoadURIDelegateParent.jsm",
|
||||
},
|
||||
child: {
|
||||
moduleURI: "resource:///actors/LoadURIDelegateChild.jsm",
|
||||
},
|
||||
messageManagerGroups: ["browsers"],
|
||||
},
|
||||
GeckoViewPermission: {
|
||||
parent: {
|
||||
moduleURI: "resource:///actors/GeckoViewPermissionParent.jsm",
|
||||
},
|
||||
child: {
|
||||
moduleURI: "resource:///actors/GeckoViewPermissionChild.jsm",
|
||||
},
|
||||
allFrames: true,
|
||||
includeChrome: true,
|
||||
},
|
||||
GeckoViewPrompt: {
|
||||
child: {
|
||||
moduleURI: "resource:///actors/GeckoViewPromptChild.jsm",
|
||||
|
@ -70,55 +100,10 @@ class GeckoViewStartup {
|
|||
switch (aTopic) {
|
||||
case "content-process-ready-for-script":
|
||||
case "app-startup": {
|
||||
// Parent and content process.
|
||||
GeckoViewUtils.addLazyGetter(this, "GeckoViewPermission", {
|
||||
service: "@mozilla.org/content-permission/prompt;1",
|
||||
observers: [
|
||||
"getUserMedia:ask-device-permission",
|
||||
"getUserMedia:request",
|
||||
"PeerConnection:request",
|
||||
],
|
||||
ppmm: ["GeckoView:AddCameraPermission"],
|
||||
});
|
||||
|
||||
GeckoViewUtils.addLazyGetter(this, "GeckoViewRecordingMedia", {
|
||||
module: "resource://gre/modules/GeckoViewMedia.jsm",
|
||||
observers: ["recording-device-events"],
|
||||
});
|
||||
|
||||
GeckoViewUtils.addLazyGetter(this, "GeckoViewConsole", {
|
||||
module: "resource://gre/modules/GeckoViewConsole.jsm",
|
||||
});
|
||||
|
||||
GeckoViewUtils.addLazyGetter(this, "GeckoViewWebExtension", {
|
||||
module: "resource://gre/modules/GeckoViewWebExtension.jsm",
|
||||
ged: [
|
||||
"GeckoView:ActionDelegate:Attached",
|
||||
"GeckoView:BrowserAction:Click",
|
||||
"GeckoView:PageAction:Click",
|
||||
"GeckoView:RegisterWebExtension",
|
||||
"GeckoView:UnregisterWebExtension",
|
||||
"GeckoView:WebExtension:CancelInstall",
|
||||
"GeckoView:WebExtension:Disable",
|
||||
"GeckoView:WebExtension:Enable",
|
||||
"GeckoView:WebExtension:EnsureBuiltIn",
|
||||
"GeckoView:WebExtension:Get",
|
||||
"GeckoView:WebExtension:Install",
|
||||
"GeckoView:WebExtension:InstallBuiltIn",
|
||||
"GeckoView:WebExtension:List",
|
||||
"GeckoView:WebExtension:PortDisconnect",
|
||||
"GeckoView:WebExtension:PortMessageFromApp",
|
||||
"GeckoView:WebExtension:SetPBAllowed",
|
||||
"GeckoView:WebExtension:Uninstall",
|
||||
"GeckoView:WebExtension:Update",
|
||||
],
|
||||
observers: [
|
||||
"devtools-installed-addon",
|
||||
"testing-installed-addon",
|
||||
"testing-uninstalled-addon",
|
||||
],
|
||||
});
|
||||
|
||||
GeckoViewUtils.addLazyGetter(this, "GeckoViewStorageController", {
|
||||
module: "resource://gre/modules/GeckoViewStorageController.jsm",
|
||||
ged: [
|
||||
|
@ -153,6 +138,36 @@ class GeckoViewStartup {
|
|||
Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT
|
||||
) {
|
||||
ActorManagerParent.addJSWindowActors(JSWINDOWACTORS);
|
||||
ActorManagerParent.addJSProcessActors(JSPROCESSACTORS);
|
||||
|
||||
GeckoViewUtils.addLazyGetter(this, "GeckoViewWebExtension", {
|
||||
module: "resource://gre/modules/GeckoViewWebExtension.jsm",
|
||||
ged: [
|
||||
"GeckoView:ActionDelegate:Attached",
|
||||
"GeckoView:BrowserAction:Click",
|
||||
"GeckoView:PageAction:Click",
|
||||
"GeckoView:RegisterWebExtension",
|
||||
"GeckoView:UnregisterWebExtension",
|
||||
"GeckoView:WebExtension:CancelInstall",
|
||||
"GeckoView:WebExtension:Disable",
|
||||
"GeckoView:WebExtension:Enable",
|
||||
"GeckoView:WebExtension:EnsureBuiltIn",
|
||||
"GeckoView:WebExtension:Get",
|
||||
"GeckoView:WebExtension:Install",
|
||||
"GeckoView:WebExtension:InstallBuiltIn",
|
||||
"GeckoView:WebExtension:List",
|
||||
"GeckoView:WebExtension:PortDisconnect",
|
||||
"GeckoView:WebExtension:PortMessageFromApp",
|
||||
"GeckoView:WebExtension:SetPBAllowed",
|
||||
"GeckoView:WebExtension:Uninstall",
|
||||
"GeckoView:WebExtension:Update",
|
||||
],
|
||||
observers: [
|
||||
"devtools-installed-addon",
|
||||
"testing-installed-addon",
|
||||
"testing-uninstalled-addon",
|
||||
],
|
||||
});
|
||||
|
||||
GeckoViewUtils.addLazyGetter(this, "ChildCrashHandler", {
|
||||
module: "resource://gre/modules/ChildCrashHandler.jsm",
|
||||
|
|
|
@ -310,7 +310,15 @@ package org.mozilla.geckoview {
|
|||
}
|
||||
|
||||
public static interface Autofill.Delegate {
|
||||
method @UiThread default public void onAutofill(@NonNull GeckoSession, int, @Nullable Autofill.Node);
|
||||
method @Deprecated @DeprecationSchedule(id="autofill-node",version=104) @UiThread default public void onAutofill(@NonNull GeckoSession, int, @Nullable Autofill.Node);
|
||||
method @UiThread default public void onNodeAdd(@NonNull GeckoSession, @NonNull Autofill.Node, @NonNull Autofill.NodeData);
|
||||
method @UiThread default public void onNodeBlur(@NonNull GeckoSession, @NonNull Autofill.Node, @NonNull Autofill.NodeData);
|
||||
method @UiThread default public void onNodeFocus(@NonNull GeckoSession, @NonNull Autofill.Node, @NonNull Autofill.NodeData);
|
||||
method @UiThread default public void onNodeRemove(@NonNull GeckoSession, @NonNull Autofill.Node, @NonNull Autofill.NodeData);
|
||||
method @UiThread default public void onNodeUpdate(@NonNull GeckoSession, @NonNull Autofill.Node, @NonNull Autofill.NodeData);
|
||||
method @UiThread default public void onSessionCancel(@NonNull GeckoSession);
|
||||
method @UiThread default public void onSessionCommit(@NonNull GeckoSession, @NonNull Autofill.Node, @NonNull Autofill.NodeData);
|
||||
method @UiThread default public void onSessionStart(@NonNull GeckoSession);
|
||||
}
|
||||
|
||||
public static final class Autofill.Hint {
|
||||
|
@ -331,7 +339,7 @@ package org.mozilla.geckoview {
|
|||
}
|
||||
|
||||
public static final class Autofill.Node {
|
||||
method @UiThread public void fillViewStructure(@NonNull View, @NonNull ViewStructure, int);
|
||||
method @Deprecated @DeprecationSchedule(id="autofill-node",version=104) @UiThread public void fillViewStructure(@NonNull View, @NonNull ViewStructure, int);
|
||||
method @AnyThread @Nullable public String getAttribute(@NonNull String);
|
||||
method @AnyThread @NonNull public Map<String,String> getAttributes();
|
||||
method @AnyThread @NonNull public Collection<Autofill.Node> getChildren();
|
||||
|
@ -339,16 +347,21 @@ package org.mozilla.geckoview {
|
|||
method @AnyThread @NonNull public String getDomain();
|
||||
method @AnyThread public boolean getEnabled();
|
||||
method @AnyThread public boolean getFocusable();
|
||||
method @AnyThread public boolean getFocused();
|
||||
method @AnyThread @Deprecated @DeprecationSchedule(id="autofill-node",version=104) public boolean getFocused();
|
||||
method @AnyThread public int getHint();
|
||||
method @AnyThread public int getId();
|
||||
method @AnyThread @Deprecated @DeprecationSchedule(id="autofill-node",version=104) public int getId();
|
||||
method @AnyThread public int getInputType();
|
||||
method @AnyThread @NonNull public String getTag();
|
||||
method @AnyThread @NonNull public String getValue();
|
||||
method @AnyThread public boolean getVisible();
|
||||
method @AnyThread @Deprecated @DeprecationSchedule(id="autofill-node",version=104) @NonNull public String getValue();
|
||||
method @AnyThread @Deprecated @DeprecationSchedule(id="autofill-node",version=104) public boolean getVisible();
|
||||
}
|
||||
|
||||
public static final class Autofill.Notify {
|
||||
public static class Autofill.NodeData {
|
||||
method @AnyThread public int getId();
|
||||
method @AnyThread @Nullable public String getValue();
|
||||
}
|
||||
|
||||
@Deprecated @DeprecationSchedule(id="autofill-node",version=104) public static final class Autofill.Notify {
|
||||
method @AnyThread @Nullable public static String toString(int);
|
||||
field public static final int NODE_ADDED = 3;
|
||||
field public static final int NODE_BLURRED = 7;
|
||||
|
@ -361,9 +374,14 @@ package org.mozilla.geckoview {
|
|||
}
|
||||
|
||||
public static final class Autofill.Session {
|
||||
method @NonNull @UiThread public Autofill.NodeData dataFor(@NonNull Autofill.Node);
|
||||
method @UiThread public void fillViewStructure(@NonNull View, @NonNull ViewStructure, int);
|
||||
method @AnyThread @NonNull public Rect getDefaultDimensions();
|
||||
method @UiThread public void fillViewStructure(@NonNull Autofill.Node, @NonNull View, @NonNull ViewStructure, int);
|
||||
method @NonNull @UiThread public Rect getDefaultDimensions();
|
||||
method @Nullable @UiThread public Autofill.Node getFocused();
|
||||
method @Nullable @UiThread public Autofill.NodeData getFocusedData();
|
||||
method @AnyThread @NonNull public Autofill.Node getRoot();
|
||||
method @UiThread public boolean isVisible(@NonNull Autofill.Node);
|
||||
}
|
||||
|
||||
@UiThread public class BasicSelectionActionDelegate implements ActionMode.Callback GeckoSession.SelectionActionDelegate {
|
||||
|
|
|
@ -12,13 +12,6 @@
|
|||
<input type="password" id="pass1" value="foo" autofocus>
|
||||
</fieldset>
|
||||
</form>
|
||||
<iframe id="iframe"></iframe>
|
||||
<script>
|
||||
addEventListener("load", function(e) {
|
||||
if (window.parent === window) {
|
||||
document.getElementById("iframe").contentWindow.location.href = window.location.href;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<iframe id="iframe" src="forms2_iframe.html"></iframe>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Forms2 iframe</title>
|
||||
</head>
|
||||
<body>
|
||||
<form>
|
||||
<fieldset>
|
||||
<input type="text" id="firstname">
|
||||
<input type="text" id="lastname">
|
||||
<input type="text" id="user1" value="foo">
|
||||
<input type="password" id="pass1" value="foo" autofocus>
|
||||
</fieldset>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
|
@ -11,13 +11,6 @@
|
|||
<input type="password">
|
||||
<input type="submit" value="submit">
|
||||
</form>
|
||||
<iframe id="iframe"></iframe>
|
||||
<script>
|
||||
addEventListener("load", function(e) {
|
||||
if (window.parent === window) {
|
||||
document.getElementById("iframe").contentWindow.location.href = window.location.href;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<iframe id="iframe" src="forms_autocomplete_iframe.html"></iframe>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Forms</title>
|
||||
<meta name="viewport" content="minimum-scale=1,width=device-width">
|
||||
</head>
|
||||
<body>
|
||||
<form>
|
||||
<input type="text" autocomplete="email" autofocus>
|
||||
<input type="text" autocomplete="username">
|
||||
<input type="password">
|
||||
<input type="submit" value="submit">
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,61 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Forms iframe</title>
|
||||
<meta name="viewport" content="minimum-scale=1,width=device-width">
|
||||
</head>
|
||||
<body>
|
||||
<form id="form1">
|
||||
<input type="text" id="user1" value="foo">
|
||||
<input type="password" id="pass1" value="foo">
|
||||
<input type="email" id="email1" value="@">
|
||||
<input type="number" id="number1" value="0">
|
||||
<input type="tel" id="tel1" value="0">
|
||||
<input type="submit" value="submit">
|
||||
</form>
|
||||
<input type="Text" id="user2" value="foo">
|
||||
<input type="PassWord" id="pass2" maxlength="8" value="foo">
|
||||
<input type="button" id="button1" value="foo"/>
|
||||
<input type="checkbox" id="checkbox1"/>
|
||||
<input type="search" id="search1">
|
||||
<input type="url" id="url1">
|
||||
<input type="hidden" id="hidden1" value="foo"/>
|
||||
</body>
|
||||
<script>
|
||||
const params = (new URL(document.location)).searchParams;
|
||||
|
||||
function getEventInterface(event) {
|
||||
if (event instanceof document.defaultView.InputEvent) {
|
||||
return "InputEvent";
|
||||
}
|
||||
if (event instanceof document.defaultView.UIEvent) {
|
||||
return "UIEvent";
|
||||
}
|
||||
if (event instanceof document.defaultView.Event) {
|
||||
return "Event";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
function getData(key, value) {
|
||||
return new Promise(resolve =>
|
||||
document.querySelector(key)
|
||||
.addEventListener('input', event => {
|
||||
resolve([
|
||||
key,
|
||||
event.target.value,
|
||||
value,
|
||||
getEventInterface(event),
|
||||
]);
|
||||
}, { once: true }))
|
||||
}
|
||||
|
||||
window.addEventListener('message', async event => {
|
||||
const { data, source, origin } = event;
|
||||
source.postMessage(
|
||||
await getData(data.key, data.value),
|
||||
origin);
|
||||
});
|
||||
</script>
|
||||
</html>
|
|
@ -0,0 +1,68 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Forms</title>
|
||||
<meta name="viewport" content="minimum-scale=1,width=device-width">
|
||||
</head>
|
||||
<body>
|
||||
<form id="form1">
|
||||
<input type="text" id="user1" value="foo">
|
||||
<input type="password" id="pass1" value="foo">
|
||||
<input type="email" id="email1" value="@">
|
||||
<input type="number" id="number1" value="0">
|
||||
<input type="tel" id="tel1" value="0">
|
||||
<input type="submit" value="submit">
|
||||
</form>
|
||||
<input type="Text" id="user2" value="foo">
|
||||
<input type="PassWord" id="pass2" maxlength="8" value="foo">
|
||||
<input type="button" id="button1" value="foo"/>
|
||||
<input type="checkbox" id="checkbox1"/>
|
||||
<input type="search" id="search1">
|
||||
<input type="url" id="url1">
|
||||
<input type="hidden" id="hidden1" value="foo"/>
|
||||
|
||||
<iframe id="iframe" src="http://example.org/tests/junit/forms_iframe.html"></iframe>
|
||||
</body>
|
||||
<script>
|
||||
const params = (new URL(document.location)).searchParams;
|
||||
const iframe = document.getElementById('iframe').contentWindow;
|
||||
|
||||
function getEventInterface(event) {
|
||||
if (event instanceof document.defaultView.InputEvent) {
|
||||
return "InputEvent";
|
||||
}
|
||||
if (event instanceof document.defaultView.UIEvent) {
|
||||
return "UIEvent";
|
||||
}
|
||||
if (event instanceof document.defaultView.Event) {
|
||||
return "Event";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
function getData(key, value) {
|
||||
return new Promise(resolve =>
|
||||
document.querySelector(key)
|
||||
.addEventListener('input', event => {
|
||||
resolve([
|
||||
key,
|
||||
event.target.value,
|
||||
value,
|
||||
getEventInterface(event),
|
||||
]);
|
||||
}, { once: true }))
|
||||
}
|
||||
|
||||
window.getDataForAllFrames = function(key, value) {
|
||||
const data = [];
|
||||
data.push(new Promise(resolve =>
|
||||
window.addEventListener('message', event => {
|
||||
resolve(event.data);
|
||||
}, { once: true })));
|
||||
iframe.postMessage({key, value}, "*");
|
||||
data.push(getData(key, value));
|
||||
return Promise.all(data);
|
||||
};
|
||||
</script>
|
||||
</html>
|
|
@ -4,50 +4,58 @@
|
|||
|
||||
package org.mozilla.geckoview.test
|
||||
|
||||
import android.graphics.Matrix
|
||||
import android.os.Bundle
|
||||
import android.os.LocaleList
|
||||
import androidx.test.filters.MediumTest
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import android.util.Pair
|
||||
import android.util.SparseArray
|
||||
import android.view.View
|
||||
import android.view.ViewStructure
|
||||
import android.view.autofill.AutofillId
|
||||
import android.view.autofill.AutofillValue
|
||||
import org.hamcrest.Matchers.*
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import org.mozilla.geckoview.Autofill
|
||||
import org.mozilla.geckoview.GeckoSession
|
||||
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule
|
||||
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
|
||||
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
|
||||
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.*
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@RunWith(Parameterized::class)
|
||||
@MediumTest
|
||||
class AutofillDelegateTest : BaseSessionTest() {
|
||||
|
||||
companion object {
|
||||
@get:Parameterized.Parameters(name = "{0}")
|
||||
@JvmStatic
|
||||
val parameters: List<Array<out Any>> = listOf(
|
||||
arrayOf("#inProcess"), arrayOf("#oop"))
|
||||
}
|
||||
|
||||
@field:Parameterized.Parameter(0) @JvmField var iframe: String = ""
|
||||
|
||||
// Whether the iframe is loaded in-process (i.e. with the same origin as the
|
||||
// outer html page) or out-of-process.
|
||||
private val pageUrl by lazy {
|
||||
when (iframe) {
|
||||
"#inProcess" -> "http://example.org/tests/junit/forms_xorigin.html"
|
||||
"#oop" -> createTestUrl(FORMS_XORIGIN_HTML_PATH)
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun autofillCommit() {
|
||||
sessionRule.setPrefsUntilTestEnd(mapOf(
|
||||
"signon.rememberSignons" to true,
|
||||
"signon.userInputRequiredToCapture.enabled" to false))
|
||||
|
||||
mainSession.loadTestPath(FORMS_HTML_PATH)
|
||||
mainSession.loadUri(pageUrl)
|
||||
// Wait for the auto-fill nodes to populate.
|
||||
sessionRule.waitUntilCalled(object : Autofill.Delegate {
|
||||
// For the root document and the iframe document, each has a form group and
|
||||
// a group for inputs outside of forms, so the total count is 4.
|
||||
@AssertCalled(count = 4)
|
||||
override fun onAutofill(session: GeckoSession,
|
||||
notification: Int,
|
||||
node: Autofill.Node?) {
|
||||
assertThat("Should be starting auto-fill",
|
||||
notification,
|
||||
equalTo(forEachCall(
|
||||
Autofill.Notify.SESSION_STARTED,
|
||||
Autofill.Notify.NODE_ADDED)))
|
||||
}
|
||||
sessionRule.waitUntilCalled(object : Autofill.Delegate, GeckoSession.ProgressDelegate {
|
||||
// We expect to get a call to onSessionStart and many calls to onNodeAdd depending
|
||||
// on timing.
|
||||
@AssertCalled(count = 1)
|
||||
override fun onSessionStart(session: GeckoSession) {}
|
||||
@AssertCalled(count = -1)
|
||||
override fun onNodeAdd(session: GeckoSession,
|
||||
node: Autofill.Node,
|
||||
data: Autofill.NodeData) {}
|
||||
@AssertCalled(count = 1)
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {}
|
||||
})
|
||||
|
||||
// Assign node values.
|
||||
|
@ -60,34 +68,33 @@ class AutofillDelegateTest : BaseSessionTest() {
|
|||
mainSession.evaluateJS("document.querySelector('#form1').submit()")
|
||||
|
||||
sessionRule.waitUntilCalled(object : Autofill.Delegate {
|
||||
@AssertCalled(count = 5)
|
||||
override fun onAutofill(session: GeckoSession,
|
||||
notification: Int,
|
||||
node: Autofill.Node?) {
|
||||
val info = sessionRule.currentCall
|
||||
@AssertCalled(order = [1,2,3,4])
|
||||
override fun onNodeUpdate(session: GeckoSession,
|
||||
node: Autofill.Node,
|
||||
data: Autofill.NodeData) {
|
||||
}
|
||||
|
||||
if (info.counter < 5) {
|
||||
assertThat("Should be an update notification",
|
||||
notification,
|
||||
equalTo(Autofill.Notify.NODE_UPDATED))
|
||||
} else {
|
||||
assertThat("Should be a commit notification",
|
||||
notification,
|
||||
equalTo(Autofill.Notify.SESSION_COMMITTED))
|
||||
|
||||
assertThat("Values should match",
|
||||
countAutofillNodes({ it.value == "user1x" }),
|
||||
equalTo(1))
|
||||
assertThat("Values should match",
|
||||
countAutofillNodes({ it.value == "pass1x" }),
|
||||
equalTo(1))
|
||||
assertThat("Values should match",
|
||||
countAutofillNodes({ it.value == "e@mail.com" }),
|
||||
equalTo(1))
|
||||
assertThat("Values should match",
|
||||
countAutofillNodes({ it.value == "1" }),
|
||||
equalTo(1))
|
||||
}
|
||||
@AssertCalled(order = [5])
|
||||
override fun onSessionCommit(session: GeckoSession,
|
||||
node: Autofill.Node,
|
||||
data: Autofill.NodeData) {
|
||||
val autofillSession = mainSession.autofillSession
|
||||
assertThat("Values should match",
|
||||
countAutofillNodes({
|
||||
autofillSession.dataFor(it).value == "user1x" }),
|
||||
equalTo(1))
|
||||
assertThat("Values should match",
|
||||
countAutofillNodes({
|
||||
autofillSession.dataFor(it).value == "pass1x" }),
|
||||
equalTo(1))
|
||||
assertThat("Values should match",
|
||||
countAutofillNodes({
|
||||
autofillSession.dataFor(it).value == "e@mail.com" }),
|
||||
equalTo(1))
|
||||
assertThat("Values should match",
|
||||
countAutofillNodes({
|
||||
autofillSession.dataFor(it).value == "1" }),
|
||||
equalTo(1))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -99,17 +106,15 @@ class AutofillDelegateTest : BaseSessionTest() {
|
|||
|
||||
mainSession.loadTestPath(FORMS_ID_VALUE_HTML_PATH)
|
||||
// Wait for the auto-fill nodes to populate.
|
||||
sessionRule.waitUntilCalled(object : Autofill.Delegate {
|
||||
sessionRule.waitUntilCalled(object : Autofill.Delegate, GeckoSession.ProgressDelegate {
|
||||
@AssertCalled(count = 1)
|
||||
override fun onAutofill(session: GeckoSession,
|
||||
notification: Int,
|
||||
node: Autofill.Node?) {
|
||||
assertThat("Should be starting auto-fill",
|
||||
notification,
|
||||
equalTo(forEachCall(
|
||||
Autofill.Notify.SESSION_STARTED,
|
||||
Autofill.Notify.NODE_ADDED)))
|
||||
}
|
||||
override fun onSessionStart(session: GeckoSession) {}
|
||||
@AssertCalled(count = -1)
|
||||
override fun onNodeAdd(session: GeckoSession,
|
||||
node: Autofill.Node,
|
||||
data: Autofill.NodeData) {}
|
||||
@AssertCalled(count = 1)
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {}
|
||||
})
|
||||
|
||||
// Assign node values.
|
||||
|
@ -119,25 +124,20 @@ class AutofillDelegateTest : BaseSessionTest() {
|
|||
mainSession.evaluateJS("document.querySelector('#form1').submit()")
|
||||
|
||||
sessionRule.waitUntilCalled(object : Autofill.Delegate {
|
||||
@AssertCalled(count = 2)
|
||||
override fun onAutofill(session: GeckoSession,
|
||||
notification: Int,
|
||||
node: Autofill.Node?) {
|
||||
val info = sessionRule.currentCall
|
||||
|
||||
if (info.counter < 2) {
|
||||
assertThat("Should be an update notification",
|
||||
notification,
|
||||
equalTo(Autofill.Notify.NODE_UPDATED))
|
||||
} else {
|
||||
assertThat("Should be a commit notification",
|
||||
notification,
|
||||
equalTo(Autofill.Notify.SESSION_COMMITTED))
|
||||
|
||||
assertThat("Values should match",
|
||||
countAutofillNodes({ it.value == "pass1x" }),
|
||||
equalTo(1))
|
||||
}
|
||||
@AssertCalled(order = [1])
|
||||
override fun onNodeUpdate(session: GeckoSession,
|
||||
node: Autofill.Node,
|
||||
data: Autofill.NodeData) {
|
||||
}
|
||||
@AssertCalled(order = [2])
|
||||
override fun onSessionCommit(session: GeckoSession,
|
||||
node: Autofill.Node,
|
||||
data: Autofill.NodeData) {
|
||||
assertThat("Values should match",
|
||||
countAutofillNodes({
|
||||
mainSession.autofillSession.dataFor(it).value == "pass1x"
|
||||
}),
|
||||
equalTo(1))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -145,16 +145,16 @@ class AutofillDelegateTest : BaseSessionTest() {
|
|||
@Test fun autofill() {
|
||||
// Test parts of the Oreo auto-fill API; there is another autofill test in
|
||||
// SessionAccessibility for a11y auto-fill support.
|
||||
mainSession.loadTestPath(FORMS_HTML_PATH)
|
||||
mainSession.loadUri(pageUrl)
|
||||
// Wait for the auto-fill nodes to populate.
|
||||
sessionRule.waitUntilCalled(object : Autofill.Delegate {
|
||||
// For the root document and the iframe document, each has a form group and
|
||||
// a group for inputs outside of forms, so the total count is 4.
|
||||
@AssertCalled(count = 4)
|
||||
override fun onAutofill(session: GeckoSession,
|
||||
notification: Int,
|
||||
node: Autofill.Node?) {
|
||||
}
|
||||
sessionRule.waitUntilCalled(object : Autofill.Delegate, GeckoSession.ProgressDelegate {
|
||||
// We expect many call to onNodeAdd while loading the page
|
||||
@AssertCalled(count = -1)
|
||||
override fun onNodeAdd(session: GeckoSession,
|
||||
node: Autofill.Node,
|
||||
data: Autofill.NodeData) {}
|
||||
@AssertCalled(count = 1)
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {}
|
||||
})
|
||||
|
||||
val autofills = mapOf(
|
||||
|
@ -163,45 +163,36 @@ class AutofillDelegateTest : BaseSessionTest() {
|
|||
"#number1" to "24", "#tel1" to "42")
|
||||
|
||||
// Set up promises to monitor the values changing.
|
||||
val promises = autofills.flatMap { entry ->
|
||||
val promises = autofills.map { entry ->
|
||||
// Repeat each test with both the top document and the iframe document.
|
||||
arrayOf("document", "document.querySelector('#iframe').contentDocument").map { doc ->
|
||||
mainSession.evaluatePromiseJS("""new Promise(resolve =>
|
||||
$doc.querySelector('${entry.key}').addEventListener(
|
||||
'input', event => {
|
||||
let eventInterface =
|
||||
event instanceof $doc.defaultView.InputEvent ? "InputEvent" :
|
||||
event instanceof $doc.defaultView.UIEvent ? "UIEvent" :
|
||||
event instanceof $doc.defaultView.Event ? "Event" : "Unknown";
|
||||
resolve([
|
||||
'${entry.key}',
|
||||
event.target.value,
|
||||
'${entry.value}',
|
||||
eventInterface
|
||||
]);
|
||||
}, { once: true }))""")
|
||||
}
|
||||
mainSession.evaluatePromiseJS("""
|
||||
window.getDataForAllFrames('${entry.key}', '${entry.value}')
|
||||
""")
|
||||
}
|
||||
|
||||
val autofillValues = SparseArray<CharSequence>()
|
||||
|
||||
// Perform auto-fill and return number of auto-fills performed.
|
||||
fun checkAutofillChild(child: Autofill.Node) {
|
||||
fun checkAutofillChild(child: Autofill.Node, domain: String) {
|
||||
// Seal the node info instance so we can perform actions on it.
|
||||
if (child.children.count() > 0) {
|
||||
if (child.children.isNotEmpty()) {
|
||||
for (c in child.children) {
|
||||
checkAutofillChild(c!!)
|
||||
checkAutofillChild(c!!, child.domain)
|
||||
}
|
||||
}
|
||||
|
||||
if (child.id == View.NO_ID) {
|
||||
if (child == mainSession.autofillSession.root) {
|
||||
return
|
||||
}
|
||||
|
||||
assertThat("Should have HTML tag",
|
||||
child.tag, not(isEmptyOrNullString()))
|
||||
assertThat("Web domain should match",
|
||||
child.domain, equalTo(GeckoSessionTestRule.TEST_ENDPOINT))
|
||||
if (domain != "") {
|
||||
assertThat(
|
||||
"Web domain should match its parent.",
|
||||
child.domain, equalTo(domain)
|
||||
)
|
||||
}
|
||||
|
||||
if (child.inputType == Autofill.InputType.TEXT) {
|
||||
assertThat("Input should be enabled", child.enabled, equalTo(true))
|
||||
|
@ -212,7 +203,8 @@ class AutofillDelegateTest : BaseSessionTest() {
|
|||
assertThat("Should have ID attribute", child.attributes.get("id"), not(isEmptyOrNullString()))
|
||||
}
|
||||
|
||||
autofillValues.append(child.id, when (child.inputType) {
|
||||
val childId = mainSession.autofillSession.dataFor(child).id
|
||||
autofillValues.append(childId, when (child.inputType) {
|
||||
Autofill.InputType.NUMBER -> "24"
|
||||
Autofill.InputType.PHONE -> "42"
|
||||
Autofill.InputType.TEXT -> when (child.hint) {
|
||||
|
@ -225,28 +217,37 @@ class AutofillDelegateTest : BaseSessionTest() {
|
|||
}
|
||||
|
||||
val nodes = mainSession.autofillSession.root
|
||||
checkAutofillChild(nodes)
|
||||
checkAutofillChild(nodes, "")
|
||||
|
||||
mainSession.autofill(autofillValues)
|
||||
|
||||
// Wait on the promises and check for correct values.
|
||||
for ((key, actual, expected, eventInterface) in promises.map { it.value.asJSList<String>() }) {
|
||||
assertThat("Auto-filled value must match ($key)", actual, equalTo(expected))
|
||||
assertThat("input event should be dispatched with InputEvent interface", eventInterface, equalTo("InputEvent"))
|
||||
for (values in promises.map { it.value.asJsonArray() }) {
|
||||
for (i in 0 until values.length()) {
|
||||
val (key, actual, expected, eventInterface) = values.get(i).asJSList<String>()
|
||||
|
||||
assertThat("Auto-filled value must match ($key)", actual, equalTo(expected))
|
||||
assertThat(
|
||||
"input event should be dispatched with InputEvent interface",
|
||||
eventInterface,
|
||||
equalTo("InputEvent")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun autofillUnknownValue() {
|
||||
// Test parts of the Oreo auto-fill API; there is another autofill test in
|
||||
// SessionAccessibility for a11y auto-fill support.
|
||||
mainSession.loadTestPath(FORMS_HTML_PATH)
|
||||
mainSession.loadUri(pageUrl)
|
||||
// Wait for the auto-fill nodes to populate.
|
||||
sessionRule.waitUntilCalled(object : Autofill.Delegate {
|
||||
sessionRule.waitUntilCalled(object : Autofill.Delegate, GeckoSession.ProgressDelegate {
|
||||
@AssertCalled(count = -1)
|
||||
override fun onNodeAdd(session: GeckoSession,
|
||||
node: Autofill.Node,
|
||||
data: Autofill.NodeData) {}
|
||||
@AssertCalled(count = 1)
|
||||
override fun onAutofill(session: GeckoSession,
|
||||
notification: Int,
|
||||
node: Autofill.Node?) {
|
||||
}
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {}
|
||||
})
|
||||
|
||||
val autofillValues = SparseArray<CharSequence>()
|
||||
|
@ -266,19 +267,29 @@ class AutofillDelegateTest : BaseSessionTest() {
|
|||
@WithDisplay(width = 100, height = 100)
|
||||
@Test fun autofillNavigation() {
|
||||
// Wait for the accessibility nodes to populate.
|
||||
mainSession.loadTestPath(FORMS_HTML_PATH)
|
||||
sessionRule.waitUntilCalled(object : Autofill.Delegate {
|
||||
@AssertCalled(count = 4)
|
||||
override fun onAutofill(session: GeckoSession,
|
||||
notification: Int,
|
||||
node: Autofill.Node?) {
|
||||
assertThat("Should be starting auto-fill",
|
||||
notification,
|
||||
equalTo(forEachCall(
|
||||
Autofill.Notify.SESSION_STARTED,
|
||||
Autofill.Notify.NODE_ADDED)))
|
||||
mainSession.loadUri(pageUrl)
|
||||
|
||||
sessionRule.waitUntilCalled(object : Autofill.Delegate, ShouldContinue,
|
||||
GeckoSession.ProgressDelegate
|
||||
{
|
||||
var nodeCount = 0
|
||||
|
||||
// Continue waiting util we get all 16 nodes
|
||||
override fun shouldContinue(): Boolean = nodeCount < 16
|
||||
|
||||
@AssertCalled(count = 1)
|
||||
override fun onSessionStart(session: GeckoSession) {}
|
||||
|
||||
@AssertCalled(count = -1)
|
||||
override fun onNodeAdd(session: GeckoSession,
|
||||
node: Autofill.Node,
|
||||
data: Autofill.NodeData) {
|
||||
assertThat("Node should be valid", node, notNullValue())
|
||||
nodeCount = countAutofillNodes()
|
||||
}
|
||||
|
||||
@AssertCalled(count = 1)
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {}
|
||||
})
|
||||
|
||||
assertThat("Initial auto-fill count should match",
|
||||
|
@ -286,99 +297,101 @@ class AutofillDelegateTest : BaseSessionTest() {
|
|||
|
||||
// Now wait for the nodes to clear.
|
||||
mainSession.loadTestPath(HELLO_HTML_PATH)
|
||||
sessionRule.waitUntilCalled(object : Autofill.Delegate {
|
||||
sessionRule.waitUntilCalled(object : Autofill.Delegate, GeckoSession.ProgressDelegate {
|
||||
@AssertCalled(count = 1)
|
||||
override fun onAutofill(session: GeckoSession,
|
||||
notification: Int,
|
||||
node: Autofill.Node?) {
|
||||
assertThat("Should be canceling auto-fill",
|
||||
notification,
|
||||
equalTo(Autofill.Notify.SESSION_CANCELED))
|
||||
assertThat("Node should be null", node, nullValue())
|
||||
}
|
||||
override fun onSessionCancel(session: GeckoSession) {}
|
||||
|
||||
@AssertCalled
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {}
|
||||
})
|
||||
|
||||
assertThat("Should not have auto-fill fields",
|
||||
countAutofillNodes(), equalTo(0))
|
||||
|
||||
// Now wait for the nodes to reappear.
|
||||
mainSession.waitForPageStop()
|
||||
mainSession.goBack()
|
||||
sessionRule.waitUntilCalled(object : Autofill.Delegate, GeckoSession.ProgressDelegate {
|
||||
@AssertCalled(count = 4)
|
||||
override fun onAutofill(session: GeckoSession,
|
||||
notification: Int,
|
||||
node: Autofill.Node?) {
|
||||
assertThat("Should be starting auto-fill",
|
||||
notification,
|
||||
equalTo(forEachCall(
|
||||
Autofill.Notify.SESSION_STARTED,
|
||||
Autofill.Notify.NODE_ADDED)))
|
||||
assertThat("ID should be valid", node, notNullValue())
|
||||
sessionRule.waitUntilCalled(object : Autofill.Delegate, GeckoSession.ProgressDelegate,
|
||||
ShouldContinue
|
||||
{
|
||||
var nodeCount = 0
|
||||
override fun shouldContinue(): Boolean = nodeCount < 16
|
||||
|
||||
@AssertCalled(count = 1)
|
||||
override fun onSessionStart(session: GeckoSession) {}
|
||||
|
||||
@AssertCalled(count = -1)
|
||||
override fun onNodeAdd(session: GeckoSession,
|
||||
node: Autofill.Node,
|
||||
data: Autofill.NodeData) {
|
||||
assertThat("Node should be valid", node, notNullValue())
|
||||
nodeCount = countAutofillNodes()
|
||||
}
|
||||
|
||||
@AssertCalled(count = 1)
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
||||
}
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {}
|
||||
})
|
||||
|
||||
assertThat("Should have auto-fill fields again",
|
||||
countAutofillNodes(), equalTo(16))
|
||||
|
||||
var focused = mainSession.autofillSession.focused
|
||||
assertThat("Should not have focused field",
|
||||
countAutofillNodes({ it.focused }), equalTo(0))
|
||||
countAutofillNodes({ it == focused }), equalTo(0))
|
||||
|
||||
mainSession.evaluateJS("document.querySelector('#pass2').focus()")
|
||||
|
||||
sessionRule.waitUntilCalled(object : Autofill.Delegate {
|
||||
@AssertCalled(count = 1)
|
||||
override fun onAutofill(session: GeckoSession,
|
||||
notification: Int,
|
||||
node: Autofill.Node?) {
|
||||
assertThat("Should be entering auto-fill view",
|
||||
notification,
|
||||
equalTo(Autofill.Notify.NODE_FOCUSED))
|
||||
override fun onNodeFocus(session: GeckoSession,
|
||||
node: Autofill.Node,
|
||||
data: Autofill.NodeData) {
|
||||
assertThat("ID should be valid", node, notNullValue())
|
||||
}
|
||||
})
|
||||
|
||||
focused = mainSession.autofillSession.focused
|
||||
assertThat("Should have one focused field",
|
||||
countAutofillNodes({ it.focused }), equalTo(1))
|
||||
countAutofillNodes({ it == focused }), equalTo(1))
|
||||
// The focused field, its siblings, its parent, and the root node should
|
||||
// be visible.
|
||||
// Hidden elements are ignored.
|
||||
// TODO: Is this actually correct? Should the whole focused branch be
|
||||
// visible or just the nodes as described above?
|
||||
assertThat("Should have nine visible nodes",
|
||||
countAutofillNodes({ node -> node.visible }),
|
||||
countAutofillNodes({ node -> mainSession.autofillSession.isVisible(node) }),
|
||||
equalTo(8))
|
||||
|
||||
mainSession.evaluateJS("document.querySelector('#pass2').blur()")
|
||||
sessionRule.waitUntilCalled(object : Autofill.Delegate {
|
||||
@AssertCalled(count = 1)
|
||||
override fun onAutofill(session: GeckoSession,
|
||||
notification: Int,
|
||||
node: Autofill.Node?) {
|
||||
assertThat("Should be exiting auto-fill view",
|
||||
notification,
|
||||
equalTo(Autofill.Notify.NODE_BLURRED))
|
||||
override fun onNodeBlur(session: GeckoSession,
|
||||
node: Autofill.Node,
|
||||
data: Autofill.NodeData) {
|
||||
assertThat("ID should be valid", node, notNullValue())
|
||||
}
|
||||
})
|
||||
|
||||
focused = mainSession.autofillSession.focused
|
||||
assertThat("Should not have focused field",
|
||||
countAutofillNodes({ it.focused }), equalTo(0))
|
||||
countAutofillNodes({ it == focused }), equalTo(0))
|
||||
}
|
||||
|
||||
@WithDisplay(height = 100, width = 100)
|
||||
@Test fun autofillUserpass() {
|
||||
mainSession.loadTestPath(FORMS2_HTML_PATH)
|
||||
// Wait for the auto-fill nodes to populate.
|
||||
sessionRule.waitUntilCalled(object : Autofill.Delegate {
|
||||
@AssertCalled(count = 3)
|
||||
override fun onAutofill(session: GeckoSession,
|
||||
notification: Int,
|
||||
node: Autofill.Node?) {
|
||||
assertThat("Autofill notification should match", notification,
|
||||
equalTo(forEachCall(Autofill.Notify.SESSION_STARTED,
|
||||
Autofill.Notify.NODE_FOCUSED,
|
||||
Autofill.Notify.NODE_ADDED)))
|
||||
}
|
||||
sessionRule.waitUntilCalled(object : Autofill.Delegate, GeckoSession.ProgressDelegate {
|
||||
@AssertCalled(count = 1)
|
||||
override fun onSessionStart(session: GeckoSession) {}
|
||||
@AssertCalled(count = 1)
|
||||
override fun onNodeFocus(session: GeckoSession,
|
||||
node: Autofill.Node,
|
||||
data: Autofill.NodeData) {}
|
||||
@AssertCalled(count = -1)
|
||||
override fun onNodeAdd(session: GeckoSession,
|
||||
node: Autofill.Node,
|
||||
data: Autofill.NodeData) {}
|
||||
@AssertCalled(count = 1)
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {}
|
||||
})
|
||||
|
||||
// Perform auto-fill and return number of auto-fills performed.
|
||||
|
@ -393,7 +406,8 @@ class AutofillDelegateTest : BaseSessionTest() {
|
|||
return sum
|
||||
}
|
||||
|
||||
assertThat("ID should be valid", child.id, not(equalTo(View.NO_ID)))
|
||||
val childId = mainSession.autofillSession.dataFor(child).id
|
||||
assertThat("ID should be valid", childId, not(equalTo(View.NO_ID)))
|
||||
assertThat("Should have HTML tag", child.tag, equalTo("input"))
|
||||
|
||||
return sum + 1
|
||||
|
@ -410,48 +424,42 @@ class AutofillDelegateTest : BaseSessionTest() {
|
|||
@Test fun autofillActiveChange() {
|
||||
// We should blur the active autofill node if the session is set
|
||||
// inactive. Likewise, we should focus a node once we return.
|
||||
mainSession.loadTestPath(FORMS_HTML_PATH)
|
||||
mainSession.loadUri(pageUrl)
|
||||
// Wait for the auto-fill nodes to populate.
|
||||
sessionRule.waitUntilCalled(object : Autofill.Delegate {
|
||||
sessionRule.waitUntilCalled(object : Autofill.Delegate, GeckoSession.ProgressDelegate {
|
||||
// For the root document and the iframe document, each has a form group and
|
||||
// a group for inputs outside of forms, so the total count is 4.
|
||||
@AssertCalled(count = 4)
|
||||
override fun onAutofill(session: GeckoSession,
|
||||
notification: Int,
|
||||
node: Autofill.Node?) {
|
||||
assertThat("Should be starting auto-fill",
|
||||
notification,
|
||||
equalTo(forEachCall(
|
||||
Autofill.Notify.SESSION_STARTED,
|
||||
Autofill.Notify.NODE_ADDED)))
|
||||
}
|
||||
@AssertCalled(count = 1)
|
||||
override fun onSessionStart(session: GeckoSession) {}
|
||||
@AssertCalled(count = -1)
|
||||
override fun onNodeAdd(session: GeckoSession,
|
||||
node: Autofill.Node,
|
||||
data: Autofill.NodeData) {}
|
||||
@AssertCalled(count = 1)
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {}
|
||||
})
|
||||
|
||||
mainSession.evaluateJS("document.querySelector('#pass2').focus()")
|
||||
sessionRule.waitUntilCalled(object : Autofill.Delegate {
|
||||
@AssertCalled(count = 1)
|
||||
override fun onAutofill(session: GeckoSession,
|
||||
notification: Int,
|
||||
node: Autofill.Node?) {
|
||||
assertThat("Should be entering auto-fill view",
|
||||
notification,
|
||||
equalTo(Autofill.Notify.NODE_FOCUSED))
|
||||
override fun onNodeFocus(session: GeckoSession,
|
||||
node: Autofill.Node,
|
||||
data: Autofill.NodeData) {
|
||||
assertThat("ID should be valid", node, notNullValue())
|
||||
}
|
||||
})
|
||||
|
||||
var focused = mainSession.autofillSession.focused
|
||||
assertThat("Should have one focused field",
|
||||
countAutofillNodes({ it.focused }), equalTo(1))
|
||||
countAutofillNodes({ it == focused }), equalTo(1))
|
||||
|
||||
// Make sure we get NODE_BLURRED when inactive
|
||||
mainSession.setActive(false)
|
||||
sessionRule.waitUntilCalled(object : Autofill.Delegate {
|
||||
@AssertCalled(count = 1)
|
||||
override fun onAutofill(session: GeckoSession,
|
||||
notification: Int,
|
||||
node: Autofill.Node?) {
|
||||
assertThat("Should be exiting auto-fill view",
|
||||
notification,
|
||||
equalTo(Autofill.Notify.NODE_BLURRED))
|
||||
override fun onNodeBlur(session: GeckoSession,
|
||||
node: Autofill.Node,
|
||||
data: Autofill.NodeData) {
|
||||
assertThat("ID should be valid", node, notNullValue())
|
||||
}
|
||||
})
|
||||
|
@ -460,29 +468,30 @@ class AutofillDelegateTest : BaseSessionTest() {
|
|||
mainSession.setActive(true)
|
||||
sessionRule.waitUntilCalled(object : Autofill.Delegate {
|
||||
@AssertCalled(count = 1)
|
||||
override fun onAutofill(session: GeckoSession,
|
||||
notification: Int,
|
||||
node: Autofill.Node?) {
|
||||
assertThat("Should be entering auto-fill view",
|
||||
notification,
|
||||
equalTo(Autofill.Notify.NODE_FOCUSED))
|
||||
override fun onNodeFocus(session: GeckoSession,
|
||||
node: Autofill.Node,
|
||||
data: Autofill.NodeData) {
|
||||
assertThat("ID should be valid", node, notNullValue())
|
||||
}
|
||||
})
|
||||
|
||||
focused = mainSession.autofillSession.focused
|
||||
assertThat("Should have one focused field",
|
||||
countAutofillNodes({ it.focused }), equalTo(1))
|
||||
countAutofillNodes({ focused == it }), equalTo(1))
|
||||
}
|
||||
|
||||
@WithDisplay(width = 100, height = 100)
|
||||
@Test fun autofillAutocompleteAttribute() {
|
||||
mainSession.loadTestPath(FORMS_AUTOCOMPLETE_HTML_PATH)
|
||||
sessionRule.waitUntilCalled(object : Autofill.Delegate {
|
||||
@AssertCalled(count = 3)
|
||||
override fun onAutofill(session: GeckoSession,
|
||||
notification: Int,
|
||||
node: Autofill.Node?) {
|
||||
}
|
||||
});
|
||||
sessionRule.waitUntilCalled(object : Autofill.Delegate, GeckoSession.ProgressDelegate {
|
||||
@AssertCalled(count = -1)
|
||||
override fun onNodeAdd(session: GeckoSession,
|
||||
node: Autofill.Node,
|
||||
data: Autofill.NodeData) {}
|
||||
|
||||
@AssertCalled(count = 1)
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {}
|
||||
})
|
||||
|
||||
fun checkAutofillChild(child: Autofill.Node): Int {
|
||||
var sum = 0
|
||||
|
@ -501,266 +510,4 @@ class AutofillDelegateTest : BaseSessionTest() {
|
|||
assertThat("autofill hint count",
|
||||
checkAutofillChild(root), equalTo(6))
|
||||
}
|
||||
|
||||
class MockViewNode : ViewStructure() {
|
||||
private var mClassName: String? = null
|
||||
private var mEnabled = false
|
||||
private var mVisibility = -1
|
||||
private var mPackageName: String? = null
|
||||
private var mTypeName: String? = null
|
||||
private var mEntryName: String? = null
|
||||
private var mAutofillType = -1
|
||||
private var mAutofillHints: Array<String>? = null
|
||||
private var mInputType = -1
|
||||
private var mHtmlInfo: HtmlInfo? = null
|
||||
private var mWebDomain: String? = null
|
||||
private var mFocused = false
|
||||
private var mFocusable = false
|
||||
|
||||
var children = ArrayList<MockViewNode?>()
|
||||
var id = View.NO_ID
|
||||
var height = 0
|
||||
var width = 0
|
||||
|
||||
val className get() = mClassName
|
||||
val htmlInfo get() = mHtmlInfo
|
||||
val autofillHints get() = mAutofillHints
|
||||
val autofillType get() = mAutofillType
|
||||
val webDomain get() = mWebDomain
|
||||
val isEnabled get() = mEnabled
|
||||
val isFocused get() = mFocused
|
||||
val isFocusable get() = mFocusable
|
||||
val visibility get() = mVisibility
|
||||
val inputType get() = mInputType
|
||||
|
||||
override fun setId(id: Int, packageName: String?, typeName: String?, entryName: String?) {
|
||||
this.id = id
|
||||
mPackageName = packageName
|
||||
mTypeName = typeName
|
||||
mEntryName = entryName
|
||||
}
|
||||
|
||||
override fun setHint(hint: CharSequence?) {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun setElevation(elevation: Float) {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun getText(): CharSequence {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun setText(text: CharSequence?) {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun setText(text: CharSequence?, selectionStart: Int, selectionEnd: Int) {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun asyncCommit() {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun getChildCount(): Int = children.size
|
||||
|
||||
override fun setEnabled(state: Boolean) {
|
||||
mEnabled = state
|
||||
}
|
||||
|
||||
override fun setLocaleList(localeList: LocaleList?) {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun setDimens(left: Int, top: Int, scrollX: Int, scrollY: Int, width: Int, height: Int) {
|
||||
this.width = width
|
||||
this.height = height
|
||||
}
|
||||
|
||||
override fun setChecked(state: Boolean) {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun setContextClickable(state: Boolean) {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun setAccessibilityFocused(state: Boolean) {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Float) {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun setTransformation(matrix: Matrix?) {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun setClassName(className: String?) {
|
||||
mClassName = className
|
||||
}
|
||||
|
||||
override fun setLongClickable(state: Boolean) {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun newChild(index: Int): ViewStructure {
|
||||
val child = MockViewNode()
|
||||
children[index] = child
|
||||
return child
|
||||
}
|
||||
|
||||
override fun getHint(): CharSequence {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun setInputType(inputType: Int) {
|
||||
mInputType = inputType
|
||||
}
|
||||
|
||||
override fun setWebDomain(domain: String?) {
|
||||
mWebDomain = domain
|
||||
}
|
||||
|
||||
override fun setAutofillOptions(options: Array<out CharSequence>?) {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun setTextStyle(size: Float, fgColor: Int, bgColor: Int, style: Int) {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun setVisibility(visibility: Int) {
|
||||
mVisibility = visibility
|
||||
}
|
||||
|
||||
override fun getAutofillId(): AutofillId? {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun setHtmlInfo(htmlInfo: HtmlInfo) {
|
||||
mHtmlInfo = htmlInfo
|
||||
}
|
||||
|
||||
override fun setTextLines(charOffsets: IntArray?, baselines: IntArray?) {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun getExtras(): Bundle {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun setClickable(state: Boolean) {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun newHtmlInfoBuilder(tagName: String): HtmlInfo.Builder {
|
||||
return MockHtmlInfoBuilder(tagName)
|
||||
}
|
||||
|
||||
override fun getTextSelectionEnd(): Int {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun setAutofillId(id: AutofillId) {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun setAutofillId(parentId: AutofillId, virtualId: Int) {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun hasExtras(): Boolean {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun addChildCount(num: Int): Int {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun setAutofillType(type: Int) {
|
||||
mAutofillType = type
|
||||
}
|
||||
|
||||
override fun setActivated(state: Boolean) {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun setFocused(state: Boolean) {
|
||||
mFocused = state
|
||||
}
|
||||
|
||||
override fun getTextSelectionStart(): Int {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun setChildCount(num: Int) {
|
||||
children = ArrayList()
|
||||
for (i in 0 until num) {
|
||||
children.add(null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setAutofillValue(value: AutofillValue?) {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun setAutofillHints(hint: Array<String>?) {
|
||||
mAutofillHints = hint
|
||||
}
|
||||
|
||||
override fun setContentDescription(contentDescription: CharSequence?) {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun setFocusable(state: Boolean) {
|
||||
mFocusable = state
|
||||
}
|
||||
|
||||
override fun setCheckable(state: Boolean) {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun asyncNewChild(index: Int): ViewStructure {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun setSelected(state: Boolean) {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun setDataIsSensitive(sensitive: Boolean) {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun setOpaque(opaque: Boolean) {
|
||||
TODO("not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
class MockHtmlInfoBuilder(tagName: String) : ViewStructure.HtmlInfo.Builder() {
|
||||
val mTagName = tagName
|
||||
val mAttributes: MutableList<Pair<String, String>> = mutableListOf()
|
||||
|
||||
override fun addAttribute(name: String, value: String): ViewStructure.HtmlInfo.Builder {
|
||||
mAttributes.add(Pair(name, value))
|
||||
return this
|
||||
}
|
||||
|
||||
override fun build(): ViewStructure.HtmlInfo {
|
||||
return MockHtmlInfo(mTagName, mAttributes)
|
||||
}
|
||||
}
|
||||
|
||||
class MockHtmlInfo(tagName: String, attributes: MutableList<Pair<String, String>>)
|
||||
: ViewStructure.HtmlInfo() {
|
||||
private val mTagName = tagName
|
||||
private val mAttributes = attributes
|
||||
|
||||
override fun getTag() = mTagName
|
||||
override fun getAttributes() = mAttributes
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ open class BaseSessionTest(noErrorCollector: Boolean = false) {
|
|||
const val DOWNLOAD_HTML_PATH = "/assets/www/download.html"
|
||||
const val FORM_BLANK_HTML_PATH = "/assets/www/form_blank.html"
|
||||
const val FORMS_HTML_PATH = "/assets/www/forms.html"
|
||||
const val FORMS_XORIGIN_HTML_PATH = "/assets/www/forms_xorigin.html"
|
||||
const val FORMS2_HTML_PATH = "/assets/www/forms2.html"
|
||||
const val FORMS3_HTML_PATH = "/assets/www/forms3.html"
|
||||
const val FORMS4_HTML_PATH = "/assets/www/forms4.html"
|
||||
|
|
|
@ -59,15 +59,15 @@ class MediaDelegateXOriginTest : BaseSessionTest() {
|
|||
mainSession.delegateDuringNextWait(object : MediaDelegate {
|
||||
@GeckoSessionTestRule.AssertCalled(count = 1)
|
||||
override fun onRecordingStatusChanged(session: GeckoSession,
|
||||
devices: Array<org.mozilla.geckoview.GeckoSession.MediaDelegate.RecordingDevice>) {
|
||||
devices: Array<MediaDelegate.RecordingDevice>) {
|
||||
var audioActive = false
|
||||
var cameraActive = false
|
||||
for (device in devices) {
|
||||
if (device.type == org.mozilla.geckoview.GeckoSession.MediaDelegate.RecordingDevice.Type.MICROPHONE) {
|
||||
audioActive = device.status != org.mozilla.geckoview.GeckoSession.MediaDelegate.RecordingDevice.Status.INACTIVE
|
||||
if (device.type == MediaDelegate.RecordingDevice.Type.MICROPHONE) {
|
||||
audioActive = device.status != MediaDelegate.RecordingDevice.Status.INACTIVE
|
||||
}
|
||||
if (device.type == org.mozilla.geckoview.GeckoSession.MediaDelegate.RecordingDevice.Type.CAMERA) {
|
||||
cameraActive = device.status != org.mozilla.geckoview.GeckoSession.MediaDelegate.RecordingDevice.Status.INACTIVE
|
||||
if (device.type == MediaDelegate.RecordingDevice.Type.CAMERA) {
|
||||
cameraActive = device.status != MediaDelegate.RecordingDevice.Status.INACTIVE
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,7 +76,6 @@ class MediaDelegateXOriginTest : BaseSessionTest() {
|
|||
|
||||
assertThat("Audio is ${if (allowAudio ) { "active" } else { "inactive" }}" ,
|
||||
audioActive, Matchers.equalTo(allowAudio))
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -149,9 +148,6 @@ class MediaDelegateXOriginTest : BaseSessionTest() {
|
|||
}
|
||||
|
||||
@Test fun testDeviceRecordingEventAudioAndVideoInXOriginIframe() {
|
||||
// TODO: Bug 1648153
|
||||
assumeThat(sessionRule.env.isFission, Matchers.equalTo(false))
|
||||
|
||||
// TODO: needs bug 1700243
|
||||
assumeThat(sessionRule.env.isIsolatedProcess, Matchers.equalTo(false))
|
||||
|
||||
|
@ -167,9 +163,6 @@ class MediaDelegateXOriginTest : BaseSessionTest() {
|
|||
}
|
||||
|
||||
@Test fun testDeviceRecordingEventAudioAndVideoInXOriginIframeNoAllow() {
|
||||
// TODO: Bug 1648153
|
||||
assumeThat(sessionRule.env.isFission, Matchers.equalTo(false))
|
||||
|
||||
mainSession.loadTestPath(GETUSERMEDIA_XORIGIN_CONTAINER_HTML_PATH)
|
||||
mainSession.waitForPageStop()
|
||||
|
||||
|
|
|
@ -151,7 +151,7 @@ class PermissionDelegateTest : BaseSessionTest() {
|
|||
window.navigator.mediaDevices.getUserMedia({ video: true })""")
|
||||
} else {
|
||||
mainSession.waitForJS("""
|
||||
window.navigator.mediaDevices.getUserMedia({ audio: true: video: true })""")
|
||||
window.navigator.mediaDevices.getUserMedia({ audio: true, video: true })""")
|
||||
}
|
||||
fail("Request should have failed")
|
||||
} catch (e: RejectedPromiseException) {
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -831,6 +831,7 @@ public class GeckoSession {
|
|||
final String event,
|
||||
final GeckoBundle message,
|
||||
final EventCallback callback) {
|
||||
Log.d(LOGTAG, "handleMessage: " + event);
|
||||
if (delegate == null) {
|
||||
callback.sendSuccess(/* granted */ false);
|
||||
return;
|
||||
|
@ -900,7 +901,7 @@ public class GeckoSession {
|
|||
final @SelectionActionDelegateAction HashSet<String> actionsSet =
|
||||
new HashSet<>(Arrays.asList(message.getStringArray("actions")));
|
||||
final SelectionActionDelegate.Selection selection =
|
||||
new SelectionActionDelegate.Selection(message, actionsSet, callback);
|
||||
new SelectionActionDelegate.Selection(message, actionsSet, mEventDispatcher);
|
||||
|
||||
delegate.onShowActionRequest(GeckoSession.this, selection);
|
||||
|
||||
|
@ -1318,14 +1319,12 @@ public class GeckoSession {
|
|||
@Override
|
||||
public void handleMessage(
|
||||
final String event, final GeckoBundle message, final EventCallback callback) {
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "handleMessage: event = " + event);
|
||||
}
|
||||
Log.d(LOGTAG, "handleMessage " + event);
|
||||
|
||||
if ("GeckoView:PinOnScreen".equals(event)) {
|
||||
GeckoSession.this.setShouldPinOnScreen(message.getBoolean("pinned"));
|
||||
} else if ("GeckoView:Prompt".equals(event)) {
|
||||
mPromptController.handleEvent(GeckoSession.this, message, callback);
|
||||
mPromptController.handleEvent(GeckoSession.this, message.getBundle("prompt"), callback);
|
||||
} else if ("GeckoView:Prompt:Dismiss".equals(event)) {
|
||||
mPromptController.dismissPrompt(message.getString("id"));
|
||||
}
|
||||
|
@ -3406,14 +3405,14 @@ public class GeckoSession {
|
|||
/** Set of valid actions available through {@link Selection#execute(String)} */
|
||||
public final @NonNull @SelectionActionDelegateAction Collection<String> availableActions;
|
||||
|
||||
private final int mSeqNo;
|
||||
private final String mActionId;
|
||||
|
||||
private final EventCallback mEventCallback;
|
||||
private final WeakReference<EventDispatcher> mEventDispatcher;
|
||||
|
||||
/* package */ Selection(
|
||||
final GeckoBundle bundle,
|
||||
final @NonNull @SelectionActionDelegateAction Set<String> actions,
|
||||
final EventCallback callback) {
|
||||
final EventDispatcher eventDispatcher) {
|
||||
flags =
|
||||
(bundle.getBoolean("collapsed") ? SelectionActionDelegate.FLAG_IS_COLLAPSED : 0)
|
||||
| (bundle.getBoolean("editable") ? SelectionActionDelegate.FLAG_IS_EDITABLE : 0)
|
||||
|
@ -3421,8 +3420,8 @@ public class GeckoSession {
|
|||
text = bundle.getString("selection");
|
||||
clientRect = bundle.getRectF("clientRect");
|
||||
availableActions = actions;
|
||||
mSeqNo = bundle.getInt("seqNo");
|
||||
mEventCallback = callback;
|
||||
mActionId = bundle.getString("actionId");
|
||||
mEventDispatcher = new WeakReference<>(eventDispatcher);
|
||||
}
|
||||
|
||||
/** Empty constructor for tests. */
|
||||
|
@ -3431,8 +3430,8 @@ public class GeckoSession {
|
|||
text = "";
|
||||
clientRect = null;
|
||||
availableActions = new HashSet<>();
|
||||
mSeqNo = 0;
|
||||
mEventCallback = null;
|
||||
mActionId = null;
|
||||
mEventDispatcher = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3458,10 +3457,16 @@ public class GeckoSession {
|
|||
if (!isActionAvailable(action)) {
|
||||
throw new IllegalStateException("Action not available");
|
||||
}
|
||||
final EventDispatcher eventDispatcher = mEventDispatcher.get();
|
||||
if (eventDispatcher == null) {
|
||||
// The session is not available anymore, nothing really to do
|
||||
Log.w(LOGTAG, "Calling execute on a stale Selection.");
|
||||
return;
|
||||
}
|
||||
final GeckoBundle response = new GeckoBundle(2);
|
||||
response.putString("id", action);
|
||||
response.putInt("seqNo", mSeqNo);
|
||||
mEventCallback.sendSuccess(response);
|
||||
response.putString("actionId", mActionId);
|
||||
eventDispatcher.dispatch("GeckoView:ExecuteSelectionAction", response);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -23,7 +23,6 @@ import android.os.Build;
|
|||
import android.os.Handler;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.util.TypedValue;
|
||||
import android.view.DisplayCutout;
|
||||
|
@ -50,6 +49,7 @@ import androidx.annotation.UiThread;
|
|||
import androidx.core.view.ViewCompat;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.Objects;
|
||||
import org.mozilla.gecko.AndroidGamepadManager;
|
||||
import org.mozilla.gecko.EventDispatcher;
|
||||
import org.mozilla.gecko.InputMethods;
|
||||
|
@ -272,7 +272,12 @@ public class GeckoView extends FrameLayout {
|
|||
mSelectionActionDelegate = new BasicSelectionActionDelegate(activity);
|
||||
}
|
||||
|
||||
mAutofillDelegate = new AndroidAutofillDelegate();
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
mAutofillDelegate = new AndroidAutofillDelegate();
|
||||
} else {
|
||||
// We don't support Autofill on SDK < 26
|
||||
mAutofillDelegate = new Autofill.Delegate() {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -915,10 +920,26 @@ public class GeckoView extends FrameLayout {
|
|||
return mAutofillEnabled;
|
||||
}
|
||||
|
||||
@TargetApi(26)
|
||||
private class AndroidAutofillDelegate implements Autofill.Delegate {
|
||||
AutofillManager mAutofillManager;
|
||||
boolean mDisabled = false;
|
||||
|
||||
private void ensureAutofillManager() {
|
||||
if (mDisabled || mAutofillManager != null) {
|
||||
// Nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
mAutofillManager = GeckoView.this.getContext().getSystemService(AutofillManager.class);
|
||||
if (mAutofillManager == null) {
|
||||
// If we can't get a reference to the autofill manager, we cannot run the autofill service
|
||||
mDisabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private Rect displayRectForId(
|
||||
@NonNull final GeckoSession session, @NonNull final Autofill.Node node) {
|
||||
@NonNull final GeckoSession session, @Nullable final Autofill.Node node) {
|
||||
if (node == null) {
|
||||
return new Rect(0, 0, 0, 0);
|
||||
}
|
||||
|
@ -934,43 +955,95 @@ public class GeckoView extends FrameLayout {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onAutofill(
|
||||
@NonNull final GeckoSession session, final int notification, final Autofill.Node node) {
|
||||
ThreadUtils.assertOnUiThread();
|
||||
if (Build.VERSION.SDK_INT < 26) {
|
||||
public void onNodeBlur(
|
||||
final @NonNull GeckoSession session,
|
||||
final @NonNull Autofill.Node prev,
|
||||
final @NonNull Autofill.NodeData data) {
|
||||
ensureAutofillManager();
|
||||
if (mAutofillManager != null) {
|
||||
mAutofillManager.notifyViewExited(GeckoView.this, data.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNodeAdd(
|
||||
final @NonNull GeckoSession session,
|
||||
final @NonNull Autofill.Node node,
|
||||
final @NonNull Autofill.NodeData data) {
|
||||
if (!mSession.getAutofillSession().isVisible(node)) {
|
||||
return;
|
||||
}
|
||||
final Autofill.Node focused = mSession.getAutofillSession().getFocused();
|
||||
// We must have a focused node because |node| is visible
|
||||
Objects.requireNonNull(focused);
|
||||
|
||||
final AutofillManager manager =
|
||||
GeckoView.this.getContext().getSystemService(AutofillManager.class);
|
||||
if (manager == null) {
|
||||
return;
|
||||
final Autofill.NodeData focusedData = mSession.getAutofillSession().dataFor(focused);
|
||||
Objects.requireNonNull(focusedData);
|
||||
|
||||
ensureAutofillManager();
|
||||
if (mAutofillManager != null) {
|
||||
mAutofillManager.notifyViewExited(GeckoView.this, focusedData.getId());
|
||||
mAutofillManager.notifyViewEntered(
|
||||
GeckoView.this, focusedData.getId(), displayRectForId(session, focused));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
switch (notification) {
|
||||
case Autofill.Notify.SESSION_STARTED:
|
||||
// This line seems necessary for auto-fill to work on the initial page.
|
||||
case Autofill.Notify.SESSION_CANCELED:
|
||||
manager.cancel();
|
||||
break;
|
||||
case Autofill.Notify.SESSION_COMMITTED:
|
||||
manager.commit();
|
||||
break;
|
||||
case Autofill.Notify.NODE_FOCUSED:
|
||||
manager.notifyViewEntered(
|
||||
GeckoView.this, node.getId(), displayRectForId(session, node));
|
||||
break;
|
||||
case Autofill.Notify.NODE_BLURRED:
|
||||
manager.notifyViewExited(GeckoView.this, node.getId());
|
||||
break;
|
||||
case Autofill.Notify.NODE_UPDATED:
|
||||
manager.notifyValueChanged(
|
||||
GeckoView.this, node.getId(), AutofillValue.forText(node.getValue()));
|
||||
break;
|
||||
}
|
||||
} catch (final SecurityException e) {
|
||||
Log.e(LOGTAG, "Failed to call Autofill Manager API: ", e);
|
||||
@Override
|
||||
public void onNodeFocus(
|
||||
final @NonNull GeckoSession session,
|
||||
final @NonNull Autofill.Node focused,
|
||||
final @NonNull Autofill.NodeData data) {
|
||||
ensureAutofillManager();
|
||||
if (mAutofillManager != null) {
|
||||
mAutofillManager.notifyViewEntered(
|
||||
GeckoView.this, data.getId(), displayRectForId(session, focused));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNodeRemove(
|
||||
final @NonNull GeckoSession session,
|
||||
final @NonNull Autofill.Node node,
|
||||
final @NonNull Autofill.NodeData data) {}
|
||||
|
||||
@Override
|
||||
public void onNodeUpdate(
|
||||
final @NonNull GeckoSession session,
|
||||
final @NonNull Autofill.Node node,
|
||||
final @NonNull Autofill.NodeData data) {
|
||||
ensureAutofillManager();
|
||||
if (mAutofillManager != null) {
|
||||
mAutofillManager.notifyValueChanged(
|
||||
GeckoView.this, data.getId(), AutofillValue.forText(data.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionCancel(final @NonNull GeckoSession session) {
|
||||
ensureAutofillManager();
|
||||
if (mAutofillManager != null) {
|
||||
// This line seems necessary for auto-fill to work on the initial page.
|
||||
mAutofillManager.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionCommit(
|
||||
final @NonNull GeckoSession session,
|
||||
final @NonNull Autofill.Node node,
|
||||
final @NonNull Autofill.NodeData data) {
|
||||
ensureAutofillManager();
|
||||
if (mAutofillManager != null) {
|
||||
mAutofillManager.commit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionStart(final @NonNull GeckoSession session) {
|
||||
ensureAutofillManager();
|
||||
if (mAutofillManager != null) {
|
||||
// This line seems necessary for auto-fill to work on the initial page.
|
||||
mAutofillManager.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,6 +79,7 @@ import org.mozilla.geckoview.GeckoSession.PromptDelegate.TextPrompt;
|
|||
|
||||
public void handleEvent(
|
||||
final GeckoSession session, final GeckoBundle message, final EventCallback callback) {
|
||||
Log.d(LOGTAG, "handleEvent " + message.getString("type"));
|
||||
final PromptDelegate delegate = session.getPromptDelegate();
|
||||
if (delegate == null) {
|
||||
// Default behavior is same as calling dismiss() on callback.
|
||||
|
|
|
@ -20,6 +20,12 @@ exclude: true
|
|||
- Added [`GeckoSession.setPriorityHint`][102.5] function to set the session to either high priority or default.
|
||||
- [`WebRequestError.ERROR_HTTPS_ONLY`][102.6] now has error category
|
||||
`ERROR_CATEGORY_NETWORK` rather than `ERROR_CATEGORY_SECURITY`.
|
||||
- ⚠️ The Autofill.Delegate API now receives a [`AutofillNode`][102.7] object instead of
|
||||
the entire [`Node`][102.8] structure. The `onAutofill` delegate method is now split
|
||||
into several methods: [`onNodeAdd`][102.9], [`onNodeBlur`][102.10],
|
||||
[`onNodeFocus`][102.11], [`onNodeRemove`][102.12], [`onNodeUpdate`][102.13],
|
||||
[`onSessionCancel`][102.14], [`onSessionCommit`][102.15],
|
||||
[`onSessionStart`][102.16].
|
||||
|
||||
[102.1]: {{javadoc_uri}}/GeckoSession.PromptDelegate.DateTimePrompt.html#stepValue
|
||||
[102.2]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date#step
|
||||
|
@ -27,6 +33,16 @@ exclude: true
|
|||
[102.4]: {{javadoc_uri}}/GeckoSession.NavigationDelegate.html#onLocationChange(org.mozilla.geckoview.GeckoSession,java.lang.String,java.util.List)
|
||||
[102.5]: {{javadoc_uri}}/GeckoSession.html#setPriorityHint(int)
|
||||
[102.6]: {{javadoc_uri}}/WebRequestError.html#ERROR_HTTPS_ONLY
|
||||
[102.7]: {{javadoc_uri}}/Autofill.AutofillNode.html
|
||||
[102.8]: {{javadoc_uri}}/Autofill.Node.html
|
||||
[102.9]: {{javadoc_uri}}/Autofill.Delegate.html#onNodeAdd(org.mozilla.geckoview.GeckoSession,org.mozilla.geckoview.Autofill.Node,org.mozilla.geckoview.Autofill.NodeData)
|
||||
[102.10]: {{javadoc_uri}}/Autofill.Delegate.html#onNodeBlur(org.mozilla.geckoview.GeckoSession,org.mozilla.geckoview.Autofill.Node,org.mozilla.geckoview.Autofill.NodeData)
|
||||
[102.11]: {{javadoc_uri}}/Autofill.Delegate.html#onNodeFocus(org.mozilla.geckoview.GeckoSession,org.mozilla.geckoview.Autofill.Node,org.mozilla.geckoview.Autofill.NodeData)
|
||||
[102.12]: {{javadoc_uri}}/Autofill.Delegate.html#onNodeRemove(org.mozilla.geckoview.GeckoSession,org.mozilla.geckoview.Autofill.Node,org.mozilla.geckoview.Autofill.NodeData)
|
||||
[102.13]: {{javadoc_uri}}/Autofill.Delegate.html#onNodeUpdate(org.mozilla.geckoview.GeckoSession,org.mozilla.geckoview.Autofill.Node,org.mozilla.geckoview.Autofill.NodeData)
|
||||
[102.14]: {{javadoc_uri}}/Autofill.Delegate.html#onSessionCancel(org.mozilla.geckoview.GeckoSession)
|
||||
[102.15]: {{javadoc_uri}}/Autofill.Delegate.html#onSessionCommit(org.mozilla.geckoview.GeckoSession,org.mozilla.geckoview.Autofill.Node,org.mozilla.geckoview.Autofill.NodeData)
|
||||
[102.16]: {{javadoc_uri}}/Autofill.Delegate.html#onSessionStart(org.mozilla.geckoview.GeckoSession)
|
||||
|
||||
## v101
|
||||
- Added [`GeckoDisplay.surfaceChanged`][101.1] function taking new type [`GeckoDisplay.SurfaceInfo`][101.2].
|
||||
|
@ -1181,4 +1197,4 @@ to allow adding gecko profiler markers.
|
|||
[65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport(android.content.Context,android.os.Bundle,java.lang.String)
|
||||
[65.25]: {{javadoc_uri}}/GeckoResult.html
|
||||
|
||||
[api-version]: 2f401dd976431a7a150b8743f3949a50adbeeb4b
|
||||
[api-version]: dbe27cda45ed1bdff264b14f4471fdbe8438ca94
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
const { GeckoViewUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/GeckoViewUtils.jsm"
|
||||
);
|
||||
const { EventDispatcher } = ChromeUtils.import(
|
||||
"resource://gre/modules/Messaging.jsm"
|
||||
);
|
||||
|
||||
var EXPORTED_SYMBOLS = ["GeckoViewActorChild"];
|
||||
|
||||
|
@ -15,9 +18,7 @@ class GeckoViewActorChild extends JSWindowActorChild {
|
|||
}
|
||||
|
||||
actorCreated() {
|
||||
this.eventDispatcher = GeckoViewUtils.getDispatcherForWindow(
|
||||
this.docShell.domWindow
|
||||
);
|
||||
this.eventDispatcher = EventDispatcher.forActor(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,15 +19,37 @@ class GeckoViewActorParent extends JSWindowActorParent {
|
|||
}
|
||||
|
||||
get window() {
|
||||
return this.browser.ownerGlobal;
|
||||
const { browsingContext } = this;
|
||||
// If this is a chrome actor, the chrome window will be at
|
||||
// browsingContext.window.
|
||||
if (!browsingContext.isContent && browsingContext.window) {
|
||||
return browsingContext.window;
|
||||
}
|
||||
return this.browser?.ownerGlobal;
|
||||
}
|
||||
|
||||
get eventDispatcher() {
|
||||
return this.window.moduleManager.eventDispatcher;
|
||||
return this.window?.moduleManager.eventDispatcher;
|
||||
}
|
||||
|
||||
receiveMessage(aMessage) {
|
||||
if (!this.window) {
|
||||
// If we have no window, it means that this browsingContext has been
|
||||
// destroyed already and there's nothing to do here for us.
|
||||
debug`receiveMessage window destroyed ${aMessage.name} ${aMessage.data?.type}`;
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (aMessage.name) {
|
||||
case "DispatcherMessage":
|
||||
return this.eventDispatcher.sendRequest(aMessage.data);
|
||||
case "DispatcherQuery":
|
||||
return this.eventDispatcher.sendRequestForResult(aMessage.data);
|
||||
}
|
||||
|
||||
// By default messages are forwarded to the module.
|
||||
this.window.moduleManager.onMessageFromActor(this.name, aMessage);
|
||||
return this.window.moduleManager.onMessageFromActor(this.name, aMessage);
|
||||
}
|
||||
}
|
||||
|
||||
const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewActorParent");
|
||||
|
|
|
@ -1,356 +1,101 @@
|
|||
// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
|
||||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["GeckoViewAutofill"];
|
||||
|
||||
const { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
const { GeckoViewUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/GeckoViewUtils.jsm"
|
||||
);
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
DeferredTask: "resource://gre/modules/DeferredTask.jsm",
|
||||
FormLikeFactory: "resource://gre/modules/FormLikeFactory.jsm",
|
||||
LoginManagerChild: "resource://gre/modules/LoginManagerChild.jsm",
|
||||
Services: "resource://gre/modules/Services.jsm",
|
||||
});
|
||||
var EXPORTED_SYMBOLS = ["gAutofillManager"];
|
||||
|
||||
const { debug, warn } = GeckoViewUtils.initLogging("Autofill");
|
||||
|
||||
class GeckoViewAutofill {
|
||||
constructor(aEventDispatcher) {
|
||||
this._eventDispatcher = aEventDispatcher;
|
||||
this._autofillElements = undefined;
|
||||
this._autofillInfos = undefined;
|
||||
this._autofillTasks = undefined;
|
||||
class Autofill {
|
||||
constructor(sessionId, eventDispatcher) {
|
||||
this.eventDispatcher = eventDispatcher;
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an auto-fillable form and send the relevant details of the form
|
||||
* to Java. Multiple calls within a short time period for the same form are
|
||||
* coalesced, so that, e.g., if multiple inputs are added to a form in
|
||||
* succession, we will only perform one processing pass. Note that for inputs
|
||||
* without forms, FormLikeFactory treats the document as the "form", but
|
||||
* there is no difference in how we process them.
|
||||
*
|
||||
* @param aFormLike A FormLike object produced by FormLikeFactory.
|
||||
*/
|
||||
addElement(aFormLike) {
|
||||
this._addElement(aFormLike, /* fromDeferredTask */ false);
|
||||
}
|
||||
|
||||
_getInfo(aElement, aParent, aRoot, aUsernameField) {
|
||||
if (!this._autofillInfos) {
|
||||
this._autofillInfos = new WeakMap();
|
||||
this._autofillElements = new Map();
|
||||
}
|
||||
|
||||
let info = this._autofillInfos.get(aElement);
|
||||
if (info) {
|
||||
return info;
|
||||
}
|
||||
|
||||
const window = aElement.ownerGlobal;
|
||||
const bounds = aElement.getBoundingClientRect();
|
||||
const isInputElement = window.HTMLInputElement.isInstance(aElement);
|
||||
|
||||
info = {
|
||||
isInputElement,
|
||||
uuid: Services.uuid
|
||||
.generateUUID()
|
||||
.toString()
|
||||
.slice(1, -1), // discard the surrounding curly braces
|
||||
parentUuid: aParent,
|
||||
rootUuid: aRoot,
|
||||
tag: aElement.tagName,
|
||||
type: isInputElement ? aElement.type : null,
|
||||
value: isInputElement ? aElement.value : null,
|
||||
editable:
|
||||
isInputElement &&
|
||||
[
|
||||
"color",
|
||||
"date",
|
||||
"datetime-local",
|
||||
"email",
|
||||
"month",
|
||||
"number",
|
||||
"password",
|
||||
"range",
|
||||
"search",
|
||||
"tel",
|
||||
"text",
|
||||
"time",
|
||||
"url",
|
||||
"week",
|
||||
].includes(aElement.type),
|
||||
disabled: isInputElement ? aElement.disabled : null,
|
||||
attributes: Object.assign(
|
||||
{},
|
||||
...Array.from(aElement.attributes)
|
||||
.filter(attr => attr.localName !== "value")
|
||||
.map(attr => ({ [attr.localName]: attr.value }))
|
||||
),
|
||||
origin: aElement.ownerDocument.location.origin,
|
||||
autofillhint: "",
|
||||
bounds: {
|
||||
left: bounds.left,
|
||||
top: bounds.top,
|
||||
right: bounds.right,
|
||||
bottom: bounds.bottom,
|
||||
},
|
||||
};
|
||||
|
||||
if (aElement === aUsernameField) {
|
||||
info.autofillhint = "username"; // AUTOFILL.HINT.USERNAME
|
||||
} else if (isInputElement) {
|
||||
// Using autocomplete attribute if it is email.
|
||||
const autocompleteInfo = aElement.getAutocompleteInfo();
|
||||
if (autocompleteInfo) {
|
||||
const autocompleteAttr = autocompleteInfo.fieldName;
|
||||
if (autocompleteAttr == "email") {
|
||||
info.type = "email";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._autofillInfos.set(aElement, info);
|
||||
this._autofillElements.set(info.uuid, Cu.getWeakReference(aElement));
|
||||
return info;
|
||||
}
|
||||
|
||||
_updateInfoValues(aElements) {
|
||||
if (!this._autofillInfos) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const updated = [];
|
||||
for (const element of aElements) {
|
||||
const info = this._autofillInfos.get(element);
|
||||
|
||||
if (!info?.isInputElement || info.value === element.value) {
|
||||
continue;
|
||||
}
|
||||
debug`Updating value ${info.value} to ${element.value}`;
|
||||
|
||||
info.value = element.value;
|
||||
this._autofillInfos.set(element, info);
|
||||
updated.push(info);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
_addElement(aFormLike, aFromDeferredTask) {
|
||||
let task =
|
||||
this._autofillTasks && this._autofillTasks.get(aFormLike.rootElement);
|
||||
if (task && !aFromDeferredTask) {
|
||||
// We already have a pending task; cancel that and start a new one.
|
||||
debug`Canceling previous auto-fill task`;
|
||||
task.disarm();
|
||||
task = null;
|
||||
}
|
||||
|
||||
if (!task) {
|
||||
if (aFromDeferredTask) {
|
||||
// Canceled before we could run the task.
|
||||
debug`Auto-fill task canceled`;
|
||||
return;
|
||||
}
|
||||
// Start a new task so we can coalesce adding elements in one batch.
|
||||
debug`Deferring auto-fill task`;
|
||||
task = new DeferredTask(() => this._addElement(aFormLike, true), 100);
|
||||
task.arm();
|
||||
if (!this._autofillTasks) {
|
||||
this._autofillTasks = new WeakMap();
|
||||
}
|
||||
this._autofillTasks.set(aFormLike.rootElement, task);
|
||||
return;
|
||||
}
|
||||
|
||||
debug`Adding auto-fill ${aFormLike.rootElement.tagName}`;
|
||||
|
||||
this._autofillTasks.delete(aFormLike.rootElement);
|
||||
|
||||
const window = aFormLike.rootElement.ownerGlobal;
|
||||
// Get password field to get better form data via LoginManagerChild.
|
||||
let passwordField;
|
||||
for (const field of aFormLike.elements) {
|
||||
if (
|
||||
ChromeUtils.getClassName(field) === "HTMLInputElement" &&
|
||||
field.type == "password"
|
||||
) {
|
||||
passwordField = field;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const [usernameField] = LoginManagerChild.forWindow(
|
||||
window
|
||||
).getUserNameAndPasswordFields(passwordField || aFormLike.elements[0]);
|
||||
|
||||
const focusedElement = aFormLike.rootElement.ownerDocument.activeElement;
|
||||
let sendFocusEvent = aFormLike.rootElement === focusedElement;
|
||||
|
||||
const rootInfo = this._getInfo(
|
||||
aFormLike.rootElement,
|
||||
null,
|
||||
undefined,
|
||||
null
|
||||
);
|
||||
|
||||
rootInfo.rootUuid = rootInfo.uuid;
|
||||
rootInfo.children = aFormLike.elements
|
||||
.filter(
|
||||
element =>
|
||||
element.type != "hidden" &&
|
||||
(!usernameField ||
|
||||
element.type != "text" ||
|
||||
element == usernameField ||
|
||||
(element.getAutocompleteInfo() &&
|
||||
element.getAutocompleteInfo().fieldName == "email"))
|
||||
)
|
||||
.map(element => {
|
||||
sendFocusEvent |= element === focusedElement;
|
||||
return this._getInfo(
|
||||
element,
|
||||
rootInfo.uuid,
|
||||
rootInfo.uuid,
|
||||
usernameField
|
||||
);
|
||||
});
|
||||
|
||||
this._eventDispatcher.dispatch("GeckoView:AddAutofill", rootInfo, {
|
||||
onSuccess: responses => {
|
||||
// `responses` is an object with global IDs as keys.
|
||||
debug`Performing auto-fill ${Object.keys(responses)}`;
|
||||
|
||||
const AUTOFILL_STATE = "autofill";
|
||||
const winUtils = window.windowUtils;
|
||||
|
||||
for (const uuid in responses) {
|
||||
const entry =
|
||||
this._autofillElements && this._autofillElements.get(uuid);
|
||||
const element = entry && entry.get();
|
||||
const value = responses[uuid] || "";
|
||||
|
||||
if (
|
||||
window.HTMLInputElement.isInstance(element) &&
|
||||
!element.disabled &&
|
||||
element.parentElement
|
||||
) {
|
||||
element.setUserInput(value);
|
||||
if (winUtils && element.value === value) {
|
||||
// Add highlighting for autofilled fields.
|
||||
winUtils.addManuallyManagedState(element, AUTOFILL_STATE);
|
||||
|
||||
// Remove highlighting when the field is changed.
|
||||
element.addEventListener(
|
||||
"input",
|
||||
_ =>
|
||||
winUtils.removeManuallyManagedState(element, AUTOFILL_STATE),
|
||||
{ mozSystemGroup: true, once: true }
|
||||
);
|
||||
}
|
||||
} else if (element) {
|
||||
warn`Don't know how to auto-fill ${element.tagName}`;
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: error => {
|
||||
warn`Cannot perform autofill ${error}`;
|
||||
},
|
||||
start() {
|
||||
this.eventDispatcher.sendRequest({
|
||||
type: "GeckoView:StartAutofill",
|
||||
sessionId: this.sessionId,
|
||||
});
|
||||
|
||||
if (sendFocusEvent) {
|
||||
// We might have missed sending a focus event for the active element.
|
||||
this.onFocus(aFormLike.ownerDocument.activeElement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an auto-fillable field is focused or blurred.
|
||||
*
|
||||
* @param aTarget Focused element, or null if an element has lost focus.
|
||||
*/
|
||||
onFocus(aTarget) {
|
||||
debug`Auto-fill focus on ${aTarget && aTarget.tagName}`;
|
||||
|
||||
const info =
|
||||
aTarget && this._autofillInfos && this._autofillInfos.get(aTarget);
|
||||
if (!aTarget || info) {
|
||||
this._eventDispatcher.dispatch("GeckoView:OnAutofillFocus", info);
|
||||
}
|
||||
add(node) {
|
||||
return this.eventDispatcher.sendRequestForResult({
|
||||
type: "GeckoView:AddAutofill",
|
||||
node,
|
||||
});
|
||||
}
|
||||
|
||||
commitAutofill(aFormLike) {
|
||||
if (!aFormLike) {
|
||||
throw new Error("null-form on autofill commit");
|
||||
}
|
||||
|
||||
debug`Committing auto-fill for ${aFormLike.rootElement.tagName}`;
|
||||
|
||||
const updatedNodeInfos = this._updateInfoValues([
|
||||
aFormLike.rootElement,
|
||||
...aFormLike.elements,
|
||||
]);
|
||||
|
||||
for (const updatedInfo of updatedNodeInfos) {
|
||||
debug`Updating node ${updatedInfo}`;
|
||||
this._eventDispatcher.dispatch("GeckoView:UpdateAutofill", updatedInfo);
|
||||
}
|
||||
|
||||
const info = this._getInfo(aFormLike.rootElement);
|
||||
if (info) {
|
||||
debug`Committing node ${info}`;
|
||||
this._eventDispatcher.dispatch("GeckoView:CommitAutofill", info);
|
||||
}
|
||||
focus(node) {
|
||||
this.eventDispatcher.sendRequest({
|
||||
type: "GeckoView:OnAutofillFocus",
|
||||
node,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all tracked auto-fill forms and notify Java.
|
||||
*/
|
||||
clearElements() {
|
||||
debug`Clearing auto-fill`;
|
||||
update(node) {
|
||||
this.eventDispatcher.sendRequest({
|
||||
type: "GeckoView:UpdateAutofill",
|
||||
node,
|
||||
});
|
||||
}
|
||||
|
||||
this._autofillTasks = undefined;
|
||||
this._autofillInfos = undefined;
|
||||
this._autofillElements = undefined;
|
||||
commit(node) {
|
||||
this.eventDispatcher.sendRequest({
|
||||
type: "GeckoView:CommitAutofill",
|
||||
node,
|
||||
});
|
||||
}
|
||||
|
||||
this._eventDispatcher.sendRequest({
|
||||
clear() {
|
||||
this.eventDispatcher.sendRequest({
|
||||
type: "GeckoView:ClearAutofill",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan for auto-fillable forms and add them if necessary. Called when a page
|
||||
* is navigated to through history, in which case we don't get our typical
|
||||
* "input added" notifications.
|
||||
*
|
||||
* @param aDoc Document to scan.
|
||||
*/
|
||||
scanDocument(aDoc) {
|
||||
// Add forms first; only check forms with password inputs.
|
||||
const inputs = aDoc.querySelectorAll("input[type=password]");
|
||||
let inputAdded = false;
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
if (inputs[i].form) {
|
||||
// Let _addElement coalesce multiple calls for the same form.
|
||||
this._addElement(FormLikeFactory.createFromForm(inputs[i].form));
|
||||
} else if (!inputAdded) {
|
||||
// Treat inputs without forms as one unit, and process them only once.
|
||||
inputAdded = true;
|
||||
this._addElement(FormLikeFactory.createFromField(inputs[i]));
|
||||
}
|
||||
}
|
||||
class AutofillManager {
|
||||
sessions = new Set();
|
||||
autofill = null;
|
||||
|
||||
// Finally add frames.
|
||||
const frames = aDoc.defaultView.frames;
|
||||
for (let i = 0; i < frames.length; i++) {
|
||||
this.scanDocument(frames[i].document);
|
||||
ensure(sessionId, eventDispatcher) {
|
||||
if (!this.sessions.has(sessionId)) {
|
||||
this.autofill = new Autofill(sessionId, eventDispatcher);
|
||||
this.sessions.add(sessionId);
|
||||
this.autofill.start();
|
||||
}
|
||||
// This could be called for an outdated session, in which case we will just
|
||||
// ignore the autofill call.
|
||||
if (sessionId !== this.autofill.sessionId) {
|
||||
return null;
|
||||
}
|
||||
return this.autofill;
|
||||
}
|
||||
|
||||
get(sessionId) {
|
||||
if (!this.autofill || sessionId !== this.autofill.sessionId) {
|
||||
warn`Disregarding old session ${sessionId}`;
|
||||
// We disregard old sessions
|
||||
return null;
|
||||
}
|
||||
return this.autofill;
|
||||
}
|
||||
|
||||
delete(sessionId) {
|
||||
this.sessions.delete(sessionId);
|
||||
if (!this.autofill || sessionId !== this.autofill.sessionId) {
|
||||
// this delete call might happen *after* the next session already
|
||||
// started, in that case, we can safely ignore this call.
|
||||
return;
|
||||
}
|
||||
this.autofill.clear();
|
||||
this.autofill = null;
|
||||
}
|
||||
}
|
||||
|
||||
var gAutofillManager = new AutofillManager();
|
||||
|
||||
const { debug, warn } = GeckoViewUtils.initLogging("Autofill");
|
||||
|
|
|
@ -1,100 +0,0 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["GeckoViewRecordingMedia"];
|
||||
|
||||
const { GeckoViewUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/GeckoViewUtils.jsm"
|
||||
);
|
||||
const { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(
|
||||
this,
|
||||
"MediaManagerService",
|
||||
"@mozilla.org/mediaManagerService;1",
|
||||
"nsIMediaManagerService"
|
||||
);
|
||||
|
||||
const STATUS_RECORDING = "recording";
|
||||
const STATUS_INACTIVE = "inactive";
|
||||
const TYPE_CAMERA = "camera";
|
||||
const TYPE_MICROPHONE = "microphone";
|
||||
|
||||
const GeckoViewRecordingMedia = {
|
||||
// The event listener for this is hooked up in GeckoViewStartup.jsm
|
||||
observe(aSubject, aTopic, aData) {
|
||||
debug`observe: aTopic=${aTopic}`;
|
||||
switch (aTopic) {
|
||||
case "recording-device-events": {
|
||||
this.handleRecordingDeviceEvents();
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
handleRecordingDeviceEvents() {
|
||||
const [dispatcher] = GeckoViewUtils.getActiveDispatcherAndWindow();
|
||||
if (dispatcher) {
|
||||
const windows = MediaManagerService.activeMediaCaptureWindows;
|
||||
const devices = [];
|
||||
|
||||
const getStatusString = function(activityStatus) {
|
||||
switch (activityStatus) {
|
||||
case MediaManagerService.STATE_CAPTURE_ENABLED:
|
||||
case MediaManagerService.STATE_CAPTURE_DISABLED:
|
||||
return STATUS_RECORDING;
|
||||
case MediaManagerService.STATE_NOCAPTURE:
|
||||
return STATUS_INACTIVE;
|
||||
default:
|
||||
throw new Error("Unexpected activityStatus value");
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < windows.length; i++) {
|
||||
const win = windows.queryElementAt(i, Ci.nsIDOMWindow);
|
||||
const hasCamera = {};
|
||||
const hasMicrophone = {};
|
||||
const screen = {};
|
||||
const window = {};
|
||||
const browser = {};
|
||||
const mediaDevices = {};
|
||||
MediaManagerService.mediaCaptureWindowState(
|
||||
win,
|
||||
hasCamera,
|
||||
hasMicrophone,
|
||||
screen,
|
||||
window,
|
||||
browser,
|
||||
mediaDevices
|
||||
);
|
||||
var cameraStatus = getStatusString(hasCamera.value);
|
||||
var microphoneStatus = getStatusString(hasMicrophone.value);
|
||||
if (hasCamera.value != MediaManagerService.STATE_NOCAPTURE) {
|
||||
devices.push({
|
||||
type: TYPE_CAMERA,
|
||||
status: cameraStatus,
|
||||
});
|
||||
}
|
||||
if (hasMicrophone.value != MediaManagerService.STATE_NOCAPTURE) {
|
||||
devices.push({
|
||||
type: TYPE_MICROPHONE,
|
||||
status: microphoneStatus,
|
||||
});
|
||||
}
|
||||
}
|
||||
dispatcher.sendRequestForResult({
|
||||
type: "GeckoView:MediaRecordingStatusChanged",
|
||||
devices,
|
||||
});
|
||||
} else {
|
||||
console.log("no dispatcher present");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewMedia");
|
|
@ -0,0 +1,50 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["GeckoViewSelectionAction"];
|
||||
|
||||
const { GeckoViewModule } = ChromeUtils.import(
|
||||
"resource://gre/modules/GeckoViewModule.jsm"
|
||||
);
|
||||
|
||||
const { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
Services: "resource://gre/modules/Services.jsm",
|
||||
});
|
||||
|
||||
class GeckoViewSelectionAction extends GeckoViewModule {
|
||||
onEnable() {
|
||||
debug`onEnable`;
|
||||
this.registerListener(["GeckoView:ExecuteSelectionAction"]);
|
||||
}
|
||||
|
||||
onDisable() {
|
||||
debug`onDisable`;
|
||||
this.unregisterListener();
|
||||
}
|
||||
|
||||
get actor() {
|
||||
return this.getActor("SelectionActionDelegate");
|
||||
}
|
||||
|
||||
// Bundle event handler.
|
||||
onEvent(aEvent, aData, aCallback) {
|
||||
debug`onEvent: ${aEvent}`;
|
||||
|
||||
switch (aEvent) {
|
||||
case "GeckoView:ExecuteSelectionAction": {
|
||||
this.actor.executeSelectionAction(aData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { debug, warn } = GeckoViewSelectionAction.initLogging(
|
||||
"GeckoViewSelectionAction"
|
||||
);
|
|
@ -211,47 +211,6 @@ var GeckoViewUtils = {
|
|||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add lazy event listeners on the per-window EventDispatcher, and only load
|
||||
* the actual handler when an event is being handled.
|
||||
*
|
||||
* @param window Window with the target EventDispatcher.
|
||||
* @param events Event name as a string or array.
|
||||
* @param handler If specified, function that, for a given event, returns the
|
||||
* actual event handler as an object or an array of objects.
|
||||
* If handler is not specified, the actual event handler is
|
||||
* specified using the scope and name pair.
|
||||
* @param scope See handler.
|
||||
* @param name See handler.
|
||||
* @param once If true, only listen to the specified events once.
|
||||
*/
|
||||
registerLazyWindowEventListener(
|
||||
window,
|
||||
events,
|
||||
{ handler, scope, name, once }
|
||||
) {
|
||||
const dispatcher = this.getDispatcherForWindow(window);
|
||||
|
||||
this._addLazyListeners(
|
||||
events,
|
||||
handler,
|
||||
scope,
|
||||
name,
|
||||
(events, listener) => {
|
||||
dispatcher.registerListener(listener, events);
|
||||
},
|
||||
(handlers, listener, args) => {
|
||||
if (!once) {
|
||||
dispatcher.unregisterListener(listener, args[0]);
|
||||
handlers.forEach(handler =>
|
||||
dispatcher.registerListener(handler, args[0])
|
||||
);
|
||||
}
|
||||
handlers.forEach(handler => handler.onEvent(...args));
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add lazy pref observers, and only load the actual handler once the pref
|
||||
* value changes from default, and every time the pref value changes
|
||||
|
@ -365,23 +324,6 @@ var GeckoViewUtils = {
|
|||
return null;
|
||||
},
|
||||
|
||||
getActiveDispatcherAndWindow() {
|
||||
const bc = Services.focus.activeBrowsingContext;
|
||||
const win = bc ? bc.window : null; // WON'T WORK FOR OOP IFRAMES!
|
||||
let dispatcher = this.getDispatcherForWindow(win);
|
||||
if (dispatcher) {
|
||||
return [dispatcher, win];
|
||||
}
|
||||
|
||||
for (const win of Services.wm.getEnumerator(/* windowType */ null)) {
|
||||
dispatcher = this.getDispatcherForWindow(win);
|
||||
if (dispatcher) {
|
||||
return [dispatcher, win];
|
||||
}
|
||||
}
|
||||
return [null, null];
|
||||
},
|
||||
|
||||
/**
|
||||
* Add logging functions to the specified scope that forward to the given
|
||||
* Log.jsm logger. Currently "debug" and "warn" functions are supported. To
|
||||
|
|
|
@ -13,12 +13,47 @@ var EXPORTED_SYMBOLS = ["EventDispatcher"];
|
|||
const IS_PARENT_PROCESS =
|
||||
Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT;
|
||||
|
||||
class ChildActorDispatcher {
|
||||
constructor(actor) {
|
||||
this._actor = actor;
|
||||
}
|
||||
|
||||
// TODO: Bug 1658980
|
||||
registerListener(aListener, aEvents) {
|
||||
throw new Error("Cannot registerListener in child actor");
|
||||
}
|
||||
unregisterListener(aListener, aEvents) {
|
||||
throw new Error("Cannot registerListener in child actor");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to Java.
|
||||
*
|
||||
* @param aMsg Message to send; must be an object with a "type" property
|
||||
*/
|
||||
sendRequest(aMsg) {
|
||||
this._actor.sendAsyncMessage("DispatcherMessage", aMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to Java, returning a Promise that resolves to the response.
|
||||
*
|
||||
* @param aMsg Message to send; must be an object with a "type" property
|
||||
* @return A Promise resolving to the response
|
||||
*/
|
||||
sendRequestForResult(aMsg) {
|
||||
return this._actor.sendQuery("DispatcherQuery", aMsg);
|
||||
}
|
||||
}
|
||||
|
||||
function DispatcherDelegate(aDispatcher, aMessageManager) {
|
||||
this._dispatcher = aDispatcher;
|
||||
this._messageManager = aMessageManager;
|
||||
|
||||
if (!aDispatcher) {
|
||||
// Child process.
|
||||
// TODO: this doesn't work with Fission, remove this code path once every
|
||||
// consumer has been migrated. Bug 1569360.
|
||||
this._replies = new Map();
|
||||
(aMessageManager || Services.cpmm).addMessageListener(
|
||||
"GeckoView:MessagingReply",
|
||||
|
@ -226,6 +261,15 @@ var EventDispatcher = {
|
|||
return new DispatcherDelegate(null, aMessageManager);
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the EventDispatcher instance associated with an actor.
|
||||
*
|
||||
* @param aActor an actor
|
||||
*/
|
||||
forActor(aActor) {
|
||||
return new ChildActorDispatcher(aActor);
|
||||
},
|
||||
|
||||
receiveMessage(aMsg) {
|
||||
// aMsg.data includes keys: global, event, data, uuid
|
||||
let callback;
|
||||
|
|
|
@ -18,7 +18,6 @@ EXTRA_JS_MODULES += [
|
|||
"GeckoViewConsole.jsm",
|
||||
"GeckoViewContent.jsm",
|
||||
"GeckoViewContentBlocking.jsm",
|
||||
"GeckoViewMedia.jsm",
|
||||
"GeckoViewMediaControl.jsm",
|
||||
"GeckoViewModule.jsm",
|
||||
"GeckoViewNavigation.jsm",
|
||||
|
@ -26,6 +25,7 @@ EXTRA_JS_MODULES += [
|
|||
"GeckoViewProgress.jsm",
|
||||
"GeckoViewPushController.jsm",
|
||||
"GeckoViewRemoteDebugger.jsm",
|
||||
"GeckoViewSelectionAction.jsm",
|
||||
"GeckoViewSettings.jsm",
|
||||
"GeckoViewStorageController.jsm",
|
||||
"GeckoViewTab.jsm",
|
||||
|
|
|
@ -72,6 +72,8 @@ DIRS += [
|
|||
]
|
||||
|
||||
TEST_HARNESS_FILES.testing.mochitest.tests.junit += [
|
||||
"geckoview/src/androidTest/assets/www/forms_iframe.html",
|
||||
"geckoview/src/androidTest/assets/www/forms_xorigin.html",
|
||||
"geckoview/src/androidTest/assets/www/hello.html",
|
||||
"geckoview/src/androidTest/assets/www/iframe_http_only.html",
|
||||
"geckoview/src/androidTest/assets/www/simple_redirect.sjs",
|
||||
|
|
|
@ -48,7 +48,6 @@ CustomProtocolHandler.prototype = {
|
|||
}
|
||||
return this.QueryInterface(aIID);
|
||||
},
|
||||
lockFactory() {},
|
||||
|
||||
/** nsISupports */
|
||||
QueryInterface: ChromeUtils.generateQI(["nsIProtocolHandler", "nsIFactory"]),
|
||||
|
|
|
@ -24,9 +24,6 @@ var factory = {
|
|||
}
|
||||
return unsafeAboutModule.QueryInterface(aIID);
|
||||
},
|
||||
lockFactory(aLock) {
|
||||
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
||||
},
|
||||
QueryInterface: ChromeUtils.generateQI(["nsIFactory"]),
|
||||
};
|
||||
|
||||
|
|
|
@ -116,7 +116,6 @@ ProtocolHandler.prototype = {
|
|||
}
|
||||
return this.QueryInterface(aIID);
|
||||
},
|
||||
lockFactory() {},
|
||||
|
||||
/** nsISupports */
|
||||
QueryInterface: ChromeUtils.generateQI([
|
||||
|
|
|
@ -27,9 +27,6 @@ var sniffer = {
|
|||
}
|
||||
return this.QueryInterface(iid);
|
||||
},
|
||||
lockFactory: function sniffer_lockf(lock) {
|
||||
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
||||
},
|
||||
|
||||
getMIMETypeFromContent(request, data, length) {
|
||||
return sniffedType;
|
||||
|
|
|
@ -26,9 +26,6 @@ var eventsink = {
|
|||
}
|
||||
return this.QueryInterface(iid);
|
||||
},
|
||||
lockFactory: function eventsink_lockf(lock) {
|
||||
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
||||
},
|
||||
|
||||
asyncOnChannelRedirect: function eventsink_onredir(
|
||||
oldChan,
|
||||
|
|
|
@ -38,7 +38,6 @@ TestProtocolHandlerFactory.prototype = {
|
|||
createInstance(delegate, iid) {
|
||||
return new TestProtocolHandler().QueryInterface(iid);
|
||||
},
|
||||
lockFactory(lock) {},
|
||||
};
|
||||
|
||||
function register_test_protocol_handler() {
|
||||
|
|
|
@ -52,7 +52,6 @@ TestProtocolHandlerFactory.prototype = {
|
|||
createInstance(delegate, iid) {
|
||||
return new TestProtocolHandler().QueryInterface(iid);
|
||||
},
|
||||
lockFactory(lock) {},
|
||||
};
|
||||
|
||||
function register_test_protocol_handler() {
|
||||
|
|
|
@ -10,7 +10,7 @@ import re
|
|||
import subprocess
|
||||
import sys
|
||||
|
||||
from distutils.version import StrictVersion as Version
|
||||
from packaging.version import Version
|
||||
|
||||
import buildconfig
|
||||
from mozbuild.action.util import log_build_task
|
||||
|
|
|
@ -21,7 +21,7 @@ import attr
|
|||
import psutil
|
||||
import requests
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
from packaging.version import Version
|
||||
|
||||
import mozpack.path as mozpath
|
||||
|
||||
|
@ -35,7 +35,7 @@ MEMORY_THRESHOLD = 7.4
|
|||
FREESPACE_THRESHOLD = 10
|
||||
|
||||
# Latest MozillaBuild version.
|
||||
LATEST_MOZILLABUILD_VERSION = LooseVersion("3.3")
|
||||
LATEST_MOZILLABUILD_VERSION = Version("4.0")
|
||||
|
||||
DISABLE_LASTACCESS_WIN = """
|
||||
Disable the last access time feature?
|
||||
|
@ -436,7 +436,7 @@ def mozillabuild(**kwargs) -> DoctorCheck:
|
|||
display_text=["Could not get local MozillaBuild version."],
|
||||
)
|
||||
|
||||
if LooseVersion(local_version) < LATEST_MOZILLABUILD_VERSION:
|
||||
if Version(local_version) < LATEST_MOZILLABUILD_VERSION:
|
||||
status = CheckStatus.WARNING
|
||||
desc = "MozillaBuild %s in use, <%s" % (
|
||||
local_version,
|
||||
|
|
|
@ -6,7 +6,7 @@ from __future__ import absolute_import, print_function, unicode_literals
|
|||
|
||||
import re
|
||||
import six
|
||||
from distutils.version import LooseVersion
|
||||
from packaging.version import Version
|
||||
from mozpack.errors import errors
|
||||
from collections import OrderedDict
|
||||
|
||||
|
@ -160,14 +160,14 @@ class VersionFlag(object):
|
|||
assert definition.startswith(self.name)
|
||||
value = definition[len(self.name) :]
|
||||
if value.startswith("="):
|
||||
self.values.append(("==", LooseVersion(value[1:])))
|
||||
self.values.append(("==", Version(value[1:])))
|
||||
elif len(value) > 1 and value[0] in ["<", ">"]:
|
||||
if value[1] == "=":
|
||||
if len(value) < 3:
|
||||
return errors.fatal("Malformed flag: %s" % definition)
|
||||
self.values.append((value[0:2], LooseVersion(value[2:])))
|
||||
self.values.append((value[0:2], Version(value[2:])))
|
||||
else:
|
||||
self.values.append((value[0], LooseVersion(value[1:])))
|
||||
self.values.append((value[0], Version(value[1:])))
|
||||
else:
|
||||
return errors.fatal("Malformed flag: %s" % definition)
|
||||
|
||||
|
@ -189,7 +189,7 @@ class VersionFlag(object):
|
|||
flag.matches('1.0') returns True
|
||||
flag.matches('0.6') returns False
|
||||
"""
|
||||
value = LooseVersion(value)
|
||||
value = Version(value)
|
||||
if not self.values:
|
||||
return True
|
||||
for comparison, val in self.values:
|
||||
|
|
|
@ -311,8 +311,8 @@ sccache:
|
|||
fetch:
|
||||
type: git
|
||||
repo: https://github.com/mozilla/sccache
|
||||
# 0.2.15 release
|
||||
revision: 6b6d2f7d2dceefeb4f583712aa4c221db62be0bd
|
||||
# 0.3.0 release
|
||||
revision: f6e36e68c6cb1e17127273956df9ebf2127dff1a
|
||||
|
||||
fxc2:
|
||||
description: fxc2 source code
|
||||
|
|
|
@ -68,9 +68,6 @@ var MockRegistrar = Object.freeze({
|
|||
|
||||
return wrappedMock.QueryInterface(iid);
|
||||
},
|
||||
lockFactory(lock) {
|
||||
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
||||
},
|
||||
QueryInterface: ChromeUtils.generateQI(["nsIFactory"]),
|
||||
};
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ from commit messages.
|
|||
from __future__ import absolute_import, print_function
|
||||
|
||||
from collections.abc import Iterable
|
||||
from distutils.version import StrictVersion
|
||||
from packaging.version import Version
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
|
@ -48,12 +48,12 @@ def changelog(args):
|
|||
for line in diff.splitlines():
|
||||
if line.startswith("-PACKAGE_VERSION"):
|
||||
try:
|
||||
minus_version = StrictVersion(line.split()[-1].strip("\"'"))
|
||||
minus_version = Version(line.split()[-1].strip("\"'"))
|
||||
except ValueError:
|
||||
pass
|
||||
elif line.startswith("+PACKAGE_VERSION"):
|
||||
try:
|
||||
plus_version = StrictVersion(line.split()[-1].strip("\"'"))
|
||||
plus_version = Version(line.split()[-1].strip("\"'"))
|
||||
except ValueError:
|
||||
break
|
||||
|
||||
|
@ -62,7 +62,7 @@ def changelog(args):
|
|||
if not v:
|
||||
return rev
|
||||
|
||||
if StrictVersion(v) == plus_version:
|
||||
if Version(v) == plus_version:
|
||||
return rev
|
||||
|
||||
print(
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from __future__ import absolute_import, division
|
||||
from distutils.version import LooseVersion
|
||||
from packaging.version import Version
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
|
@ -23,7 +23,7 @@ from mozharness.base.script import BaseScript
|
|||
|
||||
# ensure all versions are 3 part (i.e. 99.1.0)
|
||||
# ensure all text (i.e. 'esr') is in the last part
|
||||
class CompareVersion(LooseVersion):
|
||||
class CompareVersion(Version):
|
||||
version = ""
|
||||
|
||||
def __init__(self, versionMap):
|
||||
|
@ -38,7 +38,7 @@ class CompareVersion(LooseVersion):
|
|||
else:
|
||||
parts.append("0")
|
||||
self.version = ".".join(parts)
|
||||
LooseVersion(versionMap)
|
||||
Version(versionMap)
|
||||
|
||||
|
||||
def is_triangualar(x):
|
||||
|
@ -549,9 +549,9 @@ class UpdateVerifyConfigCreator(BaseScript):
|
|||
|
||||
# Use new build targets for Windows, but only on compatible
|
||||
# versions (42+). See bug 1185456 for additional context.
|
||||
if self.config["platform"] not in ("win32", "win64") or LooseVersion(
|
||||
if self.config["platform"] not in ("win32", "win64") or Version(
|
||||
fromVersion
|
||||
) < LooseVersion("42.0"):
|
||||
) < Version("42.0"):
|
||||
update_platform = ftp2updatePlatforms(self.config["platform"])[0]
|
||||
else:
|
||||
update_platform = ftp2updatePlatforms(self.config["platform"])[1]
|
||||
|
|
|
@ -32,9 +32,6 @@ var newFactory = function(window) {
|
|||
}
|
||||
return new MockColorPickerInstance(window).QueryInterface(aIID);
|
||||
},
|
||||
lockFactory(aLock) {
|
||||
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
||||
},
|
||||
QueryInterface: ChromeUtils.generateQI(["nsIFactory"]),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -37,9 +37,6 @@ var newFactory = function(window) {
|
|||
}
|
||||
return new MockFilePickerInstance(window).QueryInterface(aIID);
|
||||
},
|
||||
lockFactory(aLock) {
|
||||
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
||||
},
|
||||
QueryInterface: ChromeUtils.generateQI(["nsIFactory"]),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -20,9 +20,6 @@ var newFactory = {
|
|||
}
|
||||
return new MockPermissionPromptInstance().QueryInterface(aIID);
|
||||
},
|
||||
lockFactory(aLock) {
|
||||
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
||||
},
|
||||
QueryInterface: ChromeUtils.generateQI(["nsIFactory"]),
|
||||
};
|
||||
|
||||
|
|
|
@ -308,9 +308,6 @@ var _fakeIdleService = {
|
|||
}
|
||||
return _fakeIdleService.QueryInterface(aIID);
|
||||
},
|
||||
lockFactory(aLock) {
|
||||
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
||||
},
|
||||
QueryInterface: ChromeUtils.generateQI(["nsIFactory"]),
|
||||
},
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
{"files":{"Cargo.lock":"d658acfaa27a2b30de98cf004d4d3f4ec0f1757b136610289cbbd1c847ae2e6c","Cargo.toml":"e2176be78c2989884eba4a20a58a672277b3a8a99a72b0ba7347f48eb827ae0e","LICENSE-APACHE":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2","LICENSE-MIT":"27995d58ad5c1145c1a8cd86244ce844886958a35eb2b78c6b772748669999ac","README.md":"2d8b6f07819ad7adfab1d153216bfdcde154ffd4a870d98794125c910b0f4593","examples/integers.rs":"589ff4271566dfa322becddf3e2c7b592e6e0bc97b02892ce75619b7e452e930","examples/paths.rs":"1b30e466b824ce8df7ad0a55334424131d9d2573d6cf9f7d5d50c09c8901d526","examples/traits.rs":"cbee6a3e1f7db60b02ae25b714926517144a77cb492021f492774cf0e1865a9e","examples/versions.rs":"38535e6d9f5bfae0de474a3db79a40e8f5da8ba9334c5ff4c363de9bc99d4d12","src/error.rs":"12de7dafea4a35d1dc2f0fa79bfa038386bbbea72bf083979f4ddf227999eeda","src/lib.rs":"9b450d90730624807979045ea7ff48374355314cd894345e1b9651485ba1b2ff","src/tests.rs":"a902fbd42b0f0b81a2830f2368fab733041b02fcb902c8e2520d07b3bff10713","src/version.rs":"175727d5f02f2fe2271ddc9b041db2a5b9c6fe0f95afd17c73a4d982612764a3","tests/rustflags.rs":"441fb0c6606e243c31d3817a5ae2240b65fcae0ea8ab583f80f8f6d6c267e614"},"package":"cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"}
|
||||
{"files":{"Cargo.lock":"3d91565ed13de572a9ebde408a0c98e33f931d6ab52f212b0830a60b4ab26b77","Cargo.toml":"39f627122dceaad42146634719fde802fca3baa1b3908753af723074ae2a6d69","LICENSE-APACHE":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2","LICENSE-MIT":"27995d58ad5c1145c1a8cd86244ce844886958a35eb2b78c6b772748669999ac","README.md":"4c8f9b5016f2a0c3dbeca5bc41241f57db5568f803e58c1fa480ae2b3638d0a9","examples/integers.rs":"589ff4271566dfa322becddf3e2c7b592e6e0bc97b02892ce75619b7e452e930","examples/paths.rs":"1b30e466b824ce8df7ad0a55334424131d9d2573d6cf9f7d5d50c09c8901d526","examples/traits.rs":"cbee6a3e1f7db60b02ae25b714926517144a77cb492021f492774cf0e1865a9e","examples/versions.rs":"38535e6d9f5bfae0de474a3db79a40e8f5da8ba9334c5ff4c363de9bc99d4d12","src/error.rs":"12de7dafea4a35d1dc2f0fa79bfa038386bbbea72bf083979f4ddf227999eeda","src/lib.rs":"6fa01458e8f9258d84f83ead24fdb0cdf9aec10838b0262f1dfbdf79c530c537","src/tests.rs":"f0e6dc1ad9223c0336c02e215ea3940acb2af6c3bc8fd791e16cd4e786e6a608","src/version.rs":"175727d5f02f2fe2271ddc9b041db2a5b9c6fe0f95afd17c73a4d982612764a3","tests/rustflags.rs":"5c8169b88216055019db61b5d7baf4abdf675e3b14b54f5037bb1e3acd0a5d3f"},"package":"d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"}
|
|
@ -1,6 +1,7 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.0.1"
|
||||
|
||||
version = "1.1.0"
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче