diff --git a/addon-sdk/source/doc/dev-guide-source/credits.md b/addon-sdk/source/doc/dev-guide-source/credits.md index 30386881911b..73de5d5fb304 100644 --- a/addon-sdk/source/doc/dev-guide-source/credits.md +++ b/addon-sdk/source/doc/dev-guide-source/credits.md @@ -140,6 +140,7 @@ We'd like to thank our many Jetpack project contributors! They include: * Tim Taubert * Shane Tomlinson * Dave Townsend +* [Fraser Tweedale](https://github.com/frasertweedale) * [Matthias Tylkowski](https://github.com/tylkomat) ### V ### diff --git a/addon-sdk/source/doc/dev-guide-source/guides/contributors-guide/classes-and-inheritance.md b/addon-sdk/source/doc/dev-guide-source/guides/contributors-guide/classes-and-inheritance.md new file mode 100644 index 000000000000..f4a8f4776e6a --- /dev/null +++ b/addon-sdk/source/doc/dev-guide-source/guides/contributors-guide/classes-and-inheritance.md @@ -0,0 +1,272 @@ + + +#Classes and Inheritance +A class is a blueprint from which individual objects are created. These +individual objects are the instances of the class. Each class defines one or +more members, which are initialized to a given value when the class is +instantiated. Data members are properties that allow each instance to have +their own state, whereas member functions are properties that allow instances to +have behavior. Inheritance allows classes to inherit state and behavior from an +existing classes, known as the base class. Unlike languages like C++ and Java, +JavaScript does not have native support for classical inheritance. Instead, it +uses something called prototypal inheritance. As it turns out, it is possible to +emulate classical inheritance using prototypal inheritance, but not without +writing a significant amount of boilerplate code. + +Classes in JavaScript are defined using constructor functions. Each constructor +function has an associated object, known as its prototype, which is shared +between all instances of that class. We will show how to define classes using +constructors, and how to use prototypes to efficiently define member functions +on each instance. Classical inheritance can be implemented in JavaScript using +constructors and prototypes. We will show how to make inheritance work correctly +with respect to constructors, prototypes, and the instanceof operator, and how +to override methods in subclasses. The SDK uses a special constructor internally, +known as `Class`, to create constructors that behave properly with respect to +inheritance. The last section shows how to work with the `Class` constructor. It +is possible to read this section on its own. However, to fully appreciate how +`Class` works, and the problem it is supposed to solve, it is recommended that +you read the entire article. + +##Constructors +In JavaScript, a class is defined by defining a constructor function for that +class. To illustrate this, let's define a simple constructor for a class +`Shape`: + + function Shape(x, y) { + this.x = x; + this.y = y; + } + +We can now use this constructor to create instances of `Shape`: + + let shape = new Shape(2, 3); + shape instanceof Shape; // => true + shape.x; // => 2 + shape.y; // => 3 + +The keyword new tells JavaScript that we are performing a constructor call. +Constructor calls differ from ordinary function calls in that JavaScript +automatically creates a new object and binds it to the keyword this for the +duration of the call. Moreover, if the constructor does not return a value, the +result of the call defaults to the value of this. Constructors are just ordinary +functions, however, so it is perfectly legal to perform ordinary function calls +on them. In fact, some people (including the Add-on SDK team) prefer to use +constructors this way. However, since the value of this is undefined for +ordinary function calls, we need to add some boilerplate code to convert them to +constructor calls: + + function Shape(x, y) { + if (!this) + return new Shape(x, y); + this.x = x; + this.y = y; + } + +##Prototypes +Every object has an implicit property, known as its prototype. When JavaScript +looks for a property, it first looks for it in the object itself. If it cannot +find the property there, it looks for it in the object's prototype. If the +property is found on the prototype, the lookup succeeds, and JavaScript pretends +that it found the property on the original object. Every function has an +explicit property, known as `prototype`. When a function is used in a +constructor call, JavaScript makes the value of this property the prototype of +the newly created object: + + let shape = Shape(2, 3); + Object.getPrototypeOf(shape) == Shape.prototype; // => true + +All instances of a class have the same prototype. This makes the prototype the +perfect place to define properties that are shared between instances of the +class. To illustrate this, let's add a member function to the class `Shape`: + + Shape.prototype.draw = function () { + throw Error("not yet implemented"); + } + let shape = Shape(2, 3); + Shape.draw(); // => Error: not yet implemented + +##Inheritance and Constructors +Suppose we want to create a new class, `Circle`, and inherit it from `Shape`. +Since every `Circle` is also a `Shape`, the constructor for `Circle` must be +called every time we call the constructor for `Shape`. Since JavaScript does +not have native support for inheritance, it doesn't do this automatically. +Instead, we need to call the constructor for `Shape` explicitly. The resulting +constructor looks as follows: + + function Circle(x, y, radius) { + if (!this) + return new Circle(x, y, radius); + Shape.call(this, x, y); + this.radius = radius; + } + +Note that the constructor for `Shape` is called as an ordinary function, and +reuses the object created for the constructor call to `Circle`. Had we used a +constructor call instead, the constructor for `Shape` would have been applied to +a different object than the constructor for `Circle`. We can now use the above +constructor to create instances of the class `Circle`: + + let circle = Circle(2, 3, 5); + circle instanceof Circle; // => true + circle.x; // => 2 + circle.y; // => 3 + circle.radius; // => 5 + +##Inheritance and Prototypes +There is a problem with the definition of `Circle` in the previous section that +we have glossed over thus far. Consider the following: + + let circle = Circle(2, 3, 5); + circle.draw(); // => TypeError: circle.draw is not a function + +This is not quite right. The method `draw` is defined on instances of `Shape`, +so we definitely want it to be defined on instances of `Circle`. The problem is +that `draw` is defined on the prototype of `Shape`, but not on the prototype of +`Circle`. We could of course copy every property from the prototype of `Shape` +over to the prototype of `Circle`, but this is needlessly inefficient. Instead, +we use a clever trick, based on the observation that prototypes are ordinary +objects. Since prototypes are objects, they have a prototype as well. We can +thus override the prototype of `Circle` with an object which prototype is the +prototype of `Shape`. + + Circle.prototype = Object.create(Shape.prototype); + +Now when JavaScript looks for the method draw on an instance of Circle, it first +looks for it on the object itself. When it cannot find the property there, it +looks for it on the prototype of `Circle`. When it cannot find the property +there either, it looks for it on `Shape`, at which point the lookup succeeds. +The resulting behavior is what we were aiming for. + +##Inheritance and Instanceof +The single line of code we added in the previous section solved the problem with +prototypes, but introduced a new problem with the **instanceof** operator. +Consider the following: + + let circle = Circle(2, 3, 5); + circle instanceof Shape; // => false + +Since instances of `Circle` inherit from `Shape`, we definitely want the result +of this expression to be true. To understand why it is not, we need to +understand how **instanceof** works. Every prototype has a `constructor` +property, which is a reference to the constructor for objects with this +prototype. In other words: + + Circle.prototype.constructor == Circle // => true + +The **instanceof** operator compares the `constructor` property of the prototype +of the left hand side with that of the right hand side, and returns true if they +are equal. Otherwise, it repeats the comparison for the prototype of the right +hand side, and so on, until either it returns **true**, or the prototype becomes +**null**, in which case it returns **false**. The problem is that when we +overrode the prototype of `Circle` with an object whose prototype is the +prototype of `Shape`, we didn't correctly set its `constructor` property. This +property is set automatically for the `prototype` property of a constructor, but +not for objects created with `Object.create`. The `constructor` property is +supposed to be non-configurable, non-enumberable, and non-writable, so the +correct way to define it is as follows: + + Circle.prototype = Object.create(Shape.prototype, { + constructor: { + value: Circle + } + }); + +##Overriding Methods +As a final example, we show how to override the stub implementation of the +method `draw` in `Shape` with a more specialized one in `Circle`. Recall that +JavaScript returns the first property it finds when walking the prototype chain +of an object from the bottom up. Consequently, overriding a method is as simple +as providing a new definition on the prototype of the subclass: + + Circle.prototype.draw = function (ctx) { + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius, + 0, 2 * Math.PI, false); + ctx.fill(); + }; + +With this definition in place, we get: + + let shape = Shape(2, 3); + shape.draw(); // Error: not yet implemented + let circle = Circle(2, 3, 5); + circle.draw(); // TypeError: ctx is not defined + +which is the behavior we were aiming for. + +##Classes in the Add-on SDK +We have shown how to emulate classical inheritance in JavaScript using +constructors and prototypes. However, as we have seen, this takes a significant +amount of boilerplate code. The Add-on SDK team consists of highly trained +professionals, but they are also lazy: that is why the SDK contains a helper +function that handles this boilerplate code for us. It is defined in the module +“core/heritage”: + + const { Class } = require('sdk/core/heritage'); + +The function `Class` is a meta-constructor: it creates constructors that behave +properly with respect to inheritance. It takes a single argument, which is an +object which properties will be defined on the prototype of the resulting +constructor. The semantics of `Class` are based on what we've learned earlier. +For instance, to define a constructor for a class `Shape` in terms of `Class`, +we can write: + + let Shape = Class({ + initialize: function (x, y) { + this.x = x; + this.y = y; + }, + draw: function () { + throw new Error("not yet implemented"); + } + }); + +The property `initialize` is special. When it is present, the call to the +constructor is forwarded to it, as are any arguments passed to it (including the +this object). In effect, initialize specifies the body of the constructor. Note +that the constructors created with `Class` automatically check whether they are +called as constructors, so an explicit check is no longer necessary. + +Another special property is `extends`. It specifies the base class from which +this class inherits, if any. `Class` uses this information to automatically set +up the prototype chain of the constructor. If the extends property is omitted, +`Class` itself is used as the base class: + + var shape = new Shape(2, 3); + shape instanceof Shape; // => true + shape instanceof Class; // => true + +To illustrate the use of the `extends` property, let's redefine the constructor +for the class `Circle` in terms of `Class`: + + var Circle = Class({ + extends: Shape, + initialize: function(x, y, radius) { + Shape.prototype.initialize.call(this, x, y); + this.radius = radius; + }, + draw: function () { + context.beginPath(); + context.arc(this.x, this.y, this.radius, + 0, 2 * Math.PI, false); + context.fill(); + } + }); + +Unlike the definition of `Circle` in the previous section, we no longer have to +override its prototype, or set its `constructor` property. This is all handled +automatically. On the other hand, the call to the constructor for `Shape` still +has to be made explicitly. This is done by forwarding to the initialize method +of the prototype of the base class. Note that this is always safe, even if there +is no `initialize` method defined on the base class: in that case the call is +forwarded to a stub implementation defined on `Class` itself. + +The last special property we will look into is `implements`. It specifies a list +of objects, which properties are to be copied to the prototype of the +constructor. Note that only properties defined on the object itself are copied: +properties defined on one of its prototypes are not. This allows objects to +inherit from more than one class. It is not true multiple inheritance, however: +no constructors are called for objects inherited via `implements`, and +**instanceof** only works correctly for classes inherited via `extends`. diff --git a/addon-sdk/source/doc/dev-guide-source/guides/contributors-guide/content-processes.md b/addon-sdk/source/doc/dev-guide-source/guides/contributors-guide/content-processes.md new file mode 100644 index 000000000000..f77d921a85fe --- /dev/null +++ b/addon-sdk/source/doc/dev-guide-source/guides/contributors-guide/content-processes.md @@ -0,0 +1,149 @@ + + +#Content Processes +A content process was supposed to run all the code associated with a single tab. +Conversely, an add-on process was supposed to run all the code associated with a +single add-on. Neither content or add-on proceses were ever actually +implemented, but by the time they were cancelled, the SDK was already designed +with them in mind. To understand this article, it's probably best to read it as +if content and add-on processes actually exist. + +To communicate between add-on and content processes, the SDK uses something +called content scripts. These are explained in the first section. Content +scripts communicate with add-on code using something called event emitters. +These are explained in the next section. Content workers combine these ideas, +allowing you to inject a content script into a content process, and +automatically set up a communication channel between them. These are explained +in the third section. + +In the next section, we will look at how content scripts interact with the DOM +in a content process. There are several caveats here, all of them related to +security, that might cause things to not behave in the way you might expect. + +The final section explains why the SDK still uses the notion of content scripts +and message passing, even though the multiprocess model for which they were +designed never materialized. This, too, is primarily related to security. + +##Content Scripts +When the SDK was first designed, Firefox was being refactored towards a +multiprocess model. In this model, the UI would be rendered in one process +(called the chrome process), whereas each tab and each add-on would run in their +own dedicated process (called content and add-on processes, respectively). The +project behind this refactor was known as Electrolysis, or E10s. Although E10s +has now been suspended, the SDK was designed with this multiprocess model in +mind. Afterwards, it was decided to keep the design the way it is: even though +its no longer necessary, it turns out that from a security point of view there +are several important advantages to thinking about content and add-on code as +living in different processes. + +Many add-ons have to interact with content. The problem with the multiprocess +model is that add-ons and content are now in different processes, and scripts in +one process cannot interact directly with scripts in another. We can, however, +pass JSON messages between scripts in different processes. The solution we've +come up with is to introduce the notion of content scripts. A content script is +a script that is injected into a content process by the main script running in +the add-on process. Content scripts differ from scripts that are loaded by the +page itself in that they are provided with a messaging API that can be used to +send messages back to the add-on script. + +##Event Emitters +The messaging API we use to send JSON messages between scripts in different +processes is based on the use of event emitters. An event emitter maintains a +list of callbacks (or listeners) for one or more named events. Each event +emitter has several methods: the method on is used to add a listener for an +event. Conversely, the method removeListener is used to remove a listener for an +event. The method once is a helper function which adds a listener for an event, +and automatically removes it the first time it is called. + +Each event emitter has two associated emit functions. One emit function is +associated with the event emitter itself. When this function is called with a +given event name, it calls all the listeners currently associated with that +event. The other emit function is associated with another event emitter: it was +passed as an argument to the constructor of this event emitter, and made into a +method. Calling this method causes an event to be emitted on the other event +emitter. + +Suppose we have two event emitters in different processes, and we want them to +be able to emit events to each other. In this case, we would replace the emit +function passed to the constructor of each emitter with a function that sends a +message to the other process. We can then hook up a listener to be called when +this message arrives at the other process, which in turn calls the emit function +on the other event emitter. The combination of this function and the +corresponding listener is referred to as a pipe. + +##Content Workers +A content worker is an object that is used to inject content scripts into a +content process, and to provide a pipe between each content script and the main +add-on script. The idea is to use a single content worker for each content +process. The constructor for the content worker takes an object containing one +or more named options. Among other things, this allows us to specify one or more +content scripts to be loaded. + +When a content script is first loaded, the content worker automatically imports +a messaging API that allows the it to emit messages over a pipe. On the add-on +side, this pipe is exposed via the the port property on the worker. In addition +to the port property, workers also support the web worker API, which allows +scripts to send messages to each other using the postMessage function. This +function uses the same pipe internally, and causes a 'message' event to be +emitted on the other side. + +As explained earlier, Firefox doesn't yet use separate processes for tabs or +add-ons, so instead, each content script is loaded in a sandbox. Sandboxes were +explained [this article]("dev-guide/guides/contributors-guide/modules.html"). + +##Accessing the DOM +The global for the content sandbox has the window object as its prototype. This +allows the content script to access any property on the window object, even +though that object lives outside the sandbox. Recall that the window object +inside the sandbox is actually a wrapper to the real object. A potential +problem with the content script having access to the window object is that a +malicious page could override methods on the window object that it knows are +being used by the add-on, in order to trick the add-on into doing something it +does not expect. Similarly, if the content script defines any values on the +window object, a malicious page could potentially steal that information. + +To avoid problems like this, content scripts should always see the built-in +properties of the window object, even when they are overridden by another +script. Conversely, other scripts should not see any properties added to the +window object by the content script. This is where xray wrappers come in. Xray +wrappers automatically wrap native objects like the window object, and only +exposes their native properties, even if they have been overridden on the +wrapped object. Conversely, any properties defined on the wrapper are not +visible from the wrapped object. This avoids both problems we mentioned earlier. + +The fact that you can't override the properties of the window object via a +content script is sometimes inconvenient, so it is possible to circumvent this: +by defining the property on window.wrappedObject, the property is defined on the +underlying object, rather than the wrapper itself. This feature should only be +used when you really need it, however. + +##A few Notes on Security +As we stated earlier, the SDK was designed with multiprocess support in mind, +despite the fact that work on implementing this in Firefox has currently been +suspended. Since both add-on modules and content scripts are currently loaded in +sandboxes rather than separate processes, and sandboxes can communicate with +each other directly (using imports/exports), you might be wondering why we have +to go through all the trouble of passing messages between add-on and content +scripts. The reason for this extra complexity is that the code for add-on +modules and content scripts has different privileges. Every add-on module can +get chrome privileges simply by asking for them, whereas content scripts have +the same privileges as the page it is running on. + +When two sandboxes have the same privileges, a wrapper in one sandbox provides +transparent access to an object in the other sandbox. When the two sandboxes +have different privileges, things become more complicated, however. Code with +content privileges should not be able to acces code with chrome privileges, so +we use specialized wrappers, called security wrappers, to limit access to the +object in the other sandbox. The xray wrappers we saw earlier are an example of +such a security wrapper. Security wrappers are created automatically, by the +underlying host application. + +A full discussion of the different kinds of security wrappers and how they work +is out of scope for this document, but the main point is this: security wrappers +are very complex, and very error-prone. They are subject to change, in order to +fix some security leak that recently popped up. As a result, code that worked +just fine last week suddenly does not work the way you expect. By only passing +messages between add-on modules and content scripts, these problems can be +avoided, making your add-on both easier to debug and to maintain. diff --git a/addon-sdk/source/doc/dev-guide-source/guides/contributors-guide/getting-started.md b/addon-sdk/source/doc/dev-guide-source/guides/contributors-guide/getting-started.md new file mode 100644 index 000000000000..bdaa61a57cae --- /dev/null +++ b/addon-sdk/source/doc/dev-guide-source/guides/contributors-guide/getting-started.md @@ -0,0 +1,318 @@ + + +#Getting Started +The contribution process consists of a number of steps. First, you need to get +a copy of the code. Next, you need to open a bug for the bug or feature you want +to work on, and assign it to yourself. Alternatively, you can take an existing +bug to work on. Once you've taken a bug, you can start writing a patch. Once +your patch is complete, you've made sure it doesn't break any tests, and you've +gotten a positive review for it, the last step is to request for your patch to +be merged with the main codebase. + +Although these individual steps are all obvious, there are quite some details +involved. The rest of this article will cover each individual step of the +contribution process in more detail. + +##Getting the Code +The Add-on SDK code is hosted on GitHub. GitHub is a web-based hosting service +for software projects that is based on Git, a distributed version control +system. Both GitHub and Git are an integral part of our workflow. If you haven't +familiarized yourself with Git before, I strongly suggest you do so now. You're +free to ignore that suggestion if you want, but it's going to hurt you later on +(don't come crying to me if you end up accidentally detaching your head, for +instance). A full explanation of how to use Git is out of scope for this +document, but a very good one +[can be found online here](http://git-scm.com/book). Reading at least sections +1-3 from that book should be enough to get you started. + +If you're already familiar with Git, or if you decided to ignore my advice and +jump right in, the following steps will get you a local copy of the Add-on SDK +code on your machine: + +1. Fork the SDK repository to your GitHub account +2. Clone the forked repository to your machine + +A fork is similar to a clone in that it creates a complete copy of a repository, +including the history of every file. The difference is that a fork copies the +repository to your GitHub account, whereas a clone copies it to your machine. To +create a fork of the SDK repository, you need a GitHub account. If you don't +already have one, you can [create one here](https://github.com/) (don't worry: +it's free!). Once you got yourself an account, go to +[the Add-on SDK repository](https://github.com/mozilla/addon-sdk), and click the +fork button in the upper-right corner. This will start the forking process. +This could take anywhere between a couple of seconds and a couple of minutes. + +Once the forking process is complete, the forked repository will be available at +https://github.com/\/addon-sdk. To create a clone of the this +repository, you need to have Git installed on your machine. If you don’t have it +already, you can [download it here](http://git-scm.com/). Once you have Git +installed (make sure you also configured your name and e-mail +address), open your terminal, and enter the following command from the directory +where you want to have the clone stored: + +> `git clone ssh://github.com//addon-sdk` + +This will start the cloning process. Like the forking process, this could take +anywhere between a couple of seconds and a couple of minutes, depending on the +speed of your connection. + +If you did everything correctly so far, once the cloning process is complete, +the cloned repository will have been stored inside the directory from which you +ran the clone command, in a new directory called addon-sdk. Now we can start +working with it. Yay! + +As a final note: it is possible to skip step 1, and clone the SDK repository +directly to your machine. This is useful if you only want to study the SDK code. +However, if your goal is to actually contribute to the SDK, skipping step 1 is a +bad idea, because you won’t be able to make pull requests in that case. + +##Opening a Bug +In any large software project, keeping track of bugs is crucially important. +Without it, developers wouldn't be able to answer questions such as: what do I +need to work on, who is working on what, etc. Mozilla uses its own web-based, +general-purpose bugtracker, called Bugzilla, to keep track of bugs. Like GitHub +and Git, Bugzilla is an integral part of our workflow. When you discover a new +bug, or want to implement a new feature, you start by creating an entry for it +in Bugzilla. By doing so, you give the SDK team a chance to confirm whether your +bug isn't actually a feature, or your feature isn't actually a bug +(that is, a feature we feel doesn't belong into the SDK). + +Within Bugzilla, the term _bug_ is often used interchangably to refer to both +bugs and features. Similarly, the Bugzilla entry for a bug is also named bug, +and the process of creating it is known as _opening a bug_. It is important that +you understand this terminology, as other people will regularly refer to it. + +I really urge you to open a bug first and wait for it to get confirmed before +you start working on something. Nothing sucks more than your patch getting +rejected because we felt it shouldn't go into the SDK. Having this discussion +first saves you from doing useless work. If you have questions about a bug, but +don't know who to ask (or the person you need to ask isn't online), Bugzilla is +the communication channel of choice. When you open a bug, the relevant people +are automatically put on the cc-list, so they will get an e-mail every time you +write a comment in the bug. + +To open a bug, you need a Bugzilla account. If you don't already have one, you +can [create it here](https://bugzilla.mozilla.org/). Once you got yourself an +account, click the "new" link in the upper-left corner. This will take you to a +page where you need to select the product that is affected by your bug. It isn't +immediately obvious what you should pick here (and with not immediately obvious +I mean completely non-obvious), so I'll just point it out to you: as you might +expect, the Add-on SDK is listed under "Other products", at the bottom of the +page. + +After selecting the Add-on SDK, you will be taken to another page, where you +need to fill out the details for the bug. The important fields are the component +affected by this bug, the summary, and a short description of the bug (don't +worry about coming up with the perfect description for your bug. If something is +not clear, someone from the SDK team will simply write a comment asking for +clarification). The other fields are optional, and you can leave them as is, if +you so desire. + +Note that when you fill out the summary field, Bugzilla automatically looks for +bugs that are possible duplicates of the one you're creating. If you spot such a +duplicate, there's no need to create another bug. In fact, doing so is +pointless, as duplicate bugs are almost always immediately closed. Don't worry +about accidentally opening a duplicate bug though. Doing so is not considered a +major offense (unless you do it on purpose, of course). + +After filling out the details for the bug, the final step is to click the +"Submit Bug" button at the bottom of the page. Once you click this button, the +bug will be stored in Bugzilla’s database, and the creation process is +completed. The initial status of your bug will be `UNCONFIRMED`. All you need to +do now is wait for someone from the SDK team to change the status to either +`NEW` or `WONTFIX`. + +##Taking a Bug +Since this is a contributor's guide, I've assumed until now that if you opened a +bug, you did so with the intention of fixing it. Simply because you're the one +that opened it doesn't mean you have to fix a bug, however. Conversely, simply +because you're _not_ the one that opened it doesn't mean you can't fix a bug. In +fact, you can work on any bug you like, provided nobody else is already working +on it. To check if somebody is already working on a bug, go to the entry for +that bug and check the "Assigned To" field. If it says "Nobody; OK to take it +and work on it", you're good to go: you can assign the bug to yourself by +clicking on "(take)" right next to it. + +Keep in mind that taking a bug to creates the expectation that you will work on +it. It's perfectly ok to take your time, but if this is the first bug you're +working on, you might want to make sure that this isn't something that has very +high priority for the SDK team. You can do so by checking the importance field +on the bug page (P1 is the highest priority). If you've assigned a bug to +yourself that looked easy at the time, but turns out to be too hard for you to +fix, don't feel bad! It happens to all of us. Just remove yourself as the +assignee for the bug, and write a comment explaining why you're no longer able +to work on it, so somebody else can take a shot at it. + +A word of warning: taking a bug that is already assigned to someone else is +considered extremely rude. Just imagine yourself working hard on a series of +patches, when suddenly this jerk comes out of nowhere and submits his own +patches for the bug. Not only is doing so an inefficient use of time, it also +shows a lack of respect for other the hard work of other contributors. The other +side of the coin is that contributors do get busy every now and then, so if you +stumble upon a bug that is already assigned to someone else but hasn't shown any +activity lately, chances are the person to which the bug is assigned will gladly +let you take it off his/her hands. The general rule is to always ask the person +assigned to the bug if it is ok for you to take it. + +As a final note, if you're not sure what bug to work on, or having a hard time +finding a bug you think you can handle, a useful tip is to search for the term +"good first bug". Bugs that are particularly easy, or are particularly well +suited to familiarize yourself with the SDK, are often given this label by the +SDK team when they're opened. + +##Writing a Patch +Once you've taken a bug, you're ready to start doing what you really want to do: +writing some code. The changes introduced by your code are known as a patch. +Your goal, of course, is to get this patch landed in the main SDK repository. In +case you aren't familiar with git, the following command will cause it to +generate a diff: + +> `git diff` + +A diff describes all the changes introduced by your patch. These changes are not +yet final, since they are not yet stored in the repository. Once your patch is +complete, you can _commit_ it to the repository by writing: + +> `git commit` + +After pressing enter, you will be prompted for a commit message. What goes in +the commit message is more or less up to you, but you should at least include +the bug number and a short summary (usually a single line) of what the patch +does. This makes it easier to find your commit later on. + +It is considered good manners to write your code in the same style as the rest +of a file. It doesn't really matter what coding style you use, as long as it's +consistent. The SDK might not always use the exact same coding style for each +file, but it strives to be as consistent as possible. Having said that: if +you're not completely sure what coding style to use, just pick something and +don't worry about it. If the rest of the file doesn't make it clear what you +should do, it most likely doesn't matter. + +##Making a Pull Request +To submit a patch for review, you need to make a pull request. Basically, a pull +request is a way of saying: "Hey, I've created this awesome patch on top of my +fork of the SDK repository, could you please merge it with the global +repository?". GitHub has built-in support for pull requests. However, you can +only make pull requests from repositories on your GitHub account, not from +repositories on your local machine. This is why I told you to fork the SDK +repository to your GitHub account first (you did listen to me, didn't you?). + +In the previous section, you commited your patch to your local repository, so +here, the next step is to synchronize your local repository with the remote one, +by writing: + +> `git push` + +This pushes the changes from your local repository into the remote repository. +As you might have guessed, a push is the opposite of a pull, where somebody else +pulls changes from a remote repository into their own repository (hence the term +'pull request'). After pressing enter, GitHub will prompt you for your username +and password before actually allowing the push. + +If you did everything correctly up until this point, your patch should now show +up in your remote repository (take a look at your repository on GitHub to make +sure). We're now ready to make a pull request. To do so, go to your repository +on GitHub and click the "Pull Request" button at the top of the page. This will +take you to a new page, where you need to fill out the title of your pull +request, as well as a short description of what the patch does. As we said +before, it is common practice to at least include the bug number and a short +summary in the title. After you've filled in both fields, click the "Send Pull +Request" button. + +That's it, we're done! Or are we? This is software development after all, so +we'd expect there to be at least one redundant step. Luckily, there is such a +step, because we also have to submit our patch for review on Bugzilla. I imagine +you might be wondering to yourself right now: "WHY???". Let me try to explain. +The reason we have this extra step is that most Mozilla projects use Mercurial +and Bugzilla as their version control and project management tool, respectively. +To stay consistent with the rest of Mozilla, we provide a Mercurial mirror of +our Git repository, and submit our patches for review in both GitHub and +Bugzilla. + +If that doesn't make any sense to you, that's ok: it doesn't to me, either. The +good news, however, is that you don't have to redo all the work you just did. +Normally, when you want to submit a patch for review on Bugzilla, you have to +create a diff for the patch and add it as an attachment to the bug (if you still +haven't opened one, this would be the time to do it). However, these changes are +also described by the commit of your patch, so its sufficient to attach a file +that links to the pull request. To find the link to your pull request, go to +your GitHub account and click the "Pull Requests" button at the top. This will +take you to a list of your active pull requests. You can use the template here +below as your attachment. Simply copy the link to your pull request, and use it +to replace all instances of \: + + + + + Bugzilla Code Review +

You can review this patch at , + or wait 5 seconds to be redirected there automatically.

+ +Finally, to add the attachment to the bug, go to the bug in Bugzilla, and click +on "Add an attachment" right above the comments. Make sure you fill out a +description for the attachment, and to set the review flag to '?' (you can find +a list of reviewers on +[this page](https://github.com/mozilla/addon-sdk/wiki/contribute)). The '?' here +means that you're making a request. If your patch gets a positive review, the +reviewer will set this flag to '+'. Otherwise, he/she will set it to '-', with +some feedback on why your patch got rejected. Of course, since we also use +GitHub for our review process, you're most likely to get your feedback there, +instead of Bugzilla. If your patch didn't get a positive review right away, +don't sweat it. If you waited for your bug to get confirmed before submitting +your patch, you'll usually only have to change a few small things to get a +positive review for your next attempt. Once your patch gets a positive review, +you don't need to do anything else. Since you did a pull request, it will +automatically be merged into the remote repository, usually by the person that +reviewed your patch. + +##Getting Additional Help +If something in this article wasn't clear to you, or if you need additional +help, the best place to go is irc. Mozilla relies heavily on irc for direct +communication between contributors. The SDK team hangs out on the #jetpack +channel on the irc.mozilla.org server (Jetpack was the original name of the +SDK, in case you're wondering). + +Unless you are know what you are doing, it can be hard to get the information +you need from irc, uso here are a few useful tips: + +* Mozilla is a global organization, with contributors all over the world, so the + people you are trying to reach are likely not in the same timezone as you. + Most contributors to the SDK are currently based in the US, so if you're in + Europe, and asking a question on irc in the early afternoon, you're not likely + to get many replies. + +* Most members of the SDK team are also Mozilla employees, which means they're + often busy doing other stuff. That doesn't mean they don't want to help you. + On the contrary: Mozilla encourages employees to help out contributors + whenever they can. But it does mean that we're sometimes busy doing other + things than checking irc, so your question may go unnoticed. If that happens, + the best course of action is often to just ask again. + +* If you direct your question to a specific person, rather than the entire + channel, your chances of getting an answer are a lot better. If you prefix + your message with that person's irc name, he/she will get a notification in + his irc client. Try to make sure that the person you're asking is actually the + one you need, though. Don't just ask random questions to random persons in the + hopes you'll get more response that way. + +* If you're not familiar with irc, a common idiom is to send someone a message + saying "ping" to ask if that person is there. When that person actually shows + up and sees the ping, he will send you a message back saying "pong". Cute, + isn't it? But hey, it works. + +* Even if someone does end up answering your questions, it can happen that that + person gets distracted by some other task and forget he/she was talking to + you. Please don't take that as a sign we don't care about your questions. We + do, but we too get busy sometimes: we're only human. If you were talking to + somebody and haven't gotten any reply to your last message for some time, feel + free to just ask again. + +* If you've decided to pick up a good first bug, you can (in theory at least) + get someone from the SDK team to mentor you. A mentor is someone who is + already familiar with the code who can walk you through it, and who is your go + to guy in case you have any questions about it. The idea of mentoring was + introduced a while ago to make it easier for new contributors to familiarize + themselves with the code. Unfortunately, it hasn't really caught on yet, but + we're trying to change that. So by all means: ask! diff --git a/addon-sdk/source/doc/dev-guide-source/guides/contributors-guide/modules.md b/addon-sdk/source/doc/dev-guide-source/guides/contributors-guide/modules.md new file mode 100644 index 000000000000..fedfd5a69abb --- /dev/null +++ b/addon-sdk/source/doc/dev-guide-source/guides/contributors-guide/modules.md @@ -0,0 +1,316 @@ + + +#Modules +A module is a self-contained unit of code, which is usually stored in a file, +and has a well defined interface. The use of modules greatly improves the +maintainability of code, by splitting it up into independent components, and +enforcing logical boundaries between them. Unfortunately, JavaScript does not +yet have native support for modules: it has to rely on the host application to +provide it with functionality such as loading subscripts, and exporting/ +importing names. We will show how to do each of these things using the built-in +Components object provided by Xulrunner application such as Firefox and +Thunderbird. + +To improve encapsulation, each module should be defined in the scope of its own +global object. This is made possible by the use of sandboxes. Each sandbox lives +in its own compartment. A compartment is a separate memory space. Each +compartment has a set of privileges that determines what scripts running in that +compartment can and cannot do. We will show how sandboxes and compartments can +be used to improve security in our module system. + +The module system used by the SDK is based on the CommonJS specification: it is +implemented using a loader object, which handles all the bookkeeping related to +module loading, such as resolving and caching URLs. We show how to create your +own custom loaders, using the `Loader` constructor provided by the SDK. The SDK +uses its own internal loader, known as Cuddlefish. All modules within the SDK +are loaded using Cuddlefish by default. Like any other custom loader, Cuddlefish +is created using the `Loader` constructor. In the final section, we will take a +look at some of the options passed by the SDK to the `Loader` constructor to +create the Cuddlefish loader. + +##Loading Subscripts +When a JavaScript project reaches a certain size, it becomes necessary to split +it up into multiple files. Unfortunately, JavaScript does not provide any means +to load scripts from other locations: we have to rely on the host application to +provide us with this functionality. Applications such as Firefox and Thunderbird +are based on Xulrunner. Xulrunner adds a built-in object, known as `Components`, +to the global scope. This object forms the central access point for all +functionality provided by the host application. A complete explanation of how to +use `Components` is out of scope for this document. However, the following +example shows how it can be used to load scripts from other locations: + + const { + classes: Cc + interfaces: Ci + } = Components; + + var instance = Cc["@mozilla.org/moz/jssubscript-loader;1"]; + var loader = instance.getService(Ci.mozIJSSubScriptLoader); + + function loadScript(url) { + loader.loadSubScript(url); + } + +When a script is loaded, it is evaluated in the scope of the global object of +the script that loaded it. Any property defined on the global object will be +accessible from both scripts: + + index.js: + loadScript("www.foo.com/a.js"); + foo; // => 3 + + a.js: + foo = 3; + +##Exporting Names +The script loader we obtained from the `Components` object allows us load +scripts from other locations, but its API is rather limited. For instance, it +does not know how to handle relative URLs, which is cumbersome if you want to +organize your project hierarchically. A more serious problem with the +`loadScript` function, however, is that it evaluates all scripts in the scope of +the same global object. This becomes a problem when two scripts try to define +the same property: + + index.js: + loadScript("www.foo.com/a.js"); + loadScript("www.foo.com/b.js"); + foo; // => 5 + + a.js: + foo = 3; + + b.js: + foo = 5; + +In the above example, the value of `foo` depends on the order in which the +subscripts are loaded: there is no way to access the property foo defined by +"a.js", since it is overwritten by "b.js". To prevent scripts from interfering +with each other, `loadScript` should evaluate each script to be loaded in the +scope of their own global object, and then return the global object as its +result. In effect, any properties defined by the script being loaded on its +global object are exported to the loading script. The script loader we obtained +from `Components` allows us to do just that: + + function loadScript(url) { + let global = {}; + loader.loadSubScript(url, global); + return global; + } + +If present, the `loadSubScript` function evaluates the script to be loaded in +the scope of the second argument. Using this new version of `loadScript`, we can +now rewrite our earlier example as follows + + index.js: + let a = loadScript("www.foo.com/a.js"); + let b = loadScript("www.foo.com/b.js"); + + a.foo // => 3 + b.foo; // => 5 + + a.js: + foo = 3; + + b.js: + foo = 5;: + +##Importing Names +In addition to exporting properties from the script being loaded to the loading +script, we can also import properties from the loading script to the script +being loaded: + + function loadScript(url, imports) { + let global = { + imports: imports, + exports: {} + }; + loader.loadSubScript(url, global); + return global.exports; + } + +Among other things, this allows us to import `loadScript` to scripts being +loaded, allowing them to load further scripts: + + index.js: + loadScript("www.foo.com/a.js", { + loadScript: loadScript + }).foo; => 5 + + a.js: + exports.foo = imports.loadScript("www.foo.com/b.js").bar; + + b.js: + exports.bar = 5; + +##Sandboxes and Compartments +The `loadScript` function as defined int the previous section still has some +serious shortcomings. The object it passed to the `loadSubScript` function is an +ordinary object, which has the global object of the loading script as its +prototype. This breaks encapsulation, as it allows the script being loaded to +access the built-in constructors of the loading script, which are defined on its +global object. The problem with breaking encapsulation like this is that +malicious scripts can use it to get the loading script to execute arbitrary +code, by overriding one of the methods on the built-in constructors. If the +loading script has chrome privileges, then so will any methods called by the +loading script, even if that method was installed by a malicious script. + +To avoid problems like this, the object passed to `loadSubScript` should be a +true global object, having its own instances of the built-in constructors. This +is exactly what sandboxes are for. A sandbox is a global object that lives in a +separate compartment. Compartments are a fairly recent addition to SpiderMonkey, +and can be seen as a separate memory space. Objects living in one compartment +cannot be accessed directly from another compartment: they need to be accessed +through an intermediate object, known as a wrapper. Compartments are very +useful from a security point of view: each compartment has a set of privileges +that determines what a script running in that compartment can and cannot do. +Compartments with chrome privileges have access to the `Components` object, +giving them full access to the host platform. In contrast, compartments with +content privileges can only use those features available to ordinary websites. + +The `Sandbox` constructor takes a `URL` parameter, which is used to determine +the set of privileges for the compartment in which the sandbox will be created. +Passing an XUL URL will result in a compartment with chrome privileges (note, +however, that if you ever actually do this in any of your code, Gabor will be +forced to hunt you down and kill you). Otherwise, the compartment will have +content privileges by default. Rewriting the `loadScript` function using +sandboxes, we end up with: + + function loadScript(url, imports) { + let global = Components.utils.Sandbox(url); + global.imports = imports; + global.exports = {}; + loader.loadSubScript(url, global); + return global.exports; + } + +Note that the object returned by `Sandbox` is a wrapper to the sandbox, not the +sandbox itself. A wrapper behaves exactly like the wrapped object, with one +difference: for each property access/function it performs an access check to +make sure that the calling script is actually allowed to access/call that +property/function. If the script being loaded is less privileged than the +loading script, the access is prevented, as the following example shows: + + index.js: + let a = loadScript("www.foo.com/a.js", { + Components: Components + }); + + // index.js has chrome privileges + Components.utils; // => [object nsXPCComponents_Utils] + + a.js: + // a.js has content privileges + imports.Components.utils; // => undefined + +##Modules in the Add-on SDK +The module system used by the SDK is based on what we learned so far: it follows +the CommonJS specification, which attempts to define a standardized module API. +A CommonJS module defines three global variables: `require`, which is a function +that behaves like `loadScript` in our examples, `exports`, which behaves +like the `exports` object, and `module`, which is an object representing +the module itself. The `require` function has some extra features not provided +by `loadScript`: it solves the problem of resolving relative URLs (which we have +left unresolved), and provides a caching mechanism, so that when the same module +is loaded twice, it returns the cached module object rather than triggering +another download. The module system is implemented using a loader object, which +is actually provided as a module itself. It is defined in the module +“toolkit/loader”: + + const { Loader } = require('toolkit/loader') + +The `Loader` constructor allows you to create your own custom loader objects. It +takes a single argument, which is a named options object. For instance, the +option `paths` is used to specify a list of paths to be used by the loader to +resolve relative URLs: + + let loader = Loader({ + paths: ["./": http://www.foo.com/"] + }); + +CommonJS also defines the notion of a main module. The main module is always the +first to be loaded, and differs from ordinary modules in two respects. Firstly, +since they do not have a requiring module. Instead, the main module is loaded +using a special function, called `main`: + + const { Loader, main } = require('toolkit/loader'); + + let loader = Loader({ + paths: ["./": http://www.foo.com/"] + }); + + main(loader, "./main.js"); + +Secondly, the main module is defined as a property on `require`. This allows +modules to check if it they have been loaded as the main module: + + function main() { + ... + } + + if (require.main === module) + main(); + +##The Cuddlefish Loader +The SDK uses its own internal loader, known as Cuddlefish (because we like crazy +names). Like any other custom loader, Cuddlefish is created using the `Loader` +constructor: Let's take a look at some of the options used by Cuddlefish to +customize its behavior. The way module ids are resolved can be customized by +passing a custom `resolve` function as an option. This function takes the id to +be resolved and the requiring module as an argument, and returns the resolved id +as its result. The resolved id is then further resolved using the paths array: + + const { Loader, main } = require('toolkit/loader'); + + let loader = Loader({ + paths: ["./": "http://www.foo.com/"], + resolve: function (id, requirer) { + // Your code here + return id; + } + }); + main(loader, "./main.js"); + +Cuddlefish uses a custom `resolve` function to implement a form of access +control: modules can only require modules for which they have been explicitly +granted access. A whitelist of modules is generated statically when the add-on +is linked. It is possible to pass a list of predefined modules as an option to +the `Loader` constructor. This is useful if the API to be exposed does not have +a corresponding JS file, or is written in an incompatible format. Cuddlefish +uses this option to expose the `Components` object as a module called `chrome`, +in a way similar to the code here below: + + const { + classes: Cc, + Constructor: CC, + interfaces: Ci, + utils: Cu, + results: Cr, + manager: Cm + } = Components; + + let loader = Loader({ + paths: ["./": "http://www.foo.com/"], + resolve: function (id, requirer) { + // Your logic here + return id; + }, + modules: { + 'chrome': { + components: Components, + Cc: Cc, + CC: bind(CC, Components), + Ci: Ci, + Cu: Cu, + Cr: Cr, + Cm: Cm + } + } + }); + +All accesses to the `chrome` module go through this one point. As a result, we +don't have to give modules chrome privileges on a case by case basis. More +importantly, however, any module that wants access to `Components` has to +explicitly express its intent via a call to `require("chrome")`. This makes it +possible to reason about which modules have chrome capabilities and which don't. diff --git a/addon-sdk/source/doc/dev-guide-source/guides/contributors-guide/private-properties.md b/addon-sdk/source/doc/dev-guide-source/guides/contributors-guide/private-properties.md new file mode 100644 index 000000000000..cc2b9428425c --- /dev/null +++ b/addon-sdk/source/doc/dev-guide-source/guides/contributors-guide/private-properties.md @@ -0,0 +1,261 @@ + + +#Private Properties + +A private property is a property that is only accessible to member +functions of instances of the same class. Unlike other languages, JavaScript +does not have native support for private properties. However, people have come +up with several ways to emulate private properties using existing language +features. We will take a look at two different techniques, using prefixes, and +closures, respectively. + +Prefixes and closures both have drawbacks in that they are either not +restrictive enough or too restrictive, respectively. We will therefore introduce +a third technique, based on the use of WeakMaps, that solves both these +problems. Note, however, that WeakMaps might not be supported by all +implementations yet. Next, we generalize the idea of using WeakMaps from +associating one or more private properties with an object to associating one or +more namespaces with each object. A namespace is simply an object on which one +or more private properties are defined. + +The SDK uses namespaces internally to implement private properties. The last +section explains how to work with the particular namespace implementation used +by the SDK. It is possible to read this section on its own, but to fully +appreciate how namespaces work, and the problem they are supposed to solve, it +is recommended that you read the entire article. + +##Using Prefixes + +A common technique to implement private properties is to prefix each private +property name with an underscore. Consider the following example: + + function Point(x, y) { + this._x = x; + this._y = y; + } + +The properties `_x` and `_y` are private, and should only be accessed by member +functions. + +To make a private property readable/writable from any function, it is common to +define a getter/setter function for the property, respectively: + + Point.prototype.getX = function () { + return this._x; + }; + + Point.prototype.setX = function (x) { + this._x = x; + }; + + Point.prototype.getY = function () { + return this._y; + }; + + Point.prototype.setY = function (y) { + this._y = y; + }; + +The above technique is simple, and clearly expresses our intent. However, the +use of an underscore prefix is just a coding convention, and is not enforced by +the language: there is nothing to prevent a user from directly accessing a +property that is supposed to be private. + +##Using Closures +Another common technique is to define private properties as variables, and their +getter and/or setter function as a closure over these variables: + + function Point(_x, _y) { + this.getX = function () { + return _x; + }; + + this.setX = function (x) { + _x = x; + }; + + this.getY = function () { + return _y; + }; + + this.setY = function (y) { + _y = y; + }; + } + +Note that this technique requires member functions that need access to private +properties to be defined on the object itself, instead of its prototype. This is +slightly less efficient, but this is probably acceptable. + +The advantage of this technique is that it offers more protection: there is no +way for the user to access a private property except by using its getter and/or +setter function. However, the use of closures makes private properties too +restrictive: since there is no way to access variables in one closure from +within another closure, there is no way for objects of the same class to access +each other's private properties. + +##Using WeakMaps + +The techniques we've seen so far ar either not restrictive enough (prefixes) or +too restrictive (closures). Until recently, a technique that solves both these +problems didn't exist. That changed with the introduction of WeakMaps. WeakMaps +were introduced to JavaScript in ES6, and have recently been implemented in +SpiderMonkey. Before we explain how WeakMaps work, let's take a look at how +ordinary objects can be used as hash maps, by creating a simple image cache: + + let images = {}; + + function getImage(name) { + let image = images[name]; + if (!image) { + image = loadImage(name); + images[name] = image; + } + return image; + } + +Now suppose we want to associate a thumbnail with each image. Moreover, we want +to create each thumbnail lazily, when it is first required: + + function getThumbnail(image) { + let thumbnail = image._thumbnail; + if (!thumbnail) { + thumbnail = createThumbnail(image); + image._thumbnail = thumbnail; + } + return thumbnail; + } + +This approach is straightforward, but relies on the use of prefixes. A better +approach would be to store thumbnails in their own, separate hash map: + + let thumbnails = {}; + + function getThumbnail(image) { + let thumbnail = thumbnails[image]; + if (!thumbnail) { + thumbnail = createThumbnail(image); + thumbnails[image] = thumbnail; + } + return thumbnail; + } + +There are two problems with the above approach. First, it's not possible to use +objects as keys. When an object is used as a key, it is converted to a string +using its toString method. To make the above code work, we'd have to associate a +unique identifier with each image, and override its `toString` method. The +second problem is more severe: the thumbnail cache maintains a strong reference +to each thumbnail object, so they will never be freed, even when their +corresponding image has gone out of scope. This is a memory leak waiting to +happen. + +The above two problems are exactly what WeakMaps were designed to solve. A +WeakMap is very similar to an ordinary hash map, but differs from it in two +crucial ways: + +1. It can use ordinary objects as keys +2. It does not maintain a strong reference to its values + +To understand how WeakMaps are used in practice, let's rewrite the thumbnail +cache using WeakMaps: + + let thumbnails = new WeakMap(); + + function getThumbnail(image) { + let thumbnail = thumbnails.get(image); + if (!thumbnail) { + thumbnail = createThumbnail(image); + thumbnails.set(image, thumbnail); + } + return thumbnail; + } + +This version suffers of none of the problems we mentioned earlier. When a +thumbnail's image goes out of scope, the WeakMap ensures that its entry in the +thumbnail cache will eventually be garbage collected. As a final caveat: the +image cache we created earlier suffers from the same problem, so for the above +code to work properly, we'd have to rewrite the image cache using WeakMaps, too. + +#From WeakMaps to Namespaces +In the previous section we used a WeakMap to associate a private property with +each object. Note that we need a separate WeakMap for each private property. +This is cumbersome if the number of private properties becomes large. A better +solution would be to store all private properties on a single object, called a +namespace, and then store the namespace as a private property on the original +object. Using namespaces, our earlier example can be rewritten as follows: + + let map = new WeakMap(); + + let internal = function (object) { + if (!map.has(object)) + map.set(object, {}); + return map.get(object); + } + + function Point(x, y) { + internal(this).x = x; + internal(this).y = y; + } + + Point.prototype.getX = function () { + return internal(shape).x; + }; + + Point.prototype.setX = function (x) { + internal(shape).x = x; + }; + + Point.prototype.getY = function () { + return internal(shape).y; + }; + + Point.prototype.setY = function () { + internal(shape).y = y; + }; + +The only way for a function to access the properties `x` and `y` is if it has a +reference to an instance of `Point` and its `internal` namespace. By keeping the +namespace hidden from all functions except members of `Point`, we have +effectively implemented private properties. Moreover, because members of `Point` +have a reference to the `internal` namespace, they can access private properties +on other instances of `Point`. + +##Namespaces in the Add-on SDK +The Add-on SDK is built on top of XPCOM, the interface between JavaScript and +C++ code. Since XPCOM allows the user to do virtually anything, security is very +important. Among other things, we don't want add-ons to be able to access +variables that are supposed to be private. The SDK uses namespaces internally to +ensure this. As always with code that is heavily reused, the SDK defines a +helper function to create namespaces. It is defined in the module +"core/namespace", and it's usage is straightforward. To illustrate this, let's +reimplement the class `Point` using namespaces: + + const { ns } = require("./core/namespace"); + + var internal = ns(); + + function Point(x, y) { + internal(this).x = x; + internal(this).y = y; + } + + Point.prototype.getX = function () { + return internal(shape).x; + }; + + Point.prototype.setX = function (x) { + internal(shape).x = x; + }; + + Point.prototype.getY = function () { + return internal(shape).y; + }; + + Point.prototype.setY = function () { + internal(shape).y = y; + }; + +As a final note, the function `ns` returns a namespace that uses the namespace +associated with the prototype of the object as its prototype. diff --git a/addon-sdk/source/doc/dev-guide-source/guides/index.md b/addon-sdk/source/doc/dev-guide-source/guides/index.md index 4e66e7cc7c41..e288dadfb092 100644 --- a/addon-sdk/source/doc/dev-guide-source/guides/index.md +++ b/addon-sdk/source/doc/dev-guide-source/guides/index.md @@ -8,6 +8,59 @@ This page lists more theoretical in-depth articles about the SDK.
+

Contributor's Guide

+ + ++++ + + + + + + + + + + + + + + + + + + +
+

Getting Started

+ Learn how to contribute to the SDK: getting the code, opening/taking a + bug, filing a patch, getting reviews, and getting help. +
+

Private Properties

+ Learn how private properties can be implemented in JavaScript using + prefixes, closures, and WeakMaps, and how the SDK supports private + properties by using namespaces (which are a generalization of WeakMaps). +
+

Modules

+ Learn about the module system used by the SDK (which is based on the + CommonJS specification), how sandboxes and compartments can be used to + improve security, and about the built-in SDK module loader, known as + Cuddlefish. +
+

Content Processes

+ The SDK was designed to work in an environment where the code to + manipulate web content runs in a different process from the main add-on + code. This article highlights the main features of that design. +
+

Classes and Inheritance

+ Learn how classes and inheritance can be implemented in JavaScript, using + constructors and prototypes, and about the helper functions provided by + the SDK to simplify this. +
+
+

SDK Infrastructure

diff --git a/addon-sdk/source/doc/dev-guide-source/guides/modules.md b/addon-sdk/source/doc/dev-guide-source/guides/modules.md index d56d6dc29667..ece017542fe3 100644 --- a/addon-sdk/source/doc/dev-guide-source/guides/modules.md +++ b/addon-sdk/source/doc/dev-guide-source/guides/modules.md @@ -31,67 +31,21 @@ SDK-based add-ons. ## SDK Modules ## -All the modules supplied with the SDK can be found in the "lib" -directory under the SDK root. The following diagram shows a reduced view -of the SDK tree, with the "lib" directory highlighted. +The modules supplied by the SDK are divided into two sorts: -
    -
  • addon-sdk -
      -
    • app-extension
    • -
    • bin
    • -
    • data
    • -
    • doc
    • -
    • examples
    • -
    • lib -
        -
      • sdk -
          -
        • core -
            -
          • heritage.js
          • -
          • namespace.js
          • -
          -
        • -
        • panel.js
        • -
        • page-mod.js
        • -
        -
      • -
      • toolkit -
          -
        • loader.js
        • -
        -
      • -
      -
    • -
    • python-lib
    • -
    • test
    • -
    -
  • -
- -All modules that are specifically intended for users of the -SDK are stored under "lib" in the "sdk" directory. - -[High-level modules](dev-guide/high-level-apis.html) like +* [High-level modules](dev-guide/high-level-apis.html) like [`panel`](modules/sdk/panel.html) and - [`page-mod`](modules/sdk/page-mod.html) are directly underneath -the "sdk" directory. - -[Low-level modules](dev-guide/low-level-apis.html) like + [`page-mod`](modules/sdk/page-mod.html) provide relatively simple, +stable APIs for the most common add-on development tasks. +* [Low-level modules](dev-guide/low-level-apis.html) like [`heritage`](modules/sdk/core/heritage.html) and -[`namespace`](modules/sdk/core/heritage.html) are grouped in subdirectories -of "sdk" such as "core". +[`namespace`](modules/sdk/core/heritage.html) provide more +powerful functionality, and are typically less stable and more +complex. -Very generic, platform-agnostic modules that are shared with other -projects, such as [`loader`](modules/toolkit/loader.html), are stored -in "toolkit". - -
- -To use SDK modules, you can pass `require` a complete path from -(but not including) the "lib" directory to the module you want to use. -For high-level modules this is just `sdk/`, and for low-level +To use SDK modules, you can pass `require()` a complete path, starting with +"sdk", to the module you want to use. For high-level modules this is just +`sdk/`, and for low-level modules it is `sdk//`: // load the high-level "tabs" module @@ -100,13 +54,19 @@ modules it is `sdk//`: // load the low-level "uuid" module var uuid = require('sdk/util/uuid'); -For high-level modules only, you can also pass just the name of the module: +The path to specify for a low-level module is given along with the module +name itself in the title of the module's documentation page (for example, +[system/environment](modules/sdk/system/environment.html)). - var tabs = require("tabs"); - -However, this is ambiguous, as it could also refer to a local module in your -add-on named `tabs`. For this reason it is better to use the full path from -"lib". +Although the [SDK repository in GitHub](https://github.com/mozilla/addon-sdk) +includes copies of these modules, they are built into Firefox and by +default, when you run or build an add-on using +[`cfx run`](dev-guide/cfx-tool.html#cfx-run) +or [`cfx xpi`](dev-guide/cfx-tool.html#cfx-xpi), it is the versions of +the modules in Firefox that are used. If you need to use a different version +of the modules, you can do this by checking out the version of the SDK +that you need and passing the `-o` or +`--overload-modules` option to `cfx run` or `cfx xpi`. ## Local Modules ## diff --git a/addon-sdk/source/doc/dev-guide-source/index.md b/addon-sdk/source/doc/dev-guide-source/index.md index 802e823434be..36df858c2790 100644 --- a/addon-sdk/source/doc/dev-guide-source/index.md +++ b/addon-sdk/source/doc/dev-guide-source/index.md @@ -77,6 +77,27 @@ Learn about common development techniques, such as + + + + + + + @@ -106,11 +128,6 @@ Learn about common development techniques, such as diff --git a/addon-sdk/source/doc/dev-guide-source/tutorials/getting-started-with-cfx.md b/addon-sdk/source/doc/dev-guide-source/tutorials/getting-started-with-cfx.md index 1f423776f703..cb56d3b53c37 100644 --- a/addon-sdk/source/doc/dev-guide-source/tutorials/getting-started-with-cfx.md +++ b/addon-sdk/source/doc/dev-guide-source/tutorials/getting-started-with-cfx.md @@ -168,3 +168,24 @@ add-on. Now you have the basic `cfx` commands, you can try out the [SDK's features](dev-guide/tutorials/index.html). + +## Overriding the Built-in Modules ## + +The SDK modules you use to implement your add-on are built into Firefox. +When you run or package an add-on using `cfx run` or `cfx xpi`, the add-on +will use the versions of the modules in the version of Firefox that hosts +it. + +As an add-on developer, this is usually what you want. But if you're +developing the SDK modules themselves, of course it won't work at all. +In this case it's assumed that you will have checked out the SDK from +its [GitHub repo](https://github.com/mozilla/addon-sdk) and will have +run [`source/activate`](dev-guide/tutorials/installation.html) from +the root of your checkout. + +Then when you invoke `cfx run` or `cfx xpi`, you pass the `"-o"` option: + +
cfx run -o
+ +This instructs cfx to use the local copies of the SDK modules, not the +ones in Firefox. diff --git a/addon-sdk/source/doc/module-source/sdk/indexed-db.md b/addon-sdk/source/doc/module-source/sdk/indexed-db.md index 81390e17e275..260374593ab9 100644 --- a/addon-sdk/source/doc/module-source/sdk/indexed-db.md +++ b/addon-sdk/source/doc/module-source/sdk/indexed-db.md @@ -32,20 +32,128 @@ So you can use the `indexed-db` module to access the same API: console.log("success"); }; -This module also exports all the other objects that implement -the IndexedDB API, listed below under -[API Reference](modules/sdk/indexed-db.html#API Reference). +Most of the objects that implement the IndexedDB API, such as +[IDBTransaction](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBTransaction), +[IDBOpenDBRequest](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBOpenDBRequest), +and [IDBObjectStore](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBObjectStore), +are accessible through the indexedDB object itself. The API exposed by `indexed-db` is almost identical to the DOM IndexedDB API, so we haven't repeated its documentation here, but refer you to the [IndexedDB API documentation](https://developer.mozilla.org/en-US/docs/IndexedDB) for all the details. -The database created will be unique and private per addon, and is not linked to any website database. The module cannot be used to interact with a given website database. See [bug 778197](https://bugzilla.mozilla.org/show_bug.cgi?id=779197) and [bug 786688](https://bugzilla.mozilla.org/show_bug.cgi?id=786688). +The database created will be unique and private per add-on, and is not linked +to any website database. The module cannot be used to interact with a given +website database. See +[bug 778197](https://bugzilla.mozilla.org/show_bug.cgi?id=779197) and +[bug 786688](https://bugzilla.mozilla.org/show_bug.cgi?id=786688). -## Example of Usage +## Example -[Promise-based example using indexedDB for record storage](https://github.com/gregglind/micropilot/blob/ec65446d611a65b0646be1806359c463193d5a91/lib/micropilot.js#L80-L198). +Here's a complete add-on that adds two widgets to the browser: the widget labeled +"Add" add the title of the current tab to a database, while the widget labeled +"List" lists all the titles in the database. + +The add-on implements helper functions `open()`, `addItem()` and `getItems()` +to open the database, add a new item to the database, and get all items in the +database. + + var { indexedDB, IDBKeyRange } = require('sdk/indexed-db'); + var widgets = require("sdk/widget"); + + var database = {}; + + database.onerror = function(e) { + console.error(e.value) + } + + function open(version) { + var request = indexedDB.open("stuff", version); + + request.onupgradeneeded = function(e) { + var db = e.target.result; + e.target.transaction.onerror = database.onerror; + + if(db.objectStoreNames.contains("items")) { + db.deleteObjectStore("items"); + } + + var store = db.createObjectStore("items", + {keyPath: "time"}); + }; + + request.onsuccess = function(e) { + database.db = e.target.result; + }; + + request.onerror = database.onerror; + }; + + function addItem(name) { + var db = database.db; + var trans = db.transaction(["items"], "readwrite"); + var store = trans.objectStore("items"); + var time = new Date().getTime(); + var request = store.put({ + "name": name, + "time": time + }); + + request.onerror = database.onerror; + }; + + function getItems(callback) { + var cb = callback; + var db = database.db; + var trans = db.transaction(["items"], "readwrite"); + var store = trans.objectStore("items"); + var items = new Array(); + + trans.oncomplete = function() { + cb(items); + } + + var keyRange = IDBKeyRange.lowerBound(0); + var cursorRequest = store.openCursor(keyRange); + + cursorRequest.onsuccess = function(e) { + var result = e.target.result; + if(!!result == false) + return; + + items.push(result.value.name); + result.continue(); + }; + + cursorRequest.onerror = database.onerror; + }; + + function listItems(itemList) { + console.log(itemList); + } + + open("1"); + + widgets.Widget({ + id: "add-it", + width: 50, + label: "Add", + content: "Add", + onClick: function() { + addItem(require("sdk/tabs").activeTab.title); + } + }); + + widgets.Widget({ + id: "list-them", + width: 50, + label: "List", + content: "List", + onClick: function() { + getItems(listItems); + } + }); @property {object} @@ -61,71 +169,6 @@ Defines a range of keys. See the [IDBKeyRange documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBKeyRange). - -@property {object} - -For traversing or iterating records in a database. -See the [IDBCursor documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBCursor). - - - - -@property {object} - -Represents a database transaction. -See the [IDBTransaction documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBTransaction). - - - -@property {object} - -Represents an asynchronous request to open a database. -See the [IDBOpenDBRequest documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBOpenDBRequest). - - - -@property {object} - -Event indicating that the database version has changed. -See the [IDBVersionChangeEvent documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBVersionChangeEvent). - - - -@property {object} - -Represents a connection to a database. -See the [IDBDatabase documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBDatabase). - - - -@property {object} - -Enables you to create, open, and delete databases. -See the [IDBFactory documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBFactory). - - - -@property {object} - -Provides access to a database index. -See the [IDBIndex documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBIndex). - - - -@property {object} - -Represents an object store in a database. -See the [IDBObjectStore documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBObjectStore). - - - -@property {object} - -Provides access to the results of asynchronous requests to databases -and database objects. -See the [IDBRequest documentation](https://developer.mozilla.org/en-US/docs/IndexedDB/IDBRequest). - - @property {object} diff --git a/addon-sdk/source/doc/module-source/sdk/places/bookmarks.md b/addon-sdk/source/doc/module-source/sdk/places/bookmarks.md new file mode 100644 index 000000000000..a98a83b5f5c6 --- /dev/null +++ b/addon-sdk/source/doc/module-source/sdk/places/bookmarks.md @@ -0,0 +1,450 @@ + + +The `places/bookmarks` module provides functions for creating, modifying and searching bookmark items. It exports: + +* three constructors: [Bookmark](modules/sdk/places/bookmarks.html#Bookmark), [Group](modules/sdk/places/bookmarks.html#Group), and [Separator](modules/sdk/places/bookmarks.html#Separator), corresponding to the types of objects, referred to as **bookmark items**, in the Bookmarks database in Firefox +* two additional functions, [`save()`](modules/sdk/places/bookmarks.html#save(bookmarkItems%2C%20options)) to create, update, and remove bookmark items, and [`search()`](modules/sdk/places/bookmarks.html#search(queries%2C%20options)) to retrieve the bookmark items that match a particular set of criteria. + +`save()` and `search()` are both asynchronous functions: they synchronously return a [`PlacesEmitter`](modules/sdk/places/bookmarks.html#PlacesEmitter) object, which then asynchronously emits events as the operation progresses and completes. + +Each retrieved bookmark item represents only a snapshot of state at a specific time. The module does not automatically sync up a `Bookmark` instance with ongoing changes to that item in the database from the same add-on, other add-ons, or the user. + +## Examples + +### Creating a new bookmark + + let { Bookmark, save } = require("sdk/places/bookmarks"); + + // Create a new bookmark instance, unsaved + let bookmark = Bookmark({ title: "Mozilla", url: "http://mozila.org" }); + + // Attempt to save the bookmark instance to the Bookmarks database + // and store the emitter + let emitter = save(bookmark); + + // Listen for events + emitter.on("data", function (saved, inputItem) { + // on a "data" event, an item has been updated, passing in the + // latest snapshot from the server as `saved` (with properties + // such as `updated` and `id`), as well as the initial input + // item as `inputItem` + console.log(saved.title === inputItem.title); // true + console.log(saved !== inputItem); // true + console.log(inputItem === bookmark); // true + }).on("end", function (savedItems, inputItems) { + // Similar to "data" events, except "end" is an aggregate of + // all progress events, with ordered arrays as `savedItems` + // and `inputItems` + }); + +### Creating several bookmarks with a new group + + let { Bookmark, Group, save } = require("sdk/places/bookmarks"); + + let group = Group({ title: "Guitars" }); + let bookmarks = [ + Bookmark({ title: "Ran", url: "http://ranguitars.com", group: group }), + Bookmark({ title: "Ibanez", url: "http://ibanez.com", group: group }), + Bookmark({ title: "ESP", url: "http://espguitars.com", group: group }) + ]; + + // Save `bookmarks` array -- notice we don't have `group` in the array, + // although it needs to be saved since all bookmarks are children + // of `group`. This will be saved implicitly. + + save(bookmarks).on("data", function (saved, input) { + // A data event is called once for each item saved, as well + // as implicit items, like `group` + console.log(input === group || ~bookmarks.indexOf(input)); // true + }).on("end", function (saves, inputs) { + // like the previous example, the "end" event returns an + // array of all of our updated saves. Only explicitly saved + // items are returned in this array -- the `group` won't be + // present here. + console.log(saves[0].title); // "Ran" + console.log(saves[2].group.title); // "Guitars" + }); + +### Searching for bookmarks + +Bookmarks can be queried with the [`search()`](modules/sdk/places/bookmarks.html#search(queries%2C%20options)) function, which accepts a query object or an array of query objects, as well as a query options object. Query properties are AND'd together within a single query object, but are OR'd together across multiple query objects. + + let { search, UNSORTED } = require("sdk/places/bookmarks"); + + // Simple query with one object + search( + { query: "firefox" }, + { sort: "title" } + ).on(end, function (results) { + // results matching any bookmark that has "firefox" + // in its URL, title or tag, sorted by title + }); + + // Multiple queries are OR'd together + search( + [{ query: "firefox" }, { group: UNSORTED, tags: ["mozilla"] }], + { sort: "title" } + ).on("end", function (results) { + // Our first query is the same as the simple query above; + // all of those results are also returned here. Since multiple + // queries are OR'd together, we also get bookmarks that + // match the second query. The second query's properties + // are AND'd together, so results that are in the platform's unsorted + // bookmarks folder, AND are also tagged with 'mozilla', get returned + // as well in this query + }); + + +@class + +@constructor + +Creates an unsaved bookmark instance. +@param options {object} + Options for the bookmark, with the following parameters: + @prop title {string} + The title for the bookmark. Required. + @prop url {string} + The URL for the bookmark. Required. + @prop [group] {Group} + The parent group that the bookmark lives under. Defaults to the [Bookmarks.UNSORTED](modules/sdk/places/bookmarks.html#UNSORTED) group. + @prop [index] {number} + The index of the bookmark within its group. Last item within the group by default. + @prop [tags] {set} + A set of tags to be applied to the bookmark. + + + +@property {string} + The bookmark's title. + + + +@property {string} + The bookmark's URL. + + + +@property {Group} + The group instance that the bookmark lives under. + + + +@property {number} + The index of the bookmark within its group. + + + +@property {number} + A Unix timestamp indicating when the bookmark was last updated on the platform. + + + +@property {set} + A [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) of tags that the bookmark is tagged with. + + + + +@class + +@constructor + +Creates an unsaved bookmark group instance. +@param options {object} + Options for the bookmark group, with the following parameters: + @prop title {string} + The title for the group. Required. + @prop [group] {Group} + The parent group that the bookmark group lives under. Defaults to the [Bookmarks.UNSORTED](modules/sdk/places/bookmarks.html#UNSORTED) group. + @prop [index] {number} + The index of the bookmark group within its parent group. Last item within the group by default. + + + +@property {string} + The bookmark group's title. + + + +@property {Group} + The group instance that the bookmark group lives under. + + + +@property {number} + The index of the bookmark group within its group. + + + +@property {number} + A Unix timestamp indicating when the bookmark was last updated on the platform. + + + + +@class + +@constructor + +Creates an unsaved bookmark separator instance. +@param options {object} + Options for the bookmark group, with the following parameters: + @prop [group] {Group} + The parent group that the bookmark group lives under. Defaults to the [Bookmarks.UNSORTED](modules/sdk/places/bookmarks.html#UNSORTED) group. + @prop [index] {number} + The index of the bookmark group within its parent group. Last item within the group by default. + + + +@property {Group} + The group instance that the bookmark group lives under. + + + +@property {number} + The index of the bookmark group within its group. + + + +@property {number} + A Unix timestamp indicating when the bookmark was last updated on the platform. + + + + +@function + +Creating, saving, and deleting bookmarks are all done with the `save()` function. This function takes in any of: + +* a bookmark item (Bookmark, Group, Separator) +* a duck-typed object (the relative properties for a bookmark item, in addition to a `type` property of `'bookmark'`, `'group'`, or `'separator'`) +* an array of bookmark items. + +All of the items passed in are pushed to the platform and are either created, updated or deleted. + +* adding items: if passing in freshly instantiated bookmark items or a duck-typed object, the item is created on the platform. +* updating items: for an item referenced from a previous `save()` or from the result of a `search()` query, changing the properties and calling `save()` will update the item on the server. +* deleting items: to delete a bookmark item, pass in a bookmark item with a property `remove` set to `true`. + +The function returns a [`PlacesEmitter`](modules/sdk/places/bookmarks.html#PlacesEmitter) that emits a `data` event for each item as it is saved, and an `end` event when all items have been saved. + + let { Bookmark, Group } = require("sdk/places/bookmarks"); + + let myGroup = Group({ title: "My Group" }); + let myBookmark = Bookmark({ + title: "Moz", + url: "http://mozilla.com", + group: myGroup + }); + + save(myBookmark).on("data", function (item, inputItem) { + // The `data` event returns the latest snapshot from the + // host, so this is a new instance of the bookmark item, + // where `item !== myBookmark`. To match it with the input item, + // use the second argument, so `inputItem === myBookmark` + + // All explicitly saved items have data events called, as + // well as implicitly saved items. In this case, + // `myGroup` has to be saved before `myBookmark`, since + // `myBookmark` is a child of `myGroup`. `myGroup` will + // also have a `data` event called for it. + }).on("end", function (items, inputItems) { + // The `end` event returns all items that are explicitly + // saved. So `myGroup` will not be in this array, + // but `myBookmark` will be. + // `inputItems` matches the initial input as an array, + // so `inputItems[0] === myBookmark` + }); + + // Saving multiple bookmarks, as duck-types in this case + + let bookmarks = [ + { title: "mozilla", url: "http://mozilla.org", type: "bookmark" }, + { title: "firefox", url: "http://firefox.com", type: "bookmark" }, + { title: "twitter", url: "http://twitter.com", type: "bookmark" } + ]; + + save(bookmarks).on("data", function (item, inputItem) { + // Each item in `bookmarks` has its own `data` event + }).on("end", function (results, inputResults) { + // `results` is an array of items saved in the same order + // as they were passed in. + }); + +@param bookmarkItems {bookmark|group|separator|array} + A bookmark item ([Bookmark](modules/sdk/places/bookmarks.html#Bookmark), [Group](modules/sdk/places/bookmarks.html#Group), [Separator](modules/sdk/places/bookmarks.html#Separator)), or an array of bookmark items to be saved. + +@param [options] {object} + An optional options object that takes the following properties: + @prop [resolve] {function} + A resolution function that is invoked during an attempt to save + a bookmark item that is not derived from the latest state from + the platform. Invoked with two arguments, `mine` and `platform`, where + `mine` is the item that is being saved, and `platform` is the + current state of the item on the item. The object returned from + this function is what is saved on the platform. By default, all changes + on an outdated bookmark item overwrite the platform's bookmark item. + +@returns {PlacesEmitter} + Returns a [PlacesEmitter](modules/sdk/places/bookmarks.html#PlacesEmitter). + + + +@function + +A helper function that takes in a bookmark item, or an `Array` of several bookmark items, and sets each item's `remove` property to true. This does not remove the bookmark item from the database: it must be subsequently saved. + + let { search, save, remove } = require("sdk/places/bookmarks"); + + search({ tags: ["php"] }).on("end", function (results) { + // The search returns us all bookmark items that are + // tagged with `"php"`. + + // We then pass `results` into the remove function to mark + // all items to be removed, which returns the new modified `Array` + // of items, which is passed into save. + save(remove(results)).on("end", function (results) { + // items tagged with `"php"` are now removed! + }); + }) + +@param items {Bookmark|Group|Separator|array} + A bookmark item, or `Array` of bookmark items to be transformed to set their `remove` property to `true`. + +@returns {array} + An array of the transformed bookmark items. + + + +@function + +Queries can be performed on bookmark items by passing in one or more query objects, each of which is given one or more properties. + +Within each query object, the properties are AND'd together: so only objects matching all properties are retrieved. Across query objects, the results are OR'd together, meaning that if an item matches any of the query objects, it will be retrieved. + +For example, suppose we called `search()` with two query objects: + +
[{ url: "mozilla.org", tags: ["mobile"]},
+ { tags: ["firefox-os"]}]
+ +This will return: + +* all bookmark items from mozilla.org that are also tagged "mobile" +* all bookmark items that are tagged "firefox-os" + +An `options` object may be used to determine overall settings such as sort order and how many objects should be returned. + +@param queries {object|array} +An `Object` representing a query, or an `Array` of `Objects` representing queries. Each query object can take several properties, which are queried against the bookmarks database. Each property is AND'd together, meaning that bookmarks must match each property within a query object. Multiple query objects are then OR'd together. + @prop [group] {Group} + Group instance that should be owners of the returned children bookmarks. If no `group` specified, all bookmarks are under the search space. + @prop [tags] {set|array} + Bookmarks with corresponding tags. These are AND'd together. + @prop [url] {string} + A string that matches bookmarks' URL. The following patterns are accepted: + + `'*.mozilla.com'`: matches any URL with 'mozilla.com' as the host, accepting any subhost. + + `'mozilla.com'`: matches any URL with 'mozilla.com' as the host. + + `'http://mozilla.com'`: matches 'http://mozilla.com' exactly. + + `'http://mozilla.com/*'`: matches any URL that starts with 'http://mozilla.com/'. + @prop [query] {string} + A string that matches bookmarks' URL, title and tags. + +@param [options] {object} +An `Object` with options for the search query. + @prop [count] {number} + The number of bookmark items to return. If left undefined, no limit is set. + @prop [sort] {string} + A string specifying how the results should be sorted. Possible options are `'title'`, `'date'`, `'url'`, `'visitCount'`, `'dateAdded'` and `'lastModified'`. + @prop [descending] {boolean} + A boolean specifying whether the results should be in descending order. By default, results are in ascending order. + +
+ + +@class + +The `PlacesEmitter` is not exported from the module, but returned from the `save` and `search` functions. The `PlacesEmitter` inherits from [`event/target`](modules/sdk/event/target.html), and emits `data`, `error`, and `end`. + +`data` events are emitted for every individual operation (such as: each item saved, or each item found by a search query), whereas `end` events are emitted as the aggregate of an operation, passing an array of objects into the handler. + + +@event +The `data` event is emitted when a bookmark item that was passed into the `save` method has been saved to the platform. This includes implicit saves that are dependencies of the explicit items saved. For example, when creating a new bookmark group with two bookmark items as its children, and explicitly saving the two bookmark children, the unsaved parent group will also emit a `data` event. + + let { Bookmark, Group, save } = require("sdk/places/bookmarks"); + + let group = Group({ title: "my group" }); + let bookmarks = [ + Bookmark({ title: "mozilla", url: "http://mozilla.com", group: group }), + Bookmark({ title: "w3", url: "http://w3.org", group: group }) + ]; + + save(bookmarks).on("data", function (item) { + // This function will be called three times: + // once for each bookmark saved + // once for the new group specified implicitly + // as the parent of the two items + }); + +The `data` event is also called for `search` requests, with each result being passed individually into its own `data` event. + + let { search } = require("sdk/places/bookmarks"); + + search({ query: "firefox" }).on("data", function (item) { + // each bookmark item that matches the query will + // be called in this function + }); + +@argument {Bookmark|Group|Separator} + For the `save` function, this is the saved, latest snapshot of the bookmark item. For `search`, this is a snapshot of a bookmark returned from the search query. + +@argument {Bookmark|Group|Separator|object} + Only in `save` data events. The initial instance of the item that was used for the save request. + + + +@event +The `error` event is emitted whenever a bookmark item's save could not be completed. + +@argument {string} + A string indicating the error that occurred. + +@argument {Bookmark|Group|Separator|object} + Only in `save` error events. The initial instance of the item that was used for the save request. + + + +@event +The `end` event is called when all bookmark items and dependencies +have been saved, or an aggregate of all items returned from a search query. + +@argument {array} + The array is an ordered list of the input bookmark items, replaced + with their updated, latest snapshot instances (the first argument + in the `data` handler), or in the case of an error, the initial instance + of the item that was used for the save request + (the second argument in the `data` or `error` handler). + + + + +@property {group} +This is a constant, default [`Group`](modules/sdk/places/bookmarks.html#Group) on the Firefox platform, the **Bookmarks Menu**. It can be used in queries or specifying the parent of a bookmark item, but it cannot be modified. + + + +@property {group} +This is a constant, default [`Group`](modules/sdk/places/bookmarks.html#Group) on the Firefox platform, the **Bookmarks Toolbar**. It can be used in queries or specifying the parent of a bookmark item, but it cannot be modified. + + + +@property {group} +This is a constant, default [`Group`](modules/sdk/places/bookmarks.html#Group) on the Firefox platform, the **Unsorted Bookmarks** group. It can be used in queries or specifying the parent of a bookmark item, but it cannot be modified. + diff --git a/addon-sdk/source/doc/module-source/sdk/places/history.md b/addon-sdk/source/doc/module-source/sdk/places/history.md new file mode 100644 index 000000000000..fe3ed144588b --- /dev/null +++ b/addon-sdk/source/doc/module-source/sdk/places/history.md @@ -0,0 +1,110 @@ + + +The `places/history` module provides a single function, [`search()`](modules/sdk/places/history.html#search(queries%2C%20options)), for querying the user's browsing history. + +It synchronously returns a [`PlacesEmitter`](modules/sdk/places/history.html#PlacesEmitter) object which then asynchronously emits [`data`](modules/sdk/places/history.html#data) and [`end`](modules/sdk/places/history.html#end) or [`error`](modules/sdk/places/history.html#error) events that contain information about the state of the operation. + +## Example + + let { search } = require("sdk/places/history"); + + // Simple query + search( + { url: "https://developers.mozilla.org/*" }, + { sort: "visitCount" } + ).on("end", function (results) { + // results is an array of objects containing + // data about visits to any site on developers.mozilla.org + // ordered by visit count + }); + + // Complex query + // The query objects are OR'd together + // Let's say we want to retrieve all visits from before a week ago + // with the query of 'ruby', but from last week onwards, we want + // all results with 'javascript' in the URL or title. + // We'd compose the query with the following options + let lastWeek = Date.now - (1000*60*60*24*7); + search( + // First query looks for all entries before last week with 'ruby' + [{ query: "ruby", to: lastWeek }, + // Second query searches all entries after last week with 'javascript' + { query: "javascript", from: lastWeek }], + // We want to order chronologically by visit date + { sort: "date" } + ).on("end", function (results) { + // results is an array of objects containing visit data, + // sorted by visit date, with all entries from more than a week ago + // that contain 'ruby', *in addition to* entries from this last week + // that contain 'javascript' + }); + + +@function + +Queries can be performed on history entries by passing in one or more query options. Each query option can take several properties, which are **AND**'d together to make one complete query. For additional queries within the query, passing more query options in will **OR** the total results. An `options` object may be specified to determine overall settings, like sorting and how many objects should be returned. + +@param queries {object|array} +An `Object` representing a query, or an `Array` of `Objects` representing queries. Each query object can take several properties, which are queried against the history database. Each property is **AND**'d together, meaning that bookmarks must match each property within a query object. Multiple query objects are then **OR**'d together. + @prop [url] {string} + A string that matches bookmarks' URL. The following patterns are accepted: + + `'*.mozilla.com'`: matches any URL with 'mozilla.com' as the host, accepting any subhost. + + `'mozilla.com'`: matches any URL with 'mozilla.com' as the host. + + `'http://mozilla.com'`: matches 'http://mozilla.com' directlry. + + `'http://mozilla.com/*'`: matches any URL that starts with 'http://mozilla.com/'. + @prop [query] {string} + A string that matches bookmarks' URL, or title. + @prop [from] {number|date} + Time relative from the [Unix epoch](http://en.wikipedia.org/wiki/Unix_time) that history results should be limited to occuring after. Can accept a [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object, or milliseconds from the epoch. Default is to return all items since the epoch (all time). + @prop [to] {number|date} + Time relative from the [Unix epoch](http://en.wikipedia.org/wiki/Unix_time) that history results should be limited to occuring before. Can accept a [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object, or milliseconds from the epoch. Default is the current time. + +@param [options] {object} +An `Object` with options for the search query. + @prop [count] {number} + The number of bookmark items to return. If left undefined, no limit is set. + @prop [sort] {string} + A string specifying how the results should be sorted. Possible options are `'title'`, `'date'`, `'url'`, `'visitCount'`, `'keyword'`, `'dateAdded'` and `'lastModified'`. + @prop [descending] {boolean} + A boolean specifying whether the results should be in descending order. By default, results are in ascending order. + + + + + +@class + +The `PlacesEmitter` is not exposed in the module, but returned from the `search` functions. The `PlacesEmitter` inherits from [`event/target`](modules/sdk/event/target.html), and emits `data`, `error`, and `end`. `data` events are emitted for every individual search result found, whereas `end` events are emitted as an aggregate of an entire search, passing in an array of all results into the handler. + + +@event +The `data` event is emitted for every item returned from a search. + +@argument {Object} + This is an object representing a history entry. Contains `url`, `time`, `accessCount` and `title` of the entry. + + + +@event +The `error` event is emitted whenever a search could not be completed. + +@argument {string} + A string indicating the error that occurred. + + + +@event +The `end` event is called when all search results have returned. + +@argument {array} + The value passed into the handler is an array of all entries found in the + history search. Each entry is an object containing the properties + `url`, `time`, `accessCount` and `title`. + + diff --git a/addon-sdk/source/doc/module-source/sdk/request.md b/addon-sdk/source/doc/module-source/sdk/request.md index 3f87aefb3610..a249ddb85ac8 100644 --- a/addon-sdk/source/doc/module-source/sdk/request.md +++ b/addon-sdk/source/doc/module-source/sdk/request.md @@ -2,7 +2,7 @@ - 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/. --> -The `request` module lets you make simple yet powerful network requests. +The `request` module lets you make simple yet powerful network requests. For more advanced usage, check out the [net/xhr](modules/sdk/net/xhr.html) module, based on the browser's [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) object. @class diff --git a/addon-sdk/source/doc/module-source/sdk/simple-prefs.md b/addon-sdk/source/doc/module-source/sdk/simple-prefs.md index ecabb6a7e17e..09e2449e6350 100644 --- a/addon-sdk/source/doc/module-source/sdk/simple-prefs.md +++ b/addon-sdk/source/doc/module-source/sdk/simple-prefs.md @@ -100,6 +100,23 @@ These are attributes that all settings *may* have: this may be an integer, string, or boolean value. +
+ + + +
+

Contributor's Guide

+ Learn + how to start contributing to the SDK, + and about the most important idioms used in the SDK code, such as + modules, + classes and inheritance, + private properties, and + content processes. +
+

SDK idioms

+ The SDK's + event framework and the + distinction between add-on scripts and content scripts. +

SDK infrastructure

@@ -88,10 +109,11 @@ Learn about common development techniques, such as
-

SDK idioms

- The SDK's - event framework and the - distinction between add-on scripts and content scripts. +

XUL migration

+ A guide to porting XUL add-ons to the SDK. + This guide includes a + comparison of the two toolsets and a + worked example of porting a XUL add-on.
-

XUL migration

- A guide to porting XUL add-ons to the SDK. - This guide includes a - comparison of the two toolsets and a - worked example of porting a XUL add-on.
hidden

A boolean value which, if present and set to true, + means that the setting won't appear in the Add-ons Manager interface, + so users of your add-on won't be able to see or alter it.

+
+{
+    "name": "myHiddenInteger",
+    "type": "integer",
+    "title": "How Many?",
+    "hidden": true
+}
+

Your add-on's code will still be able to access and modify it, + just like any other preference you define.

+
### Setting-Specific Attributes ### diff --git a/addon-sdk/source/lib/sdk/content/worker.js b/addon-sdk/source/lib/sdk/content/worker.js index f8742db0affe..2eddaee035de 100644 --- a/addon-sdk/source/lib/sdk/content/worker.js +++ b/addon-sdk/source/lib/sdk/content/worker.js @@ -369,7 +369,7 @@ const WorkerSandbox = EventEmitter.compose({ /** * Message-passing facility for communication between code running * in the content and add-on process. - * @see https://jetpack.mozillalabs.com/sdk/latest/docs/#module/api-utils/content/worker + * @see https://addons.mozilla.org/en-US/developers/docs/sdk/latest/modules/sdk/content/worker.html */ const Worker = EventEmitter.compose({ on: Trait.required, diff --git a/addon-sdk/source/lib/sdk/deprecated/api-utils.js b/addon-sdk/source/lib/sdk/deprecated/api-utils.js index 568042b734e2..d4dc835eed95 100644 --- a/addon-sdk/source/lib/sdk/deprecated/api-utils.js +++ b/addon-sdk/source/lib/sdk/deprecated/api-utils.js @@ -5,12 +5,16 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; - module.metadata = { "stability": "deprecated" }; const memory = require("./memory"); + +const { merge } = require("../util/object"); +const { union } = require("../util/array"); +const { isNil } = require("../lang/type"); + // The possible return values of getTypeOf. const VALID_TYPES = [ "array", @@ -23,6 +27,8 @@ const VALID_TYPES = [ "undefined", ]; +const { isArray } = Array; + /** * Returns a function C that creates instances of privateCtor. C may be called * with or without the new keyword. The prototype of each instance returned @@ -86,6 +92,7 @@ exports.validateOptions = function validateOptions(options, requirements) { let validatedOptions = {}; for (let key in requirements) { + let isOptional = false; let mapThrew = false; let req = requirements[key]; let [optsVal, keyInOpts] = (key in options) ? @@ -103,17 +110,27 @@ exports.validateOptions = function validateOptions(options, requirements) { } } if (req.is) { - // Sanity check the caller's type names. - req.is.forEach(function (typ) { - if (VALID_TYPES.indexOf(typ) < 0) { - let msg = 'Internal error: invalid requirement type "' + typ + '".'; - throw new Error(msg); - } - }); - if (req.is.indexOf(getTypeOf(optsVal)) < 0) - throw new RequirementError(key, req); + let types = req.is; + + if (!isArray(types) && isArray(types.is)) + types = types.is; + + if (isArray(types)) { + isOptional = ['undefined', 'null'].every(v => ~types.indexOf(v)); + + // Sanity check the caller's type names. + types.forEach(function (typ) { + if (VALID_TYPES.indexOf(typ) < 0) { + let msg = 'Internal error: invalid requirement type "' + typ + '".'; + throw new Error(msg); + } + }); + if (types.indexOf(getTypeOf(optsVal)) < 0) + throw new RequirementError(key, req); + } } - if (req.ok && !req.ok(optsVal)) + + if (req.ok && ((!isOptional || !isNil(optsVal)) && !req.ok(optsVal))) throw new RequirementError(key, req); if (keyInOpts || (req.map && !mapThrew && optsVal !== undefined)) @@ -142,7 +159,7 @@ let getTypeOf = exports.getTypeOf = function getTypeOf(val) { if (typ === "object") { if (!val) return "null"; - if (Array.isArray(val)) + if (isArray(val)) return "array"; } return typ; @@ -164,3 +181,38 @@ function RequirementError(key, requirement) { this.message = msg; } RequirementError.prototype = Object.create(Error.prototype); + +let string = { is: ['string', 'undefined', 'null'] }; +exports.string = string; + +let number = { is: ['number', 'undefined', 'null'] }; +exports.number = number; + +let boolean = { is: ['boolean', 'undefined', 'null'] }; +exports.boolean = boolean; + +let object = { is: ['object', 'undefined', 'null'] }; +exports.object = object; + +let isTruthyType = type => !(type === 'undefined' || type === 'null'); +let findTypes = v => { while (!isArray(v) && v.is) v = v.is; return v }; + +function required(req) { + let types = (findTypes(req) || VALID_TYPES).filter(isTruthyType); + + return merge({}, req, {is: types}); +} +exports.required = required; + +function optional(req) { + req = merge({is: []}, req); + req.is = findTypes(req).filter(isTruthyType).concat('undefined', 'null'); + + return req; +} +exports.optional = optional; + +function either(...types) { + return union.apply(null, types.map(findTypes)); +} +exports.either = either; diff --git a/addon-sdk/source/lib/sdk/net/xhr.js b/addon-sdk/source/lib/sdk/net/xhr.js index f9c7c05217c0..415b9cbf44de 100644 --- a/addon-sdk/source/lib/sdk/net/xhr.js +++ b/addon-sdk/source/lib/sdk/net/xhr.js @@ -4,7 +4,7 @@ "use strict"; module.metadata = { - "stability": "unstable" + "stability": "stable" }; const { deprecateFunction } = require("../util/deprecate"); @@ -33,4 +33,4 @@ function forceAllowThirdPartyCookie(xhr) { exports.forceAllowThirdPartyCookie = forceAllowThirdPartyCookie; // No need to handle add-on unloads as addon/window is closed at unload -// and it will take down all the associated requests. \ No newline at end of file +// and it will take down all the associated requests. diff --git a/addon-sdk/source/lib/sdk/places/events.js b/addon-sdk/source/lib/sdk/places/events.js new file mode 100644 index 000000000000..4703d2a65277 --- /dev/null +++ b/addon-sdk/source/lib/sdk/places/events.js @@ -0,0 +1,121 @@ +/* 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'; + +module.metadata = { + 'stability': 'experimental', + 'engines': { + 'Firefox': '*' + } +}; + +const { Cc, Ci } = require('chrome'); +const { Unknown } = require('../platform/xpcom'); +const { Class } = require('../core/heritage'); +const { merge } = require('../util/object'); +const bookmarkService = Cc['@mozilla.org/browser/nav-bookmarks-service;1'] + .getService(Ci.nsINavBookmarksService); +const historyService = Cc['@mozilla.org/browser/nav-history-service;1'] + .getService(Ci.nsINavHistoryService); +const { mapBookmarkItemType } = require('./utils'); +const { EventTarget } = require('../event/target'); +const { emit } = require('../event/core'); + +const emitter = EventTarget(); + +let HISTORY_ARGS = { + onBeginUpdateBatch: [], + onEndUpdateBatch: [], + onClearHistory: [], + onDeleteURI: ['url'], + onDeleteVisits: ['url', 'visitTime'], + onPageChanged: ['url', 'property', 'value'], + onTitleChanged: ['url', 'title'], + onVisit: [ + 'url', 'visitId', 'time', 'sessionId', 'referringId', 'transitionType' + ] +}; + +let HISTORY_EVENTS = { + onBeginUpdateBatch: 'history-start-batch', + onEndUpdateBatch: 'history-end-batch', + onClearHistory: 'history-start-clear', + onDeleteURI: 'history-delete-url', + onDeleteVisits: 'history-delete-visits', + onPageChanged: 'history-page-changed', + onTitleChanged: 'history-title-changed', + onVisit: 'history-visit' +}; + +let BOOKMARK_ARGS = { + onItemAdded: [ + 'id', 'parentId', 'index', 'type', 'url', 'title', 'dateAdded' + ], + onItemChanged: [ + 'id', 'property', null, 'value', 'lastModified', 'type', 'parentId' + ], + onItemMoved: [ + 'id', 'previousParentId', 'previousIndex', 'currentParentId', + 'currentIndex', 'type' + ], + onItemRemoved: ['id', 'parentId', 'index', 'type', 'url'], + onItemVisited: ['id', 'visitId', 'time', 'transitionType', 'url', 'parentId'] +}; + +let BOOKMARK_EVENTS = { + onItemAdded: 'bookmark-item-added', + onItemChanged: 'bookmark-item-changed', + onItemMoved: 'bookmark-item-moved', + onItemRemoved: 'bookmark-item-removed', + onItemVisited: 'bookmark-item-visited', +}; + +function createHandler (type, propNames) { + propNames = propNames || []; + return function (...args) { + let data = propNames.reduce((acc, prop, i) => { + if (prop) + acc[prop] = formatValue(prop, args[i]); + return acc; + }, {}); + + emit(emitter, 'data', { + type: type, + data: data + }); + }; +} + +/* + * Creates an observer, creating handlers based off of + * the `events` names, and ordering arguments from `propNames` hash + */ +function createObserverInstance (events, propNames) { + let definition = Object.keys(events).reduce((prototype, eventName) => { + prototype[eventName] = createHandler(events[eventName], propNames[eventName]); + return prototype; + }, {}); + + return Class(merge(definition, { extends: Unknown }))(); +} + +/* + * Formats `data` based off of the value of `type` + */ +function formatValue (type, data) { + if (type === 'type') + return mapBookmarkItemType(data); + if (type === 'url' && data) + return data.spec; + return data; +} + +let historyObserver = createObserverInstance(HISTORY_EVENTS, HISTORY_ARGS); +historyService.addObserver(historyObserver, false); + +let bookmarkObserver = createObserverInstance(BOOKMARK_EVENTS, BOOKMARK_ARGS); +bookmarkService.addObserver(bookmarkObserver, false); + +exports.events = emitter; diff --git a/addon-sdk/source/lib/sdk/places/host/host-bookmarks.js b/addon-sdk/source/lib/sdk/places/host/host-bookmarks.js index f138bb22e8db..acac020231d5 100644 --- a/addon-sdk/source/lib/sdk/places/host/host-bookmarks.js +++ b/addon-sdk/source/lib/sdk/places/host/host-bookmarks.js @@ -104,17 +104,22 @@ function saveBookmarkItem (data) { let group = bmsrv.getFolderIdForItem(id); let index = bmsrv.getItemIndex(id); let type = bmsrv.getItemType(id); + let title = typeMap(type) !== 'separator' ? + bmsrv.getItemTitle(id) : + undefined; + let url = typeMap(type) === 'bookmark' ? + bmsrv.getBookmarkURI(id).spec : + undefined; - if (data.url) { + if (url != data.url) bmsrv.changeBookmarkURI(id, newURI(data.url)); - } else if (typeMap(type) === 'bookmark') - data.url = bmsrv.getBookmarkURI(id).spec; + data.url = url; - if (data.title) + if (title != data.title) bmsrv.setItemTitle(id, data.title); else if (typeMap(type) !== 'separator') - data.title = bmsrv.getItemTitle(id); + data.title = title; if (data.group && data.group !== group) bmsrv.moveItem(id, data.group, data.index || -1); @@ -123,7 +128,7 @@ function saveBookmarkItem (data) { // so we don't have to manage the indicies of the siblings bmsrv.moveItem(id, group, data.index); } else if (data.index == null) - data.index = bmsrv.getItemIndex(id); + data.index = index; data.updated = bmsrv.getItemLastModified(data.id); diff --git a/addon-sdk/source/lib/sdk/places/utils.js b/addon-sdk/source/lib/sdk/places/utils.js index 1ce87d717fa0..306902fb472e 100644 --- a/addon-sdk/source/lib/sdk/places/utils.js +++ b/addon-sdk/source/lib/sdk/places/utils.js @@ -235,3 +235,16 @@ function createQueryOptions (type, options) { } exports.createQueryOptions = createQueryOptions; + +function mapBookmarkItemType (type) { + if (typeof type === 'number') { + if (bmsrv.TYPE_BOOKMARK === type) return 'bookmark'; + if (bmsrv.TYPE_FOLDER === type) return 'group'; + if (bmsrv.TYPE_SEPARATOR === type) return 'separator'; + } else { + if ('bookmark' === type) return bmsrv.TYPE_BOOKMARK; + if ('group' === type) return bmsrv.TYPE_FOLDER; + if ('separator' === type) return bmsrv.TYPE_SEPARATOR; + } +} +exports.mapBookmarkItemType = mapBookmarkItemType; diff --git a/addon-sdk/source/lib/sdk/tabs/events.js b/addon-sdk/source/lib/sdk/tabs/events.js index 118231b67150..65650f9dcf71 100644 --- a/addon-sdk/source/lib/sdk/tabs/events.js +++ b/addon-sdk/source/lib/sdk/tabs/events.js @@ -26,7 +26,14 @@ exports.EVENTS = EVENTS; Object.keys(EVENTS).forEach(function(name) { EVENTS[name] = { name: name, - listener: ON_PREFIX + name.charAt(0).toUpperCase() + name.substr(1), + listener: createListenerName(name), dom: EVENTS[name] } }); + +function createListenerName (name) { + if (name === 'pageshow') + return 'onPageShow'; + else + return ON_PREFIX + name.charAt(0).toUpperCase() + name.substr(1); +} diff --git a/addon-sdk/source/lib/sdk/tabs/tab-fennec.js b/addon-sdk/source/lib/sdk/tabs/tab-fennec.js index 65929c184c19..70a7659beb68 100644 --- a/addon-sdk/source/lib/sdk/tabs/tab-fennec.js +++ b/addon-sdk/source/lib/sdk/tabs/tab-fennec.js @@ -33,6 +33,9 @@ const Tab = Class({ // TabReady let onReady = tabInternals.onReady = onTabReady.bind(this); tab.browser.addEventListener(EVENTS.ready.dom, onReady, false); + + let onPageShow = tabInternals.onPageShow = onTabPageShow.bind(this); + tab.browser.addEventListener(EVENTS.pageshow.dom, onPageShow, false); // TabClose let onClose = tabInternals.onClose = onTabClose.bind(this); @@ -180,8 +183,10 @@ function cleanupTab(tab) { if (tabInternals.tab.browser) { tabInternals.tab.browser.removeEventListener(EVENTS.ready.dom, tabInternals.onReady, false); + tabInternals.tab.browser.removeEventListener(EVENTS.pageshow.dom, tabInternals.onPageShow, false); } tabInternals.onReady = null; + tabInternals.onPageShow = null; tabInternals.window.BrowserApp.deck.removeEventListener(EVENTS.close.dom, tabInternals.onClose, false); tabInternals.onClose = null; rawTabNS(tabInternals.tab).tab = null; @@ -198,6 +203,12 @@ function onTabReady(event) { } } +function onTabPageShow(event) { + let win = event.target.defaultView; + if (win === win.top) + emit(this, 'pageshow', this, event.persisted); +} + // TabClose function onTabClose(event) { let rawTab = getTabForBrowser(event.target); diff --git a/addon-sdk/source/lib/sdk/url.js b/addon-sdk/source/lib/sdk/url.js index 87bca3686974..06a383ca18f6 100644 --- a/addon-sdk/source/lib/sdk/url.js +++ b/addon-sdk/source/lib/sdk/url.js @@ -1,7 +1,6 @@ /* 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"; module.metadata = { @@ -277,3 +276,16 @@ let isValidURI = exports.isValidURI = function (uri) { } return true; } + +function isLocalURL(url) { + if (String.indexOf(url, './') === 0) + return true; + + try { + return ['resource', 'data', 'chrome'].indexOf(URL(url).scheme) > -1; + } + catch(e) {} + + return false; +} +exports.isLocalURL = isLocalURL; diff --git a/addon-sdk/source/lib/sdk/util/array.js b/addon-sdk/source/lib/sdk/util/array.js index 75ccf9e8630f..198024624c7f 100644 --- a/addon-sdk/source/lib/sdk/util/array.js +++ b/addon-sdk/source/lib/sdk/util/array.js @@ -72,12 +72,22 @@ exports.remove = function remove(array, element) { * Source array. * @returns {Array} */ -exports.unique = function unique(array) { - return array.reduce(function(values, element) { - add(values, element); - return values; +function unique(array) { + return array.reduce(function(result, item) { + add(result, item); + return result; }, []); }; +exports.unique = unique; + +/** + * Produce an array that contains the union: each distinct element from all + * of the passed-in arrays. + */ +function union() { + return unique(Array.concat.apply(null, arguments)); +}; +exports.union = union; exports.flatten = function flatten(array){ var flat = []; diff --git a/addon-sdk/source/lib/sdk/windows/tabs-fennec.js b/addon-sdk/source/lib/sdk/windows/tabs-fennec.js index ba6793d274f8..f5eca6f29740 100644 --- a/addon-sdk/source/lib/sdk/windows/tabs-fennec.js +++ b/addon-sdk/source/lib/sdk/windows/tabs-fennec.js @@ -77,6 +77,9 @@ const Tabs = Class({ if (options.onReady) tab.on('ready', options.onReady); + if (options.onPageShow) + tab.on('pageshow', options.onPageShow); + if (options.onActivate) tab.on('activate', options.onActivate); @@ -131,9 +134,12 @@ function onTabOpen(event) { tab.on('ready', function() emit(gTabs, 'ready', tab)); tab.once('close', onTabClose); + tab.on('pageshow', function(_tab, persisted) + emit(gTabs, 'pageshow', tab, persisted)); + emit(tab, 'open', tab); emit(gTabs, 'open', tab); -}; +} // TabSelect function onTabSelect(event) { @@ -153,10 +159,10 @@ function onTabSelect(event) { emit(t, 'deactivate', t); emit(gTabs, 'deactivate', t); } -}; +} // TabClose function onTabClose(tab) { removeTab(tab); emit(gTabs, EVENTS.close.name, tab); -}; +} diff --git a/addon-sdk/source/python-lib/mozrunner/__init__.py b/addon-sdk/source/python-lib/mozrunner/__init__.py index 2e5c57abd908..87c2c320fc58 100644 --- a/addon-sdk/source/python-lib/mozrunner/__init__.py +++ b/addon-sdk/source/python-lib/mozrunner/__init__.py @@ -406,7 +406,8 @@ class Runner(object): def find_binary(self): """Finds the binary for self.names if one was not provided.""" binary = None - if sys.platform in ('linux2', 'sunos5', 'solaris'): + if sys.platform in ('linux2', 'sunos5', 'solaris') \ + or sys.platform.startswith('freebsd'): for name in reversed(self.names): binary = findInPath(name) elif os.name == 'nt' or sys.platform == 'cygwin': @@ -578,7 +579,8 @@ class FirefoxRunner(Runner): def names(self): if sys.platform == 'darwin': return ['firefox', 'nightly', 'shiretoko'] - if (sys.platform == 'linux2') or (sys.platform in ('sunos5', 'solaris')): + if sys.platform in ('linux2', 'sunos5', 'solaris') \ + or sys.platform.startswith('freebsd'): return ['firefox', 'mozilla-firefox', 'iceweasel'] if os.name == 'nt' or sys.platform == 'cygwin': return ['firefox'] diff --git a/addon-sdk/source/python-lib/mozrunner/killableprocess.py b/addon-sdk/source/python-lib/mozrunner/killableprocess.py index 892ed87fc85b..92f7ac3779fe 100644 --- a/addon-sdk/source/python-lib/mozrunner/killableprocess.py +++ b/addon-sdk/source/python-lib/mozrunner/killableprocess.py @@ -257,7 +257,8 @@ class Popen(subprocess.Popen): self.kill(group) else: - if (sys.platform == 'linux2') or (sys.platform in ('sunos5', 'solaris')): + if sys.platform in ('linux2', 'sunos5', 'solaris') \ + or sys.platform.startswith('freebsd'): def group_wait(timeout): try: os.waitpid(self.pid, 0) diff --git a/addon-sdk/source/test/addons/private-browsing-supported/main.js b/addon-sdk/source/test/addons/private-browsing-supported/main.js index 34b9f588b48b..9e8ef5d3140b 100644 --- a/addon-sdk/source/test/addons/private-browsing-supported/main.js +++ b/addon-sdk/source/test/addons/private-browsing-supported/main.js @@ -10,15 +10,20 @@ const { isGlobalPBSupported } = require('sdk/private-browsing/utils'); merge(module.exports, require('./test-tabs'), require('./test-page-mod'), - require('./test-selection'), - require('./test-panel'), require('./test-private-browsing'), isGlobalPBSupported ? require('./test-global-private-browsing') : {} ); // Doesn't make sense to test window-utils and windows on fennec, -// as there is only one window which is never private -if (!app.is('Fennec')) - merge(module.exports, require('./test-windows')); +// as there is only one window which is never private. Also ignore +// unsupported modules (panel, selection) +if (!app.is('Fennec')) { + merge(module.exports, + require('./test-selection'), + require('./test-panel'), + require('./test-window-tabs'), + require('./test-windows') + ); +} require('sdk/test/runner').runTestsFromModule(module); diff --git a/addon-sdk/source/test/addons/private-browsing-supported/test-tabs.js b/addon-sdk/source/test/addons/private-browsing-supported/test-tabs.js index dac987713789..dbc0385bef7b 100644 --- a/addon-sdk/source/test/addons/private-browsing-supported/test-tabs.js +++ b/addon-sdk/source/test/addons/private-browsing-supported/test-tabs.js @@ -1,12 +1,9 @@ 'use strict'; const tabs = require('sdk/tabs'); -const { is } = require('sdk/system/xul-app'); const { isPrivate } = require('sdk/private-browsing'); const pbUtils = require('sdk/private-browsing/utils'); const { getOwnerWindow } = require('sdk/private-browsing/window/utils'); -const { promise: windowPromise, close, focus } = require('sdk/window/helpers'); -const { getMostRecentBrowserWindow } = require('sdk/window/utils'); exports.testPrivateTabsAreListed = function (assert, done) { let originalTabCount = tabs.length; @@ -32,82 +29,5 @@ exports.testPrivateTabsAreListed = function (assert, done) { tab.close(done); } }); -} +}; -exports.testOpenTabWithPrivateActiveWindowNoIsPrivateOption = function(assert, done) { - let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: true }); - - windowPromise(window, 'load').then(focus).then(function (window) { - assert.ok(isPrivate(window), 'new window is private'); - - tabs.open({ - url: 'about:blank', - onOpen: function(tab) { - assert.ok(isPrivate(tab), 'new tab is private'); - assert.ok(isPrivate(getOwnerWindow(tab)), 'new tab window is private'); - assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the private window are the same'); - - close(window).then(done, assert.fail); - } - }) - }, assert.fail).then(null, assert.fail); -} - -exports.testOpenTabWithNonPrivateActiveWindowNoIsPrivateOption = function(assert, done) { - let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: false }); - - windowPromise(window, 'load').then(focus).then(function (window) { - assert.equal(isPrivate(window), false, 'new window is not private'); - - tabs.open({ - url: 'about:blank', - onOpen: function(tab) { - assert.equal(isPrivate(tab), false, 'new tab is not private'); - assert.equal(isPrivate(getOwnerWindow(tab)), false, 'new tab window is not private'); - assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the new window are the same'); - - close(window).then(done, assert.fail); - } - }) - }, assert.fail).then(null, assert.fail); -} - -exports.testOpenTabWithPrivateActiveWindowWithIsPrivateOptionTrue = function(assert, done) { - let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: true }); - - windowPromise(window, 'load').then(focus).then(function (window) { - assert.ok(isPrivate(window), 'new window is private'); - - tabs.open({ - url: 'about:blank', - isPrivate: true, - onOpen: function(tab) { - assert.ok(isPrivate(tab), 'new tab is private'); - assert.ok(isPrivate(getOwnerWindow(tab)), 'new tab window is private'); - assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the private window are the same'); - - close(window).then(done, assert.fail); - } - }) - }, assert.fail).then(null, assert.fail); -} - -exports.testOpenTabWithNonPrivateActiveWindowWithIsPrivateOptionFalse = function(assert, done) { - let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: false }); - - windowPromise(window, 'load').then(focus).then(function (window) { - assert.equal(isPrivate(window), false, 'new window is not private'); - - tabs.open({ - url: 'about:blank', - isPrivate: false, - onOpen: function(tab) { - assert.equal(isPrivate(tab), false, 'new tab is not private'); - assert.equal(isPrivate(getOwnerWindow(tab)), false, 'new tab window is not private'); - assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the new window are the same'); - - close(window).then(done, assert.fail); - } - }) - }, assert.fail).then(null, assert.fail); -} diff --git a/addon-sdk/source/test/addons/private-browsing-supported/test-window-tabs.js b/addon-sdk/source/test/addons/private-browsing-supported/test-window-tabs.js new file mode 100644 index 000000000000..05e787a4f3b6 --- /dev/null +++ b/addon-sdk/source/test/addons/private-browsing-supported/test-window-tabs.js @@ -0,0 +1,85 @@ +'use strict'; + +const tabs = require('sdk/tabs'); +const { isPrivate } = require('sdk/private-browsing'); +const { getOwnerWindow } = require('sdk/private-browsing/window/utils'); +const { promise: windowPromise, close, focus } = require('sdk/window/helpers'); +const { getMostRecentBrowserWindow } = require('sdk/window/utils'); + +exports.testOpenTabWithPrivateActiveWindowNoIsPrivateOption = function(assert, done) { + let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: true }); + + windowPromise(window, 'load').then(focus).then(function (window) { + assert.ok(isPrivate(window), 'new window is private'); + + tabs.open({ + url: 'about:blank', + onOpen: function(tab) { + assert.ok(isPrivate(tab), 'new tab is private'); + assert.ok(isPrivate(getOwnerWindow(tab)), 'new tab window is private'); + assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the private window are the same'); + + close(window).then(done, assert.fail); + } + }) + }, assert.fail).then(null, assert.fail); +} + +exports.testOpenTabWithNonPrivateActiveWindowNoIsPrivateOption = function(assert, done) { + let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: false }); + + windowPromise(window, 'load').then(focus).then(function (window) { + assert.equal(isPrivate(window), false, 'new window is not private'); + + tabs.open({ + url: 'about:blank', + onOpen: function(tab) { + assert.equal(isPrivate(tab), false, 'new tab is not private'); + assert.equal(isPrivate(getOwnerWindow(tab)), false, 'new tab window is not private'); + assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the new window are the same'); + + close(window).then(done, assert.fail); + } + }) + }, assert.fail).then(null, assert.fail); +} + +exports.testOpenTabWithPrivateActiveWindowWithIsPrivateOptionTrue = function(assert, done) { + let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: true }); + + windowPromise(window, 'load').then(focus).then(function (window) { + assert.ok(isPrivate(window), 'new window is private'); + + tabs.open({ + url: 'about:blank', + isPrivate: true, + onOpen: function(tab) { + assert.ok(isPrivate(tab), 'new tab is private'); + assert.ok(isPrivate(getOwnerWindow(tab)), 'new tab window is private'); + assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the private window are the same'); + + close(window).then(done, assert.fail); + } + }) + }, assert.fail).then(null, assert.fail); +} + +exports.testOpenTabWithNonPrivateActiveWindowWithIsPrivateOptionFalse = function(assert, done) { + let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: false }); + + windowPromise(window, 'load').then(focus).then(function (window) { + assert.equal(isPrivate(window), false, 'new window is not private'); + + tabs.open({ + url: 'about:blank', + isPrivate: false, + onOpen: function(tab) { + assert.equal(isPrivate(tab), false, 'new tab is not private'); + assert.equal(isPrivate(getOwnerWindow(tab)), false, 'new tab window is not private'); + assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the new window are the same'); + + close(window).then(done, assert.fail); + } + }) + }, assert.fail).then(null, assert.fail); +} diff --git a/addon-sdk/source/test/addons/privileged-panel/main.js b/addon-sdk/source/test/addons/privileged-panel/main.js index c010de9242be..8992a0a39244 100644 --- a/addon-sdk/source/test/addons/privileged-panel/main.js +++ b/addon-sdk/source/test/addons/privileged-panel/main.js @@ -4,10 +4,14 @@ "use strict"; -const { Panel } = require("sdk/panel") -const { data } = require("sdk/self") +const app = require("sdk/system/xul-app"); + +exports["test addon globa"] = app.is("Firefox") ? testAddonGlobal : unsupported; + +function testAddonGlobal (assert, done) { + const { Panel } = require("sdk/panel") + const { data } = require("sdk/self") -exports["test addon global"] = function(assert, done) { let panel = Panel({ contentURL: //"data:text/html,now?", data.url("./index.html"), @@ -17,10 +21,14 @@ exports["test addon global"] = function(assert, done) { done(); }, onError: function(error) { - asser.fail(Error("failed to recieve message")); + assert.fail(Error("failed to recieve message")); done(); } }); }; +function unsupported (assert) { + assert.pass("privileged-panel unsupported on platform"); +} + require("sdk/test/runner").runTestsFromModule(module); diff --git a/addon-sdk/source/test/favicon-helpers.js b/addon-sdk/source/test/favicon-helpers.js index 1be7592b8846..3656ae6748b1 100644 --- a/addon-sdk/source/test/favicon-helpers.js +++ b/addon-sdk/source/test/favicon-helpers.js @@ -13,42 +13,18 @@ const basePath = pathFor('ProfD'); const { atob } = Cu.import("resource://gre/modules/Services.jsm", {}); const historyService = Cc["@mozilla.org/browser/nav-history-service;1"] .getService(Ci.nsINavHistoryService); -Cu.import('resource://gre/modules/XPCOMUtils.jsm'); -const ObserverShimMethods = ['onBeginUpdateBatch', 'onEndUpdateBatch', - 'onVisit', 'onTitleChanged', 'onDeleteURI', 'onClearHistory', - 'onPageChanged', 'onDeleteVisits']; +const { events } = require('sdk/places/events'); -/* - * Shims NavHistoryObserver - */ - -let noop = function () {} -let NavHistoryObserver = function () {}; -ObserverShimMethods.forEach(function (method) { - NavHistoryObserver.prototype[method] = noop; -}); -NavHistoryObserver.prototype.QueryInterface = XPCOMUtils.generateQI([ - Ci.nsINavHistoryObserver -]); - -/* - * Uses history observer to watch for an onPageChanged event, - * which detects when a favicon is updated in the registry. - */ -function onFaviconChange (uri, callback) { - let observer = Object.create(NavHistoryObserver.prototype, { - onPageChanged: { - value: function onPageChanged(aURI, aWhat, aValue, aGUID) { - if (aWhat !== Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON) - return; - if (aURI.spec !== uri) - return; - historyService.removeObserver(this); - callback(aValue); - } - } - }); - historyService.addObserver(observer, false); +function onFaviconChange (url, callback) { + function handler ({data, type}) { + if (type !== 'history-page-changed' || + data.url !== url || + data.property !== Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON) + return; + events.off('data', handler); + callback(data.value); + } + events.on('data', handler); } exports.onFaviconChange = onFaviconChange; diff --git a/addon-sdk/source/test/places-helper.js b/addon-sdk/source/test/places-helper.js index 8ea0af17c8ae..8107b4eae0b5 100644 --- a/addon-sdk/source/test/places-helper.js +++ b/addon-sdk/source/test/places-helper.js @@ -106,6 +106,13 @@ function addVisits (urls) { } exports.addVisits = addVisits; +function removeVisits (urls) { + [].concat(urls).map(url => { + hsrv.removePage(newURI(url)); + }); +} +exports.removeVisits = removeVisits; + // Creates a mozIVisitInfo object function createVisit (url) { let place = {} diff --git a/addon-sdk/source/test/tabs/test-firefox-tabs.js b/addon-sdk/source/test/tabs/test-firefox-tabs.js index 1d5346ac783a..fd59222719fc 100644 --- a/addon-sdk/source/test/tabs/test-firefox-tabs.js +++ b/addon-sdk/source/test/tabs/test-firefox-tabs.js @@ -949,40 +949,6 @@ exports.testOnLoadEventWithImage = function(test) { }); }; -exports.testOnPageShowEvent = function (test) { - test.waitUntilDone(); - - let firstUrl = 'data:text/html;charset=utf-8,First'; - let secondUrl = 'data:text/html;charset=utf-8,Second'; - - openBrowserWindow(function(window, browser) { - let counter = 0; - tabs.on('pageshow', function onPageShow(tab, persisted) { - counter++; - if (counter === 1) { - test.assert(!persisted, 'page should not be cached on initial load'); - tab.url = secondUrl; - } - else if (counter === 2) { - test.assert(!persisted, 'second test page should not be cached either'); - tab.attach({ - contentScript: 'setTimeout(function () { window.history.back(); }, 0)' - }); - } - else { - test.assert(persisted, 'when we get back to the fist page, it has to' + - 'come from cache'); - tabs.removeListener('pageshow', onPageShow); - closeBrowserWindow(window, function() test.done()); - } - }); - - tabs.open({ - url: firstUrl - }); - }); -}; - exports.testFaviconGetterDeprecation = function (test) { const { LoaderWithHookedConsole } = require("sdk/test/loader"); let { loader, messages } = LoaderWithHookedConsole(module); diff --git a/addon-sdk/source/test/test-api-utils.js b/addon-sdk/source/test/test-api-utils.js index f0a0849ac4b4..f26682b6f989 100644 --- a/addon-sdk/source/test/test-api-utils.js +++ b/addon-sdk/source/test/test-api-utils.js @@ -6,92 +6,94 @@ const apiUtils = require("sdk/deprecated/api-utils"); -exports.testPublicConstructor = function (test) { +exports.testPublicConstructor = function (assert) { function PrivateCtor() {} PrivateCtor.prototype = {}; let PublicCtor = apiUtils.publicConstructor(PrivateCtor); - test.assert( + assert.ok( PrivateCtor.prototype.isPrototypeOf(PublicCtor.prototype), "PrivateCtor.prototype should be prototype of PublicCtor.prototype" ); function testObj(useNew) { let obj = useNew ? new PublicCtor() : PublicCtor(); - test.assert(obj instanceof PublicCtor, + assert.ok(obj instanceof PublicCtor, "Object should be instance of PublicCtor"); - test.assert(obj instanceof PrivateCtor, + assert.ok(obj instanceof PrivateCtor, "Object should be instance of PrivateCtor"); - test.assert(PublicCtor.prototype.isPrototypeOf(obj), + assert.ok(PublicCtor.prototype.isPrototypeOf(obj), "PublicCtor's prototype should be prototype of object"); - test.assertEqual(obj.constructor, PublicCtor, + assert.equal(obj.constructor, PublicCtor, "Object constructor should be PublicCtor"); } testObj(true); testObj(false); }; -exports.testValidateOptionsEmpty = function (test) { +exports.testValidateOptionsEmpty = function (assert) { let val = apiUtils.validateOptions(null, {}); - assertObjsEqual(test, val, {}); + + assert.deepEqual(val, {}); val = apiUtils.validateOptions(null, { foo: {} }); - assertObjsEqual(test, val, {}); + assert.deepEqual(val, {}); val = apiUtils.validateOptions({}, {}); - assertObjsEqual(test, val, {}); + assert.deepEqual(val, {}); val = apiUtils.validateOptions({}, { foo: {} }); - assertObjsEqual(test, val, {}); + assert.deepEqual(val, {}); }; -exports.testValidateOptionsNonempty = function (test) { +exports.testValidateOptionsNonempty = function (assert) { let val = apiUtils.validateOptions({ foo: 123 }, {}); - assertObjsEqual(test, val, {}); + assert.deepEqual(val, {}); val = apiUtils.validateOptions({ foo: 123, bar: 456 }, { foo: {}, bar: {}, baz: {} }); - assertObjsEqual(test, val, { foo: 123, bar: 456 }); + + assert.deepEqual(val, { foo: 123, bar: 456 }); }; -exports.testValidateOptionsMap = function (test) { +exports.testValidateOptionsMap = function (assert) { let val = apiUtils.validateOptions({ foo: 3, bar: 2 }, { foo: { map: function (v) v * v }, bar: { map: function (v) undefined } }); - assertObjsEqual(test, val, { foo: 9, bar: undefined }); + assert.deepEqual(val, { foo: 9, bar: undefined }); }; -exports.testValidateOptionsMapException = function (test) { +exports.testValidateOptionsMapException = function (assert) { let val = apiUtils.validateOptions({ foo: 3 }, { foo: { map: function () { throw new Error(); }} }); - assertObjsEqual(test, val, { foo: 3 }); + assert.deepEqual(val, { foo: 3 }); }; -exports.testValidateOptionsOk = function (test) { +exports.testValidateOptionsOk = function (assert) { let val = apiUtils.validateOptions({ foo: 3, bar: 2, baz: 1 }, { foo: { ok: function (v) v }, bar: { ok: function (v) v } }); - assertObjsEqual(test, val, { foo: 3, bar: 2 }); + assert.deepEqual(val, { foo: 3, bar: 2 }); - test.assertRaises( + assert.throws( function () apiUtils.validateOptions({ foo: 2, bar: 2 }, { bar: { ok: function (v) v > 2 } }), - 'The option "bar" is invalid.', + /^The option "bar" is invalid/, "ok should raise exception on invalid option" ); - test.assertRaises( + assert.throws( function () apiUtils.validateOptions(null, { foo: { ok: function (v) v }}), - 'The option "foo" is invalid.', + /^The option "foo" is invalid/, "ok should raise exception on invalid option" ); }; -exports.testValidateOptionsIs = function (test) { +exports.testValidateOptionsIs = function (assert) { let opts = { array: [], boolean: true, @@ -114,18 +116,137 @@ exports.testValidateOptionsIs = function (test) { undef2: { is: ["undefined"] } }; let val = apiUtils.validateOptions(opts, requirements); - assertObjsEqual(test, val, opts); + assert.deepEqual(val, opts); - test.assertRaises( + assert.throws( function () apiUtils.validateOptions(null, { foo: { is: ["object", "number"] } }), - 'The option "foo" must be one of the following types: object, number', + /^The option "foo" must be one of the following types: object, number/, "Invalid type should raise exception" ); }; -exports.testValidateOptionsMapIsOk = function (test) { +exports.testValidateOptionsIsWithExportedValue = function (assert) { + let { string, number, boolean, object } = apiUtils; + + let opts = { + boolean: true, + number: 1337, + object: {}, + string: "foo" + }; + let requirements = { + string: { is: string }, + number: { is: number }, + boolean: { is: boolean }, + object: { is: object } + }; + let val = apiUtils.validateOptions(opts, requirements); + assert.deepEqual(val, opts); + + // Test the types are optional by default + val = apiUtils.validateOptions({foo: 'bar'}, requirements); + assert.deepEqual(val, {}); +}; + +exports.testValidateOptionsIsWithEither = function (assert) { + let { string, number, boolean, either } = apiUtils; + let text = { is: either(string, number) }; + + let requirements = { + text: text, + boolOrText: { is: either(text, boolean) } + }; + + let val = apiUtils.validateOptions({text: 12}, requirements); + assert.deepEqual(val, {text: 12}); + + val = apiUtils.validateOptions({text: "12"}, requirements); + assert.deepEqual(val, {text: "12"}); + + val = apiUtils.validateOptions({boolOrText: true}, requirements); + assert.deepEqual(val, {boolOrText: true}); + + val = apiUtils.validateOptions({boolOrText: "true"}, requirements); + assert.deepEqual(val, {boolOrText: "true"}); + + val = apiUtils.validateOptions({boolOrText: 1}, requirements); + assert.deepEqual(val, {boolOrText: 1}); + + assert.throws( + () => apiUtils.validateOptions({text: true}, requirements), + /^The option "text" must be one of the following types/, + "Invalid type should raise exception" + ); + + assert.throws( + () => apiUtils.validateOptions({boolOrText: []}, requirements), + /^The option "boolOrText" must be one of the following types/, + "Invalid type should raise exception" + ); +}; + +exports.testValidateOptionsWithRequiredAndOptional = function (assert) { + let { string, number, required, optional } = apiUtils; + + let opts = { + number: 1337, + string: "foo" + }; + + let requirements = { + string: required(string), + number: number + }; + + let val = apiUtils.validateOptions(opts, requirements); + assert.deepEqual(val, opts); + + val = apiUtils.validateOptions({string: "foo"}, requirements); + assert.deepEqual(val, {string: "foo"}); + + assert.throws( + () => apiUtils.validateOptions({number: 10}, requirements), + /^The option "string" must be one of the following types/, + "Invalid type should raise exception" + ); + + // Makes string optional + requirements.string = optional(requirements.string); + + val = apiUtils.validateOptions({number: 10}, requirements), + assert.deepEqual(val, {number: 10}); + +}; + + + +exports.testValidateOptionsWithExportedValue = function (assert) { + let { string, number, boolean, object } = apiUtils; + + let opts = { + boolean: true, + number: 1337, + object: {}, + string: "foo" + }; + let requirements = { + string: string, + number: number, + boolean: boolean, + object: object + }; + let val = apiUtils.validateOptions(opts, requirements); + assert.deepEqual(val, opts); + + // Test the types are optional by default + val = apiUtils.validateOptions({foo: 'bar'}, requirements); + assert.deepEqual(val, {}); +}; + + +exports.testValidateOptionsMapIsOk = function (assert) { let [map, is, ok] = [false, false, false]; let val = apiUtils.validateOptions({ foo: 1337 }, { foo: { @@ -134,48 +255,48 @@ exports.testValidateOptionsMapIsOk = function (test) { ok: function (v) v.length > 0 } }); - assertObjsEqual(test, val, { foo: "1337" }); + assert.deepEqual(val, { foo: "1337" }); let requirements = { foo: { is: ["object"], - ok: function () test.fail("is should have caused us to throw by now") + ok: function () assert.fail("is should have caused us to throw by now") } }; - test.assertRaises( + assert.throws( function () apiUtils.validateOptions(null, requirements), - 'The option "foo" must be one of the following types: object', + /^The option "foo" must be one of the following types: object/, "is should be used before ok is called" ); }; -exports.testValidateOptionsErrorMsg = function (test) { - test.assertRaises( +exports.testValidateOptionsErrorMsg = function (assert) { + assert.throws( function () apiUtils.validateOptions(null, { foo: { ok: function (v) v, msg: "foo!" } }), - "foo!", + /^foo!/, "ok should raise exception with customized message" ); }; -exports.testValidateMapWithMissingKey = function (test) { +exports.testValidateMapWithMissingKey = function (assert) { let val = apiUtils.validateOptions({ }, { foo: { map: function (v) v || "bar" } }); - assertObjsEqual(test, val, { foo: "bar" }); + assert.deepEqual(val, { foo: "bar" }); val = apiUtils.validateOptions({ }, { foo: { map: function (v) { throw "bar" } } }); - assertObjsEqual(test, val, { }); + assert.deepEqual(val, { }); }; -exports.testValidateMapWithMissingKeyAndThrown = function (test) { +exports.testValidateMapWithMissingKeyAndThrown = function (assert) { let val = apiUtils.validateOptions({}, { bar: { map: function(v) { throw "bar" } @@ -184,10 +305,10 @@ exports.testValidateMapWithMissingKeyAndThrown = function (test) { map: function(v) "foo" } }); - assertObjsEqual(test, val, { baz: "foo" }); + assert.deepEqual(val, { baz: "foo" }); }; -exports.testAddIterator = function testAddIterator(test) { +exports.testAddIterator = function testAddIterator (assert) { let obj = {}; let keys = ["foo", "bar", "baz"]; let vals = [1, 2, 3]; @@ -203,34 +324,20 @@ exports.testAddIterator = function testAddIterator(test) { let keysItr = []; for (let key in obj) keysItr.push(key); - test.assertEqual(keysItr.length, keys.length, + + assert.equal(keysItr.length, keys.length, "the keys iterator returns the correct number of items"); for (let i = 0; i < keys.length; i++) - test.assertEqual(keysItr[i], keys[i], "the key is correct"); + assert.equal(keysItr[i], keys[i], "the key is correct"); let valsItr = []; for each (let val in obj) valsItr.push(val); - test.assertEqual(valsItr.length, vals.length, + assert.equal(valsItr.length, vals.length, "the vals iterator returns the correct number of items"); for (let i = 0; i < vals.length; i++) - test.assertEqual(valsItr[i], vals[i], "the val is correct"); + assert.equal(valsItr[i], vals[i], "the val is correct"); }; -function assertObjsEqual(test, obj1, obj2) { - var items = 0; - for (let key in obj1) { - items++; - test.assert(key in obj2, "obj1 key should be present in obj2"); - test.assertEqual(obj2[key], obj1[key], "obj1 value should match obj2 value"); - } - for (let key in obj2) { - items++; - test.assert(key in obj1, "obj2 key should be present in obj1"); - test.assertEqual(obj1[key], obj2[key], "obj2 value should match obj1 value"); - } - if (!items) - test.assertEqual(JSON.stringify(obj1), JSON.stringify(obj2), - "obj1 should have same JSON representation as obj2"); -} +require('test').run(exports); diff --git a/addon-sdk/source/test/test-array.js b/addon-sdk/source/test/test-array.js index af26770edbbe..8a9cb4402be8 100644 --- a/addon-sdk/source/test/test-array.js +++ b/addon-sdk/source/test/test-array.js @@ -5,67 +5,67 @@ const array = require('sdk/util/array'); -exports.testHas = function(test) { +exports.testHas = function(assert) { var testAry = [1, 2, 3]; - test.assertEqual(array.has([1, 2, 3], 1), true); - test.assertEqual(testAry.length, 3); - test.assertEqual(testAry[0], 1); - test.assertEqual(testAry[1], 2); - test.assertEqual(testAry[2], 3); - test.assertEqual(array.has(testAry, 2), true); - test.assertEqual(array.has(testAry, 3), true); - test.assertEqual(array.has(testAry, 4), false); - test.assertEqual(array.has(testAry, '1'), false); + assert.equal(array.has([1, 2, 3], 1), true); + assert.equal(testAry.length, 3); + assert.equal(testAry[0], 1); + assert.equal(testAry[1], 2); + assert.equal(testAry[2], 3); + assert.equal(array.has(testAry, 2), true); + assert.equal(array.has(testAry, 3), true); + assert.equal(array.has(testAry, 4), false); + assert.equal(array.has(testAry, '1'), false); }; -exports.testHasAny = function(test) { +exports.testHasAny = function(assert) { var testAry = [1, 2, 3]; - test.assertEqual(array.hasAny([1, 2, 3], [1]), true); - test.assertEqual(array.hasAny([1, 2, 3], [1, 5]), true); - test.assertEqual(array.hasAny([1, 2, 3], [5, 1]), true); - test.assertEqual(array.hasAny([1, 2, 3], [5, 2]), true); - test.assertEqual(array.hasAny([1, 2, 3], [5, 3]), true); - test.assertEqual(array.hasAny([1, 2, 3], [5, 4]), false); - test.assertEqual(testAry.length, 3); - test.assertEqual(testAry[0], 1); - test.assertEqual(testAry[1], 2); - test.assertEqual(testAry[2], 3); - test.assertEqual(array.hasAny(testAry, [2]), true); - test.assertEqual(array.hasAny(testAry, [3]), true); - test.assertEqual(array.hasAny(testAry, [4]), false); - test.assertEqual(array.hasAny(testAry), false); - test.assertEqual(array.hasAny(testAry, '1'), false); + assert.equal(array.hasAny([1, 2, 3], [1]), true); + assert.equal(array.hasAny([1, 2, 3], [1, 5]), true); + assert.equal(array.hasAny([1, 2, 3], [5, 1]), true); + assert.equal(array.hasAny([1, 2, 3], [5, 2]), true); + assert.equal(array.hasAny([1, 2, 3], [5, 3]), true); + assert.equal(array.hasAny([1, 2, 3], [5, 4]), false); + assert.equal(testAry.length, 3); + assert.equal(testAry[0], 1); + assert.equal(testAry[1], 2); + assert.equal(testAry[2], 3); + assert.equal(array.hasAny(testAry, [2]), true); + assert.equal(array.hasAny(testAry, [3]), true); + assert.equal(array.hasAny(testAry, [4]), false); + assert.equal(array.hasAny(testAry), false); + assert.equal(array.hasAny(testAry, '1'), false); }; -exports.testAdd = function(test) { +exports.testAdd = function(assert) { var testAry = [1]; - test.assertEqual(array.add(testAry, 1), false); - test.assertEqual(testAry.length, 1); - test.assertEqual(testAry[0], 1); - test.assertEqual(array.add(testAry, 2), true); - test.assertEqual(testAry.length, 2); - test.assertEqual(testAry[0], 1); - test.assertEqual(testAry[1], 2); + assert.equal(array.add(testAry, 1), false); + assert.equal(testAry.length, 1); + assert.equal(testAry[0], 1); + assert.equal(array.add(testAry, 2), true); + assert.equal(testAry.length, 2); + assert.equal(testAry[0], 1); + assert.equal(testAry[1], 2); }; -exports.testRemove = function(test) { +exports.testRemove = function(assert) { var testAry = [1, 2]; - test.assertEqual(array.remove(testAry, 3), false); - test.assertEqual(testAry.length, 2); - test.assertEqual(testAry[0], 1); - test.assertEqual(testAry[1], 2); - test.assertEqual(array.remove(testAry, 2), true); - test.assertEqual(testAry.length, 1); - test.assertEqual(testAry[0], 1); + assert.equal(array.remove(testAry, 3), false); + assert.equal(testAry.length, 2); + assert.equal(testAry[0], 1); + assert.equal(testAry[1], 2); + assert.equal(array.remove(testAry, 2), true); + assert.equal(testAry.length, 1); + assert.equal(testAry[0], 1); }; -exports.testFlatten = function(test) { - test.assertEqual(array.flatten([1, 2, 3]).length, 3); - test.assertEqual(array.flatten([1, [2, 3]]).length, 3); - test.assertEqual(array.flatten([1, [2, [3]]]).length, 3); - test.assertEqual(array.flatten([[1], [[2, [3]]]]).length, 3); +exports.testFlatten = function(assert) { + assert.equal(array.flatten([1, 2, 3]).length, 3); + assert.equal(array.flatten([1, [2, 3]]).length, 3); + assert.equal(array.flatten([1, [2, [3]]]).length, 3); + assert.equal(array.flatten([[1], [[2, [3]]]]).length, 3); }; -exports.testUnique = function(test) { +exports.testUnique = function(assert) { var Class = function () {}; var A = {}; var B = new Class(); @@ -73,23 +73,31 @@ exports.testUnique = function(test) { var D = {}; var E = new Class(); - compareArray(array.unique([1,2,3,1,2]), [1,2,3]); - compareArray(array.unique([1,1,1,4,9,5,5]), [1,4,9,5]); - compareArray(array.unique([A, A, A, B, B, D]), [A,B,D]); - compareArray(array.unique([A, D, A, E, E, D, A, A, C]), [A, D, E, C]) - - function compareArray (a, b) { - test.assertEqual(a.length, b.length); - for (let i = 0; i < a.length; i++) { - test.assertEqual(a[i], b[i]); - } - } + assert.deepEqual(array.unique([1,2,3,1,2]), [1,2,3]); + assert.deepEqual(array.unique([1,1,1,4,9,5,5]), [1,4,9,5]); + assert.deepEqual(array.unique([A, A, A, B, B, D]), [A,B,D]); + assert.deepEqual(array.unique([A, D, A, E, E, D, A, A, C]), [A, D, E, C]) }; -exports.testFind = function(test) { +exports.testUnion = function(assert) { + var Class = function () {}; + var A = {}; + var B = new Class(); + var C = [ 1, 2, 3 ]; + var D = {}; + var E = new Class(); + + assert.deepEqual(array.union([1, 2, 3],[7, 1, 2]), [1, 2, 3, 7]); + assert.deepEqual(array.union([1, 1, 1, 4, 9, 5, 5], [10, 1, 5]), [1, 4, 9, 5, 10]); + assert.deepEqual(array.union([A, B], [A, D]), [A, B, D]); + assert.deepEqual(array.union([A, D], [A, E], [E, D, A], [A, C]), [A, D, E, C]); +}; + +exports.testFind = function(assert) { let isOdd = (x) => x % 2; - test.assertEqual(array.find([2, 4, 5, 7, 8, 9], isOdd), 5); - test.assertEqual(array.find([2, 4, 6, 8], isOdd), undefined); - test.assertEqual(array.find([2, 4, 6, 8], isOdd, null), null); + assert.equal(array.find([2, 4, 5, 7, 8, 9], isOdd), 5); + assert.equal(array.find([2, 4, 6, 8], isOdd), undefined); + assert.equal(array.find([2, 4, 6, 8], isOdd, null), null); }; +require('test').run(exports); diff --git a/addon-sdk/source/test/test-browser-events.js b/addon-sdk/source/test/test-browser-events.js index afa7180977d5..b4d8a1b016da 100644 --- a/addon-sdk/source/test/test-browser-events.js +++ b/addon-sdk/source/test/test-browser-events.js @@ -4,6 +4,12 @@ "use strict"; +module.metadata = { + engines: { + "Firefox": "*" + } +}; + const { Loader } = require("sdk/test/loader"); const { open, getMostRecentBrowserWindow, getOuterId } = require("sdk/window/utils"); const { setTimeout } = require("sdk/timers"); diff --git a/addon-sdk/source/test/test-content-events.js b/addon-sdk/source/test/test-content-events.js index 43d4fadf9847..f4a7b1644792 100644 --- a/addon-sdk/source/test/test-content-events.js +++ b/addon-sdk/source/test/test-content-events.js @@ -44,13 +44,17 @@ exports["test multiple tabs"] = function(assert, done) { on(events, "data", function({type, target, timeStamp}) { // ignore about:blank pages and *-document-global-created // events that are not very consistent. + // ignore http:// requests, as Fennec's `about:home` page + // displays add-ons a user could install if (target.URL !== "about:blank" && + target.URL !== "about:home" && + !target.URL.match(/^https?:\/\//i) && type !== "chrome-document-global-created" && type !== "content-document-global-created") actual.push(type + " -> " + target.URL) }); - let window = getMostRecentBrowserWindow(); + let window = getMostRecentBrowserWindow(); let firstTab = open("data:text/html,first-tab", window); when("pageshow", firstTab). diff --git a/addon-sdk/source/test/test-path.js b/addon-sdk/source/test/test-path.js index 9e9cb5c53190..f0b9e06cb0a2 100644 --- a/addon-sdk/source/test/test-path.js +++ b/addon-sdk/source/test/test-path.js @@ -429,8 +429,3 @@ if (isWindows) { }; require('test').run(exports); - -// Test disabled on OSX because of bug 891698 -if (require("sdk/system/runtime").OS == "Darwin") - module.exports = {}; - diff --git a/addon-sdk/source/test/test-places-events.js b/addon-sdk/source/test/test-places-events.js new file mode 100644 index 000000000000..67f82188d8fd --- /dev/null +++ b/addon-sdk/source/test/test-places-events.js @@ -0,0 +1,292 @@ +/* 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'; + +module.metadata = { + 'engines': { + 'Firefox': '*' + } +}; + +const { Cc, Ci } = require('chrome'); +const { defer, all } = require('sdk/core/promise'); +const { filter } = require('sdk/event/utils'); +const { on, off } = require('sdk/event/core'); +const { events } = require('sdk/places/events'); +const { setTimeout } = require('sdk/timers'); +const { before, after } = require('sdk/test/utils'); +const { + search +} = require('sdk/places/history'); +const { + invalidResolve, invalidReject, createTree, createBookmark, + compareWithHost, addVisits, resetPlaces, createBookmarkItem, + removeVisits +} = require('./places-helper'); +const { save, MENU, UNSORTED } = require('sdk/places/bookmarks'); +const { promisedEmitter } = require('sdk/places/utils'); + +exports['test bookmark-item-added'] = function (assert, done) { + function handler ({type, data}) { + if (type !== 'bookmark-item-added') return; + if (data.title !== 'bookmark-added-title') return; + + assert.equal(type, 'bookmark-item-added', 'correct type in bookmark-added event'); + assert.equal(data.type, 'bookmark', 'correct data in bookmark-added event'); + assert.ok(data.id != null, 'correct data in bookmark-added event'); + assert.ok(data.parentId != null, 'correct data in bookmark-added event'); + assert.ok(data.index != null, 'correct data in bookmark-added event'); + assert.equal(data.url, 'http://moz.com/', 'correct data in bookmark-added event'); + assert.ok(data.dateAdded != null, 'correct data in bookmark-added event'); + events.off('data', handler); + done(); + } + events.on('data', handler); + createBookmark({ title: 'bookmark-added-title' }); +}; + +exports['test bookmark-item-changed'] = function (assert, done) { + let id; + let complete = makeCompleted(done); + function handler ({type, data}) { + if (type !== 'bookmark-item-changed') return; + if (data.id !== id) return; + assert.equal(type, 'bookmark-item-changed', + 'correct type in bookmark-item-changed event'); + assert.equal(data.type, 'bookmark', + 'correct data in bookmark-item-changed event'); + assert.equal(data.property, 'title', + 'correct property in bookmark-item-changed event'); + assert.equal(data.value, 'bookmark-changed-title-2', + 'correct value in bookmark-item-changed event'); + assert.ok(data.id === id, 'correct id in bookmark-item-changed event'); + assert.ok(data.parentId != null, 'correct data in bookmark-added event'); + + events.off('data', handler); + complete(); + } + events.on('data', handler); + + createBookmarkItem({ title: 'bookmark-changed-title' }).then(item => { + id = item.id; + item.title = 'bookmark-changed-title-2'; + return saveP(item); + }).then(complete); +}; + +exports['test bookmark-item-moved'] = function (assert, done) { + let id; + let complete = makeCompleted(done); + function handler ({type, data}) { + if (type !== 'bookmark-item-moved') return; + if (data.id !== id) return; + assert.equal(type, 'bookmark-item-moved', + 'correct type in bookmark-item-moved event'); + assert.equal(data.type, 'bookmark', + 'correct data in bookmark-item-moved event'); + assert.ok(data.id === id, 'correct id in bookmark-item-moved event'); + assert.equal(data.previousParentId, UNSORTED.id, + 'correct previousParentId'); + assert.equal(data.currentParentId, MENU.id, + 'correct currentParentId'); + assert.equal(data.previousIndex, 0, 'correct previousIndex'); + assert.equal(data.currentIndex, 0, 'correct currentIndex'); + + events.off('data', handler); + complete(); + } + events.on('data', handler); + + createBookmarkItem({ + title: 'bookmark-moved-title', + group: UNSORTED + }).then(item => { + id = item.id; + item.group = MENU; + return saveP(item); + }).then(complete); +}; + +exports['test bookmark-item-removed'] = function (assert, done) { + let id; + let complete = makeCompleted(done); + function handler ({type, data}) { + if (type !== 'bookmark-item-removed') return; + if (data.id !== id) return; + assert.equal(type, 'bookmark-item-removed', + 'correct type in bookmark-item-removed event'); + assert.equal(data.type, 'bookmark', + 'correct data in bookmark-item-removed event'); + assert.ok(data.id === id, 'correct id in bookmark-item-removed event'); + assert.equal(data.parentId, UNSORTED.id, + 'correct parentId in bookmark-item-removed'); + assert.equal(data.url, 'http://moz.com/', + 'correct url in bookmark-item-removed event'); + assert.equal(data.index, 0, + 'correct index in bookmark-item-removed event'); + + events.off('data', handler); + complete(); + } + events.on('data', handler); + + createBookmarkItem({ + title: 'bookmark-item-remove-title', + group: UNSORTED + }).then(item => { + id = item.id; + item.remove = true; + return saveP(item); + }).then(complete); +}; + +exports['test bookmark-item-visited'] = function (assert, done) { + let id; + let complete = makeCompleted(done); + function handler ({type, data}) { + if (type !== 'bookmark-item-visited') return; + if (data.id !== id) return; + assert.equal(type, 'bookmark-item-visited', + 'correct type in bookmark-item-visited event'); + assert.ok(data.id === id, 'correct id in bookmark-item-visited event'); + assert.equal(data.parentId, UNSORTED.id, + 'correct parentId in bookmark-item-visited'); + assert.ok(data.transitionType != null, + 'has a transition type in bookmark-item-visited event'); + assert.ok(data.time != null, + 'has a time in bookmark-item-visited event'); + assert.ok(data.visitId != null, + 'has a visitId in bookmark-item-visited event'); + assert.equal(data.url, 'http://bookmark-item-visited.com/', + 'correct url in bookmark-item-visited event'); + + events.off('data', handler); + complete(); + } + events.on('data', handler); + + createBookmarkItem({ + title: 'bookmark-item-visited', + url: 'http://bookmark-item-visited.com/' + }).then(item => { + id = item.id; + return addVisits('http://bookmark-item-visited.com/'); + }).then(complete); +}; + +exports['test history-start-batch, history-end-batch, history-start-clear'] = function (assert, done) { + let complete = makeCompleted(done, 4); + let startEvent = filter(events, ({type}) => type === 'history-start-batch'); + let endEvent = filter(events, ({type}) => type === 'history-end-batch'); + let clearEvent = filter(events, ({type}) => type === 'history-start-clear'); + function startHandler ({type, data}) { + assert.pass('history-start-batch called'); + assert.equal(type, 'history-start-batch', + 'history-start-batch has correct type'); + off(startEvent, 'data', startHandler); + on(endEvent, 'data', endHandler); + complete(); + } + function endHandler ({type, data}) { + assert.pass('history-end-batch called'); + assert.equal(type, 'history-end-batch', + 'history-end-batch has correct type'); + off(endEvent, 'data', endHandler); + complete(); + } + function clearHandler ({type, data}) { + assert.pass('history-start-clear called'); + assert.equal(type, 'history-start-clear', + 'history-start-clear has correct type'); + off(clearEvent, 'data', clearHandler); + complete(); + } + + on(startEvent, 'data', startHandler); + on(clearEvent, 'data', clearHandler); + + createBookmark().then(() => { + resetPlaces(complete); + }) +}; + +exports['test history-visit, history-title-changed'] = function (assert, done) { + let complete = makeCompleted(() => { + off(titleEvents, 'data', titleHandler); + off(visitEvents, 'data', visitHandler); + done(); + }, 6); + let visitEvents = filter(events, ({type}) => type === 'history-visit'); + let titleEvents = filter(events, ({type}) => type === 'history-title-changed'); + + let urls = ['http://moz.com/', 'http://firefox.com/', 'http://mdn.com/']; + + function visitHandler ({type, data}) { + assert.equal(type, 'history-visit', 'correct type in history-visit'); + assert.ok(~urls.indexOf(data.url), 'history-visit has correct url'); + assert.ok(data.visitId != null, 'history-visit has a visitId'); + assert.ok(data.time != null, 'history-visit has a time'); + assert.ok(data.sessionId != null, 'history-visit has a sessionId'); + assert.ok(data.referringId != null, 'history-visit has a referringId'); + assert.ok(data.transitionType != null, 'history-visit has a transitionType'); + complete(); + } + + function titleHandler ({type, data}) { + assert.equal(type, 'history-title-changed', + 'correct type in history-title-changed'); + assert.ok(~urls.indexOf(data.url), + 'history-title-changed has correct url'); + assert.ok(data.title, 'history-title-changed has title'); + complete(); + } + + on(titleEvents, 'data', titleHandler); + on(visitEvents, 'data', visitHandler); + addVisits(urls); +} + +exports['test history-delete-url'] = function (assert, done) { + let complete = makeCompleted(() => { + events.off('data', handler); + done(); + }, 3); + let urls = ['http://moz.com/', 'http://firefox.com/', 'http://mdn.com/']; + function handler({type, data}) { + if (type !== 'history-delete-url') return; + assert.equal(type, 'history-delete-url', + 'history-delete-url has correct type'); + assert.ok(~urls.indexOf(data.url), 'history-delete-url has correct url'); + complete(); + } + + events.on('data', handler); + addVisits(urls).then(() => { + removeVisits(urls); + }); +}; + +exports['test history-page-changed'] = function (assert) { + assert.pass('history-page-changed tested in test-places-favicons'); +}; + +exports['test history-delete-visits'] = function (assert) { + assert.pass('TODO test history-delete-visits'); +}; + +before(exports, (name, assert, done) => resetPlaces(done)); +after(exports, (name, assert, done) => resetPlaces(done)); + +function saveP () { + return promisedEmitter(save.apply(null, Array.slice(arguments))); +} + +function makeCompleted (done, countTo) { + let count = 0; + countTo = countTo || 2; + return function () { + if (++count === countTo) done(); + }; +} +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-places-host.js b/addon-sdk/source/test/test-places-host.js index 7204ef964f8d..aab16a2831c3 100644 --- a/addon-sdk/source/test/test-places-host.js +++ b/addon-sdk/source/test/test-places-host.js @@ -35,7 +35,7 @@ const tagsrv = Cc['@mozilla.org/browser/tagging-service;1']. exports.testBookmarksCreate = function (assert, done) { let items = [{ title: 'my title', - url: 'http://moz.com', + url: 'http://test-places-host.com/testBookmarksCreate/', tags: ['some', 'tags', 'yeah'], type: 'bookmark' }, { @@ -71,26 +71,26 @@ exports.testBookmarksCreateFail = function (assert, done) { return send('sdk-places-bookmarks-create', item).then(null, function (reason) { assert.ok(reason, 'bookmark create should fail'); }); - })).then(function () { - done(); - }); + })).then(done); }; exports.testBookmarkLastUpdated = function (assert, done) { let timestamp; let item; - createBookmark().then(function (data) { + createBookmark({ + url: 'http://test-places-host.com/testBookmarkLastUpdated' + }).then(function (data) { item = data; timestamp = item.updated; return send('sdk-places-bookmarks-last-updated', { id: item.id }); }).then(function (updated) { + let { resolve, promise } = defer(); assert.equal(timestamp, updated, 'should return last updated time'); item.title = 'updated mozilla'; - return send('sdk-places-bookmarks-save', item).then(function (data) { - let deferred = defer(); - setTimeout(function () deferred.resolve(data), 100); - return deferred.promise; - }); + setTimeout(() => { + resolve(send('sdk-places-bookmarks-save', item)); + }, 100); + return promise; }).then(function (data) { assert.ok(data.updated > timestamp, 'time has elapsed and updated the updated property'); done(); @@ -99,7 +99,9 @@ exports.testBookmarkLastUpdated = function (assert, done) { exports.testBookmarkRemove = function (assert, done) { let id; - createBookmark().then(function (data) { + createBookmark({ + url: 'http://test-places-host.com/testBookmarkRemove/' + }).then(function (data) { id = data.id; compareWithHost(assert, data); // ensure bookmark exists bmsrv.getItemTitle(id); // does not throw an error @@ -114,7 +116,9 @@ exports.testBookmarkRemove = function (assert, done) { exports.testBookmarkGet = function (assert, done) { let bookmark; - createBookmark().then(function (data) { + createBookmark({ + url: 'http://test-places-host.com/testBookmarkGet/' + }).then(function (data) { bookmark = data; return send('sdk-places-bookmarks-get', { id: data.id }); }).then(function (data) { @@ -136,7 +140,9 @@ exports.testBookmarkGet = function (assert, done) { exports.testTagsTag = function (assert, done) { let url; - createBookmark().then(function (data) { + createBookmark({ + url: 'http://test-places-host.com/testTagsTag/', + }).then(function (data) { url = data.url; return send('sdk-places-tags-tag', { url: data.url, tags: ['mozzerella', 'foxfire'] @@ -153,7 +159,10 @@ exports.testTagsTag = function (assert, done) { exports.testTagsUntag = function (assert, done) { let item; - createBookmark({tags: ['tag1', 'tag2', 'tag3']}).then(function (data) { + createBookmark({ + url: 'http://test-places-host.com/testTagsUntag/', + tags: ['tag1', 'tag2', 'tag3'] + }).then(data => { item = data; return send('sdk-places-tags-untag', { url: item.url, @@ -172,7 +181,9 @@ exports.testTagsUntag = function (assert, done) { exports.testTagsGetURLsByTag = function (assert, done) { let item; - createBookmark().then(function (data) { + createBookmark({ + url: 'http://test-places-host.com/testTagsGetURLsByTag/' + }).then(function (data) { item = data; return send('sdk-places-tags-get-urls-by-tag', { tag: 'firefox' @@ -186,7 +197,10 @@ exports.testTagsGetURLsByTag = function (assert, done) { exports.testTagsGetTagsByURL = function (assert, done) { let item; - createBookmark({ tags: ['firefox', 'mozilla', 'metal']}).then(function (data) { + createBookmark({ + url: 'http://test-places-host.com/testTagsGetURLsByTag/', + tags: ['firefox', 'mozilla', 'metal'] + }).then(function (data) { item = data; return send('sdk-places-tags-get-tags-by-url', { url: data.url, @@ -202,9 +216,15 @@ exports.testTagsGetTagsByURL = function (assert, done) { exports.testHostQuery = function (assert, done) { all([ - createBookmark({ url: 'http://firefox.com', tags: ['firefox', 'mozilla'] }), - createBookmark({ url: 'http://mozilla.com', tags: ['mozilla'] }), - createBookmark({ url: 'http://thunderbird.com' }) + createBookmark({ + url: 'http://firefox.com/testHostQuery/', + tags: ['firefox', 'mozilla'] + }), + createBookmark({ + url: 'http://mozilla.com/testHostQuery/', + tags: ['mozilla'] + }), + createBookmark({ url: 'http://thunderbird.com/testHostQuery/' }) ]).then(data => { return send('sdk-places-query', { queries: { tags: ['mozilla'] }, @@ -212,34 +232,44 @@ exports.testHostQuery = function (assert, done) { }); }).then(results => { assert.equal(results.length, 2, 'should only return two'); - assert.equal(results[0].url, 'http://mozilla.com/', 'is sorted by URI asc'); + assert.equal(results[0].url, + 'http://mozilla.com/testHostQuery/', 'is sorted by URI asc'); return send('sdk-places-query', { queries: { tags: ['mozilla'] }, options: { sortingMode: 5, queryType: 1 } // sort by URI descending, bookmarks only }); }).then(results => { assert.equal(results.length, 2, 'should only return two'); - assert.equal(results[0].url, 'http://firefox.com/', 'is sorted by URI desc'); + assert.equal(results[0].url, + 'http://firefox.com/testHostQuery/', 'is sorted by URI desc'); done(); }); }; exports.testHostMultiQuery = function (assert, done) { all([ - createBookmark({ url: 'http://firefox.com', tags: ['firefox', 'mozilla'] }), - createBookmark({ url: 'http://mozilla.com', tags: ['mozilla'] }), - createBookmark({ url: 'http://thunderbird.com' }) + createBookmark({ + url: 'http://firefox.com/testHostMultiQuery/', + tags: ['firefox', 'mozilla'] + }), + createBookmark({ + url: 'http://mozilla.com/testHostMultiQuery/', + tags: ['mozilla'] + }), + createBookmark({ url: 'http://thunderbird.com/testHostMultiQuery/' }) ]).then(data => { return send('sdk-places-query', { - queries: [{ tags: ['firefox'] }, { uri: 'http://thunderbird.com/' }], + queries: [{ tags: ['firefox'] }, { uri: 'http://thunderbird.com/testHostMultiQuery/' }], options: { sortingMode: 5, queryType: 1 } // sort by URI descending, bookmarks only }); }).then(results => { assert.equal(results.length, 2, 'should return 2 results ORing queries'); - assert.equal(results[0].url, 'http://firefox.com/', 'should match URL or tag'); - assert.equal(results[1].url, 'http://thunderbird.com/', 'should match URL or tag'); + assert.equal(results[0].url, + 'http://firefox.com/testHostMultiQuery/', 'should match URL or tag'); + assert.equal(results[1].url, + 'http://thunderbird.com/testHostMultiQuery/', 'should match URL or tag'); return send('sdk-places-query', { - queries: [{ tags: ['firefox'], url: 'http://mozilla.com/' }], + queries: [{ tags: ['firefox'], url: 'http://mozilla.com/testHostMultiQuery/' }], options: { sortingMode: 5, queryType: 1 } // sort by URI descending, bookmarks only }); }).then(results => { @@ -269,7 +299,6 @@ exports.testGetAllChildren = function (assert, done) { }); }; - before(exports, (name, assert, done) => resetPlaces(done)); after(exports, (name, assert, done) => resetPlaces(done)); diff --git a/addon-sdk/source/test/test-plain-text-console.js b/addon-sdk/source/test/test-plain-text-console.js index a0ecea6cfee4..f757f8343e42 100644 --- a/addon-sdk/source/test/test-plain-text-console.js +++ b/addon-sdk/source/test/test-plain-text-console.js @@ -36,15 +36,15 @@ exports.testPlainTextConsole = function(test) { test.pass("PlainTextConsole instantiates"); con.log('testing', 1, [2, 3, 4]); - test.assertEqual(lastPrint(), "console.log: " + name + ": testing, 1, Array [2,3,4]\n", + test.assertEqual(lastPrint(), "console.log: " + name + ": testing 1 Array [2,3,4]\n", "PlainTextConsole.log() must work."); con.info('testing', 1, [2, 3, 4]); - test.assertEqual(lastPrint(), "console.info: " + name + ": testing, 1, Array [2,3,4]\n", + test.assertEqual(lastPrint(), "console.info: " + name + ": testing 1 Array [2,3,4]\n", "PlainTextConsole.info() must work."); con.warn('testing', 1, [2, 3, 4]); - test.assertEqual(lastPrint(), "console.warn: " + name + ": testing, 1, Array [2,3,4]\n", + test.assertEqual(lastPrint(), "console.warn: " + name + ": testing 1 Array [2,3,4]\n", "PlainTextConsole.warn() must work."); con.error('testing', 1, [2, 3, 4]); @@ -64,20 +64,20 @@ exports.testPlainTextConsole = function(test) { prints = []; con.log('testing', undefined); - test.assertEqual(lastPrint(), "console.log: " + name + ": testing, undefined\n", + test.assertEqual(lastPrint(), "console.log: " + name + ": testing undefined\n", "PlainTextConsole.log() must stringify undefined."); con.log('testing', null); - test.assertEqual(lastPrint(), "console.log: " + name + ": testing, null\n", + test.assertEqual(lastPrint(), "console.log: " + name + ": testing null\n", "PlainTextConsole.log() must stringify null."); // TODO: Fix console.jsm to detect custom toString. con.log("testing", { toString: function() "obj.toString()" }); - test.assertEqual(lastPrint(), "console.log: " + name + ": testing, {}\n", + test.assertEqual(lastPrint(), "console.log: " + name + ": testing {}\n", "PlainTextConsole.log() doesn't printify custom toString."); con.log("testing", { toString: function() { throw "fail!"; } }); - test.assertEqual(lastPrint(), "console.log: " + name + ": testing, {}\n", + test.assertEqual(lastPrint(), "console.log: " + name + ": testing {}\n", "PlainTextConsole.log() must stringify custom bad toString."); diff --git a/addon-sdk/source/test/test-tabs-common.js b/addon-sdk/source/test/test-tabs-common.js index b225b8ef47ab..247788880634 100644 --- a/addon-sdk/source/test/test-tabs-common.js +++ b/addon-sdk/source/test/test-tabs-common.js @@ -457,3 +457,100 @@ exports.testTabReload = function(test) { } }); }; + +exports.testOnPageShowEvent = function (test) { + test.waitUntilDone(); + + let events = []; + let firstUrl = 'data:text/html;charset=utf-8,First'; + let secondUrl = 'data:text/html;charset=utf-8,Second'; + + let counter = 0; + function onPageShow (tab, persisted) { + events.push('pageshow'); + counter++; + if (counter === 1) { + test.assertEqual(persisted, false, 'page should not be cached on initial load'); + tab.url = secondUrl; + } + else if (counter === 2) { + test.assertEqual(persisted, false, 'second test page should not be cached either'); + tab.attach({ + contentScript: 'setTimeout(function () { window.history.back(); }, 0)' + }); + } + else { + test.assertEqual(persisted, true, 'when we get back to the fist page, it has to' + + 'come from cache'); + tabs.removeListener('pageshow', onPageShow); + tabs.removeListener('open', onOpen); + tabs.removeListener('ready', onReady); + tab.close(() => { + ['open', 'ready', 'pageshow', 'ready', + 'pageshow', 'pageshow'].map((type, i) => { + test.assertEqual(type, events[i], 'correct ordering of events'); + }); + test.done() + }); + } + } + + function onOpen () events.push('open'); + function onReady () events.push('ready'); + + tabs.on('pageshow', onPageShow); + tabs.on('open', onOpen); + tabs.on('ready', onReady); + tabs.open({ + url: firstUrl + }); +}; + +exports.testOnPageShowEventDeclarative = function (test) { + test.waitUntilDone(); + + let events = []; + let firstUrl = 'data:text/html;charset=utf-8,First'; + let secondUrl = 'data:text/html;charset=utf-8,Second'; + + let counter = 0; + function onPageShow (tab, persisted) { + events.push('pageshow'); + counter++; + if (counter === 1) { + test.assertEqual(persisted, false, 'page should not be cached on initial load'); + tab.url = secondUrl; + } + else if (counter === 2) { + test.assertEqual(persisted, false, 'second test page should not be cached either'); + tab.attach({ + contentScript: 'setTimeout(function () { window.history.back(); }, 0)' + }); + } + else { + test.assertEqual(persisted, true, 'when we get back to the fist page, it has to' + + 'come from cache'); + tabs.removeListener('pageshow', onPageShow); + tabs.removeListener('open', onOpen); + tabs.removeListener('ready', onReady); + tab.close(() => { + ['open', 'ready', 'pageshow', 'ready', + 'pageshow', 'pageshow'].map((type, i) => { + test.assertEqual(type, events[i], 'correct ordering of events'); + }); + test.done() + }); + } + } + + function onOpen () events.push('open'); + function onReady () events.push('ready'); + + tabs.open({ + url: firstUrl, + onPageShow: onPageShow, + onOpen: onOpen, + onReady: onReady + }); +}; + diff --git a/addon-sdk/source/test/test-url.js b/addon-sdk/source/test/test-url.js index 6f383a37b237..bfd55572a546 100644 --- a/addon-sdk/source/test/test-url.js +++ b/addon-sdk/source/test/test-url.js @@ -3,7 +3,15 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 'use strict'; -const { URL, toFilename, fromFilename, isValidURI, getTLD, DataURL } = require('sdk/url'); +const { + URL, + toFilename, + fromFilename, + isValidURI, + getTLD, + DataURL, + isLocalURL } = require('sdk/url'); + const { pathFor } = require('sdk/system'); const file = require('sdk/io/file'); const tabs = require('sdk/tabs'); @@ -63,11 +71,11 @@ exports.testParseHttpSearchAndHash = function (assert) { var info = URL('https://www.moz.com/some/page.html'); assert.equal(info.hash, ''); assert.equal(info.search, ''); - + var hashOnly = URL('https://www.sub.moz.com/page.html#justhash'); assert.equal(hashOnly.search, ''); assert.equal(hashOnly.hash, '#justhash'); - + var queryOnly = URL('https://www.sub.moz.com/page.html?my=query'); assert.equal(queryOnly.search, '?my=query'); assert.equal(queryOnly.hash, ''); @@ -75,11 +83,11 @@ exports.testParseHttpSearchAndHash = function (assert) { var qMark = URL('http://www.moz.org?'); assert.equal(qMark.search, ''); assert.equal(qMark.hash, ''); - + var hash = URL('http://www.moz.org#'); assert.equal(hash.search, ''); assert.equal(hash.hash, ''); - + var empty = URL('http://www.moz.org?#'); assert.equal(hash.search, ''); assert.equal(hash.hash, ''); @@ -347,6 +355,39 @@ exports.testWindowLocationMatch = function (assert, done) { }) }; +exports.testURLInRegExpTest = function(assert) { + let url = 'https://mozilla.org'; + assert.equal((new RegExp(url).test(URL(url))), true, 'URL instances work in a RegExp test'); +} + +exports.testLocalURL = function(assert) { + [ + 'data:text/html;charset=utf-8,foo and bar', + 'data:text/plain,foo and bar', + 'resource://gre/modules/commonjs/', + 'chrome://browser/content/browser.xul' + ].forEach(aUri => { + assert.ok(isLocalURL(aUri), aUri + ' is a Local URL'); + }) + +} + +exports.testLocalURLwithRemoteURL = function(assert) { + validURIs().filter(url => !url.startsWith('data:')).forEach(aUri => { + assert.ok(!isLocalURL(aUri), aUri + ' is an invalid Local URL'); + }); +} + +exports.testLocalURLwithInvalidURL = function(assert) { + invalidURIs().concat([ + 'data:foo and bar', + 'resource:// must fail', + 'chrome:// here too' + ]).forEach(aUri => { + assert.ok(!isLocalURL(aUri), aUri + ' is an invalid Local URL'); + }); +} + function validURIs() { return [ 'http://foo.com/blah_blah', diff --git a/b2g/chrome/content/content.css b/b2g/chrome/content/content.css index f42ca3e81c4d..500288839658 100644 --- a/b2g/chrome/content/content.css +++ b/b2g/chrome/content/content.css @@ -17,6 +17,7 @@ html xul|scrollbar { background-image: none !important; border: 0px solid transparent !important; z-index: 2147483647; + pointer-events: none; opacity: 1; } diff --git a/b2g/chrome/content/shell.js b/b2g/chrome/content/shell.js index 9008ed8fcf0d..b7771eb69400 100644 --- a/b2g/chrome/content/shell.js +++ b/b2g/chrome/content/shell.js @@ -791,6 +791,7 @@ var AlertsHelper = { if (!manifestUrl || !manifestUrl.length) { send(null, null); + return; } // If we have a manifest URL, get the icon and title from the manifest diff --git a/b2g/components/B2GComponents.manifest b/b2g/components/B2GComponents.manifest index a9cf8b11dd6e..87f78260702a 100644 --- a/b2g/components/B2GComponents.manifest +++ b/b2g/components/B2GComponents.manifest @@ -40,7 +40,8 @@ category app-startup ProcessGlobal service,@mozilla.org/b2g-process-global;1 # ContentHandler.js component {d18d0216-d50c-11e1-ba54-efb18d0ef0ac} ContentHandler.js -contract @mozilla.org/uriloader/content-handler;1?type=application/pdf {d18d0216-d50c-11e1-ba54-efb18d0ef0ac} +contract @mozilla.org/b2g/activities-content-handler;1 {d18d0216-d50c-11e1-ba54-efb18d0ef0ac} +category app-startup ContentHandler service,@mozilla.org/b2g/activities-content-handler;1 # PaymentGlue.js component {8b83eabc-7929-47f4-8b48-4dea8d887e4b} PaymentGlue.js diff --git a/b2g/components/ContentHandler.js b/b2g/components/ContentHandler.js index ec37a51d1c94..38b25fb33f3e 100644 --- a/b2g/components/ContentHandler.js +++ b/b2g/components/ContentHandler.js @@ -18,22 +18,28 @@ XPCOMUtils.defineLazyGetter(this, "cpmm", function() { .getService(Ci.nsIMessageSender); }); -function log(aMsg) { - let msg = "ContentHandler.js: " + (aMsg.join ? aMsg.join("") : aMsg); - Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService) - .logStringMessage(msg); - dump(msg + "\n"); +function debug(aMsg) { + //dump("--*-- ContentHandler: " + aMsg + "\n"); } const NS_ERROR_WONT_HANDLE_CONTENT = 0x805d0001; -function ContentHandler() { + +let ActivityContentFactory = { + createInstance: function createInstance(outer, iid) { + if (outer != null) { + throw Cr.NS_ERROR_NO_AGGREGATION; + } + return new ActivityContentHandler().QueryInterface(iid); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory]) } -ContentHandler.prototype = { - handleContent: function handleContent(aMimetype, aContext, aRequest) { - if (aMimetype != PDF_CONTENT_TYPE) - throw NS_ERROR_WONT_HANDLE_CONTENT; +function ActivityContentHandler() { +} +ActivityContentHandler.prototype = { + handleContent: function handleContent(aMimetype, aContext, aRequest) { if (!(aRequest instanceof Ci.nsIChannel)) throw NS_ERROR_WONT_HANDLE_CONTENT; @@ -46,8 +52,96 @@ ContentHandler.prototype = { aRequest.cancel(Cr.NS_BINDING_ABORTED); }, - classID: Components.ID("{d18d0216-d50c-11e1-ba54-efb18d0ef0ac}"), QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentHandler]) +} + +function ContentHandler() { + this.classIdMap = {}; +} + +ContentHandler.prototype = { + observe: function(aSubject, aTopic, aData) { + if (aTopic == "app-startup") { + // We only want to register these from content processes. + let appInfo = Cc["@mozilla.org/xre/app-info;1"]; + if (appInfo.getService(Ci.nsIXULRuntime) + .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) { + return; + } + } + + cpmm.addMessageListener("Activities:RegisterContentTypes", this); + cpmm.addMessageListener("Activities:UnregisterContentTypes", this); + cpmm.sendAsyncMessage("Activities:GetContentTypes", { }); + }, + + /** + * Do the component registration for a content type. + * We only need to register one component per content type, even if several + * apps provide it, so we keep track of the number of providers for each + * content type. + */ + registerContentHandler: function registerContentHandler(aContentType) { + debug("Registering " + aContentType); + + // We already have a provider for this content type, just increase the + // tracking count. + if (this.classIdMap[aContentType]) { + this.classIdMap[aContentType].count++; + return; + } + + let contractID = "@mozilla.org/uriloader/content-handler;1?type=" + + aContentType; + let uuidGen = Cc["@mozilla.org/uuid-generator;1"] + .getService(Ci.nsIUUIDGenerator); + let id = Components.ID(uuidGen.generateUUID().toString()); + this.classIdMap[aContentType] = { count: 1, id: id }; + let cr = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + cr.registerFactory(Components.ID(id), "Activity Content Handler", contractID, + ActivityContentFactory); + }, + + /** + * Do the component unregistration for a content type. + */ + unregisterContentHandler: function registerContentHandler(aContentType) { + debug("Unregistering " + aContentType); + + let record = this.classIdMap[aContentType]; + if (!record) { + return; + } + + // Bail out if we still have providers left for this content type. + if (--record.count > 0) { + return; + } + + let contractID = "@mozilla.org/uriloader/content-handler;1?type=" + + aContentType; + let cr = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + cr.unregisterFactory(record.id, ActivityContentFactory); + delete this.classIdMap[aContentType] + }, + + receiveMessage: function(aMessage) { + let data = aMessage.data; + + switch (aMessage.name) { + case "Activities:RegisterContentTypes": + data.contentTypes.forEach(this.registerContentHandler, this); + break; + case "Activities:UnregisterContentTypes": + data.contentTypes.forEach(this.unregisterContentHandler, this); + break; + } + }, + + classID: Components.ID("{d18d0216-d50c-11e1-ba54-efb18d0ef0ac}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentHandler, + Ci.nsIObserver, + Ci.nsISupportsWeakReference]) }; this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentHandler]); diff --git a/b2g/components/ContentPermissionPrompt.js b/b2g/components/ContentPermissionPrompt.js index 02755592bd8d..1202e99d98b3 100644 --- a/b2g/components/ContentPermissionPrompt.js +++ b/b2g/components/ContentPermissionPrompt.js @@ -188,7 +188,9 @@ ContentPermissionPrompt.prototype = { this.sendToBrowserWindow("permission-prompt", request, requestId, function(type, remember) { if (type == "permission-allow") { rememberPermission(request.type, principal, !remember); - callback(); + if (callback) { + callback(); + } request.allow(); return; } @@ -202,7 +204,9 @@ ContentPermissionPrompt.prototype = { Ci.nsIPermissionManager.EXPIRE_SESSION, 0); } - callback(); + if (callback) { + callback(); + } request.cancel(); }); }, diff --git a/b2g/components/Keyboard.jsm b/b2g/components/Keyboard.jsm index 0895d7a34404..98aa81c826e9 100644 --- a/b2g/components/Keyboard.jsm +++ b/b2g/components/Keyboard.jsm @@ -106,7 +106,7 @@ let Keyboard = { switch (msg.name) { case 'Forms:Input': - this.forwardEvent('Keyboard:FocusChange', msg); + this.handleFocusChange(msg); break; case 'Forms:SelectionChange': case 'Forms:GetText:Result:OK': @@ -163,6 +163,17 @@ let Keyboard = { ppmm.broadcastAsyncMessage(newEventName, msg.data); }, + handleFocusChange: function keyboardHandleFocusChange(msg) { + this.forwardEvent('Keyboard:FocusChange', msg); + + let browser = Services.wm.getMostRecentWindow("navigator:browser"); + + browser.shell.sendChromeEvent({ + type: 'inputmethod-contextchange', + inputType: msg.data.type + }); + }, + setSelectedOption: function keyboardSetSelectedOption(msg) { this.messageManager.sendAsyncMessage('Forms:Select:Choice', msg.data); }, @@ -191,14 +202,14 @@ let Keyboard = { showInputMethodPicker: function keyboardShowInputMethodPicker() { let browser = Services.wm.getMostRecentWindow("navigator:browser"); browser.shell.sendChromeEvent({ - type: "input-method-show-picker" + type: "inputmethod-showall" }); }, switchToNextInputMethod: function keyboardSwitchToNextInputMethod() { let browser = Services.wm.getMostRecentWindow("navigator:browser"); browser.shell.sendChromeEvent({ - type: "input-method-switch-to-next" + type: "inputmethod-next" }); }, diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index f0ca48d5befd..71528e42bf1c 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -1,4 +1,4 @@ { - "revision": "fd03fbd18a09517bc5eb4e2af62314421ae7124a", + "revision": "0e3e30e489ff38cceb8b9cf9ee5caea5fb072457", "repo_path": "/integration/gaia-central" } diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 988a50af571b..099fcc6cf78e 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -762,7 +762,6 @@ var gBrowserInit = { // initialize observers and listeners // and give C++ access to gBrowser - gBrowser.init(); XULBrowserWindow.init(); window.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(nsIWebNavigation) diff --git a/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml index ab04ed159049..8305311828e7 100644 --- a/browser/base/content/tabbrowser.xml +++ b/browser/base/content/tabbrowser.xml @@ -2956,7 +2956,17 @@ filter.addProgressListener(tabListener, nsIWebProgress.NOTIFY_ALL); this.mTabListeners[0] = tabListener; this.mTabFilters[0] = filter; - this.init(); + + try { + // We assume this can only fail because mCurrentBrowser's docShell + // hasn't been created, yet. This may be caused by code accessing + // gBrowser before the window has finished loading. + this._addProgressListenerForInitialTab(); + } catch (e) { + // The binding was constructed too early, wait until the initial + // tab's document is ready, then add the progress listener. + this._waitForInitialContentDocument(); + } this.style.backgroundColor = Services.prefs.getBoolPref("browser.display.use_system_colors") ? @@ -2970,17 +2980,28 @@ ]]> - + + + + + { + if (this.browsers[0].contentWindow == subject) { + Services.obs.removeObserver(obs, topic); + this._addProgressListenerForInitialTab(); } - } + }; + + // We use content-document-global-created as an approximation for + // "docShell is initialized". We can do this because in the + // mTabProgressListener we care most about the STATE_STOP notification + // that will reset mBlank. That means it's important to at least add + // the progress listener before the initial about:blank load stops + // if we can't do it before the load starts. + Services.obs.addObserver(obs, "content-document-global-created", false); ]]> diff --git a/browser/base/content/test/Makefile.in b/browser/base/content/test/Makefile.in index 3f782e4f9756..745e3e921c06 100644 --- a/browser/base/content/test/Makefile.in +++ b/browser/base/content/test/Makefile.in @@ -184,6 +184,7 @@ MOCHITEST_BROWSER_FILES = \ browser_bug822367.js \ browser_bug832435.js \ browser_bug839103.js \ + browser_bug880101.js \ browser_bug882977.js \ browser_bug887515.js \ browser_canonizeURL.js \ diff --git a/browser/base/content/test/browser_bug880101.js b/browser/base/content/test/browser_bug880101.js new file mode 100644 index 000000000000..abe05b8646df --- /dev/null +++ b/browser/base/content/test/browser_bug880101.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const URL = "about:robots"; + +function test() { + let win; + + let listener = { + onLocationChange: (webProgress, request, uri, flags) => { + ok(webProgress.isTopLevel, "Received onLocationChange from top frame"); + is(uri.spec, URL, "Received onLocationChange for correct URL"); + finish(); + } + }; + + waitForExplicitFinish(); + + // Remove the listener and window when we're done. + registerCleanupFunction(() => { + win.gBrowser.removeProgressListener(listener); + win.close(); + }); + + // Wait for the newly opened window. + whenNewWindowOpened(w => win = w); + + // Open a link in a new window. + openLinkIn(URL, "window", {}); + + // On the next tick, but before the window has finished loading, access the + // window's gBrowser property to force the tabbrowser constructor early. + (function tryAddProgressListener() { + executeSoon(() => { + try { + win.gBrowser.addProgressListener(listener); + } catch (e) { + // win.gBrowser wasn't ready, yet. Try again in a tick. + tryAddProgressListener(); + } + }); + })(); +} + +function whenNewWindowOpened(cb) { + Services.obs.addObserver(function obs(win) { + Services.obs.removeObserver(obs, "domwindowopened"); + cb(win); + }, "domwindowopened", false); +} diff --git a/browser/components/sessionstore/src/SessionStore.jsm b/browser/components/sessionstore/src/SessionStore.jsm index 54d6263e5183..83da80cc6606 100644 --- a/browser/components/sessionstore/src/SessionStore.jsm +++ b/browser/components/sessionstore/src/SessionStore.jsm @@ -311,9 +311,6 @@ let SessionStoreInternal = { // states for all recently closed windows _closedWindows: [], - // not-"dirty" windows usually don't need to have their data updated - _dirtyWindows: {}, - // collection of session states yet to be restored _statesToRestore: {}, @@ -488,8 +485,6 @@ let SessionStoreInternal = { this._prefBranch.getBoolPref("sessionstore.resume_session_once")) this._prefBranch.setBoolPref("sessionstore.resume_session_once", false); - this._initEncoding(); - this._performUpgradeBackup(); this._sessionInitialized = true; @@ -524,13 +519,6 @@ let SessionStoreInternal = { }.bind(this)); }, - _initEncoding : function ssi_initEncoding() { - // The (UTF-8) encoder used to write to files. - XPCOMUtils.defineLazyGetter(this, "_writeFileEncoder", function () { - return new TextEncoder(); - }); - }, - _initPrefs : function() { this._prefBranch = Services.prefs.getBranch("browser."); @@ -1000,7 +988,7 @@ let SessionStoreInternal = { var activeWindow = this._getMostRecentBrowserWindow(); if (activeWindow) this.activeWindowSSiCache = activeWindow.__SSi || ""; - this._dirtyWindows = []; + DirtyWindows.clear(); }, /** @@ -2544,14 +2532,14 @@ let SessionStoreInternal = { this._forEachBrowserWindow(function(aWindow) { if (!this._isWindowLoaded(aWindow)) // window data is still in _statesToRestore return; - if (aUpdateAll || this._dirtyWindows[aWindow.__SSi] || aWindow == activeWindow) { + if (aUpdateAll || DirtyWindows.has(aWindow) || aWindow == activeWindow) { this._collectWindowData(aWindow); } else { // always update the window features (whose change alone never triggers a save operation) this._updateWindowFeatures(aWindow); } }); - this._dirtyWindows = []; + DirtyWindows.clear(); } // collect the data for all windows @@ -2682,7 +2670,7 @@ let SessionStoreInternal = { this._windows[aWindow.__SSi].__lastSessionWindowID = aWindow.__SS_lastSessionWindowID; - this._dirtyWindows[aWindow.__SSi] = false; + DirtyWindows.remove(aWindow); }, /* ........ Restoring Functionality .............. */ @@ -3010,7 +2998,7 @@ let SessionStoreInternal = { // It's important to set the window state to dirty so that // we collect their data for the first time when saving state. - this._dirtyWindows[aWindow.__SSi] = true; + DirtyWindows.add(aWindow); } if (aTabs.length == 0) { @@ -3701,7 +3689,7 @@ let SessionStoreInternal = { */ saveStateDelayed: function ssi_saveStateDelayed(aWindow = null, aDelay = 2000) { if (aWindow) { - this._dirtyWindows[aWindow.__SSi] = true; + DirtyWindows.add(aWindow); } if (!this._saveTimer) { @@ -4689,6 +4677,28 @@ let DyingWindowCache = { } }; +// A weak set of dirty windows. We use it to determine which windows we need to +// recollect data for when _getCurrentState() is called. +let DirtyWindows = { + _data: new WeakMap(), + + has: function (window) { + return this._data.has(window); + }, + + add: function (window) { + return this._data.set(window, true); + }, + + remove: function (window) { + this._data.delete(window); + }, + + clear: function (window) { + this._data.clear(); + } +}; + // A map storing the number of tabs last closed per windoow. This only // stores the most recent tab-close operation, and is used to undo // batch tab-closing operations. diff --git a/browser/components/tabview/groupitems.js b/browser/components/tabview/groupitems.js index df3a2670cb39..df489429c710 100644 --- a/browser/components/tabview/groupitems.js +++ b/browser/components/tabview/groupitems.js @@ -1920,7 +1920,6 @@ let GroupItems = { minGroupHeight: 110, minGroupWidth: 125, _lastActiveList: null, - _lastGroupToUpdateTabBar: null, // ---------- // Function: toString @@ -2286,10 +2285,6 @@ let GroupItems = { }); this._lastActiveList.remove(groupItem); - - if (this._lastGroupToUpdateTabBar == groupItem) - this._lastGroupToUpdateTabBar = null; - UI.updateTabButton(); }, @@ -2423,13 +2418,8 @@ let GroupItems = { Utils.assert(this._activeGroupItem, "There must be something to show in the tab bar!"); - // Update list of visible tabs only once after switching to another group. - if (this._activeGroupItem == this._lastGroupToUpdateTabBar) - return; - let tabItems = this._activeGroupItem._children; gBrowser.showOnlyTheseTabs(tabItems.map(function(item) item.tab)); - this._lastGroupToUpdateTabBar = this._activeGroupItem; }, // ---------- @@ -2547,7 +2537,7 @@ let GroupItems = { if (tab._tabViewTabItem.parent && tab._tabViewTabItem.parent.id == groupItemId) return; - let shouldHideTab = false; + let shouldUpdateTabBar = false; let shouldShowTabView = false; let groupItem; @@ -2555,12 +2545,12 @@ let GroupItems = { if (tab.selected) { if (gBrowser.visibleTabs.length > 1) { gBrowser._blurTab(tab); - shouldHideTab = true; + shouldUpdateTabBar = true; } else { shouldShowTabView = true; } } else { - shouldHideTab = true; + shouldUpdateTabBar = true } // remove tab item from a groupItem @@ -2583,8 +2573,8 @@ let GroupItems = { new GroupItem([ tab._tabViewTabItem ], { bounds: box, immediately: true }); } - if (shouldHideTab) - gBrowser.hideTab(tab); + if (shouldUpdateTabBar) + this._updateTabBar(); else if (shouldShowTabView) UI.showTabView(); }, diff --git a/browser/components/tabview/test/browser_tabview_bug624265_perwindowpb.js b/browser/components/tabview/test/browser_tabview_bug624265_perwindowpb.js index e1d32e59e083..f6450bb7403c 100644 --- a/browser/components/tabview/test/browser_tabview_bug624265_perwindowpb.js +++ b/browser/components/tabview/test/browser_tabview_bug624265_perwindowpb.js @@ -99,14 +99,47 @@ function test() { }, aWindow); } - function testOnWindow(aCallback) { - let win = OpenBrowserWindow({private: false}); + // [624102] check state after return from private browsing + let testPrivateBrowsing = function (aWindow) { + aWindow.gBrowser.loadOneTab('http://mochi.test:8888/#1', {inBackground: true}); + aWindow.gBrowser.loadOneTab('http://mochi.test:8888/#2', {inBackground: true}); + + let cw = getContentWindow(aWindow); + let box = new cw.Rect(20, 20, 250, 200); + let groupItem = new cw.GroupItem([], {bounds: box, immediately: true}); + cw.UI.setActive(groupItem); + + aWindow.gBrowser.selectedTab = aWindow.gBrowser.loadOneTab('http://mochi.test:8888/#3', {inBackground: true}); + aWindow.gBrowser.loadOneTab('http://mochi.test:8888/#4', {inBackground: true}); + + afterAllTabsLoaded(function () { + assertNumberOfVisibleTabs(aWindow, 2); + + enterAndLeavePrivateBrowsing(function () { + assertNumberOfVisibleTabs(aWindow, 2); + aWindow.gBrowser.selectedTab = aWindow.gBrowser.tabs[0]; + closeGroupItem(cw.GroupItems.groupItems[1], function() { + next(aWindow); + }); + }); + }, aWindow); + } + + function testOnWindow(aIsPrivate, aCallback) { + let win = OpenBrowserWindow({private: aIsPrivate}); win.addEventListener("load", function onLoad() { win.removeEventListener("load", onLoad, false); executeSoon(function() { aCallback(win) }); }, false); } + function enterAndLeavePrivateBrowsing(callback) { + testOnWindow(true, function (aWindow) { + aWindow.close(); + callback(); + }); + } + waitForExplicitFinish(); // Tests for #624265 @@ -116,7 +149,10 @@ function test() { tests.push(testDuplicateTab); tests.push(testBackForwardDuplicateTab); - testOnWindow(function(aWindow) { + // Tests for #624102 + tests.push(testPrivateBrowsing); + + testOnWindow(false, function(aWindow) { loadTabView(function() { next(aWindow); }, aWindow); @@ -127,4 +163,4 @@ function loadTabView(callback, aWindow) { showTabView(function () { hideTabView(callback, aWindow); }, aWindow); -} +} \ No newline at end of file diff --git a/browser/metro/base/content/ContextUI.js b/browser/metro/base/content/ContextUI.js index 5e0a05077071..8e8e167d396f 100644 --- a/browser/metro/base/content/ContextUI.js +++ b/browser/metro/base/content/ContextUI.js @@ -181,7 +181,7 @@ var ContextUI = { // Dismiss the navbar if visible. dismissNavbar: function dismissNavbar() { - if (!StartUI.isVisible) { + if (!BrowserUI.isStartTabVisible) { Elements.navbar.dismiss(); } }, @@ -230,17 +230,11 @@ var ContextUI = { _onEdgeUIStarted: function(aEvent) { this._hasEdgeSwipeStarted = true; this._clearDelayedTimeout(); - - if (StartUI.hide()) { - this.dismiss(); - return; - } this.toggleNavUI(); }, _onEdgeUICanceled: function(aEvent) { this._hasEdgeSwipeStarted = false; - StartUI.hide(); this.dismiss(); }, @@ -251,10 +245,6 @@ var ContextUI = { } this._clearDelayedTimeout(); - if (StartUI.hide()) { - this.dismiss(); - return; - } this.toggleNavUI(); }, @@ -283,16 +273,21 @@ var ContextUI = { this.dismissTabs(); break; case "mousedown": + if (BrowserUI.isStartTabVisible) + break; if (aEvent.button == 0 && this.isVisible) this.dismiss(); break; - case "ToolPanelShown": case "ToolPanelHidden": - case "touchstart": case "AlertActive": this.dismiss(); break; + case "touchstart": + if (!BrowserUI.isStartTabVisible) { + this.dismiss(); + } + break; } }, diff --git a/browser/metro/base/content/TopSites.js b/browser/metro/base/content/TopSites.js index 753433b4593d..e5cd48b2c297 100644 --- a/browser/metro/base/content/TopSites.js +++ b/browser/metro/base/content/TopSites.js @@ -3,10 +3,6 @@ * 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'; - let prefs = Components.classes["@mozilla.org/preferences-service;1"]. - getService(Components.interfaces.nsIPrefBranch); -Cu.import("resource://gre/modules/PageThumbs.jsm"); -Cu.import("resource:///modules/colorUtils.jsm"); /** * singleton to provide data-level functionality to the views @@ -163,290 +159,3 @@ let TopSites = { } }; -function TopSitesView(aGrid, aMaxSites) { - this._set = aGrid; - this._set.controller = this; - this._topSitesMax = aMaxSites; - - // clean up state when the appbar closes - window.addEventListener('MozAppbarDismissing', this, false); - let history = Cc["@mozilla.org/browser/nav-history-service;1"]. - getService(Ci.nsINavHistoryService); - history.addObserver(this, false); - - PageThumbs.addExpirationFilter(this); - Services.obs.addObserver(this, "Metro:RefreshTopsiteThumbnail", false); - Services.obs.addObserver(this, "metro_viewstate_changed", false); - - NewTabUtils.allPages.register(this); - TopSites.prepareCache().then(function(){ - this.populateGrid(); - }.bind(this)); -} - -TopSitesView.prototype = Util.extend(Object.create(View.prototype), { - _set:null, - _topSitesMax: null, - // _lastSelectedSites used to temporarily store blocked/removed sites for undo/restore-ing - _lastSelectedSites: null, - // isUpdating used only for testing currently - isUpdating: false, - - handleItemClick: function tabview_handleItemClick(aItem) { - let url = aItem.getAttribute("value"); - BrowserUI.goToURI(url); - }, - - doActionOnSelectedTiles: function(aActionName, aEvent) { - let tileGroup = this._set; - let selectedTiles = tileGroup.selectedItems; - let sites = Array.map(selectedTiles, TopSites._linkFromNode); - let nextContextActions = new Set(); - - switch (aActionName){ - case "delete": - for (let aNode of selectedTiles) { - // add some class to transition element before deletion? - aNode.contextActions.delete('delete'); - // we need new context buttons to show (the tile node will go away though) - } - this._lastSelectedSites = (this._lastSelectedSites || []).concat(sites); - // stop the appbar from dismissing - aEvent.preventDefault(); - nextContextActions.add('restore'); - TopSites.hideSites(sites); - break; - case "restore": - // usually restore is an undo action, so we have to recreate the tiles and grid selection - if (this._lastSelectedSites) { - let selectedUrls = this._lastSelectedSites.map((site) => site.url); - // re-select the tiles once the tileGroup is done populating and arranging - tileGroup.addEventListener("arranged", function _onArranged(aEvent){ - for (let url of selectedUrls) { - let tileNode = tileGroup.querySelector("richgriditem[value='"+url+"']"); - if (tileNode) { - tileNode.setAttribute("selected", true); - } - } - tileGroup.removeEventListener("arranged", _onArranged, false); - // we can't just call selectItem n times on tileGroup as selecting means trigger the default action - // for seltype="single" grids. - // so we toggle the attributes and raise the selectionchange "manually" - let event = tileGroup.ownerDocument.createEvent("Events"); - event.initEvent("selectionchange", true, true); - tileGroup.dispatchEvent(event); - }, false); - - TopSites.restoreSites(this._lastSelectedSites); - // stop the appbar from dismissing, - // the selectionchange event will trigger re-population of the context appbar - aEvent.preventDefault(); - } - break; - case "pin": - let pinIndices = []; - Array.forEach(selectedTiles, function(aNode) { - pinIndices.push( Array.indexOf(aNode.control.children, aNode) ); - aNode.contextActions.delete('pin'); - aNode.contextActions.add('unpin'); - }); - TopSites.pinSites(sites, pinIndices); - break; - case "unpin": - Array.forEach(selectedTiles, function(aNode) { - aNode.contextActions.delete('unpin'); - aNode.contextActions.add('pin'); - }); - TopSites.unpinSites(sites); - break; - // default: no action - } - if (nextContextActions.size) { - // at next tick, re-populate the context appbar - setTimeout(function(){ - // fire a MozContextActionsChange event to update the context appbar - let event = document.createEvent("Events"); - event.actions = [...nextContextActions]; - event.initEvent("MozContextActionsChange", true, false); - tileGroup.dispatchEvent(event); - },0); - } - }, - - handleEvent: function(aEvent) { - switch (aEvent.type){ - case "MozAppbarDismissing": - // clean up when the context appbar is dismissed - we don't remember selections - this._lastSelectedSites = null; - } - }, - - update: function() { - // called by the NewTabUtils.allPages.update, notifying us of data-change in topsites - let grid = this._set, - dirtySites = TopSites.dirty(); - - if (dirtySites.size) { - // we can just do a partial update and refresh the node representing each dirty tile - for (let site of dirtySites) { - let tileNode = grid.querySelector("[value='"+site.url+"']"); - if (tileNode) { - this.updateTile(tileNode, new Site(site)); - } - } - } else { - // flush, recreate all - this.isUpdating = true; - // destroy and recreate all item nodes, skip calling arrangeItems - grid.clearAll(true); - this.populateGrid(); - } - }, - - updateTile: function(aTileNode, aSite, aArrangeGrid) { - this._updateFavicon(aTileNode, Util.makeURI(aSite.url)); - - Task.spawn(function() { - let filepath = PageThumbsStorage.getFilePathForURL(aSite.url); - if (yield OS.File.exists(filepath)) { - aSite.backgroundImage = 'url("'+PageThumbs.getThumbnailURL(aSite.url)+'")'; - aTileNode.setAttribute("customImage", aSite.backgroundImage); - if (aTileNode.refresh) { - aTileNode.refresh() - } - } - }); - - aSite.applyToTileNode(aTileNode); - if (aArrangeGrid) { - this._set.arrangeItems(); - } - }, - - populateGrid: function populateGrid() { - this.isUpdating = true; - - let sites = TopSites.getSites(); - let length = Math.min(sites.length, this._topSitesMax || Infinity); - let tileset = this._set; - - // if we're updating with a collection that is smaller than previous - // remove any extra tiles - while (tileset.children.length > length) { - tileset.removeChild(tileset.children[tileset.children.length -1]); - } - - for (let idx=0; idx < length; idx++) { - let isNew = !tileset.children[idx], - site = sites[idx]; - let item = isNew ? tileset.createItemElement(site.title, site.url) : tileset.children[idx]; - - this.updateTile(item, site); - if (isNew) { - tileset.appendChild(item); - } - } - tileset.arrangeItems(); - this.isUpdating = false; - }, - - forceReloadOfThumbnail: function forceReloadOfThumbnail(url) { - let nodes = this._set.querySelectorAll('richgriditem[value="'+url+'"]'); - for (let item of nodes) { - if ("isBound" in item && item.isBound) { - item.refreshBackgroundImage(); - } - } - }, - - filterForThumbnailExpiration: function filterForThumbnailExpiration(aCallback) { - aCallback([item.getAttribute("value") for (item of this._set.children)]); - }, - - isFirstRun: function isFirstRun() { - return prefs.getBoolPref("browser.firstrun.show.localepicker"); - }, - - destruct: function destruct() { - Services.obs.removeObserver(this, "Metro:RefreshTopsiteThumbnail"); - Services.obs.removeObserver(this, "metro_viewstate_changed"); - PageThumbs.removeExpirationFilter(this); - window.removeEventListener('MozAppbarDismissing', this, false); - }, - - // nsIObservers - observe: function (aSubject, aTopic, aState) { - switch(aTopic) { - case "Metro:RefreshTopsiteThumbnail": - this.forceReloadOfThumbnail(aState); - break; - case "metro_viewstate_changed": - this.onViewStateChange(aState); - for (let item of this._set.children) { - if (aState == "snapped") { - item.removeAttribute("tiletype"); - } else { - item.setAttribute("tiletype", "thumbnail"); - } - } - break; - } - }, - - // nsINavHistoryObserver - onBeginUpdateBatch: function() { - }, - - onEndUpdateBatch: function() { - }, - - onVisit: function(aURI, aVisitID, aTime, aSessionID, - aReferringID, aTransitionType) { - }, - - onTitleChanged: function(aURI, aPageTitle) { - }, - - onDeleteURI: function(aURI) { - }, - - onClearHistory: function() { - this._set.clearAll(); - }, - - onPageChanged: function(aURI, aWhat, aValue) { - }, - - onDeleteVisits: function (aURI, aVisitTime, aGUID, aReason, aTransitionType) { - }, - - QueryInterface: function(iid) { - if (iid.equals(Components.interfaces.nsINavHistoryObserver) || - iid.equals(Components.interfaces.nsISupports)) { - return this; - } - throw Cr.NS_ERROR_NO_INTERFACE; - } - -}); - -let TopSitesStartView = { - _view: null, - get _grid() { return document.getElementById("start-topsites-grid"); }, - - init: function init() { - this._view = new TopSitesView(this._grid, 8); - if (this._view.isFirstRun()) { - let topsitesVbox = document.getElementById("start-topsites"); - topsitesVbox.setAttribute("hidden", "true"); - } - }, - - uninit: function uninit() { - this._view.destruct(); - }, - - show: function show() { - this._grid.arrangeItems(); - } -}; diff --git a/browser/metro/base/content/WebProgress.js b/browser/metro/base/content/WebProgress.js index ab708daa69b3..130ce6d9d652 100644 --- a/browser/metro/base/content/WebProgress.js +++ b/browser/metro/base/content/WebProgress.js @@ -134,7 +134,6 @@ const WebProgress = { browser.messageManager.removeMessageListener(aMessage.name, arguments.callee); aTab._firstPaint = true; aTab.scrolledAreaChanged(true); - aTab.updateThumbnailSource(); }); }, diff --git a/browser/metro/base/content/appbar.js b/browser/metro/base/content/appbar.js index 57ff45be3b43..d4c371acd535 100644 --- a/browser/metro/base/content/appbar.js +++ b/browser/metro/base/content/appbar.js @@ -31,6 +31,14 @@ var Appbar = { switch (aEvent.type) { case 'URLChanged': case 'TabSelect': + this.update(); + // Switching away from or loading a site into a startui tab that has actions + // pending, we consider this confirmation that the user wants to flush changes. + if (this.activeTileset && aEvent.lastTab && aEvent.lastTab.browser.currentURI.spec == kStartURI) { + ContextUI.dismiss(); + } + break; + case 'MozAppbarShowing': this.update(); break; @@ -102,7 +110,7 @@ var Appbar = { onMenuButton: function(aEvent) { let typesArray = []; - if (!StartUI.isVisible) + if (!BrowserUI.isStartTabVisible) typesArray.push("find-in-page"); if (ConsolePanelView.enabled) typesArray.push("open-error-console"); diff --git a/browser/metro/base/content/apzc.js b/browser/metro/base/content/apzc.js index f9c30c6013c9..9729ca299b98 100644 --- a/browser/metro/base/content/apzc.js +++ b/browser/metro/base/content/apzc.js @@ -77,32 +77,21 @@ var APZCObserver = { let resolution = frameMetrics.resolution; let compositedRect = frameMetrics.compositedRect; - if (StartUI.isStartPageVisible) { - let windowUtils = Browser.windowUtils; - Browser.selectedBrowser.contentWindow.scrollTo(scrollTo.x, scrollTo.y); - windowUtils.setResolution(resolution, resolution); - windowUtils.setDisplayPortForElement(displayPort.x * resolution, - displayPort.y * resolution, - displayPort.width * resolution, - displayPort.height * resolution, - Elements.startUI); - } else { - let windowUtils = Browser.selectedBrowser.contentWindow. - QueryInterface(Ci.nsIInterfaceRequestor). - getInterface(Ci.nsIDOMWindowUtils); - windowUtils.setScrollPositionClampingScrollPortSize(compositedRect.width, - compositedRect.height); - Browser.selectedBrowser.messageManager.sendAsyncMessage("Content:SetCacheViewport", { - scrollX: scrollTo.x, - scrollY: scrollTo.y, - x: displayPort.x + scrollTo.x, - y: displayPort.y + scrollTo.y, - w: displayPort.width, - h: displayPort.height, - scale: resolution, - id: scrollId - }); - } + let windowUtils = Browser.selectedBrowser.contentWindow. + QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils); + windowUtils.setScrollPositionClampingScrollPortSize(compositedRect.width, + compositedRect.height); + Browser.selectedBrowser.messageManager.sendAsyncMessage("Content:SetCacheViewport", { + scrollX: scrollTo.x, + scrollY: scrollTo.y, + x: displayPort.x + scrollTo.x, + y: displayPort.y + scrollTo.y, + w: displayPort.width, + h: displayPort.height, + scale: resolution, + id: scrollId + }); Util.dumpLn("APZC scrollId: " + scrollId); Util.dumpLn("APZC scrollTo.x: " + scrollTo.x + ", scrollTo.y: " + scrollTo.y); diff --git a/browser/metro/base/content/bindings/tabs.xml b/browser/metro/base/content/bindings/tabs.xml index f4dcec2c5799..e8a3fcdee863 100644 --- a/browser/metro/base/content/bindings/tabs.xml +++ b/browser/metro/base/content/bindings/tabs.xml @@ -15,16 +15,15 @@ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> - + - - + + - @@ -36,12 +35,19 @@ - document.getAnonymousElementByAttribute(this, "anonid", "thumbnail"); + document.getAnonymousElementByAttribute(this, "anonid", "thumbnail-canvas"); document.getAnonymousElementByAttribute(this, "anonid", "close"); document.getAnonymousElementByAttribute(this, "anonid", "title"); document.getAnonymousElementByAttribute(this, "anonid", "favicon"); this.parentNode; + + + + - - - - - - diff --git a/browser/metro/base/content/bindings/urlbar.xml b/browser/metro/base/content/bindings/urlbar.xml index bc60a14df681..5803c7214650 100644 --- a/browser/metro/base/content/bindings/urlbar.xml +++ b/browser/metro/base/content/bindings/urlbar.xml @@ -272,7 +272,7 @@ @@ -284,7 +284,7 @@ if (this.isEditing) return; - Elements.urlbarState.setAttribute("mode", "edit"); + Elements.urlbarState.setAttribute("editing", true); this._lastKnownGoodURL = this.value; if (!this.focused) @@ -306,7 +306,7 @@ if (!this.isEditing) return; - Elements.urlbarState.setAttribute("mode", "view"); + Elements.urlbarState.removeAttribute("editing"); this.closePopup(); this.formatValue(); diff --git a/browser/metro/base/content/bookmarks.js b/browser/metro/base/content/bookmarks.js index 29cbe5c75782..193b0edb838e 100644 --- a/browser/metro/base/content/bookmarks.js +++ b/browser/metro/base/content/bookmarks.js @@ -65,372 +65,3 @@ var Bookmarks = { }); } }; - -/** - * Wraps a list/grid control implementing nsIDOMXULSelectControlElement and - * fills it with the user's bookmarks. - * - * @param aSet Control implementing nsIDOMXULSelectControlElement. - * @param {Number} aLimit Maximum number of items to show in the view. - * @param aRoot Bookmark root to show in the view. - */ -function BookmarksView(aSet, aLimit, aRoot, aFilterUnpinned) { - this._set = aSet; - this._set.controller = this; - this._inBatch = false; // batch up grid updates to avoid redundant arrangeItems calls - - this._limit = aLimit; - this._filterUnpinned = aFilterUnpinned; - this._bookmarkService = PlacesUtils.bookmarks; - this._navHistoryService = gHistSvc; - - this._changes = new BookmarkChangeListener(this); - this._pinHelper = new ItemPinHelper("metro.bookmarks.unpinned"); - this._bookmarkService.addObserver(this._changes, false); - Services.obs.addObserver(this, "metro_viewstate_changed", false); - window.addEventListener('MozAppbarDismissing', this, false); - window.addEventListener('BookmarksNeedsRefresh', this, false); - - this.root = aRoot; -} - -BookmarksView.prototype = Util.extend(Object.create(View.prototype), { - _limit: null, - _set: null, - _changes: null, - _root: null, - _sort: 0, // Natural bookmark order. - _toRemove: null, - - get sort() { - return this._sort; - }, - - set sort(aSort) { - this._sort = aSort; - this.clearBookmarks(); - this.getBookmarks(); - }, - - get root() { - return this._root; - }, - - set root(aRoot) { - this._root = aRoot; - }, - - handleItemClick: function bv_handleItemClick(aItem) { - let url = aItem.getAttribute("value"); - BrowserUI.goToURI(url); - }, - - _getItemForBookmarkId: function bv__getItemForBookmark(aBookmarkId) { - return this._set.querySelector("richgriditem[bookmarkId='" + aBookmarkId + "']"); - }, - - _getBookmarkIdForItem: function bv__getBookmarkForItem(aItem) { - return +aItem.getAttribute("bookmarkId"); - }, - - _updateItemWithAttrs: function dv__updateItemWithAttrs(anItem, aAttrs) { - for (let name in aAttrs) - anItem.setAttribute(name, aAttrs[name]); - }, - - getBookmarks: function bv_getBookmarks(aRefresh) { - let options = this._navHistoryService.getNewQueryOptions(); - options.queryType = options.QUERY_TYPE_BOOKMARKS; - options.excludeQueries = true; // Don't include "smart folders" - options.sortingMode = this._sort; - - let limit = this._limit || Infinity; - - let query = this._navHistoryService.getNewQuery(); - query.setFolders([Bookmarks.metroRoot], 1); - - let result = this._navHistoryService.executeQuery(query, options); - let rootNode = result.root; - rootNode.containerOpen = true; - let childCount = rootNode.childCount; - - this._inBatch = true; // batch up grid updates to avoid redundant arrangeItems calls - - for (let i = 0, addedCount = 0; i < childCount && addedCount < limit; i++) { - let node = rootNode.getChild(i); - - // Ignore folders, separators, undefined item types, etc. - if (node.type != node.RESULT_TYPE_URI) - continue; - - // If item is marked for deletion, skip it. - if (this._toRemove && this._toRemove.indexOf(node.itemId) !== -1) - continue; - - let item = this._getItemForBookmarkId(node.itemId); - - // Item has been unpinned. - if (this._filterUnpinned && !this._pinHelper.isPinned(node.itemId)) { - if (item) - this.removeBookmark(node.itemId); - - continue; - } - - if (!aRefresh || !item) { - // If we're not refreshing or the item is not in the grid, add it. - this.addBookmark(node.itemId, addedCount); - } else if (aRefresh && item) { - // Update context action in case it changed in another view. - this._setContextActions(item); - } - - addedCount++; - } - - // Remove extra items in case a refresh added more than the limit. - // This can happen when undoing a delete. - if (aRefresh) { - while (this._set.itemCount > limit) - this._set.removeItemAt(this._set.itemCount - 1, true); - } - this._set.arrangeItems(); - this._inBatch = false; - rootNode.containerOpen = false; - }, - - inCurrentView: function bv_inCurrentView(aParentId, aItemId) { - if (this._root && aParentId != this._root) - return false; - - return !!this._getItemForBookmarkId(aItemId); - }, - - clearBookmarks: function bv_clearBookmarks() { - this._set.clearAll(); - }, - - addBookmark: function bv_addBookmark(aBookmarkId, aPos) { - let index = this._bookmarkService.getItemIndex(aBookmarkId); - let uri = this._bookmarkService.getBookmarkURI(aBookmarkId); - let title = this._bookmarkService.getItemTitle(aBookmarkId) || uri.spec; - let item = this._set.insertItemAt(aPos || index, title, uri.spec, this._inBatch); - item.setAttribute("bookmarkId", aBookmarkId); - this._setContextActions(item); - this._updateFavicon(item, uri); - }, - - _setContextActions: function bv__setContextActions(aItem) { - let itemId = this._getBookmarkIdForItem(aItem); - aItem.setAttribute("data-contextactions", "delete," + (this._pinHelper.isPinned(itemId) ? "unpin" : "pin")); - if (aItem.refresh) aItem.refresh(); - }, - - _sendNeedsRefresh: function bv__sendNeedsRefresh(){ - // Event sent when all view instances need to refresh. - let event = document.createEvent("Events"); - event.initEvent("BookmarksNeedsRefresh", true, false); - window.dispatchEvent(event); - }, - - updateBookmark: function bv_updateBookmark(aBookmarkId) { - let item = this._getItemForBookmarkId(aBookmarkId); - - if (!item) - return; - - let oldIndex = this._set.getIndexOfItem(item); - let index = this._bookmarkService.getItemIndex(aBookmarkId); - - if (oldIndex != index) { - this.removeBookmark(aBookmarkId); - this.addBookmark(aBookmarkId); - return; - } - - let uri = this._bookmarkService.getBookmarkURI(aBookmarkId); - let title = this._bookmarkService.getItemTitle(aBookmarkId) || uri.spec; - - item.setAttribute("value", uri.spec); - item.setAttribute("label", title); - - this._updateFavicon(item, uri); - }, - - removeBookmark: function bv_removeBookmark(aBookmarkId) { - let item = this._getItemForBookmarkId(aBookmarkId); - let index = this._set.getIndexOfItem(item); - this._set.removeItemAt(index, this._inBatch); - }, - - destruct: function bv_destruct() { - this._bookmarkService.removeObserver(this._changes); - Services.obs.removeObserver(this, "metro_viewstate_changed"); - window.removeEventListener('MozAppbarDismissing', this, false); - window.removeEventListener('BookmarksNeedsRefresh', this, false); - }, - - doActionOnSelectedTiles: function bv_doActionOnSelectedTiles(aActionName, aEvent) { - let tileGroup = this._set; - let selectedTiles = tileGroup.selectedItems; - - switch (aActionName){ - case "delete": - Array.forEach(selectedTiles, function(aNode) { - if (!this._toRemove) { - this._toRemove = []; - } - - let itemId = this._getBookmarkIdForItem(aNode); - - this._toRemove.push(itemId); - this.removeBookmark(itemId); - }, this); - - // stop the appbar from dismissing - aEvent.preventDefault(); - - // at next tick, re-populate the context appbar. - setTimeout(function(){ - // fire a MozContextActionsChange event to update the context appbar - let event = document.createEvent("Events"); - // we need the restore button to show (the tile node will go away though) - event.actions = ["restore"]; - event.initEvent("MozContextActionsChange", true, false); - tileGroup.dispatchEvent(event); - }, 0); - break; - - case "restore": - // clear toRemove and let _sendNeedsRefresh update the items. - this._toRemove = null; - break; - - case "unpin": - Array.forEach(selectedTiles, function(aNode) { - let itemId = this._getBookmarkIdForItem(aNode); - - if (this._filterUnpinned) - this.removeBookmark(itemId); - - this._pinHelper.setUnpinned(itemId); - }, this); - break; - - case "pin": - Array.forEach(selectedTiles, function(aNode) { - let itemId = this._getBookmarkIdForItem(aNode); - - this._pinHelper.setPinned(itemId); - }, this); - break; - - default: - return; - } - - // Send refresh event so all view are in sync. - this._sendNeedsRefresh(); - }, - - // nsIObservers - observe: function (aSubject, aTopic, aState) { - switch(aTopic) { - case "metro_viewstate_changed": - this.onViewStateChange(aState); - break; - } - }, - - handleEvent: function bv_handleEvent(aEvent) { - switch (aEvent.type){ - case "MozAppbarDismissing": - // If undo wasn't pressed, time to do definitive actions. - if (this._toRemove) { - for (let bookmarkId of this._toRemove) { - this._bookmarkService.removeItem(bookmarkId); - } - this._toRemove = null; - } - break; - - case "BookmarksNeedsRefresh": - this.getBookmarks(true); - break; - } - } -}); - -var BookmarksStartView = { - _view: null, - get _grid() { return document.getElementById("start-bookmarks-grid"); }, - - init: function init() { - this._view = new BookmarksView(this._grid, StartUI.maxResultsPerSection, Bookmarks.metroRoot, true); - this._view.getBookmarks(); - }, - - uninit: function uninit() { - this._view.destruct(); - }, - - show: function show() { - this._grid.arrangeItems(); - } -}; - -/** - * Observes bookmark changes and keeps a linked BookmarksView updated. - * - * @param aView An instance of BookmarksView. - */ -function BookmarkChangeListener(aView) { - this._view = aView; -} - -BookmarkChangeListener.prototype = { - ////////////////////////////////////////////////////////////////////////////// - //// nsINavBookmarkObserver - onBeginUpdateBatch: function () { }, - onEndUpdateBatch: function () { }, - - onItemAdded: function bCL_onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded, aGUID, aParentGUID) { - this._view.getBookmarks(true); - }, - - onItemChanged: function bCL_onItemChanged(aItemId, aProperty, aIsAnnotationProperty, aNewValue, aLastModified, aItemType, aParentId, aGUID, aParentGUID) { - let itemIndex = PlacesUtils.bookmarks.getItemIndex(aItemId); - if (!this._view.inCurrentView(aParentId, aItemId)) - return; - - this._view.updateBookmark(aItemId); - }, - - onItemMoved: function bCL_onItemMoved(aItemId, aOldParentId, aOldIndex, aNewParentId, aNewIndex, aItemType, aGUID, aOldParentGUID, aNewParentGUID) { - let wasInView = this._view.inCurrentView(aOldParentId, aItemId); - let nowInView = this._view.inCurrentView(aNewParentId, aItemId); - - if (!wasInView && nowInView) - this._view.addBookmark(aItemId); - - if (wasInView && !nowInView) - this._view.removeBookmark(aItemId); - - this._view.getBookmarks(true); - }, - - onBeforeItemRemoved: function (aItemId, aItemType, aParentId, aGUID, aParentGUID) { }, - onItemRemoved: function bCL_onItemRemoved(aItemId, aParentId, aIndex, aItemType, aURI, aGUID, aParentGUID) { - if (!this._view.inCurrentView(aParentId, aItemId)) - return; - - this._view.removeBookmark(aItemId); - this._view.getBookmarks(true); - }, - - onItemVisited: function(aItemId, aVisitId, aTime, aTransitionType, aURI, aParentId, aGUID, aParentGUID) { }, - - ////////////////////////////////////////////////////////////////////////////// - //// nsISupports - QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]) -}; diff --git a/browser/metro/base/content/browser-scripts.js b/browser/metro/base/content/browser-scripts.js index 5e5537d1b9f2..da46dd48c123 100644 --- a/browser/metro/base/content/browser-scripts.js +++ b/browser/metro/base/content/browser-scripts.js @@ -34,13 +34,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "Promise", XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "CrossSlide", - "resource:///modules/CrossSlide.jsm"); - XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "View", - "resource:///modules/View.jsm"); /* * Services @@ -123,13 +118,8 @@ let ScriptContexts = {}; ["Bookmarks", "chrome://browser/content/bookmarks.js"], ["Downloads", "chrome://browser/content/downloads.js"], ["ConsolePanelView", "chrome://browser/content/console.js"], - ["BookmarksStartView", "chrome://browser/content/bookmarks.js"], - ["HistoryView", "chrome://browser/content/history.js"], - ["HistoryStartView", "chrome://browser/content/history.js"], ["Site", "chrome://browser/content/Site.js"], ["TopSites", "chrome://browser/content/TopSites.js"], - ["TopSitesView", "chrome://browser/content/TopSites.js"], - ["TopSitesStartView", "chrome://browser/content/TopSites.js"], ["Sanitizer", "chrome://browser/content/sanitize.js"], ["SanitizeUI", "chrome://browser/content/sanitizeUI.js"], ["SSLExceptions", "chrome://browser/content/exceptions.js"], @@ -137,10 +127,6 @@ let ScriptContexts = {}; ["NavButtonSlider", "chrome://browser/content/NavButtonSlider.js"], ["ContextUI", "chrome://browser/content/ContextUI.js"], ["FlyoutPanelsUI", "chrome://browser/content/flyoutpanels/FlyoutPanelsUI.js"], -#ifdef MOZ_SERVICES_SYNC - ["RemoteTabsView", "chrome://browser/content/RemoteTabs.js"], - ["RemoteTabsStartView", "chrome://browser/content/RemoteTabs.js"], -#endif ].forEach(function (aScript) { let [name, script] = aScript; XPCOMUtils.defineLazyGetter(window, name, function() { diff --git a/browser/metro/base/content/browser-ui.js b/browser/metro/base/content/browser-ui.js index f3b4ee49e6cc..ccafe6d67ea7 100644 --- a/browser/metro/base/content/browser-ui.js +++ b/browser/metro/base/content/browser-ui.js @@ -11,9 +11,6 @@ Cu.import("resource://gre/modules/devtools/dbg-server.jsm") * Constants */ -// Page for which the start UI is shown -const kStartOverlayURI = "about:start"; - // Devtools Messages const debugServerStateChanged = "devtools.debugger.remote-enabled"; const debugServerPortChanged = "devtools.debugger.remote-port"; @@ -33,6 +30,7 @@ let Elements = {}; [ ["contentShowing", "bcast_contentShowing"], ["urlbarState", "bcast_urlbarState"], + ["loadingState", "bcast_loadingState"], ["windowState", "bcast_windowState"], ["mainKeyset", "mainKeyset"], ["stack", "stack"], @@ -40,7 +38,6 @@ let Elements = {}; ["tabs", "tabs-container"], ["controls", "browser-controls"], ["panelUI", "panel-container"], - ["startUI", "start-container"], ["tray", "tray"], ["toolbar", "toolbar"], ["browsers", "browsers"], @@ -109,7 +106,6 @@ var BrowserUI = { // Init core UI modules ContextUI.init(); - StartUI.init(); PanelUI.init(); FlyoutPanelsUI.init(); PageThumbs.init(); @@ -184,7 +180,6 @@ var BrowserUI = { messageManager.removeMessageListener("Browser:MozApplicationManifest", OfflineApps); PanelUI.uninit(); - StartUI.uninit(); Downloads.uninit(); SettingsCharm.uninit(); messageManager.removeMessageListener("Content:StateChange", this); @@ -231,7 +226,7 @@ var BrowserUI = { }, showContent: function showContent(aURI) { - StartUI.update(aURI); + this.updateStartURIAttributes(aURI); ContextUI.dismissTabs(); ContextUI.dismissContextAppbar(); FlyoutPanelsUI.hide(); @@ -329,7 +324,7 @@ var BrowserUI = { let flags = aFlags || 0; if (!(flags & this.NO_STARTUI_VISIBILITY)) { let uri = this.getDisplayURI(Browser.selectedBrowser); - StartUI.update(uri); + this.updateStartURIAttributes(uri); } this._updateButtons(); this._updateToolbar(); @@ -342,6 +337,25 @@ var BrowserUI = { this._edit.value = cleanURI; }, + get isStartTabVisible() { + return this.isStartURI(); + }, + + isStartURI: function isStartURI(aURI) { + aURI = aURI || Browser.selectedBrowser.currentURI.spec; + return aURI == kStartURI; + }, + + updateStartURIAttributes: function (aURI) { + aURI = aURI || Browser.selectedBrowser.currentURI.spec; + if (this.isStartURI(aURI)) { + ContextUI.displayNavbar(); + Elements.windowState.setAttribute("startpage", "true"); + } else if (aURI != "about:blank") { // about:blank is loaded briefly for new tabs; ignore it + Elements.windowState.removeAttribute("startpage"); + } + }, + getDisplayURI: function(browser) { let uri = browser.currentURI; let spec = uri.spec; @@ -422,7 +436,7 @@ var BrowserUI = { */ newTab: function newTab(aURI, aOwner, aPeekTabs) { - aURI = aURI || kStartOverlayURI; + aURI = aURI || kStartURI; if (aPeekTabs) { ContextUI.peekTabs(kNewTabAnimationDelayMsec); } @@ -690,13 +704,11 @@ var BrowserUI = { }, _updateToolbar: function _updateToolbar() { - let mode = Elements.urlbarState.getAttribute("mode"); - let isLoading = Browser.selectedTab.isLoading(); - - if (isLoading && mode != "loading") - Elements.urlbarState.setAttribute("mode", "loading"); - else if (!isLoading && mode != "edit") - Elements.urlbarState.setAttribute("mode", "view"); + if (Browser.selectedTab.isLoading()) { + Elements.loadingState.setAttribute("loading", true); + } else { + Elements.loadingState.removeAttribute("loading"); + } }, _closeOrQuit: function _closeOrQuit() { @@ -781,12 +793,6 @@ var BrowserUI = { return; } - if (StartUI.hide()) { - // When escaping from the start screen, hide the toolbar too. - ContextUI.dismiss(); - return; - } - if (Browser.selectedTab.isLoading()) { Browser.selectedBrowser.stop(); return; @@ -863,7 +869,6 @@ var BrowserUI = { let referrerURI = null; if (json.referrer) referrerURI = Services.io.newURI(json.referrer, null, null); - //Browser.addTab(json.uri, json.bringFront, Browser.selectedTab, { referrerURI: referrerURI }); this.goToURI(json.uri); break; case "Content:StateChange": @@ -1113,139 +1118,6 @@ var BrowserUI = { } }; -var StartUI = { - get isVisible() { return this.isStartPageVisible; }, - get isStartPageVisible() { return Elements.windowState.hasAttribute("startpage"); }, - - get maxResultsPerSection() { - return Services.prefs.getIntPref("browser.display.startUI.maxresults"); - }, - - sections: [ - "TopSitesStartView", - "BookmarksStartView", - "HistoryStartView", - "RemoteTabsStartView" - ], - - init: function init() { - Elements.startUI.addEventListener("contextmenu", this, false); - Elements.startUI.addEventListener("click", this, false); - Elements.startUI.addEventListener("MozMousePixelScroll", this, false); - - this.sections.forEach(function (sectionName) { - let section = window[sectionName]; - if (section.init) - section.init(); - }); - }, - - uninit: function() { - this.sections.forEach(function (sectionName) { - let section = window[sectionName]; - if (section.uninit) - section.uninit(); - }); - }, - - /** Show the Firefox start page / "new tab" page */ - show: function show() { - if (this.isStartPageVisible) - return false; - - ContextUI.displayNavbar(); - - Elements.contentShowing.setAttribute("disabled", "true"); - Elements.windowState.setAttribute("startpage", "true"); - - this.sections.forEach(function (sectionName) { - let section = window[sectionName]; - if (section.show) - section.show(); - }); - return true; - }, - - /** Hide the Firefox start page */ - hide: function hide(aURI) { - aURI = aURI || Browser.selectedBrowser.currentURI.spec; - if (!this.isStartPageVisible || this.isStartURI(aURI)) - return false; - - Elements.contentShowing.removeAttribute("disabled"); - Elements.windowState.removeAttribute("startpage"); - return true; - }, - - /** Is the current tab supposed to show the Firefox start page? */ - isStartURI: function isStartURI(aURI) { - aURI = aURI || Browser.selectedBrowser.currentURI.spec; - return aURI == kStartOverlayURI || aURI == "about:home"; - }, - - /** Call this to show or hide the start page when switching tabs or pages */ - update: function update(aURI) { - aURI = aURI || Browser.selectedBrowser.currentURI.spec; - if (this.isStartURI(aURI)) { - this.show(); - } else if (aURI != "about:blank") { // about:blank is loaded briefly for new tabs; ignore it - this.hide(aURI); - } - }, - - onClick: function onClick(aEvent) { - // If someone clicks / taps in empty grid space, take away - // focus from the nav bar edit so the soft keyboard will hide. - if (BrowserUI.blurNavBar()) { - // Advanced notice to CAO, so we can shuffle the nav bar in advance - // of the keyboard transition. - ContentAreaObserver.navBarWillBlur(); - } - - if (aEvent.button == 0) - ContextUI.dismissTabs(); - }, - - onNarrowTitleClick: function onNarrowTitleClick(sectionId) { - let section = document.getElementById(sectionId); - - if (section.hasAttribute("expanded")) - return; - - for (let expandedSection of Elements.startUI.querySelectorAll(".meta-section[expanded]")) - expandedSection.removeAttribute("expanded") - - section.setAttribute("expanded", "true"); - }, - - handleEvent: function handleEvent(aEvent) { - switch (aEvent.type) { - case "contextmenu": - let event = document.createEvent("Events"); - event.initEvent("MozEdgeUICompleted", true, false); - window.dispatchEvent(event); - break; - case "click": - this.onClick(aEvent); - break; - - case "MozMousePixelScroll": - let startBox = document.getElementById("start-scrollbox"); - let [, scrollInterface] = ScrollUtils.getScrollboxFromElement(startBox); - - if (Elements.windowState.getAttribute("viewstate") == "snapped") { - scrollInterface.scrollBy(0, aEvent.detail); - } else { - scrollInterface.scrollBy(aEvent.detail, 0); - } - - aEvent.preventDefault(); - aEvent.stopPropagation(); - break; - } - } -}; - var PanelUI = { get _panels() { return document.getElementById("panel-items"); }, diff --git a/browser/metro/base/content/browser.js b/browser/metro/base/content/browser.js index e9eba9477b27..a96d2420ff08 100644 --- a/browser/metro/base/content/browser.js +++ b/browser/metro/base/content/browser.js @@ -8,6 +8,11 @@ let Ci = Components.interfaces; let Cu = Components.utils; let Cr = Components.results; +Cu.import("resource://gre/modules/PageThumbs.jsm"); + +// Page for which the start UI is shown +const kStartURI = "about:start"; + const kBrowserViewZoomLevelPrecision = 10000; // allow panning after this timeout on pages with registered touch listeners @@ -16,8 +21,13 @@ const kSetInactiveStateTimeout = 100; const kDefaultMetadata = { autoSize: false, allowZoom: true, autoScale: true }; +const kTabThumbnailDelayCapture = 500; + const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +// See grid.xml, we use this to cache style info across loads of the startui. +var _richgridTileSizes = {}; + // Override sizeToContent in the main window. It breaks things (bug 565887) window.sizeToContent = function() { Cu.reportError("window.sizeToContent is not allowed in this window"); @@ -171,13 +181,10 @@ var Browser = { let self = this; function loadStartupURI() { - let uri = activationURI || commandURL || Browser.getHomePage(); - if (StartUI.isStartURI(uri)) { - self.addTab(uri, true); - StartUI.show(); // This makes about:start load a lot faster - } else if (activationURI) { - self.addTab(uri, true, null, { flags: Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP }); + if (activationURI) { + self.addTab(activationURI, true, null, { flags: Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP }); } else { + let uri = commandURL || Browser.getHomePage(); self.addTab(uri, true); } } @@ -187,9 +194,9 @@ var Browser = { if (ss.shouldRestore() || Services.prefs.getBoolPref("browser.startup.sessionRestore")) { let bringFront = false; // First open any commandline URLs, except the homepage - if (activationURI && !StartUI.isStartURI(activationURI)) { + if (activationURI && activationURI != kStartURI) { this.addTab(activationURI, true, null, { flags: Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP }); - } else if (commandURL && !StartUI.isStartURI(commandURL)) { + } else if (commandURL && commandURL != kStartURI) { this.addTab(commandURL, true); } else { bringFront = true; @@ -286,7 +293,7 @@ var Browser = { getHomePage: function getHomePage(aOptions) { aOptions = aOptions || { useDefault: false }; - let url = "about:start"; + let url = kStartURI; try { let prefs = aOptions.useDefault ? Services.prefs.getDefaultBranch(null) : Services.prefs; url = prefs.getComplexValue("browser.startup.homepage", Ci.nsIPrefLocalizedString).data; @@ -556,9 +563,16 @@ var Browser = { item.owner = null; }); + // tray tab let event = document.createEvent("Events"); event.initEvent("TabClose", true, false); aTab.chromeTab.dispatchEvent(event); + + // tab window + event = document.createEvent("Events"); + event.initEvent("TabClose", true, false); + aTab.browser.contentWindow.dispatchEvent(event); + aTab.browser.messageManager.sendAsyncMessage("Browser:TabClose"); let container = aTab.chromeTab.parentNode; @@ -1454,6 +1468,7 @@ function Tab(aURI, aParams, aOwner) { this._chromeTab = null; this._metadata = null; this._eventDeferred = null; + this._updateThumbnailTimeout = null; this.owner = aOwner || null; @@ -1603,13 +1618,45 @@ Tab.prototype = { self._eventDeferred = null; } browser.addEventListener("pageshow", onPageShowEvent, true); + browser.messageManager.addMessageListener("Content:StateChange", this); + Services.obs.addObserver(this, "metro_viewstate_changed", false); if (aOwner) this._copyHistoryFrom(aOwner); this._loadUsingParams(browser, aURI, aParams); }, + receiveMessage: function(aMessage) { + switch (aMessage.name) { + case "Content:StateChange": + // update the thumbnail now... + this.updateThumbnail(); + // ...and in a little while to capture page after load. + if (aMessage.json.stateFlags & Ci.nsIWebProgressListener.STATE_STOP) { + clearTimeout(this._updateThumbnailTimeout); + this._updateThumbnailTimeout = setTimeout(() => { + this.updateThumbnail(); + }, kTabThumbnailDelayCapture); + } + break; + } + }, + + observe: function BrowserUI_observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "metro_viewstate_changed": + if (aData !== "snapped") { + this.updateThumbnail(); + } + break; + } + }, + destroy: function destroy() { + this._browser.messageManager.removeMessageListener("Content:StateChange", this); + Services.obs.removeObserver(this, "metro_viewstate_changed", false); + clearTimeout(this._updateThumbnailTimeout); + Elements.tabList.removeTab(this._chromeTab); this._chromeTab = null; this._destroyBrowser(); @@ -1818,8 +1865,8 @@ Tab.prototype = { return this.metadata.allowZoom && !Util.isURLEmpty(this.browser.currentURI.spec); }, - updateThumbnailSource: function updateThumbnailSource() { - this._chromeTab.updateThumbnailSource(this._browser); + updateThumbnail: function updateThumbnail() { + PageThumbs.captureToCanvas(this.browser.contentWindow, this._chromeTab.thumbnailCanvas); }, updateFavicon: function updateFavicon() { diff --git a/browser/metro/base/content/browser.xul b/browser/metro/base/content/browser.xul index f06d5f01427f..594ab6507104 100644 --- a/browser/metro/base/content/browser.xul +++ b/browser/metro/base/content/browser.xul @@ -1,4 +1,4 @@ - + - + @@ -190,49 +191,6 @@ - - - - - - - - - - - - - - - - - - - - - - - @@ -270,7 +228,9 @@ - + + + @@ -294,7 +254,6 @@ - diff --git a/browser/metro/base/content/helperui/FindHelperUI.js b/browser/metro/base/content/helperui/FindHelperUI.js index d7fbe824248d..a52b8eb87218 100644 --- a/browser/metro/base/content/helperui/FindHelperUI.js +++ b/browser/metro/base/content/helperui/FindHelperUI.js @@ -107,7 +107,7 @@ var FindHelperUI = { }, show: function findHelperShow() { - if (StartUI.isVisible || this._open) + if (BrowserUI.isStartTabVisible || this._open) return; // Hide any menus diff --git a/browser/metro/base/content/helperui/MenuUI.js b/browser/metro/base/content/helperui/MenuUI.js index 47b6cd291329..cdf427528bca 100644 --- a/browser/metro/base/content/helperui/MenuUI.js +++ b/browser/metro/base/content/helperui/MenuUI.js @@ -12,7 +12,7 @@ var AutofillMenuUI = { get _panel() { return document.getElementById("autofill-container"); }, get _popup() { return document.getElementById("autofill-popup"); }, - get _commands() { return this._popup.childNodes[0]; }, + get commands() { return this._popup.childNodes[0]; }, get _menuPopup() { if (!this.__menuPopup) { @@ -32,8 +32,8 @@ var AutofillMenuUI = { }, _emptyCommands: function _emptyCommands() { - while (this._commands.firstChild) - this._commands.removeChild(this._commands.firstChild); + while (this.commands.firstChild) + this.commands.removeChild(this.commands.firstChild); }, _positionOptions: function _positionOptions() { @@ -57,7 +57,7 @@ var AutofillMenuUI = { label.setAttribute("value", aSuggestionsList[idx].label); item.setAttribute("data", aSuggestionsList[idx].value); item.appendChild(label); - this._commands.appendChild(item); + this.commands.appendChild(item); } this._menuPopup.show(this._positionOptions()); @@ -65,7 +65,7 @@ var AutofillMenuUI = { selectByIndex: function mn_selectByIndex(aIndex) { this._menuPopup.hide(); - FormHelperUI.doAutoComplete(this._commands.childNodes[aIndex].getAttribute("data")); + FormHelperUI.doAutoComplete(this.commands.childNodes[aIndex].getAttribute("data")); }, hide: function hide () { @@ -85,7 +85,7 @@ var ContextMenuUI = { get _panel() { return document.getElementById("context-container"); }, get _popup() { return document.getElementById("context-popup"); }, - get _commands() { return this._popup.childNodes[0]; }, + get commands() { return this._popup.childNodes[0]; }, get _menuPopup() { if (!this.__menuPopup) { @@ -153,12 +153,12 @@ var ContextMenuUI = { contentTypes.indexOf("selected-text") != -1)) multipleMediaTypes = true; - for (let command of Array.slice(this._commands.childNodes)) { + for (let command of Array.slice(this.commands.childNodes)) { command.hidden = true; } let optionsAvailable = false; - for (let command of Array.slice(this._commands.childNodes)) { + for (let command of Array.slice(this.commands.childNodes)) { let types = command.getAttribute("type").split(","); let lowPriority = (command.hasAttribute("priority") && command.getAttribute("priority") == "low"); @@ -221,7 +221,7 @@ var MenuControlUI = { get _panel() { return document.getElementById("menucontrol-container"); }, get _popup() { return document.getElementById("menucontrol-popup"); }, - get _commands() { return this._popup.childNodes[0]; }, + get commands() { return this._popup.childNodes[0]; }, get _menuPopup() { if (!this.__menuPopup) { @@ -240,8 +240,8 @@ var MenuControlUI = { }, _emptyCommands: function _emptyCommands() { - while (this._commands.firstChild) - this._commands.removeChild(this._commands.firstChild); + while (this.commands.firstChild) + this.commands.removeChild(this.commands.firstChild); }, _positionOptions: function _positionOptions() { @@ -314,7 +314,7 @@ var MenuControlUI = { label.setAttribute("value", child.label); item.appendChild(label); - this._commands.appendChild(item); + this.commands.appendChild(item); } this._menuPopup.show(this._positionOptions()); @@ -338,84 +338,25 @@ function MenuPopup(aPanel, aPopup) { this._panel = aPanel; this._popup = aPopup; this._wantTypeBehind = false; - this._willReshowPopup = false; window.addEventListener('MozAppbarShowing', this, false); } MenuPopup.prototype = { - get _visible() { return !this._panel.hidden; }, - get _commands() { return this._popup.childNodes[0]; }, + get visible() { return !this._panel.hidden; }, + get commands() { return this._popup.childNodes[0]; }, show: function (aPositionOptions) { - if (this._visible) { - this._willReshowPopup = true; - let self = this; - this._panel.addEventListener("transitionend", function () { - self._show(aPositionOptions); - self._panel.removeEventListener("transitionend", arguments.callee); - }); + if (this.visible) { + this._animateHide().then(() => this._animateShow(aPositionOptions)); } else { - this._show(aPositionOptions); + this._animateShow(aPositionOptions); } }, - _show: function (aPositionOptions) { - window.addEventListener("keypress", this, true); - window.addEventListener("mousedown", this, true); - Elements.stack.addEventListener("PopupChanged", this, false); - Elements.browsers.addEventListener("PanBegin", this, false); - - this._panel.hidden = false; - this._position(aPositionOptions || {}); - - let self = this; - this._panel.addEventListener("transitionend", function () { - self._panel.removeEventListener("transitionend", arguments.callee); - self._panel.removeAttribute("showingfrom"); - - let eventName = self._willReshowPopup ? "popupmoved" : "popupshown"; - let event = document.createEvent("Events"); - event.initEvent(eventName, true, false); - self._panel.dispatchEvent(event); - - self._willReshowPopup = false; - }); - - let popupFrom = !aPositionOptions.bottomAligned ? "above" : "below"; - this._panel.setAttribute("showingfrom", popupFrom); - - // Ensure the panel actually gets shifted before getting animated - setTimeout(function () { - self._panel.setAttribute("showing", "true"); - }, 0); - }, - hide: function () { - if (!this._visible) - return; - - window.removeEventListener("keypress", this, true); - window.removeEventListener("mousedown", this, true); - Elements.stack.removeEventListener("PopupChanged", this, false); - Elements.browsers.removeEventListener("PanBegin", this, false); - - let self = this; - this._panel.addEventListener("transitionend", function () { - self._panel.removeEventListener("transitionend", arguments.callee); - self._panel.removeAttribute("hiding"); - self._panel.hidden = true; - self._popup.style.maxWidth = "none"; - self._popup.style.maxHeight = "none"; - - if (!self._willReshowPopup) { - let event = document.createEvent("Events"); - event.initEvent("popuphidden", true, false); - self._panel.dispatchEvent(event); - } - }); - - this._panel.setAttribute("hiding", "true"); - setTimeout(()=>this._panel.removeAttribute("showing"), 0); + if (this.visible) { + this._animateHide(); + } }, _position: function _position(aPositionOptions) { @@ -440,7 +381,7 @@ MenuPopup.prototype = { // Add padding on the side of the menu per the user's hand preference let leftHand = MetroUtils.handPreference == MetroUtils.handPreferenceLeft; if (aSource && aSource == Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH) { - this._commands.setAttribute("left-hand", leftHand); + this.commands.setAttribute("left-hand", leftHand); } if (aPositionOptions.rightAligned) @@ -486,6 +427,66 @@ MenuPopup.prototype = { } }, + _animateShow: function (aPositionOptions) { + let deferred = Promise.defer(); + + window.addEventListener("keypress", this, true); + window.addEventListener("click", this, true); + Elements.stack.addEventListener("PopupChanged", this, false); + Elements.browsers.addEventListener("PanBegin", this, false); + + this._panel.hidden = false; + let popupFrom = !aPositionOptions.bottomAligned ? "above" : "below"; + this._panel.setAttribute("showingfrom", popupFrom); + + // This triggers a reflow, which sets transitionability. + // All animation/transition setup must happen before here. + this._position(aPositionOptions || {}); + + let self = this; + this._panel.addEventListener("transitionend", function popupshown () { + self._panel.removeEventListener("transitionend", popupshown); + self._panel.removeAttribute("showingfrom"); + + self._dispatch("popupshown"); + deferred.resolve(); + }); + + this._panel.setAttribute("showing", "true"); + return deferred.promise; + }, + + _animateHide: function () { + let deferred = Promise.defer(); + + window.removeEventListener("keypress", this, true); + window.removeEventListener("click", this, true); + Elements.stack.removeEventListener("PopupChanged", this, false); + Elements.browsers.removeEventListener("PanBegin", this, false); + + let self = this; + this._panel.addEventListener("transitionend", function popuphidden() { + self._panel.removeEventListener("transitionend", popuphidden); + self._panel.removeAttribute("hiding"); + self._panel.hidden = true; + self._popup.style.maxWidth = "none"; + self._popup.style.maxHeight = "none"; + + self._dispatch("popuphidden"); + deferred.resolve(); + }); + + this._panel.setAttribute("hiding", "true"); + this._panel.removeAttribute("showing"); + return deferred.promise; + }, + + _dispatch: function _dispatch(aName) { + let event = document.createEvent("Events"); + event.initEvent(aName, true, false); + this._panel.dispatchEvent(event); + }, + handleEvent: function handleEvent(aEvent) { switch (aEvent.type) { case "keypress": @@ -497,7 +498,7 @@ MenuPopup.prototype = { this.hide(); } break; - case "mousedown": + case "click": if (!this._popup.contains(aEvent.target)) { aEvent.stopPropagation(); this.hide(); diff --git a/browser/metro/base/content/input.js b/browser/metro/base/content/input.js index 6a60e734883f..a503cc2d50aa 100644 --- a/browser/metro/base/content/input.js +++ b/browser/metro/base/content/input.js @@ -239,7 +239,6 @@ var TouchModule = { // Don't allow kinetic panning if APZC is enabled and the pan element is the deck let deck = document.getElementById("browsers"); if (Services.prefs.getBoolPref(kAsyncPanZoomEnabled) && - !StartUI.isStartPageVisible && this._targetScrollbox == deck) { return; } @@ -364,11 +363,10 @@ var TouchModule = { if (Date.now() - this._dragStartTime > kStopKineticPanOnDragTimeout) this._kinetic._velocity.set(0, 0); - // Start kinetic pan if we i) aren't using async pan zoom or ii) if we - // are on the start page, iii) If the scroll element is not browsers + // Start kinetic pan if we aren't using async pan zoom or the scroll + // element is not browsers. let deck = document.getElementById("browsers"); if (!Services.prefs.getBoolPref(kAsyncPanZoomEnabled) || - StartUI.isStartPageVisible || this._targetScrollbox != deck) { this._kinetic.start(); } @@ -449,8 +447,10 @@ var ScrollUtils = { getScrollboxFromElement: function getScrollboxFromElement(elem) { let scrollbox = null; let qinterface = null; - // if element is content, get the browser scroll interface - if (elem.ownerDocument == Browser.selectedBrowser.contentDocument) { + + // if element is content (but not the startui page), get the browser scroll interface + if (!BrowserUI.isStartTabVisible && + elem.ownerDocument == Browser.selectedBrowser.contentDocument) { elem = Browser.selectedBrowser; } for (; elem; elem = elem.parentNode) { diff --git a/browser/metro/base/content/startui/BookmarksView.js b/browser/metro/base/content/startui/BookmarksView.js new file mode 100644 index 000000000000..74164c0a3df5 --- /dev/null +++ b/browser/metro/base/content/startui/BookmarksView.js @@ -0,0 +1,385 @@ +/* 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"; + +/** + * Wraps a list/grid control implementing nsIDOMXULSelectControlElement and + * fills it with the user's bookmarks. + * + * @param aSet Control implementing nsIDOMXULSelectControlElement. + * @param {Number} aLimit Maximum number of items to show in the view. + * @param aRoot Bookmark root to show in the view. + */ +function BookmarksView(aSet, aLimit, aRoot, aFilterUnpinned) { + this._set = aSet; + this._set.controller = this; + this._inBatch = false; // batch up grid updates to avoid redundant arrangeItems calls + + this._limit = aLimit; + this._filterUnpinned = aFilterUnpinned; + this._bookmarkService = PlacesUtils.bookmarks; + this._navHistoryService = gHistSvc; + + this._changes = new BookmarkChangeListener(this); + this._pinHelper = new ItemPinHelper("metro.bookmarks.unpinned"); + this._bookmarkService.addObserver(this._changes, false); + Services.obs.addObserver(this, "metro_viewstate_changed", false); + StartUI.chromeWin.addEventListener('MozAppbarDismissing', this, false); + StartUI.chromeWin.addEventListener('BookmarksNeedsRefresh', this, false); + window.addEventListener("TabClose", this, true); + + this.root = aRoot; +} + +BookmarksView.prototype = Util.extend(Object.create(View.prototype), { + _limit: null, + _set: null, + _changes: null, + _root: null, + _sort: 0, // Natural bookmark order. + _toRemove: null, + + get sort() { + return this._sort; + }, + + set sort(aSort) { + this._sort = aSort; + this.clearBookmarks(); + this.getBookmarks(); + }, + + get root() { + return this._root; + }, + + set root(aRoot) { + this._root = aRoot; + }, + + destruct: function bv_destruct() { + this._bookmarkService.removeObserver(this._changes); + Services.obs.removeObserver(this, "metro_viewstate_changed"); + if (StartUI.chromeWin) { + StartUI.chromeWin.removeEventListener('MozAppbarDismissing', this, false); + StartUI.chromeWin.removeEventListener('BookmarksNeedsRefresh', this, false); + } + }, + + handleItemClick: function bv_handleItemClick(aItem) { + let url = aItem.getAttribute("value"); + StartUI.goToURI(url); + }, + + _getItemForBookmarkId: function bv__getItemForBookmark(aBookmarkId) { + return this._set.querySelector("richgriditem[bookmarkId='" + aBookmarkId + "']"); + }, + + _getBookmarkIdForItem: function bv__getBookmarkForItem(aItem) { + return +aItem.getAttribute("bookmarkId"); + }, + + _updateItemWithAttrs: function dv__updateItemWithAttrs(anItem, aAttrs) { + for (let name in aAttrs) + anItem.setAttribute(name, aAttrs[name]); + }, + + getBookmarks: function bv_getBookmarks(aRefresh) { + let options = this._navHistoryService.getNewQueryOptions(); + options.queryType = options.QUERY_TYPE_BOOKMARKS; + options.excludeQueries = true; // Don't include "smart folders" + options.sortingMode = this._sort; + + let limit = this._limit || Infinity; + + let query = this._navHistoryService.getNewQuery(); + query.setFolders([Bookmarks.metroRoot], 1); + + let result = this._navHistoryService.executeQuery(query, options); + let rootNode = result.root; + rootNode.containerOpen = true; + let childCount = rootNode.childCount; + + this._inBatch = true; // batch up grid updates to avoid redundant arrangeItems calls + + for (let i = 0, addedCount = 0; i < childCount && addedCount < limit; i++) { + let node = rootNode.getChild(i); + + // Ignore folders, separators, undefined item types, etc. + if (node.type != node.RESULT_TYPE_URI) + continue; + + // If item is marked for deletion, skip it. + if (this._toRemove && this._toRemove.indexOf(node.itemId) !== -1) + continue; + + let item = this._getItemForBookmarkId(node.itemId); + + // Item has been unpinned. + if (this._filterUnpinned && !this._pinHelper.isPinned(node.itemId)) { + if (item) + this.removeBookmark(node.itemId); + + continue; + } + + if (!aRefresh || !item) { + // If we're not refreshing or the item is not in the grid, add it. + this.addBookmark(node.itemId, addedCount); + } else if (aRefresh && item) { + // Update context action in case it changed in another view. + this._setContextActions(item); + } + + addedCount++; + } + + // Remove extra items in case a refresh added more than the limit. + // This can happen when undoing a delete. + if (aRefresh) { + while (this._set.itemCount > limit) + this._set.removeItemAt(this._set.itemCount - 1, true); + } + this._set.arrangeItems(); + this._inBatch = false; + rootNode.containerOpen = false; + }, + + inCurrentView: function bv_inCurrentView(aParentId, aItemId) { + if (this._root && aParentId != this._root) + return false; + + return !!this._getItemForBookmarkId(aItemId); + }, + + clearBookmarks: function bv_clearBookmarks() { + this._set.clearAll(); + }, + + addBookmark: function bv_addBookmark(aBookmarkId, aPos) { + let index = this._bookmarkService.getItemIndex(aBookmarkId); + let uri = this._bookmarkService.getBookmarkURI(aBookmarkId); + let title = this._bookmarkService.getItemTitle(aBookmarkId) || uri.spec; + let item = this._set.insertItemAt(aPos || index, title, uri.spec, this._inBatch); + item.setAttribute("bookmarkId", aBookmarkId); + this._setContextActions(item); + this._updateFavicon(item, uri); + }, + + _setContextActions: function bv__setContextActions(aItem) { + let itemId = this._getBookmarkIdForItem(aItem); + aItem.setAttribute("data-contextactions", "delete," + (this._pinHelper.isPinned(itemId) ? "unpin" : "pin")); + if (aItem.refresh) aItem.refresh(); + }, + + _sendNeedsRefresh: function bv__sendNeedsRefresh(){ + // Event sent when all view instances need to refresh. + let event = document.createEvent("Events"); + event.initEvent("BookmarksNeedsRefresh", true, false); + window.dispatchEvent(event); + }, + + updateBookmark: function bv_updateBookmark(aBookmarkId) { + let item = this._getItemForBookmarkId(aBookmarkId); + + if (!item) + return; + + let oldIndex = this._set.getIndexOfItem(item); + let index = this._bookmarkService.getItemIndex(aBookmarkId); + + if (oldIndex != index) { + this.removeBookmark(aBookmarkId); + this.addBookmark(aBookmarkId); + return; + } + + let uri = this._bookmarkService.getBookmarkURI(aBookmarkId); + let title = this._bookmarkService.getItemTitle(aBookmarkId) || uri.spec; + + item.setAttribute("value", uri.spec); + item.setAttribute("label", title); + + this._updateFavicon(item, uri); + }, + + removeBookmark: function bv_removeBookmark(aBookmarkId) { + let item = this._getItemForBookmarkId(aBookmarkId); + let index = this._set.getIndexOfItem(item); + this._set.removeItemAt(index, this._inBatch); + }, + + doActionOnSelectedTiles: function bv_doActionOnSelectedTiles(aActionName, aEvent) { + let tileGroup = this._set; + let selectedTiles = tileGroup.selectedItems; + + switch (aActionName){ + case "delete": + Array.forEach(selectedTiles, function(aNode) { + if (!this._toRemove) { + this._toRemove = []; + } + + let itemId = this._getBookmarkIdForItem(aNode); + + this._toRemove.push(itemId); + this.removeBookmark(itemId); + }, this); + + // stop the appbar from dismissing + aEvent.preventDefault(); + + // at next tick, re-populate the context appbar. + setTimeout(function(){ + // fire a MozContextActionsChange event to update the context appbar + let event = document.createEvent("Events"); + // we need the restore button to show (the tile node will go away though) + event.actions = ["restore"]; + event.initEvent("MozContextActionsChange", true, false); + tileGroup.dispatchEvent(event); + }, 0); + break; + + case "restore": + // clear toRemove and let _sendNeedsRefresh update the items. + this._toRemove = null; + break; + + case "unpin": + Array.forEach(selectedTiles, function(aNode) { + let itemId = this._getBookmarkIdForItem(aNode); + + if (this._filterUnpinned) + this.removeBookmark(itemId); + + this._pinHelper.setUnpinned(itemId); + }, this); + break; + + case "pin": + Array.forEach(selectedTiles, function(aNode) { + let itemId = this._getBookmarkIdForItem(aNode); + + this._pinHelper.setPinned(itemId); + }, this); + break; + + default: + return; + } + + // Send refresh event so all view are in sync. + this._sendNeedsRefresh(); + }, + + // nsIObservers + observe: function (aSubject, aTopic, aState) { + switch(aTopic) { + case "metro_viewstate_changed": + this.onViewStateChange(aState); + break; + } + }, + + handleEvent: function bv_handleEvent(aEvent) { + switch (aEvent.type){ + case "MozAppbarDismissing": + // If undo wasn't pressed, time to do definitive actions. + if (this._toRemove) { + for (let bookmarkId of this._toRemove) { + this._bookmarkService.removeItem(bookmarkId); + } + this._toRemove = null; + } + break; + + case "BookmarksNeedsRefresh": + this.getBookmarks(true); + break; + + case "TabClose": + // Flush any pending actions - appbar will call us back + // before this returns with 'MozAppbarDismissing' above. + StartUI.chromeWin.ContextUI.dismiss(); + break; + } + } +}); + +let BookmarksStartView = { + _view: null, + get _grid() { return document.getElementById("start-bookmarks-grid"); }, + + init: function init() { + this._view = new BookmarksView(this._grid, StartUI.maxResultsPerSection, Bookmarks.metroRoot, true); + this._view.getBookmarks(); + }, + + uninit: function uninit() { + if (this._view) { + this._view.destruct(); + } + }, + + show: function show() { + this._grid.arrangeItems(); + } +}; + +/** + * Observes bookmark changes and keeps a linked BookmarksView updated. + * + * @param aView An instance of BookmarksView. + */ +function BookmarkChangeListener(aView) { + this._view = aView; +} + +BookmarkChangeListener.prototype = { + ////////////////////////////////////////////////////////////////////////////// + //// nsINavBookmarkObserver + onBeginUpdateBatch: function () { }, + onEndUpdateBatch: function () { }, + + onItemAdded: function bCL_onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded, aGUID, aParentGUID) { + this._view.getBookmarks(true); + }, + + onItemChanged: function bCL_onItemChanged(aItemId, aProperty, aIsAnnotationProperty, aNewValue, aLastModified, aItemType, aParentId, aGUID, aParentGUID) { + let itemIndex = PlacesUtils.bookmarks.getItemIndex(aItemId); + if (!this._view.inCurrentView(aParentId, aItemId)) + return; + + this._view.updateBookmark(aItemId); + }, + + onItemMoved: function bCL_onItemMoved(aItemId, aOldParentId, aOldIndex, aNewParentId, aNewIndex, aItemType, aGUID, aOldParentGUID, aNewParentGUID) { + let wasInView = this._view.inCurrentView(aOldParentId, aItemId); + let nowInView = this._view.inCurrentView(aNewParentId, aItemId); + + if (!wasInView && nowInView) + this._view.addBookmark(aItemId); + + if (wasInView && !nowInView) + this._view.removeBookmark(aItemId); + + this._view.getBookmarks(true); + }, + + onBeforeItemRemoved: function (aItemId, aItemType, aParentId, aGUID, aParentGUID) { }, + onItemRemoved: function bCL_onItemRemoved(aItemId, aParentId, aIndex, aItemType, aURI, aGUID, aParentGUID) { + if (!this._view.inCurrentView(aParentId, aItemId)) + return; + + this._view.removeBookmark(aItemId); + this._view.getBookmarks(true); + }, + + onItemVisited: function(aItemId, aVisitId, aTime, aTransitionType, aURI, aParentId, aGUID, aParentGUID) { }, + + ////////////////////////////////////////////////////////////////////////////// + //// nsISupports + QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]) +}; diff --git a/browser/metro/base/content/history.js b/browser/metro/base/content/startui/HistoryView.js similarity index 92% rename from browser/metro/base/content/history.js rename to browser/metro/base/content/startui/HistoryView.js index aeb71b4f844c..c26c3e0f1e76 100644 --- a/browser/metro/base/content/history.js +++ b/browser/metro/base/content/startui/HistoryView.js @@ -17,17 +17,27 @@ function HistoryView(aSet, aLimit, aFilterUnpinned) { this._pinHelper = new ItemPinHelper("metro.history.unpinned"); this._historyService.addObserver(this, false); Services.obs.addObserver(this, "metro_viewstate_changed", false); - window.addEventListener('MozAppbarDismissing', this, false); - window.addEventListener('HistoryNeedsRefresh', this, false); + StartUI.chromeWin.addEventListener('MozAppbarDismissing', this, false); + StartUI.chromeWin.addEventListener('HistoryNeedsRefresh', this, false); + window.addEventListener("TabClose", this, true); } HistoryView.prototype = Util.extend(Object.create(View.prototype), { _set: null, _toRemove: null, + destruct: function destruct() { + this._historyService.removeObserver(this); + Services.obs.removeObserver(this, "metro_viewstate_changed"); + if (StartUI.chromeWin) { + StartUI.chromeWin.removeEventListener('MozAppbarDismissing', this, false); + StartUI.chromeWin.removeEventListener('HistoryNeedsRefresh', this, false); + } + }, + handleItemClick: function tabview_handleItemClick(aItem) { let url = aItem.getAttribute("value"); - BrowserUI.goToURI(url); + StartUI.goToURI(url); }, populateGrid: function populateGrid(aRefresh) { @@ -90,13 +100,6 @@ HistoryView.prototype = Util.extend(Object.create(View.prototype), { this._inBatch--; }, - destruct: function destruct() { - this._historyService.removeObserver(this); - Services.obs.removeObserver(this, "metro_viewstate_changed"); - window.removeEventListener('MozAppbarDismissing', this, false); - window.removeEventListener('HistoryNeedsRefresh', this, false); - }, - addItemToSet: function addItemToSet(aURI, aTitle, aIcon, aPos) { let item = this._set.insertItemAt(aPos || 0, aTitle, aURI, this._inBatch); this._setContextActions(item); @@ -208,6 +211,12 @@ HistoryView.prototype = Util.extend(Object.create(View.prototype), { case "HistoryNeedsRefresh": this.populateGrid(true); break; + + case "TabClose": + // Flush any pending actions - appbar will call us back + // before this returns with 'MozAppbarDismissing' above. + StartUI.chromeWin.ContextUI.dismiss(); + break; } }, @@ -298,6 +307,8 @@ let HistoryStartView = { }, uninit: function uninit() { - this._view.destruct(); + if (this._view) { + this._view.destruct(); + } } }; diff --git a/browser/metro/base/content/RemoteTabs.js b/browser/metro/base/content/startui/RemoteTabsView.js similarity index 98% rename from browser/metro/base/content/RemoteTabs.js rename to browser/metro/base/content/startui/RemoteTabsView.js index fee9276d64a7..6e49d54925e2 100644 --- a/browser/metro/base/content/RemoteTabs.js +++ b/browser/metro/base/content/startui/RemoteTabsView.js @@ -45,7 +45,7 @@ RemoteTabsView.prototype = Util.extend(Object.create(View.prototype), { handleItemClick: function tabview_handleItemClick(aItem) { let url = aItem.getAttribute("value"); - BrowserUI.goToURI(url); + StartUI.goToURI(url); }, observe: function(subject, topic, data) { @@ -122,7 +122,9 @@ let RemoteTabsStartView = { }, uninit: function uninit() { - this._view.destruct(); + if (this._view) { + this._view.destruct(); + } }, show: function show() { diff --git a/browser/metro/base/content/startui/Start.xul b/browser/metro/base/content/startui/Start.xul new file mode 100644 index 000000000000..27ef7b078a6f --- /dev/null +++ b/browser/metro/base/content/startui/Start.xul @@ -0,0 +1,78 @@ + + + + + + + + + + +%globalDTD; + +%browserDTD; + +%brandDTD; +#ifdef MOZ_SERVICES_SYNC + +%syncBrandDTD; + +%syncDTD; +#endif +]> + + + + diff --git a/content/base/test/test_ipc_messagemanager_blob.html b/content/base/test/test_ipc_messagemanager_blob.html index c0df9137d213..7c1e2baddd9d 100644 --- a/content/base/test/test_ipc_messagemanager_blob.html +++ b/content/base/test/test_ipc_messagemanager_blob.html @@ -96,7 +96,7 @@ "set": [ ["dom.ipc.browser_frames.oop_by_default", true], ["dom.mozBrowserFramesEnabled", true], - ["browser.pagethumbnails.capturing_disabled", false] + ["browser.pagethumbnails.capturing_disabled", true] ] }, runTests); }); diff --git a/content/media/AbstractMediaDecoder.h b/content/media/AbstractMediaDecoder.h index e7e2d4f781ec..5c65c5d3aa87 100644 --- a/content/media/AbstractMediaDecoder.h +++ b/content/media/AbstractMediaDecoder.h @@ -72,6 +72,11 @@ public: // Set the duration of the media in microseconds. virtual void SetMediaDuration(int64_t aDuration) = 0; + // Sets the duration of the media in microseconds. The MediaDecoder + // fires a durationchange event to its owner (e.g., an HTML audio + // tag). + virtual void UpdateMediaDuration(int64_t aDuration) = 0; + // Set the media as being seekable or not. virtual void SetMediaSeekable(bool aMediaSeekable) = 0; diff --git a/content/media/MediaDecoder.cpp b/content/media/MediaDecoder.cpp index 7e9effd06783..1f85d8d7af17 100644 --- a/content/media/MediaDecoder.cpp +++ b/content/media/MediaDecoder.cpp @@ -632,7 +632,7 @@ nsresult MediaDecoder::Seek(double aTime) if (distanceLeft == distanceRight) { distanceLeft = Abs(leftBound - mCurrentTime); distanceRight = Abs(rightBound - mCurrentTime); - } + } aTime = (distanceLeft < distanceRight) ? leftBound : rightBound; } else { // Seek target is after the end last range in seekable data. @@ -1272,6 +1272,12 @@ void MediaDecoder::SetMediaDuration(int64_t aDuration) GetStateMachine()->SetDuration(aDuration); } +void MediaDecoder::UpdateMediaDuration(int64_t aDuration) +{ + NS_ENSURE_TRUE_VOID(GetStateMachine()); + GetStateMachine()->UpdateDuration(aDuration); +} + void MediaDecoder::SetMediaSeekable(bool aMediaSeekable) { ReentrantMonitorAutoEnter mon(GetReentrantMonitor()); MOZ_ASSERT(NS_IsMainThread() || OnDecodeThread()); diff --git a/content/media/MediaDecoder.h b/content/media/MediaDecoder.h index 2c1af472570a..12494e3b076d 100644 --- a/content/media/MediaDecoder.h +++ b/content/media/MediaDecoder.h @@ -506,6 +506,7 @@ public: virtual void SetDuration(double aDuration); void SetMediaDuration(int64_t aDuration) MOZ_OVERRIDE; + void UpdateMediaDuration(int64_t aDuration) MOZ_OVERRIDE; // Set a flag indicating whether seeking is supported virtual void SetMediaSeekable(bool aMediaSeekable) MOZ_OVERRIDE; diff --git a/content/media/MediaDecoderReader.h b/content/media/MediaDecoderReader.h index 47f128cf9ad9..f6636c9612d8 100644 --- a/content/media/MediaDecoderReader.h +++ b/content/media/MediaDecoderReader.h @@ -456,12 +456,12 @@ public: int64_t aStartTime, int64_t aEndTime, int64_t aCurrentTime) = 0; - + // Called when the decode thread is started, before calling any other // decode, read metadata, or seek functions. Do any thread local setup // in this function. virtual void OnDecodeThreadStart() {} - + // Called when the decode thread is about to finish, after all calls to // any other decode, read metadata, or seek functions. Any backend specific // thread local tear down must be done in this function. Note that another @@ -519,8 +519,8 @@ public: return functor.mResult; } - // Only used by WebMReader for now, so stub here rather than in every - // reader than inherits from MediaDecoderReader. + // Only used by WebMReader and MediaOmxReader for now, so stub here rather + // than in every reader than inherits from MediaDecoderReader. virtual void NotifyDataArrived(const char* aBuffer, uint32_t aLength, int64_t aOffset) {} virtual MediaQueue& AudioQueue() { return mAudioQueue; } diff --git a/content/media/MediaDecoderStateMachine.cpp b/content/media/MediaDecoderStateMachine.cpp index 84e5297a6080..2cb728e89b3a 100644 --- a/content/media/MediaDecoderStateMachine.cpp +++ b/content/media/MediaDecoderStateMachine.cpp @@ -140,8 +140,8 @@ private: { MOZ_COUNT_CTOR(StateMachineTracker); NS_ASSERTION(NS_IsMainThread(), "Should be on main thread."); - } - + } + ~StateMachineTracker() { NS_ASSERTION(NS_IsMainThread(), "Should be on main thread."); @@ -156,7 +156,7 @@ public: // access always occurs after this and uses the monitor to // safely access the decode thread counts. static StateMachineTracker& Instance(); - + // Instantiate the global state machine thread if required. // Call on main thread only. void EnsureGlobalStateMachine(); @@ -244,7 +244,7 @@ StateMachineTracker& StateMachineTracker::Instance() return *sInstance; } -void StateMachineTracker::EnsureGlobalStateMachine() +void StateMachineTracker::EnsureGlobalStateMachine() { NS_ASSERTION(NS_IsMainThread(), "Should be on main thread."); ReentrantMonitorAutoEnter mon(mMonitor); @@ -451,7 +451,7 @@ MediaDecoderStateMachine::~MediaDecoderStateMachine() mTimer->Cancel(); mTimer = nullptr; mReader = nullptr; - + StateMachineTracker::Instance().CleanupGlobalStateMachine(); #ifdef XP_WIN timeEndPeriod(1); @@ -490,7 +490,7 @@ void MediaDecoderStateMachine::DecodeThreadRun() { NS_ASSERTION(OnDecodeThread(), "Should be on decode thread."); mReader->OnDecodeThreadStart(); - + { ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); @@ -531,7 +531,7 @@ void MediaDecoderStateMachine::DecodeThreadRun() mDecodeThreadIdle = true; LOG(PR_LOG_DEBUG, ("%p Decode thread finished", mDecoder.get())); } - + mReader->OnDecodeThreadFinish(); } @@ -1323,7 +1323,7 @@ void MediaDecoderStateMachine::StartPlayback() NS_ASSERTION(IsPlaying(), "Should report playing by end of StartPlayback()"); if (NS_FAILED(StartAudioThread())) { - NS_WARNING("Failed to create audio thread"); + NS_WARNING("Failed to create audio thread"); } mDecoder->GetReentrantMonitor().NotifyAll(); } @@ -1446,6 +1446,16 @@ void MediaDecoderStateMachine::SetDuration(int64_t aDuration) } } +void MediaDecoderStateMachine::UpdateDuration(int64_t aDuration) +{ + if (aDuration != GetDuration()) { + SetDuration(aDuration); + nsCOMPtr event = + NS_NewRunnableMethod(mDecoder, &MediaDecoder::DurationChanged); + NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL); + } +} + void MediaDecoderStateMachine::SetMediaEndTime(int64_t aEndTime) { NS_ASSERTION(OnDecodeThread(), "Should be on decode thread"); @@ -1700,7 +1710,7 @@ MediaDecoderStateMachine::ScheduleDecodeThread() { NS_ASSERTION(OnStateMachineThread(), "Should be on state machine thread."); mDecoder->GetReentrantMonitor().AssertCurrentThreadIn(); - + mStopDecodeThread = false; if (mState >= DECODER_STATE_COMPLETED) { return NS_OK; @@ -1831,7 +1841,7 @@ int64_t MediaDecoderStateMachine::GetUndecodedData() const NS_ASSERTION(mState > DECODER_STATE_DECODING_METADATA, "Must have loaded metadata for GetBuffered() to work"); TimeRanges buffered; - + nsresult res = mDecoder->GetBuffered(&buffered); NS_ENSURE_SUCCESS(res, 0); double currentTime = GetCurrentTime(); @@ -2201,7 +2211,7 @@ nsresult MediaDecoderStateMachine::RunStateMachine() // Ensure we have a decode thread to decode metadata. return ScheduleDecodeThread(); } - + case DECODER_STATE_DECODING: { if (mDecoder->GetState() != MediaDecoder::PLAY_STATE_PLAYING && IsPlaying()) diff --git a/content/media/MediaDecoderStateMachine.h b/content/media/MediaDecoderStateMachine.h index 23179e4c5172..3cc4d8d6755c 100644 --- a/content/media/MediaDecoderStateMachine.h +++ b/content/media/MediaDecoderStateMachine.h @@ -157,6 +157,10 @@ public: // aEndTime is in microseconds. void SetMediaEndTime(int64_t aEndTime); + // Called from decode thread to update the duration. Can result in + // a durationchangeevent. aDuration is in microseconds. + void UpdateDuration(int64_t aDuration); + // Functions used by assertions to ensure we're calling things // on the appropriate threads. bool OnDecodeThread() const { diff --git a/content/media/MediaRecorder.cpp b/content/media/MediaRecorder.cpp index 35cf3acd0986..ac21747f0204 100644 --- a/content/media/MediaRecorder.cpp +++ b/content/media/MediaRecorder.cpp @@ -89,6 +89,7 @@ public: NS_IMETHODIMP Run() { MOZ_ASSERT(NS_IsMainThread()); + mRecorder->mState = RecordingState::Inactive; mRecorder->DispatchSimpleEvent(NS_LITERAL_STRING("stop")); mRecorder->mReadThread->Shutdown(); mRecorder->mReadThread = nullptr; @@ -114,6 +115,9 @@ private: MediaRecorder::~MediaRecorder() { + if (mStreamPort) { + mStreamPort->Destroy(); + } if (mTrackUnionStream) { mTrackUnionStream->Destroy(); } @@ -163,6 +167,11 @@ MediaRecorder::Start(const Optional& aTimeSlice, ErrorResult& aResult) return; } + if (mStream->GetStream()->IsFinished() || mStream->GetStream()->IsDestroyed()) { + aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + if (aTimeSlice.WasPassed()) { if (aTimeSlice.Value() < 0) { aResult.Throw(NS_ERROR_INVALID_ARG); @@ -191,8 +200,7 @@ MediaRecorder::Start(const Optional& aTimeSlice, ErrorResult& aResult) MOZ_ASSERT(mEncoder, "CreateEncoder failed"); mTrackUnionStream->SetAutofinish(true); - nsRefPtr port = - mTrackUnionStream->AllocateInputPort(mStream->GetStream(), MediaInputPort::FLAG_BLOCK_OUTPUT); + mStreamPort = mTrackUnionStream->AllocateInputPort(mStream->GetStream(), MediaInputPort::FLAG_BLOCK_OUTPUT); if (mEncoder) { mTrackUnionStream->AddListener(mEncoder); @@ -221,7 +229,6 @@ MediaRecorder::Stop(ErrorResult& aResult) return; } mTrackUnionStream->RemoveListener(mEncoder); - mState = RecordingState::Inactive; } void diff --git a/content/media/MediaRecorder.h b/content/media/MediaRecorder.h index 85350ae33f76..0a5e14217d86 100644 --- a/content/media/MediaRecorder.h +++ b/content/media/MediaRecorder.h @@ -110,6 +110,8 @@ protected: nsRefPtr mStream; // This media stream is used for notifying raw data to encoder and can be blocked. nsRefPtr mTrackUnionStream; + // This is used for destroing the inputport when destroy the mediaRecorder + nsRefPtr mStreamPort; // This object creates on start() and destroys in ~MediaRecorder. nsAutoPtr mEncodedBufferCache; // It specifies the container format as well as the audio and video capture formats. diff --git a/content/media/omx/MP3FrameParser.cpp b/content/media/omx/MP3FrameParser.cpp new file mode 100644 index 000000000000..d6ca438edc0d --- /dev/null +++ b/content/media/omx/MP3FrameParser.cpp @@ -0,0 +1,487 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#include +#include "nsMemory.h" +#include "MP3FrameParser.h" + +namespace mozilla { + +// An ID3Buffer contains data of an ID3v2 header. The supplied buffer must +// point to an ID3 header and at least the size of ID_HEADER_LENGTH. Run the +// Parse method to read in the header's values. + +class ID3Buffer +{ +public: + + enum { + ID3_HEADER_LENGTH = 10 + }; + + ID3Buffer(const uint8_t* aBuffer, uint32_t aLength) + : mBuffer(aBuffer), + mLength(aLength), + mSize(0) + { + MOZ_ASSERT(mBuffer || !mLength); + } + + nsresult Parse(); + + int64_t GetMP3Offset() const { + return ID3_HEADER_LENGTH + mSize; + } + +private: + const uint8_t* mBuffer; + uint32_t mLength; + uint32_t mSize; +}; + +nsresult ID3Buffer::Parse() +{ + NS_ENSURE_TRUE(mBuffer && mLength >= ID3_HEADER_LENGTH, NS_ERROR_INVALID_ARG); + + if ((mBuffer[0] != 'I') || + (mBuffer[1] != 'D') || + (mBuffer[2] != '3') || + (mBuffer[6] & 0x80) || + (mBuffer[7] & 0x80) || + (mBuffer[8] & 0x80) || + (mBuffer[9] & 0x80)) { + return NS_ERROR_INVALID_ARG; + } + + mSize = ((static_cast(mBuffer[6])<<21) | + (static_cast(mBuffer[7])<<14) | + (static_cast(mBuffer[8])<<7) | + static_cast(mBuffer[9])); + + return NS_OK; +} + +// The MP3Buffer contains MP3 frame data. The supplied buffer must point +// to a frame header. Call the method Parse to extract information from +// the MP3 frame headers in the supplied buffer. + +class MP3Buffer +{ +public: + + enum { + MP3_HEADER_LENGTH = 4, + MP3_FRAMESIZE_CONST = 144000, + MP3_DURATION_CONST = 8000 + }; + + MP3Buffer(const uint8_t* aBuffer, uint32_t aLength) + : mBuffer(aBuffer), + mLength(aLength), + mDurationUs(0), + mNumFrames(0), + mBitRateSum(0), + mFrameSizeSum(0), + mTrailing(0) + { + MOZ_ASSERT(mBuffer || !mLength); + } + + static const uint8_t* FindNextHeader(const uint8_t* aBuffer, uint32_t aLength); + + nsresult Parse(); + + int64_t GetDuration() const { + return mDurationUs; + } + + int64_t GetNumberOfFrames() const { + return mNumFrames; + } + + int64_t GetBitRateSum() const { + return mBitRateSum; + } + + int64_t GetFrameSizeSum() const { + return mFrameSizeSum; + } + + int64_t GetTrailing() const { + return mTrailing; + } + +private: + + enum MP3FrameHeaderField { + MP3_HDR_FIELD_SYNC, + MP3_HDR_FIELD_VERSION, + MP3_HDR_FIELD_LAYER, + MP3_HDR_FIELD_BITRATE, + MP3_HDR_FIELD_SAMPLERATE, + MP3_HDR_FIELD_PADDING, + MP3_HDR_FIELDS // Must be last enumerator value + }; + + enum { + MP3_HDR_CONST_FRAMESYNC = 0x7ff, + MP3_HDR_CONST_VERSION = 3, + MP3_HDR_CONST_LAYER = 1 + }; + + static uint32_t ExtractBits(uint32_t aValue, uint32_t aOffset, + uint32_t aBits); + static uint32_t ExtractFrameHeaderField(uint32_t aHeader, + enum MP3FrameHeaderField aField); + static uint32_t ExtractFrameHeader(const uint8_t* aBuffer); + static nsresult DecodeFrameHeader(const uint8_t* aBuffer, + size_t* aFrameSize, + uint32_t* aBitRate, + uint64_t* aDuration); + + static const uint16_t sBitRate[16]; + static const uint16_t sSampleRate[4]; + + const uint8_t* mBuffer; + uint32_t mLength; + + // The duration of this parsers data in milliseconds. + int64_t mDurationUs; + + // The number of frames in the range. + int64_t mNumFrames; + + // The sum of all frame's bit rates. + int64_t mBitRateSum; + + // The sum of all frame's sizes in byte. + int32_t mFrameSizeSum; + + // The number of trailing bytes. + int32_t mTrailing; +}; + +const uint16_t MP3Buffer::sBitRate[16] = { + 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0 +}; + +const uint16_t MP3Buffer::sSampleRate[4] = { + 44100, 48000, 32000, 0 +}; + +uint32_t MP3Buffer::ExtractBits(uint32_t aValue, uint32_t aOffset, uint32_t aBits) +{ + return (aValue >> aOffset) & ((0x1ul << aBits) - 1); +} + +uint32_t MP3Buffer::ExtractFrameHeaderField(uint32_t aHeader, enum MP3FrameHeaderField aField) +{ + static const uint8_t sField[MP3_HDR_FIELDS][2] = { + {21, 11}, {19, 2}, {17, 2}, {12, 4}, {10, 2}, {9, 1} + }; + + MOZ_ASSERT(aField < MP3_HDR_FIELDS); + return ExtractBits(aHeader, sField[aField][0], sField[aField][1]); +} + +uint32_t MP3Buffer::ExtractFrameHeader(const uint8_t* aBuffer) +{ + MOZ_ASSERT(aBuffer); + + uint32_t header = (static_cast(aBuffer[0])<<24) | + (static_cast(aBuffer[1])<<16) | + (static_cast(aBuffer[2])<<8) | + static_cast(aBuffer[3]); + + uint32_t frameSync = ExtractFrameHeaderField(header, MP3_HDR_FIELD_SYNC); + uint32_t version = ExtractFrameHeaderField(header, MP3_HDR_FIELD_VERSION); + uint32_t layer = ExtractFrameHeaderField(header, MP3_HDR_FIELD_LAYER); + uint32_t bitRate = sBitRate[ExtractFrameHeaderField(header, MP3_HDR_FIELD_BITRATE)]; + uint32_t sampleRate = sSampleRate[ExtractFrameHeaderField(header, MP3_HDR_FIELD_SAMPLERATE)]; + + // branch-less implementation of + // + // if (fields-are-valid) + // return header; + // else + // return 0; + // + return (frameSync == uint32_t(MP3_HDR_CONST_FRAMESYNC)) * + (version == uint32_t(MP3_HDR_CONST_VERSION)) * + (layer == uint32_t(MP3_HDR_CONST_LAYER)) * !!bitRate * !!sampleRate * header; +} + +const uint8_t* MP3Buffer::FindNextHeader(const uint8_t* aBuffer, uint32_t aLength) +{ + MOZ_ASSERT(aBuffer || !aLength); + + // Find MP3's frame-sync marker while there are at least 4 bytes + // left to contain the MP3 frame header + + while (aLength >= MP3_HEADER_LENGTH) { + if (ExtractFrameHeader(aBuffer)) { + break; + } + ++aBuffer; + --aLength; + } + + return aBuffer; +} + +nsresult MP3Buffer::DecodeFrameHeader(const uint8_t* aBuffer, + uint32_t* aFrameSize, + uint32_t* aBitRate, + uint64_t* aDuration) +{ + uint32_t header = ExtractFrameHeader(aBuffer); + + if (!header) { + return NS_ERROR_INVALID_ARG; + } + + uint32_t bitRate = sBitRate[ExtractFrameHeaderField(header, MP3_HDR_FIELD_BITRATE)]; + uint32_t sampleRate = sSampleRate[ExtractFrameHeaderField(header, MP3_HDR_FIELD_SAMPLERATE)]; + + uint32_t padding = ExtractFrameHeaderField(header, MP3_HDR_FIELD_PADDING); + uint32_t frameSize = (uint64_t(MP3_FRAMESIZE_CONST) * bitRate) / sampleRate + padding; + + MOZ_ASSERT(aBitRate); + *aBitRate = bitRate; + + MOZ_ASSERT(aFrameSize); + *aFrameSize = frameSize; + + MOZ_ASSERT(aDuration); + *aDuration = (uint64_t(MP3_DURATION_CONST) * frameSize) / bitRate; + + return NS_OK; +} + +nsresult MP3Buffer::Parse() +{ + // We walk over the newly arrived data and sum up the + // bit rates, sizes, durations, etc. of the contained + // MP3 frames. + + const uint8_t* buffer = mBuffer; + uint32_t length = mLength; + + while (length >= MP3_HEADER_LENGTH) { + + uint32_t frameSize; + uint32_t bitRate; + uint64_t duration; + + nsresult rv = DecodeFrameHeader(buffer, &frameSize, &bitRate, &duration); + NS_ENSURE_SUCCESS(rv, rv); + + mBitRateSum += bitRate; + mDurationUs += duration; + ++mNumFrames; + + mFrameSizeSum += frameSize; + + if (frameSize <= length) { + length -= frameSize; + } else { + length = 0; + } + + buffer += frameSize; + } + + mTrailing = length; + + return NS_OK; +} + +MP3FrameParser::MP3FrameParser(int64_t aLength) +: mBufferLength(0), + mLock("MP3FrameParser.mLock"), + mDurationUs(0), + mBitRateSum(0), + mNumFrames(0), + mOffset(0), + mUnhandled(0), + mLength(aLength), + mTrailing(0), + mIsMP3(true) +{ } + +size_t MP3FrameParser::ParseInternalBuffer(const uint8_t* aBuffer, uint32_t aLength, int64_t aOffset) +{ + if (mOffset != aOffset) { + // If we don't append, we throw away our temporary buffer. + mBufferLength = 0; + return 0; + } + + size_t copyLength = 0; + + if (mBufferLength || !mOffset) { + + // We have some data in our temporary buffer and append to it, or + // we are at the beginning of the stream. We both cases, we append + // some data to our temporary buffer and try to parse it. + copyLength = std::min(NS_ARRAY_LENGTH(mBuffer)-mBufferLength, aLength); + memcpy(mBuffer+mBufferLength, aBuffer, copyLength*sizeof(*mBuffer)); + mBufferLength += copyLength; + } + + if ((mBufferLength >= ID3Buffer::ID3_HEADER_LENGTH) && (mOffset < ID3Buffer::ID3_HEADER_LENGTH)) { + + // There might be an ID3 header at the very beginning of the stream. + ID3Buffer id3Buffer(mBuffer, mBufferLength); + nsresult rv = id3Buffer.Parse(); + + if (rv == NS_OK) { + mOffset += id3Buffer.GetMP3Offset()-(mBufferLength-copyLength); + mBufferLength = 0; + } + } + + if (mBufferLength >= MP3Buffer::MP3_HEADER_LENGTH) { + + // Or there could be a regular frame header somewhere + // in the stream. + MP3Buffer mp3Buffer(mBuffer, mBufferLength); + nsresult rv = mp3Buffer.Parse(); + + if (rv == NS_OK) { + mDurationUs += mp3Buffer.GetDuration(); + mBitRateSum += mp3Buffer.GetBitRateSum(); + mNumFrames += mp3Buffer.GetNumberOfFrames(); + mOffset += mp3Buffer.GetFrameSizeSum()-(mBufferLength-copyLength); + mBufferLength = 0; + } + } + + if (mBufferLength) { + // We have not been able to successfully parse the + // content of the temporary buffer. If the buffer is + // full already, the stream does not contain MP3. + mOffset += copyLength; + mIsMP3 = (mBufferLength < NS_ARRAY_LENGTH(mBuffer)); + } else { + // We parsed the temporary buffer. The parser code + // will update the input data. + copyLength = 0; + } + + if (mOffset > mLength) { + mLength = mOffset; + } + + return copyLength; +} + +void MP3FrameParser::Parse(const uint8_t* aBuffer, uint32_t aLength, int64_t aOffset) +{ + MutexAutoLock mon(mLock); + + // We first try to parse the remaining data from the last call that + // is stored in an internal buffer. + size_t bufferIncr = ParseInternalBuffer(aBuffer, aLength, aOffset); + + aBuffer += bufferIncr; + aLength -= bufferIncr; + aOffset += bufferIncr; + + // The number of attempts to parse the data. This should be 1 of we + // append to the end of the existing data. + int retries = 1; + + if (aOffset+aLength <= mOffset) { + // We already processed this fragment. + return; + } else if (aOffset < mOffset) { + // mOffset is within the new fragment, shorten range. + aLength -= mOffset-aOffset; + aBuffer += mOffset-aOffset; + aOffset = mOffset; + } else if (aOffset > mOffset) { + // Fragment comes after current position, store difference. + mUnhandled += aOffset-mOffset; + + // We might start in the middle of a frame and have find the next + // frame header. As our detection heuristics might return false + // positives, we simply try multiple times. The current value comes + // from experimentation with MP3 files. If you encounter false positives + // and incorrectly parsed MP3 files, try incrementing this value. + retries = 5; + } + + uint32_t trailing = 0; + + while (retries) { + + MP3Buffer mp3Buffer(aBuffer, aLength); + nsresult rv = mp3Buffer.Parse(); + + if (rv != NS_OK) { + --retries; + + if (!retries) { + mIsMP3 = false; + return; + } + + // We might be in the middle of a frame, find next frame header + const uint8_t *buffer = MP3Buffer::FindNextHeader(aBuffer+1, aLength-1); + + mUnhandled += buffer-aBuffer; + mOffset = aOffset + buffer-aBuffer; + aLength -= buffer-aBuffer; + aBuffer = buffer; + } else { + mDurationUs += mp3Buffer.GetDuration(); + mBitRateSum += mp3Buffer.GetBitRateSum(); + mNumFrames += mp3Buffer.GetNumberOfFrames(); + mOffset += mp3Buffer.GetFrameSizeSum(); + + trailing = mp3Buffer.GetTrailing(); + retries = 0; + } + } + + if (trailing) { + // Store trailing bytes in temporary buffer. + MOZ_ASSERT(trailing < (NS_ARRAY_LENGTH(mBuffer)*sizeof(*mBuffer))); + memcpy(mBuffer, aBuffer+(aLength-trailing), trailing); + mBufferLength = trailing; + } + + if (mOffset > mLength) { + mLength = mOffset; + } +} + +void MP3FrameParser::NotifyDataArrived(const char* aBuffer, uint32_t aLength, int64_t aOffset) +{ + Parse(reinterpret_cast(aBuffer), aLength, aOffset); +} + +int64_t MP3FrameParser::GetDuration() +{ + MutexAutoLock mon(mLock); + + if (!mNumFrames) { + return -1; // Not a single frame decoded yet + } + + // Compute the duration of the unhandled fragments from + // the average bitrate. + int64_t avgBitRate = mBitRateSum / mNumFrames; + NS_ENSURE_TRUE(avgBitRate > 0, mDurationUs); + + MOZ_ASSERT(mLength >= mOffset); + int64_t unhandled = mUnhandled + (mLength-mOffset); + + return mDurationUs + (uint64_t(MP3Buffer::MP3_DURATION_CONST) * unhandled) / avgBitRate; +} + +} diff --git a/content/media/omx/MP3FrameParser.h b/content/media/omx/MP3FrameParser.h new file mode 100644 index 000000000000..82b60d2245a4 --- /dev/null +++ b/content/media/omx/MP3FrameParser.h @@ -0,0 +1,76 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#include +#include "mozilla/Mutex.h" + +namespace mozilla { + +// A description of the MP3 format and its extensions is available at +// +// http://www.codeproject.com/Articles/8295/MPEG-Audio-Frame-Header +// +// The data in MP3 streams is split into small frames, with each frame +// containing a fixed number of samples. The duration of a frame depends +// on the frame's bit rate and sample rate. Both values can vary among +// frames, so it is necessary to examine each individual frame of an MP3 +// stream to calculate the stream's overall duration. +// +// The MP3 frame parser extracts information from an MP3 data stream. It +// accepts a range of frames of an MP3 stream as input, and parses all +// frames for their duration. Callers can query the stream's overall +// duration from the parser. +// +// Call the methods NotifyDataArrived or Parse to add new data. If you added +// information for a certain stream position, you cannot go back to previous +// positions. The parser will simply ignore the input. If you skip stream +// positions, the duration of the related MP3 frames will be estimated from +// the stream's average. +// +// The method GetDuration returns calculated duration of the stream, including +// estimates for skipped ranges. +// +// All public methods are thread-safe. + +class MP3FrameParser +{ +public: + MP3FrameParser(int64_t aLength=-1); + + bool IsMP3() { + MutexAutoLock mon(mLock); + return mIsMP3; + } + + void Parse(const uint8_t* aBuffer, uint32_t aLength, int64_t aOffset); + + void NotifyDataArrived(const char* aBuffer, uint32_t aLength, int64_t aOffset); + + int64_t GetDuration(); + +private: + size_t ParseInternalBuffer(const uint8_t* aBuffer, uint32_t aLength, int64_t aOffset); + + uint8_t mBuffer[10]; + uint32_t mBufferLength; + + // A low-contention lock for protecting the parser results + Mutex mLock; + + // All fields below are protected by mLock + uint64_t mDurationUs; + uint64_t mBitRateSum; + uint64_t mNumFrames; + int64_t mOffset; + int64_t mUnhandled; + int64_t mLength; + uint32_t mTrailing; + + // Contains the state of the MP3 detection + bool mIsMP3; +}; + +} diff --git a/content/media/omx/MediaOmxReader.cpp b/content/media/omx/MediaOmxReader.cpp index cf27940e75c6..5fcb456a50ff 100644 --- a/content/media/omx/MediaOmxReader.cpp +++ b/content/media/omx/MediaOmxReader.cpp @@ -270,6 +270,15 @@ bool MediaOmxReader::DecodeVideoFrame(bool &aKeyframeSkip, return true; } +void MediaOmxReader::NotifyDataArrived(const char* aBuffer, uint32_t aLength, int64_t aOffset) +{ + android::OmxDecoder *omxDecoder = mOmxDecoder.get(); + + if (omxDecoder) { + omxDecoder->NotifyDataArrived(aBuffer, aLength, aOffset); + } +} + bool MediaOmxReader::DecodeAudioData() { NS_ASSERTION(mDecoder->OnDecodeThread(), "Should be on decode thread."); diff --git a/content/media/omx/MediaOmxReader.h b/content/media/omx/MediaOmxReader.h index 13a36c595c62..db55f2d7fd67 100644 --- a/content/media/omx/MediaOmxReader.h +++ b/content/media/omx/MediaOmxReader.h @@ -40,6 +40,8 @@ public: virtual nsresult Init(MediaDecoderReader* aCloneDonor); virtual nsresult ResetDecode(); + virtual void NotifyDataArrived(const char* aBuffer, uint32_t aLength, int64_t aOffset); + virtual bool DecodeAudioData(); virtual bool DecodeVideoFrame(bool &aKeyframeSkip, int64_t aTimeThreshold); diff --git a/content/media/omx/OmxDecoder.cpp b/content/media/omx/OmxDecoder.cpp index 011ef252119b..511cd336bdcd 100644 --- a/content/media/omx/OmxDecoder.cpp +++ b/content/media/omx/OmxDecoder.cpp @@ -18,6 +18,8 @@ #include "mozilla/Preferences.h" #include "mozilla/Types.h" +#include "mozilla/Monitor.h" +#include "nsMimeTypes.h" #include "MPAPI.h" #include "prlog.h" @@ -37,6 +39,116 @@ using namespace MPAPI; using namespace mozilla; namespace mozilla { + +class OmxDecoderProcessCachedDataTask : public Task +{ +public: + OmxDecoderProcessCachedDataTask(android::OmxDecoder* aOmxDecoder, int64_t aOffset) + : mOmxDecoder(aOmxDecoder), + mOffset(aOffset) + { } + + void Run() + { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(mOmxDecoder.get()); + mOmxDecoder->ProcessCachedData(mOffset, false); + } + +private: + android::sp mOmxDecoder; + int64_t mOffset; +}; + +// When loading an MP3 stream from a file, we need to parse the file's +// content to find its duration. Reading files of 100 Mib or more can +// delay the player app noticably, so the file os read and decoded in +// smaller chunks. +// +// We first read on the decode thread, but parsing must be done on the +// main thread. After we read the file's initial MiBs in the decode +// thread, an instance of this class is scheduled to the main thread for +// parsing the MP3 stream. The decode thread waits until it has finished. +// +// If there is more data available from the file, the runnable dispatches +// a task to the IO thread for retrieving the next chunk of data, and +// the IO task dispatches a runnable to the main thread for parsing the +// data. This goes on until all of the MP3 file has been parsed. + +class OmxDecoderNotifyDataArrivedRunnable : public nsRunnable +{ +public: + OmxDecoderNotifyDataArrivedRunnable(android::OmxDecoder* aOmxDecoder, const char* aBuffer, uint64_t aLength, int64_t aOffset, uint64_t aFullLength) + : mOmxDecoder(aOmxDecoder), + mBuffer(aBuffer), + mLength(aLength), + mOffset(aOffset), + mFullLength(aFullLength), + mCompletedMonitor("OmxDecoderNotifyDataArrived.mCompleted"), + mCompleted(false) + { + MOZ_ASSERT(mOmxDecoder.get()); + MOZ_ASSERT(mBuffer.get() || !mLength); + } + + NS_IMETHOD Run() + { + NS_ASSERTION(NS_IsMainThread(), "Should be on main thread."); + + const char* buffer = mBuffer.get(); + + while (mLength) { + uint32_t length = std::min(mLength, UINT32_MAX); + mOmxDecoder->NotifyDataArrived(mBuffer.get(), mLength, mOffset); + + buffer += length; + mLength -= length; + mOffset += length; + } + + if (mOffset < mFullLength) { + // We cannot read data in the main thread because it + // might block for too long. Instead we post an IO task + // to the IO thread if there is more data available. + XRE_GetIOMessageLoop()->PostTask(FROM_HERE, new OmxDecoderProcessCachedDataTask(mOmxDecoder.get(), mOffset)); + } + + Completed(); + + return NS_OK; + } + + void WaitForCompletion() + { + MOZ_ASSERT(!NS_IsMainThread()); + + MonitorAutoLock mon(mCompletedMonitor); + if (!mCompleted) { + mCompletedMonitor.Wait(); + } + } + +private: + // Call this function at the end of Run() to notify waiting + // threads. + void Completed() + { + MonitorAutoLock mon(mCompletedMonitor); + MOZ_ASSERT(!mCompleted); + mCompleted = true; + mCompletedMonitor.Notify(); + } + + android::sp mOmxDecoder; + nsAutoArrayPtr mBuffer; + uint64_t mLength; + int64_t mOffset; + uint64_t mFullLength; + + Monitor mCompletedMonitor; + bool mCompleted; +}; + namespace layers { VideoGraphicBuffer::VideoGraphicBuffer(const android::wp aOmxDecoder, @@ -147,6 +259,7 @@ OmxDecoder::OmxDecoder(MediaResource *aResource, mAudioChannels(-1), mAudioSampleRate(-1), mDurationUs(-1), + mMP3FrameParser(aResource->GetLength()), mVideoBuffer(nullptr), mAudioBuffer(nullptr), mIsVideoSeeking(false), @@ -274,9 +387,27 @@ bool OmxDecoder::TryLoad() { if (durationUs > totalDurationUs) totalDurationUs = durationUs; } - if (mAudioTrack.get() && mAudioTrack->getFormat()->findInt64(kKeyDuration, &durationUs)) { - if (durationUs > totalDurationUs) - totalDurationUs = durationUs; + if (mAudioTrack.get()) { + durationUs = -1; + const char* audioMime; + sp meta = mAudioTrack->getFormat(); + + if (meta->findCString(kKeyMIMEType, &audioMime) && !strcasecmp(audioMime, AUDIO_MP3)) { + // Feed MP3 parser with cached data. Local files will be fully + // cached already, network streams will update with sucessive + // calls to NotifyDataArrived. + if (ProcessCachedData(0, true)) { + durationUs = mMP3FrameParser.GetDuration(); + if (durationUs > totalDurationUs) { + totalDurationUs = durationUs; + } + } + } + if ((durationUs == -1) && meta->findInt64(kKeyDuration, &durationUs)) { + if (durationUs > totalDurationUs) { + totalDurationUs = durationUs; + } + } } mDurationUs = totalDurationUs; @@ -485,6 +616,25 @@ bool OmxDecoder::SetAudioFormat() { return true; } +void OmxDecoder::NotifyDataArrived(const char* aBuffer, uint32_t aLength, int64_t aOffset) +{ + if (!mMP3FrameParser.IsMP3()) { + return; + } + + mMP3FrameParser.NotifyDataArrived(aBuffer, aLength, aOffset); + + int64_t durationUs = mMP3FrameParser.GetDuration(); + + if (durationUs != mDurationUs) { + mDurationUs = durationUs; + + MOZ_ASSERT(mDecoder); + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); + mDecoder->UpdateMediaDuration(mDurationUs); + } +} + void OmxDecoder::ReleaseVideoBuffer() { if (mVideoBuffer) { mVideoBuffer->release(); @@ -842,3 +992,43 @@ void OmxDecoder::ReleaseAllPendingVideoBuffersLocked() releasingVideoBuffers.clear(); } +bool OmxDecoder::ProcessCachedData(int64_t aOffset, bool aWaitForCompletion) +{ + // We read data in chunks of 8 MiB. We can reduce this + // value if media, such as sdcards, is too slow. + static const int64_t sReadSize = 8 * 1024 * 1024; + + NS_ASSERTION(!NS_IsMainThread(), "Should not be on main thread."); + + MOZ_ASSERT(mResource); + + int64_t resourceLength = mResource->GetCachedDataEnd(0); + NS_ENSURE_TRUE(resourceLength >= 0, false); + + if (aOffset >= resourceLength) { + return true; // Cache is empty, nothing to do + } + + int64_t bufferLength = std::min(resourceLength-aOffset, sReadSize); + + nsAutoArrayPtr buffer(new char[bufferLength]); + + nsresult rv = mResource->ReadFromCache(buffer.get(), aOffset, bufferLength); + NS_ENSURE_SUCCESS(rv, false); + + nsRefPtr runnable( + new OmxDecoderNotifyDataArrivedRunnable(this, + buffer.forget(), + bufferLength, + aOffset, + resourceLength)); + + rv = NS_DispatchToMainThread(runnable.get()); + NS_ENSURE_SUCCESS(rv, false); + + if (aWaitForCompletion) { + runnable->WaitForCompletion(); + } + + return true; +} diff --git a/content/media/omx/OmxDecoder.h b/content/media/omx/OmxDecoder.h index 870df892c567..c3e3d8f5aede 100644 --- a/content/media/omx/OmxDecoder.h +++ b/content/media/omx/OmxDecoder.h @@ -9,6 +9,7 @@ #include "GonkNativeWindow.h" #include "GonkNativeWindowClient.h" #include "GrallocImages.h" +#include "MP3FrameParser.h" #include "MPAPI.h" #include "MediaResource.h" #include "AbstractMediaDecoder.h" @@ -76,6 +77,7 @@ private: class OmxDecoder : public OMXCodecProxy::EventListener { typedef MPAPI::AudioFrame AudioFrame; typedef MPAPI::VideoFrame VideoFrame; + typedef mozilla::MP3FrameParser MP3FrameParser; typedef mozilla::MediaResource MediaResource; typedef mozilla::AbstractMediaDecoder AbstractMediaDecoder; @@ -109,6 +111,7 @@ class OmxDecoder : public OMXCodecProxy::EventListener { int64_t mDurationUs; VideoFrame mVideoFrame; AudioFrame mAudioFrame; + MP3FrameParser mMP3FrameParser; // Lifetime of these should be handled by OMXCodec, as long as we release // them after use: see ReleaseVideoBuffer(), ReleaseAudioBuffer() @@ -177,6 +180,8 @@ public: bool SetVideoFormat(); bool SetAudioFormat(); + void NotifyDataArrived(const char* aBuffer, uint32_t aLength, int64_t aOffset); + void GetDuration(int64_t *durationUs) { *durationUs = mDurationUs; } @@ -199,7 +204,7 @@ public: return mAudioSource != nullptr; } - bool ReadVideo(VideoFrame *aFrame, int64_t aSeekTimeUs, + bool ReadVideo(VideoFrame *aFrame, int64_t aSeekTimeUs, bool aKeyframeSkip = false, bool aDoSeek = false); bool ReadAudio(AudioFrame *aFrame, int64_t aSeekTimeUs); @@ -220,6 +225,7 @@ public: // Called on ALooper thread. void onMessageReceived(const sp &msg); + bool ProcessCachedData(int64_t aOffset, bool aWaitForCompletion); }; } diff --git a/content/media/omx/moz.build b/content/media/omx/moz.build index 4ad2d79a1845..e36b67f1cae5 100644 --- a/content/media/omx/moz.build +++ b/content/media/omx/moz.build @@ -14,6 +14,7 @@ EXPORTS += [ CPP_SOURCES += [ 'MediaOmxDecoder.cpp', 'MediaOmxReader.cpp', + 'MP3FrameParser.cpp', 'OmxDecoder.cpp', 'OMXCodecProxy.cpp', ] diff --git a/content/media/test/Makefile.in b/content/media/test/Makefile.in index 9d31f65bd7f2..575f0df3b299 100644 --- a/content/media/test/Makefile.in +++ b/content/media/test/Makefile.in @@ -116,6 +116,8 @@ MOCHITEST_FILES = \ test_video_to_canvas.html \ test_audiowrite.html \ test_mediarecorder_creation.html \ + test_mediarecorder_record_audiocontext.html \ + test_mediarecorder_record_stopms.html \ test_mozHasAudio.html \ test_source_media.html \ test_autoplay_contentEditable.html \ @@ -258,6 +260,7 @@ MOCHITEST_FILES += \ test-6-5.1.opus \ test-7-6.1.opus \ test-8-7.1.opus \ + vbr.mp3 \ video-overhang.ogg \ file_a4_tone.ogg \ detodos.opus \ diff --git a/content/media/test/manifest.js b/content/media/test/manifest.js index 1940cb36841d..5f0b80ab5546 100644 --- a/content/media/test/manifest.js +++ b/content/media/test/manifest.js @@ -42,6 +42,7 @@ var gPlayedTests = [ { name:"seek.webm", type:"video/webm", duration:3.966 }, { name:"gizmo.mp4", type:"video/mp4", duration:5.56 }, { name:"owl.mp3", type:"audio/mpeg", duration:3.29 }, + { name:"vbr.mp3", type:"audio/mpeg", duration:10.0 } ]; // Used by test_mozLoadFrom. Need one test file per decoder backend, plus @@ -154,10 +155,10 @@ var gPlayTests = [ // Test playback of a WebM file with non-zero start time. { name:"split.webm", type:"video/webm", duration:1.967 }, - + // Test playback of a raw file { name:"seek.yuv", type:"video/x-raw-yuv", duration:1.833 }, - + // A really short, low sample rate, single channel file. This tests whether // we can handle playing files when only push very little audio data to the // hardware. @@ -563,7 +564,7 @@ const DEBUG_TEST_LOOP_FOREVER = false; // 1. Create a new MediaTestManager object. // 2. Create a test startTest function. This takes a test object and a token, // and performs anything necessary to start the test. The test object is an -// element in one of the g*Tests above. Your startTest function must call +// element in one of the g*Tests above. Your startTest function must call // MediaTestManager.start(token) if it starts a test. The test object is // guaranteed to be playable by our supported decoders; you don't need to // check canPlayType. @@ -574,12 +575,12 @@ function MediaTestManager() { // Sets up a MediaTestManager to runs through the 'tests' array, which needs // to be one of, or have the same fields as, the g*Test arrays of tests. Uses - // the user supplied 'startTest' function to initialize the test. This + // the user supplied 'startTest' function to initialize the test. This // function must accept two arguments, the test entry from the 'tests' array, // and a token. Call MediaTestManager.started(token) if you start the test, // and MediaTestManager.finished(token) when the test finishes. You don't have // to start every test, but if you call started() you *must* call finish() - // else you'll timeout. + // else you'll timeout. this.runTests = function(tests, startTest) { this.startTime = new Date(); SimpleTest.info("Started " + this.startTime + " (" + this.startTime.getTime()/1000 + "s)"); @@ -593,7 +594,7 @@ function MediaTestManager() { SimpleTest.waitForExplicitFinish(); this.nextTest(); } - + // Registers that the test corresponding to 'token' has been started. // Don't call more than once per token. this.started = function(token) { @@ -601,7 +602,7 @@ function MediaTestManager() { this.numTestsRunning++; is(this.numTestsRunning, this.tokens.length, "[started " + token + "] Length of array should match number of running tests"); } - + // Registers that the test corresponding to 'token' has finished. Call when // you've finished your test. If all tests are complete this will finish the // run, otherwise it may start up the next run. It's ok to call multiple times @@ -626,7 +627,7 @@ function MediaTestManager() { // with live threads waiting for the GC are killed promptly, to free up the // thread stacks' address space. SpecialPowers.forceGC(); - + while (this.testNum < this.tests.length && this.tokens.length < PARALLEL_TESTS) { var test = this.tests[this.testNum]; var token = (test.name ? (test.name + "-"): "") + this.testNum; @@ -635,11 +636,11 @@ function MediaTestManager() { if (DEBUG_TEST_LOOP_FOREVER && this.testNum == this.tests.length) { this.testNum = 0; } - + // Ensure we can play the resource type. if (test.type && !document.createElement('video').canPlayType(test.type)) continue; - + // Do the init. This should start the test. this.startTest(test, token); } diff --git a/content/media/test/test_mediarecorder_record_audiocontext.html b/content/media/test/test_mediarecorder_record_audiocontext.html new file mode 100644 index 000000000000..80ca190d5071 --- /dev/null +++ b/content/media/test/test_mediarecorder_record_audiocontext.html @@ -0,0 +1,65 @@ + + + + Test MediaRecorder Record AudioContext + + + + + + + + + + diff --git a/content/media/test/test_mediarecorder_record_stopms.html b/content/media/test/test_mediarecorder_record_stopms.html new file mode 100644 index 000000000000..0dad5ba61796 --- /dev/null +++ b/content/media/test/test_mediarecorder_record_stopms.html @@ -0,0 +1,47 @@ + + + + Test MediaRecorder Record Stopped Stream + + + + +
+
+
+
+ + + diff --git a/content/media/test/vbr.mp3 b/content/media/test/vbr.mp3 new file mode 100644 index 000000000000..38eb376a9711 Binary files /dev/null and b/content/media/test/vbr.mp3 differ diff --git a/content/media/webaudio/MediaBufferDecoder.cpp b/content/media/webaudio/MediaBufferDecoder.cpp index c0e301808b66..1ac22e1bba19 100644 --- a/content/media/webaudio/MediaBufferDecoder.cpp +++ b/content/media/webaudio/MediaBufferDecoder.cpp @@ -97,6 +97,8 @@ public: virtual void SetMediaDuration(int64_t aDuration) MOZ_FINAL MOZ_OVERRIDE; + virtual void UpdateMediaDuration(int64_t aDuration) MOZ_FINAL MOZ_OVERRIDE; + virtual void SetMediaSeekable(bool aMediaSeekable) MOZ_OVERRIDE; virtual void SetTransportSeekable(bool aTransportSeekable) MOZ_FINAL MOZ_OVERRIDE; @@ -214,6 +216,12 @@ BufferDecoder::SetMediaDuration(int64_t aDuration) // ignore } +void +BufferDecoder::UpdateMediaDuration(int64_t aDuration) +{ + // ignore +} + void BufferDecoder::SetMediaSeekable(bool aMediaSeekable) { diff --git a/content/media/webaudio/MediaStreamAudioDestinationNode.cpp b/content/media/webaudio/MediaStreamAudioDestinationNode.cpp index 0c99d7002079..c2e2b983200f 100644 --- a/content/media/webaudio/MediaStreamAudioDestinationNode.cpp +++ b/content/media/webaudio/MediaStreamAudioDestinationNode.cpp @@ -5,6 +5,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "MediaStreamAudioDestinationNode.h" +#include "nsIDocument.h" #include "mozilla/dom/AudioStreamTrack.h" #include "mozilla/dom/MediaStreamAudioDestinationNodeBinding.h" #include "AudioNodeEngine.h" @@ -73,6 +74,11 @@ MediaStreamAudioDestinationNode::MediaStreamAudioDestinationNode(AudioContext* a MediaStreamDestinationEngine* engine = new MediaStreamDestinationEngine(this, tus); mStream = aContext->Graph()->CreateAudioNodeStream(engine, MediaStreamGraph::INTERNAL_STREAM); mPort = tus->AllocateInputPort(mStream, 0); + + nsIDocument* doc = aContext->GetParentObject()->GetExtantDoc(); + if (doc) { + mDOMStream->CombineWithPrincipal(doc->NodePrincipal()); + } } void diff --git a/content/media/webspeech/synth/ipc/test/test_ipc.html b/content/media/webspeech/synth/ipc/test/test_ipc.html index bb2fd49a7d72..99bccdf3420a 100644 --- a/content/media/webspeech/synth/ipc/test/test_ipc.html +++ b/content/media/webspeech/synth/ipc/test/test_ipc.html @@ -181,7 +181,7 @@ ["dom.ipc.browser_frames.oop_by_default", true], ["dom.mozBrowserFramesEnabled", true], - ["browser.pagethumbnails.capturing_disabled", false] + ["browser.pagethumbnails.capturing_disabled", true] ] }, runTests); }); diff --git a/dom/activities/src/ActivitiesService.jsm b/dom/activities/src/ActivitiesService.jsm index 955e0081b236..c3688b7f9cb5 100644 --- a/dom/activities/src/ActivitiesService.jsm +++ b/dom/activities/src/ActivitiesService.jsm @@ -17,6 +17,10 @@ XPCOMUtils.defineLazyServiceGetter(this, "ppmm", "@mozilla.org/parentprocessmessagemanager;1", "nsIMessageBroadcaster"); +XPCOMUtils.defineLazyServiceGetter(this, "NetUtil", + "@mozilla.org/network/util;1", + "nsINetUtil"); + this.EXPORTED_SYMBOLS = []; let idbGlobal = this; @@ -158,6 +162,7 @@ let Activities = { "Activities:Register", "Activities:Unregister", + "Activities:GetContentTypes" ], init: function activities_init() { @@ -311,9 +316,18 @@ let Activities = { break; case "Activities:Register": + let self = this; this.db.add(msg, function onSuccess(aEvent) { mm.sendAsyncMessage("Activities:Register:OK", null); + let res = []; + msg.forEach(function(aActivity) { + self.updateContentTypeList(aActivity, res); + }); + if (res.length) { + ppmm.broadcastAsyncMessage("Activities:RegisterContentTypes", + { contentTypes: res }); + } }, function onError(aEvent) { msg.error = "REGISTER_ERROR"; @@ -322,8 +336,60 @@ let Activities = { break; case "Activities:Unregister": this.db.remove(msg); + let res = []; + msg.forEach(function(aActivity) { + this.updateContentTypeList(aActivity, res); + }, this); + if (res.length) { + ppmm.broadcastAsyncMessage("Activities:UnregisterContentTypes", + { contentTypes: res }); + } + break; + case "Activities:GetContentTypes": + this.sendContentTypes(mm); break; } + }, + + updateContentTypeList: function updateContentTypeList(aActivity, aResult) { + // Bail out if this is not a "view" activity. + if (aActivity.name != "view") { + return; + } + + let types = aActivity.description.filters.type; + if (typeof types == "string") { + types = [types]; + } + + // Check that this is a real content type and sanitize it. + types.forEach(function(aContentType) { + let hadCharset = { }; + let charset = { }; + let contentType = + NetUtil.parseContentType(aContentType, charset, hadCharset); + if (contentType) { + aResult.push(contentType); + } + }); + }, + + sendContentTypes: function sendContentTypes(aMm) { + let res = []; + let self = this; + this.db.find({ options: { name: "view" } }, + function() { // Success callback. + if (res.length) { + aMm.sendAsyncMessage("Activities:RegisterContentTypes", + { contentTypes: res }); + } + }, + null, // Error callback. + function(aActivity) { // Matching callback. + self.updateContentTypeList(aActivity, res) + return false; + } + ); } } diff --git a/dom/apps/src/Webapps.jsm b/dom/apps/src/Webapps.jsm index 7f1f66099f63..143b8c6abaf4 100644 --- a/dom/apps/src/Webapps.jsm +++ b/dom/apps/src/Webapps.jsm @@ -688,7 +688,8 @@ this.DOMApplicationRegistry = { for (let activity in root.activities) { let description = root.activities[activity]; activitiesToUnregister.push({ "manifest": aApp.manifestURL, - "name": activity }); + "name": activity, + "description": description }); } return activitiesToUnregister; }, diff --git a/dom/base/DOMRequestHelper.jsm b/dom/base/DOMRequestHelper.jsm index 69ad623c5df8..20e3d98b7aa7 100644 --- a/dom/base/DOMRequestHelper.jsm +++ b/dom/base/DOMRequestHelper.jsm @@ -40,7 +40,7 @@ this.DOMRequestIpcHelperMessageListener = function(aHelper, aWindow, aMessages) this._messages = aMessages; this._messages.forEach(function(msgName) { - cpmm.addMessageListener(msgName, this); + cpmm.addWeakMessageListener(msgName, this); }, this); Services.obs.addObserver(this, "inner-window-destroyed", /* weakRef */ true); @@ -91,7 +91,7 @@ DOMRequestIpcHelperMessageListener.prototype = { Services.obs.removeObserver(this, "inner-window-destroyed"); this._messages.forEach(function(msgName) { - cpmm.removeMessageListener(msgName, this); + cpmm.removeWeakMessageListener(msgName, this); }, this); this._messages = null; @@ -117,7 +117,7 @@ DOMRequestIpcHelper.prototype = { new DOMRequestIpcHelperMessageListener(this, aWindow, aMessages); this._window = aWindow; - this._requests = []; + this._requests = {}; this._id = this._getRandomId(); if (this._window) { @@ -166,7 +166,7 @@ DOMRequestIpcHelper.prototype = { this._destroyed = true; this._DOMRequestIpcHelperMessageListener.destroy(); - this._requests = []; + this._requests = {}; this._window = null; if(this.uninit) { diff --git a/dom/base/Navigator.cpp b/dom/base/Navigator.cpp index e8e05c6bae8a..15a53930aeb2 100644 --- a/dom/base/Navigator.cpp +++ b/dom/base/Navigator.cpp @@ -1140,7 +1140,7 @@ Navigator::GetMozCellBroadcast(ErrorResult& aRv) return mCellBroadcast; } -nsIDOMTelephony* +telephony::Telephony* Navigator::GetMozTelephony(ErrorResult& aRv) { if (!mTelephony) { diff --git a/dom/base/Navigator.h b/dom/base/Navigator.h index 92639d4c9338..56a24b5fb673 100644 --- a/dom/base/Navigator.h +++ b/dom/base/Navigator.h @@ -33,7 +33,6 @@ class systemMessageCallback; #endif #ifdef MOZ_B2G_RIL -class nsIDOMTelephony; class nsIDOMMozMobileConnection; class nsIDOMMozCellBroadcast; class nsIDOMMozVoicemail; @@ -86,6 +85,12 @@ class MobileConnection; #endif } // namespace Connection; +#ifdef MOZ_B2G_RIL +namespace telephony { +class Telephony; +} // namespace Telephony; +#endif + namespace power { class PowerManager; } // namespace power @@ -214,7 +219,7 @@ public: ErrorResult& aRv); bool MozHasPendingMessage(const nsAString& aType, ErrorResult& aRv); #ifdef MOZ_B2G_RIL - nsIDOMTelephony* GetMozTelephony(ErrorResult& aRv); + telephony::Telephony* GetMozTelephony(ErrorResult& aRv); nsIDOMMozMobileConnection* GetMozMobileConnection(ErrorResult& aRv); nsIDOMMozCellBroadcast* GetMozCellBroadcast(ErrorResult& aRv); nsIDOMMozVoicemail* GetMozVoicemail(ErrorResult& aRv); @@ -309,7 +314,7 @@ private: nsRefPtr mPowerManager; nsRefPtr mMobileMessageManager; #ifdef MOZ_B2G_RIL - nsCOMPtr mTelephony; + nsRefPtr mTelephony; nsCOMPtr mVoicemail; #endif nsRefPtr mConnection; diff --git a/dom/base/nsDOMClassInfo.cpp b/dom/base/nsDOMClassInfo.cpp index cee777ebc19b..9f47d3d04c06 100644 --- a/dom/base/nsDOMClassInfo.cpp +++ b/dom/base/nsDOMClassInfo.cpp @@ -224,8 +224,6 @@ using mozilla::dom::workers::ResolveWorkerClasses; #include "nsIDOMConnection.h" #ifdef MOZ_B2G_RIL -#include "Telephony.h" -#include "TelephonyCall.h" #include "nsIDOMMozVoicemail.h" #include "nsIDOMIccManager.h" #include "nsIDOMMozCellBroadcast.h" @@ -628,10 +626,6 @@ static nsDOMClassInfoData sClassInfoData[] = { DOM_DEFAULT_SCRIPTABLE_FLAGS) #ifdef MOZ_B2G_RIL - NS_DEFINE_CLASSINFO_DATA(Telephony, nsEventTargetSH, - EVENTTARGET_SCRIPTABLE_FLAGS) - NS_DEFINE_CLASSINFO_DATA(TelephonyCall, nsEventTargetSH, - EVENTTARGET_SCRIPTABLE_FLAGS) NS_DEFINE_CLASSINFO_DATA(MozVoicemail, nsEventTargetSH, EVENTTARGET_SCRIPTABLE_FLAGS) NS_DEFINE_CLASSINFO_DATA(MozIccManager, nsDOMGenericSH, @@ -1521,15 +1515,6 @@ nsDOMClassInfo::Init() DOM_CLASSINFO_MAP_END #ifdef MOZ_B2G_RIL - DOM_CLASSINFO_MAP_BEGIN(Telephony, nsIDOMTelephony) - DOM_CLASSINFO_MAP_ENTRY(nsIDOMTelephony) - DOM_CLASSINFO_MAP_ENTRY(nsIDOMEventTarget) - DOM_CLASSINFO_MAP_END - - DOM_CLASSINFO_MAP_BEGIN(TelephonyCall, nsIDOMTelephonyCall) - DOM_CLASSINFO_MAP_ENTRY(nsIDOMTelephonyCall) - DOM_CLASSINFO_MAP_ENTRY(nsIDOMEventTarget) - DOM_CLASSINFO_MAP_END DOM_CLASSINFO_MAP_BEGIN(MozVoicemail, nsIDOMMozVoicemail) DOM_CLASSINFO_MAP_ENTRY(nsIDOMMozVoicemail) diff --git a/dom/base/nsDOMClassInfoClasses.h b/dom/base/nsDOMClassInfoClasses.h index 0472e1f4317a..0df3eaec6841 100644 --- a/dom/base/nsDOMClassInfoClasses.h +++ b/dom/base/nsDOMClassInfoClasses.h @@ -128,8 +128,6 @@ DOMCI_CLASS(CSSPageRule) DOMCI_CLASS(MediaQueryList) #ifdef MOZ_B2G_RIL -DOMCI_CLASS(Telephony) -DOMCI_CLASS(TelephonyCall) DOMCI_CLASS(MozVoicemail) DOMCI_CLASS(MozIccManager) #endif diff --git a/dom/bindings/Bindings.conf b/dom/bindings/Bindings.conf index 058ef8a51f84..82f435b4b073 100644 --- a/dom/bindings/Bindings.conf +++ b/dom/bindings/Bindings.conf @@ -150,6 +150,16 @@ DOMInterfaces = { 'headerFile': 'BatteryManager.h' }, +'CallEvent': { + 'nativeType': 'mozilla::dom::telephony::CallEvent', + 'headerFile': 'CallEvent.h', +}, + +'CallsList': { + 'nativeType': 'mozilla::dom::telephony::CallsList', + 'headerFile': 'CallsList.h', +}, + 'CameraManager': { 'nativeType': 'nsDOMCameraManager', 'headerFile': 'DOMCameraManager.h' @@ -1118,6 +1128,16 @@ DOMInterfaces = { 'concrete': False, }, +'Telephony' : { + 'nativeType': 'mozilla::dom::telephony::Telephony', + 'headerFile': 'Telephony.h', +}, + +'TelephonyCall' : { + 'nativeType': 'mozilla::dom::telephony::TelephonyCall', + 'headerFile': 'TelephonyCall.h', +}, + 'Text': { # Total hack to allow binding code to realize that nsTextNode can # in fact be cast to Text. diff --git a/dom/bluetooth/BluetoothService.cpp b/dom/bluetooth/BluetoothService.cpp index 18a54eeab40e..119704936714 100644 --- a/dom/bluetooth/BluetoothService.cpp +++ b/dom/bluetooth/BluetoothService.cpp @@ -9,7 +9,9 @@ #include "BluetoothService.h" #include "BluetoothCommon.h" +#include "BluetoothHfpManager.h" #include "BluetoothManager.h" +#include "BluetoothOppManager.h" #include "BluetoothParent.h" #include "BluetoothReplyRunnable.h" #include "BluetoothServiceChildProcess.h" @@ -475,6 +477,14 @@ BluetoothService::StartStopBluetooth(bool aStart, bool aIsStartup) NS_ENSURE_SUCCESS(rv, rv); } + if (!aStart) { + BluetoothHfpManager* hfp = BluetoothHfpManager::Get(); + hfp->Disconnect(); + + BluetoothOppManager* opp = BluetoothOppManager::Get(); + opp->Disconnect(); + } + nsCOMPtr runnable = new ToggleBtTask(aStart, aIsStartup); rv = mBluetoothCommandThread->Dispatch(runnable, NS_DISPATCH_NORMAL); NS_ENSURE_SUCCESS(rv, rv); diff --git a/dom/bluetooth/BluetoothTelephonyListener.cpp b/dom/bluetooth/BluetoothTelephonyListener.cpp index 3c6a448e4e69..6f5924397ab0 100644 --- a/dom/bluetooth/BluetoothTelephonyListener.cpp +++ b/dom/bluetooth/BluetoothTelephonyListener.cpp @@ -92,6 +92,12 @@ TelephonyListener::NotifyError(int32_t aCallIndex, return NS_OK; } +NS_IMETHODIMP +TelephonyListener::NotifyCdmaCallWaiting(const nsAString& aNumber) +{ + return NS_OK; +} + } // anonymous namespace BluetoothTelephonyListener::BluetoothTelephonyListener() diff --git a/dom/bluetooth/linux/BluetoothDBusService.cpp b/dom/bluetooth/linux/BluetoothDBusService.cpp index 2389ad43d9a0..2d833b91eda8 100644 --- a/dom/bluetooth/linux/BluetoothDBusService.cpp +++ b/dom/bluetooth/linux/BluetoothDBusService.cpp @@ -34,14 +34,16 @@ #include "nsDebug.h" #include "nsDataHashtable.h" #include "mozilla/Atomics.h" +#include "mozilla/dom/bluetooth/BluetoothTypes.h" #include "mozilla/Hal.h" #include "mozilla/ipc/UnixSocket.h" #include "mozilla/ipc/DBusThread.h" #include "mozilla/ipc/DBusUtils.h" #include "mozilla/ipc/RawDBusConnection.h" -#include "mozilla/Util.h" +#include "mozilla/Mutex.h" #include "mozilla/NullPtr.h" -#include "mozilla/dom/bluetooth/BluetoothTypes.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/Util.h" #if defined(MOZ_WIDGET_GONK) #include "cutils/properties.h" #endif @@ -75,6 +77,13 @@ USING_BLUETOOTH_NAMESPACE #define ERR_AVRCP_IS_DISCONNECTED "AvrcpIsDisconnected" #define ERR_UNKNOWN_PROFILE "UnknownProfileError" +/** + * To not lock Bluetooth switch button on Settings UI because of any accident, + * we will force disabling Bluetooth 5 seconds after the user requesting to + * turn off Bluetooth. + */ +#define TIMEOUT_FORCE_TO_DISABLE_BT 5 + typedef struct { const char* name; int type; @@ -159,8 +168,10 @@ static const char* sBluetoothDBusSignals[] = static nsRefPtr gThreadConnection; static nsDataHashtable sPairingReqTable; static nsDataHashtable sAuthorizeReqTable; -static Atomic sIsPairing; static nsString sAdapterPath; +static Atomic sIsPairing(0); +static int sConnectedDeviceCount = 0; +static Monitor sStopBluetoothMonitor("BluetoothService.sStopBluetoothMonitor"); typedef void (*UnpackFunc)(DBusMessage*, DBusError*, BluetoothValue&, nsAString&); typedef bool (*FilterFunc)(const BluetoothValue&); @@ -1499,6 +1510,19 @@ EventFilter(DBusConnection* aConn, DBusMessage* aMsg, void* aData) signal.path() = NS_LITERAL_STRING(KEY_ADAPTER); signal.value() = parameters; NS_DispatchToMainThread(new DistributeBluetoothSignalTask(signal)); + } else if (property.name().EqualsLiteral("Connected")) { + MonitorAutoLock lock(sStopBluetoothMonitor); + + if (property.value().get_bool()) { + ++sConnectedDeviceCount; + } else { + MOZ_ASSERT(sConnectedDeviceCount > 0); + + --sConnectedDeviceCount; + if (sConnectedDeviceCount == 0) { + lock.Notify(); + } + } } } else if (dbus_message_is_signal(aMsg, DBUS_MANAGER_IFACE, "AdapterAdded")) { const char* str; @@ -1689,11 +1713,11 @@ BluetoothDBusService::StopInternal() // This could block. It should never be run on the main thread. MOZ_ASSERT(!NS_IsMainThread()); - // If Bluetooth is turned off while connections exist, in order not to only - // disconnect with profile connections with low level ACL connections alive, - // we disconnect ACLs directly instead of closing each socket. - if (!sAdapterPath.IsEmpty()) { - DisconnectAllAcls(sAdapterPath); + { + MonitorAutoLock lock(sStopBluetoothMonitor); + if (sConnectedDeviceCount > 0) { + lock.Wait(PR_SecondsToInterval(TIMEOUT_FORCE_TO_DISABLE_BT)); + } } if (!mConnection) { @@ -1737,6 +1761,7 @@ BluetoothDBusService::StopInternal() sAuthorizeReqTable.Clear(); sIsPairing = 0; + sConnectedDeviceCount = 0; StopDBus(); return NS_OK; diff --git a/dom/browser-element/mochitest/browserElementTestHelpers.js b/dom/browser-element/mochitest/browserElementTestHelpers.js index 94bec68cac60..5b1f40c7fbac 100644 --- a/dom/browser-element/mochitest/browserElementTestHelpers.js +++ b/dom/browser-element/mochitest/browserElementTestHelpers.js @@ -206,7 +206,7 @@ function expectMozbrowserEvent(iframe, eventName) { // Set some prefs: // -// * browser.pagethumbnails.capturing_disabled: false +// * browser.pagethumbnails.capturing_disabled: true // // Disable tab view; it seriously messes us up. // @@ -244,7 +244,7 @@ function expectMozbrowserEvent(iframe, eventName) { browserElementTestHelpers.lockTestReady(); SpecialPowers.setBoolPref("network.disable.ipc.security", true); - SpecialPowers.pushPrefEnv({set: [["browser.pagethumbnails.capturing_disabled", false], + SpecialPowers.pushPrefEnv({set: [["browser.pagethumbnails.capturing_disabled", true], ["dom.ipc.browser_frames.oop_by_default", oop], ["dom.ipc.tabs.disabled", false], ["security.mixed_content.block_active_content", false]]}, diff --git a/dom/devicestorage/ipc/test_ipc.html b/dom/devicestorage/ipc/test_ipc.html index 12a5df835255..068b83a9f32a 100644 --- a/dom/devicestorage/ipc/test_ipc.html +++ b/dom/devicestorage/ipc/test_ipc.html @@ -158,7 +158,7 @@ ["dom.ipc.browser_frames.oop_by_default", true], ["dom.mozBrowserFramesEnabled", true], - ["browser.pagethumbnails.capturing_disabled", false] + ["browser.pagethumbnails.capturing_disabled", true] ] }, runTests); }); diff --git a/dom/permission/tests/test_telephony.html b/dom/permission/tests/test_telephony.html index d459e0325530..1c0917ec6926 100644 --- a/dom/permission/tests/test_telephony.html +++ b/dom/permission/tests/test_telephony.html @@ -22,7 +22,7 @@ var gData = [ perm: ["telephony"], needParentPerm: true, obj: "mozTelephony", - idl: "nsIDOMTelephony", + webidl: "Telephony", }, ] diff --git a/dom/system/gonk/RILContentHelper.js b/dom/system/gonk/RILContentHelper.js index 7334e7c86c13..f50588f4842b 100644 --- a/dom/system/gonk/RILContentHelper.js +++ b/dom/system/gonk/RILContentHelper.js @@ -105,7 +105,8 @@ const RIL_IPC_MSG_NAMES = [ "RIL:ReadIccContacts", "RIL:UpdateIccContact", "RIL:SetRoamingPreference", - "RIL:GetRoamingPreference" + "RIL:GetRoamingPreference", + "RIL:CdmaCallWaiting" ]; XPCOMUtils.defineLazyServiceGetter(this, "cpmm", @@ -1600,6 +1601,11 @@ RILContentHelper.prototype = { this.handleSimpleRequest(msg.json.requestId, msg.json.errorMsg, msg.json.mode); break; + case "RIL:CdmaCallWaiting": + this._deliverEvent("_telephonyListeners", + "notifyCdmaCallWaiting", + [msg.json.data]); + break; } }, diff --git a/dom/system/gonk/RadioInterfaceLayer.js b/dom/system/gonk/RadioInterfaceLayer.js index 4035d906d7f6..8339b28af5ad 100644 --- a/dom/system/gonk/RadioInterfaceLayer.js +++ b/dom/system/gonk/RadioInterfaceLayer.js @@ -943,6 +943,10 @@ RadioInterface.prototype = { // This one will handle its own notifications. this.handleCallDisconnected(message.call); break; + case "cdmaCallWaiting": + gMessageManager.sendTelephonyMessage("RIL:CdmaCallWaiting", + this.clientId, message.number); + break; case "enumerateCalls": // This one will handle its own notifications. this.handleEnumerateCalls(message); @@ -1829,7 +1833,8 @@ RadioInterface.prototype = { */ handleSuppSvcNotification: function handleSuppSvcNotification(message) { message.notification = convertRILSuppSvcNotification(message.notification); - this._sendTelephonyMessage("RIL:SuppSvcNotification", message); + gMessageManager.sendTelephonyMessage("RIL:SuppSvcNotification", + this.clientId, message); }, /** diff --git a/dom/system/gonk/ril_worker.js b/dom/system/gonk/ril_worker.js index 55aae6b6a736..c1bc1ae3da24 100644 --- a/dom/system/gonk/ril_worker.js +++ b/dom/system/gonk/ril_worker.js @@ -1955,7 +1955,13 @@ let RIL = { holdCall: function holdCall(options) { let call = this.currentCalls[options.callIndex]; if (call && call.state == CALL_STATE_ACTIVE) { - Buf.simpleRequest(REQUEST_SWITCH_HOLDING_AND_ACTIVE); + if (this._isCdma) { + Buf.newParcel(REQUEST_CDMA_FLASH); + Buf.writeString(""); + Buf.sendParcel(); + } else { + Buf.simpleRequest(REQUEST_SWITCH_HOLDING_AND_ACTIVE); + } } }, @@ -6179,7 +6185,19 @@ RIL[UNSOLICITED_RESPONSE_NEW_BROADCAST_SMS] = function UNSOLICITED_RESPONSE_NEW_ RIL[UNSOLICITED_CDMA_RUIM_SMS_STORAGE_FULL] = null; RIL[UNSOLICITED_RESTRICTED_STATE_CHANGED] = null; RIL[UNSOLICITED_ENTER_EMERGENCY_CALLBACK_MODE] = null; -RIL[UNSOLICITED_CDMA_CALL_WAITING] = null; +RIL[UNSOLICITED_CDMA_CALL_WAITING] = function UNSOLICITED_CDMA_CALL_WAITING(length) { + let call = {}; + call.number = Buf.readString(); + call.numberPresentation = Buf.readUint32(); + call.name = Buf.readString(); + call.namePresentation = Buf.readUint32(); + call.isPresent = Buf.readUint32(); + call.signalType = Buf.readUint32(); + call.alertPitch = Buf.readUint32(); + call.signal = Buf.readUint32(); + this.sendChromeMessage({rilMessageType: "cdmaCallWaiting", + number: call.number}); +}; RIL[UNSOLICITED_CDMA_OTA_PROVISION_STATUS] = null; RIL[UNSOLICITED_CDMA_INFO_REC] = null; RIL[UNSOLICITED_OEM_HOOK_RAW] = null; diff --git a/dom/telephony/CallEvent.cpp b/dom/telephony/CallEvent.cpp new file mode 100644 index 000000000000..a479312b23aa --- /dev/null +++ b/dom/telephony/CallEvent.cpp @@ -0,0 +1,73 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "CallEvent.h" +#include "mozilla/dom/CallEventBinding.h" + +#include "TelephonyCall.h" + +USING_TELEPHONY_NAMESPACE +using namespace mozilla::dom; + +/* static */ +already_AddRefed +CallEvent::Create(EventTarget* aOwner, const nsAString& aType, + TelephonyCall* aCall, bool aCanBubble, + bool aCancelable) +{ + nsRefPtr event = new CallEvent(aOwner, nullptr, nullptr); + event->mCall = aCall; + event->InitEvent(aType, aCanBubble, aCancelable); + return event.forget(); +} + +JSObject* +CallEvent::WrapObject(JSContext* aCx, JS::Handle aScope) +{ + return CallEventBinding::Wrap(aCx, aScope, this); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(CallEvent) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(CallEvent, nsDOMEvent) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mCall) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(CallEvent, nsDOMEvent) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCall) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_ADDREF_INHERITED(CallEvent, nsDOMEvent) +NS_IMPL_RELEASE_INHERITED(CallEvent, nsDOMEvent) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(CallEvent) +NS_INTERFACE_MAP_END_INHERITING(nsDOMEvent) + +// WebIDL + +/* static */ +already_AddRefed +CallEvent::Constructor(const GlobalObject& aGlobal, const nsAString& aType, + const CallEventInit& aOptions, ErrorResult& aRv) +{ + nsCOMPtr target = do_QueryInterface(aGlobal.Get()); + + if (!target) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + nsRefPtr event = Create(target, aType, aOptions.mCall, false, false); + + return event.forget(); +} + +already_AddRefed +CallEvent::GetCall() const +{ + nsRefPtr call = mCall; + return call.forget(); +} diff --git a/dom/telephony/CallEvent.h b/dom/telephony/CallEvent.h new file mode 100644 index 000000000000..61125968b3b4 --- /dev/null +++ b/dom/telephony/CallEvent.h @@ -0,0 +1,61 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_telephony_callevent_h +#define mozilla_dom_telephony_callevent_h + +#include "TelephonyCommon.h" + +#include "nsDOMEvent.h" + +namespace mozilla { +namespace dom { +struct CallEventInit; +} +} + +BEGIN_TELEPHONY_NAMESPACE + +class CallEvent MOZ_FINAL : public nsDOMEvent +{ + nsRefPtr mCall; + +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(CallEvent, nsDOMEvent) + NS_FORWARD_TO_NSDOMEVENT + + virtual JSObject* + WrapObject(JSContext* aCx, JS::Handle aScope) MOZ_OVERRIDE; + + // WebIDL + static already_AddRefed + Constructor(const GlobalObject& aGlobal, const nsAString& aType, + const CallEventInit& aOptions, ErrorResult& aRv); + + already_AddRefed + GetCall() const; + + static already_AddRefed + Create(EventTarget* aOwner, const nsAString& aType, TelephonyCall* aCall, + bool aCanBubble, bool aCancelable); + +private: + CallEvent(EventTarget* aOwner, + nsPresContext* aPresContext, + nsEvent* aEvent) + : nsDOMEvent(aOwner, aPresContext, aEvent) + { + SetIsDOMBinding(); + } + + virtual ~CallEvent() + { } +}; + +END_TELEPHONY_NAMESPACE + +#endif // mozilla_dom_telephony_callevent_h diff --git a/dom/telephony/CallsList.cpp b/dom/telephony/CallsList.cpp new file mode 100644 index 000000000000..20776c4cc5bd --- /dev/null +++ b/dom/telephony/CallsList.cpp @@ -0,0 +1,69 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "CallsList.h" +#include "mozilla/dom/CallsListBinding.h" + +#include "Telephony.h" +#include "TelephonyCall.h" + +USING_TELEPHONY_NAMESPACE +using namespace mozilla::dom; + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_1(CallsList, mTelephony) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(CallsList) +NS_IMPL_CYCLE_COLLECTING_RELEASE(CallsList) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CallsList) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +CallsList::CallsList(Telephony* aTelephony) +: mTelephony(aTelephony) +{ + MOZ_ASSERT(mTelephony); + + SetIsDOMBinding(); +} + +CallsList::~CallsList() +{ +} + +nsPIDOMWindow* +CallsList::GetParentObject() const +{ + return mTelephony->GetOwner(); +} + +JSObject* +CallsList::WrapObject(JSContext* aCx, JS::Handle aScope) +{ + return CallsListBinding::Wrap(aCx, aScope, this); +} + +already_AddRefed +CallsList::Item(uint32_t aIndex) const +{ + nsRefPtr call = mTelephony->CallsArray().SafeElementAt(aIndex); + return call.forget(); +} + +uint32_t +CallsList::Length() const +{ + return mTelephony->CallsArray().Length(); +} + +already_AddRefed +CallsList::IndexedGetter(uint32_t aIndex, bool& aFound) const +{ + nsRefPtr call = mTelephony->CallsArray().SafeElementAt(aIndex); + aFound = call ? true : false; + return call.forget(); +} diff --git a/dom/telephony/CallsList.h b/dom/telephony/CallsList.h new file mode 100644 index 000000000000..75fc6616de37 --- /dev/null +++ b/dom/telephony/CallsList.h @@ -0,0 +1,50 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_telephony_CallsList_h__ +#define mozilla_dom_telephony_CallsList_h__ + +#include "TelephonyCommon.h" + +#include "nsWrapperCache.h" + +BEGIN_TELEPHONY_NAMESPACE + +class CallsList MOZ_FINAL : public nsISupports, + public nsWrapperCache +{ + nsRefPtr mTelephony; + +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(CallsList) + + CallsList(Telephony* aTelephony); + + nsPIDOMWindow* + GetParentObject() const; + + // WrapperCache + virtual JSObject* + WrapObject(JSContext* aCx, JS::Handle aScope) MOZ_OVERRIDE; + + // CallsList WebIDL + already_AddRefed + Item(uint32_t aIndex) const; + + uint32_t + Length() const; + + already_AddRefed + IndexedGetter(uint32_t aIndex, bool& aFound) const; + +private: + ~CallsList(); +}; + +END_TELEPHONY_NAMESPACE + +#endif // mozilla_dom_telephony_CallsList_h__ diff --git a/dom/telephony/Telephony.cpp b/dom/telephony/Telephony.cpp index cb98b5c1eac1..2ceb4723509e 100644 --- a/dom/telephony/Telephony.cpp +++ b/dom/telephony/Telephony.cpp @@ -5,23 +5,22 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "Telephony.h" +#include "mozilla/dom/TelephonyBinding.h" #include "nsIURI.h" -#include "nsIDOMCallEvent.h" #include "nsPIDOMWindow.h" #include "nsIPermissionManager.h" -#include "GeneratedEvents.h" -#include "jsapi.h" #include "nsCharSeparatedTokenizer.h" #include "nsContentUtils.h" #include "nsCxPusher.h" -#include "nsDOMClassInfo.h" #include "nsNetUtil.h" #include "nsServiceManagerUtils.h" #include "nsTArrayHelpers.h" #include "nsThreadUtils.h" +#include "CallEvent.h" +#include "CallsList.h" #include "TelephonyCall.h" #define NS_RILCONTENTHELPER_CONTRACTID "@mozilla.org/ril/content-helper;1" @@ -78,14 +77,15 @@ public: }; Telephony::Telephony() -: mActiveCall(nullptr), mCallsArray(nullptr), mRooted(false), - mEnumerated(false) +: mActiveCall(nullptr), mEnumerated(false) { if (!gTelephonyList) { gTelephonyList = new TelephonyList(); } gTelephonyList->AppendElement(this); + + SetIsDOMBinding(); } Telephony::~Telephony() @@ -98,11 +98,6 @@ Telephony::~Telephony() } } - if (mRooted) { - mCallsArray = nullptr; - NS_DROP_JS_OBJECTS(this, Telephony); - } - NS_ASSERTION(gTelephonyList, "This should never be null!"); NS_ASSERTION(gTelephonyList->Contains(this), "Should be in the list!"); @@ -115,6 +110,12 @@ Telephony::~Telephony() } } +JSObject* +Telephony::WrapObject(JSContext* aCx, JS::Handle aScope) +{ + return TelephonyBinding::Wrap(aCx, aScope, this); +} + // static already_AddRefed Telephony::Create(nsPIDOMWindow* aOwner, ErrorResult& aRv) @@ -146,6 +147,7 @@ Telephony::Create(nsPIDOMWindow* aOwner, ErrorResult& aRv) telephony->mProvider = ril; telephony->mListener = new Listener(telephony); + telephony->mCallsList = new CallsList(telephony); nsresult rv = ril->EnumerateCalls(telephony->mListener); if (NS_FAILED(rv)) { @@ -199,12 +201,15 @@ Telephony::NotifyCallsChanged(TelephonyCall* aCall) return DispatchCallEvent(NS_LITERAL_STRING("callschanged"), aCall); } -nsresult +already_AddRefed Telephony::DialInternal(bool isEmergency, const nsAString& aNumber, - nsIDOMTelephonyCall** aResult) + ErrorResult& aRv) { - NS_ENSURE_ARG(!aNumber.IsEmpty()); + if (aNumber.IsEmpty()) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return nullptr; + } for (uint32_t index = 0; index < mCalls.Length(); index++) { const nsRefPtr& tempCall = mCalls[index]; @@ -213,7 +218,8 @@ Telephony::DialInternal(bool isEmergency, // One call has been dialed already and we only support one outgoing call // at a time. NS_WARNING("Only permitted to dial one call at a time!"); - return NS_ERROR_NOT_AVAILABLE; + aRv.Throw(NS_ERROR_NOT_AVAILABLE); + return nullptr; } } @@ -223,7 +229,10 @@ Telephony::DialInternal(bool isEmergency, } else { rv = mProvider->Dial(aNumber); } - NS_ENSURE_SUCCESS(rv, rv); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return nullptr; + } nsRefPtr call = CreateNewDialingCall(aNumber); @@ -236,169 +245,114 @@ Telephony::DialInternal(bool isEmergency, } } - call.forget(aResult); - return NS_OK; + return call.forget(); } NS_IMPL_CYCLE_COLLECTION_CLASS(Telephony) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(Telephony, nsDOMEventTargetHelper) - NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS - for (uint32_t index = 0; index < tmp->mCalls.Length(); index++) { - NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mCalls[i]"); - cb.NoteXPCOMChild(tmp->mCalls[index]->ToISupports()); - } - // Don't traverse mListener because it doesn't keep any reference to - // Telephony but a raw pointer instead. + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCalls) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCallsList) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END -NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(Telephony, - nsDOMEventTargetHelper) - NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mCallsArray) -NS_IMPL_CYCLE_COLLECTION_TRACE_END - NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(Telephony, nsDOMEventTargetHelper) - tmp->mCalls.Clear(); tmp->mActiveCall = nullptr; - tmp->mCallsArray = nullptr; + NS_IMPL_CYCLE_COLLECTION_UNLINK(mCalls) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mCallsList) NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(Telephony) - NS_INTERFACE_MAP_ENTRY(nsIDOMTelephony) - NS_DOM_INTERFACE_MAP_ENTRY_CLASSINFO(Telephony) NS_INTERFACE_MAP_END_INHERITING(nsDOMEventTargetHelper) NS_IMPL_ADDREF_INHERITED(Telephony, nsDOMEventTargetHelper) NS_IMPL_RELEASE_INHERITED(Telephony, nsDOMEventTargetHelper) -DOMCI_DATA(Telephony, Telephony) - NS_IMPL_ISUPPORTS1(Telephony::Listener, nsITelephonyListener) -// nsIDOMTelephony +// Telephony WebIDL -NS_IMETHODIMP -Telephony::Dial(const nsAString& aNumber, nsIDOMTelephonyCall** aResult) +already_AddRefed +Telephony::Dial(const nsAString& aNumber, ErrorResult& aRv) { - DialInternal(false, aNumber, aResult); - - return NS_OK; + nsRefPtr call = DialInternal(false, aNumber, aRv); + return call.forget(); } -NS_IMETHODIMP -Telephony::DialEmergency(const nsAString& aNumber, nsIDOMTelephonyCall** aResult) +already_AddRefed +Telephony::DialEmergency(const nsAString& aNumber, ErrorResult& aRv) { - DialInternal(true, aNumber, aResult); - - return NS_OK; + nsRefPtr call = DialInternal(true, aNumber, aRv); + return call.forget(); } -NS_IMETHODIMP -Telephony::GetMuted(bool* aMuted) +bool +Telephony::GetMuted(ErrorResult& aRv) const { - nsresult rv = mProvider->GetMicrophoneMuted(aMuted); - NS_ENSURE_SUCCESS(rv, rv); + bool muted = false; + aRv = mProvider->GetMicrophoneMuted(&muted); - return NS_OK; + return muted; } -NS_IMETHODIMP -Telephony::SetMuted(bool aMuted) +void +Telephony::SetMuted(bool aMuted, ErrorResult& aRv) { - nsresult rv = mProvider->SetMicrophoneMuted(aMuted); - NS_ENSURE_SUCCESS(rv, rv); - - return NS_OK; + aRv = mProvider->SetMicrophoneMuted(aMuted); } -NS_IMETHODIMP -Telephony::GetSpeakerEnabled(bool* aSpeakerEnabled) +bool +Telephony::GetSpeakerEnabled(ErrorResult& aRv) const { - nsresult rv = mProvider->GetSpeakerEnabled(aSpeakerEnabled); - NS_ENSURE_SUCCESS(rv, rv); + bool enabled = false; + aRv = mProvider->GetSpeakerEnabled(&enabled); - return NS_OK; + return enabled; } -NS_IMETHODIMP -Telephony::SetSpeakerEnabled(bool aSpeakerEnabled) +void +Telephony::SetSpeakerEnabled(bool aEnabled, ErrorResult& aRv) { - nsresult rv = mProvider->SetSpeakerEnabled(aSpeakerEnabled); - NS_ENSURE_SUCCESS(rv, rv); - - return NS_OK; + aRv = mProvider->SetSpeakerEnabled(aEnabled); } -NS_IMETHODIMP -Telephony::GetActive(nsIDOMTelephonyCall** aActive) +already_AddRefed +Telephony::GetActive() const { - nsCOMPtr activeCall = mActiveCall; - activeCall.forget(aActive); - return NS_OK; + nsCOMPtr activeCall = mActiveCall; + return activeCall.forget(); } -NS_IMETHODIMP -Telephony::GetCalls(JS::Value* aCalls) +already_AddRefed +Telephony::Calls() const { - JSObject* calls = mCallsArray; - if (!calls) { - nsresult rv; - nsIScriptContext* sc = GetContextForEventHandlers(&rv); - NS_ENSURE_SUCCESS(rv, rv); - AutoPushJSContext cx(sc ? sc->GetNativeContext() : nullptr); - if (sc) { - rv = nsTArrayToJSArray(cx, mCalls, &calls); - NS_ENSURE_SUCCESS(rv, rv); - - if (!mRooted) { - NS_HOLD_JS_OBJECTS(this, Telephony); - mRooted = true; - } - - mCallsArray = calls; - } else { - NS_ENSURE_SUCCESS(rv, rv); - } - } - - aCalls->setObject(*calls); - return NS_OK; + nsRefPtr list = mCallsList; + return list.forget(); } -NS_IMETHODIMP -Telephony::StartTone(const nsAString& aDTMFChar) +void +Telephony::StartTone(const nsAString& aDTMFChar, ErrorResult& aRv) { if (aDTMFChar.IsEmpty()) { NS_WARNING("Empty tone string will be ignored"); - return NS_OK; + return; } if (aDTMFChar.Length() > 1) { - return NS_ERROR_INVALID_ARG; + aRv.Throw(NS_ERROR_INVALID_ARG); + return; } - nsresult rv = mProvider->StartTone(aDTMFChar); - NS_ENSURE_SUCCESS(rv, rv); - - return NS_OK; + aRv = mProvider->StartTone(aDTMFChar); } -NS_IMETHODIMP -Telephony::StopTone() +void +Telephony::StopTone(ErrorResult& aRv) { - nsresult rv = mProvider->StopTone(); - NS_ENSURE_SUCCESS(rv, rv); - - return NS_OK; + aRv = mProvider->StopTone(); } -NS_IMPL_EVENT_HANDLER(Telephony, incoming) -NS_IMPL_EVENT_HANDLER(Telephony, callschanged) -NS_IMPL_EVENT_HANDLER(Telephony, remoteheld) -NS_IMPL_EVENT_HANDLER(Telephony, remoteresumed) - // EventTarget void @@ -598,9 +552,21 @@ Telephony::NotifyError(int32_t aCallIndex, return NS_OK; } +NS_IMETHODIMP +Telephony::NotifyCdmaCallWaiting(const nsAString& aNumber) +{ + MOZ_ASSERT(mActiveCall && + mActiveCall->CallState() == nsITelephonyProvider::CALL_STATE_CONNECTED); + + nsRefPtr callToNotify = mActiveCall; + callToNotify->UpdateSecondNumber(aNumber); + DispatchCallEvent(NS_LITERAL_STRING("callschanged"), callToNotify); + return NS_OK; +} + nsresult Telephony::DispatchCallEvent(const nsAString& aType, - nsIDOMTelephonyCall* aCall) + TelephonyCall* aCall) { // The call may be null in following cases: // 1. callschanged when notifying enumeration being completed @@ -610,16 +576,9 @@ Telephony::DispatchCallEvent(const nsAString& aType, aType.EqualsLiteral("remoteheld") || aType.EqualsLiteral("remtoeresumed")); - nsCOMPtr event; - NS_NewDOMCallEvent(getter_AddRefs(event), this, nullptr, nullptr); - NS_ASSERTION(event, "This should never fail!"); + nsRefPtr event = CallEvent::Create(this, aType, aCall, false, false); - nsCOMPtr callEvent = do_QueryInterface(event); - MOZ_ASSERT(callEvent); - nsresult rv = callEvent->InitCallEvent(aType, false, false, aCall); - NS_ENSURE_SUCCESS(rv, rv); - - return DispatchTrustedEvent(callEvent); + return DispatchTrustedEvent(event); } void diff --git a/dom/telephony/Telephony.h b/dom/telephony/Telephony.h index 1062c2634f5d..1cf49693af25 100644 --- a/dom/telephony/Telephony.h +++ b/dom/telephony/Telephony.h @@ -12,17 +12,13 @@ // assume they see the definition of TelephonyCall. #include "TelephonyCall.h" -#include "nsIDOMTelephony.h" -#include "nsIDOMTelephonyCall.h" #include "nsITelephonyProvider.h" -class nsIScriptContext; class nsPIDOMWindow; BEGIN_TELEPHONY_NAMESPACE -class Telephony : public nsDOMEventTargetHelper, - public nsIDOMTelephony +class Telephony MOZ_FINAL : public nsDOMEventTargetHelper { /** * Class Telephony doesn't actually inherit nsITelephonyListener. @@ -41,40 +37,73 @@ class Telephony : public nsDOMEventTargetHelper, TelephonyCall* mActiveCall; nsTArray > mCalls; + nsRefPtr mCallsList; - // Cached calls array object. Cleared whenever mCalls changes and then rebuilt - // once a page looks for the liveCalls attribute. - JS::Heap mCallsArray; - - bool mRooted; bool mEnumerated; public: NS_DECL_ISUPPORTS_INHERITED - NS_DECL_NSIDOMTELEPHONY NS_DECL_NSITELEPHONYLISTENER NS_REALLY_FORWARD_NSIDOMEVENTTARGET(nsDOMEventTargetHelper) - NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED( - Telephony, - nsDOMEventTargetHelper) + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(Telephony, + nsDOMEventTargetHelper) + + nsPIDOMWindow* + GetParentObject() const + { + return GetOwner(); + } + + // WrapperCache + virtual JSObject* + WrapObject(JSContext* aCx, JS::Handle aScope) MOZ_OVERRIDE; + + // WebIDL + already_AddRefed + Dial(const nsAString& aNumber, ErrorResult& aRv); + + already_AddRefed + DialEmergency(const nsAString& aNumber, ErrorResult& aRv); + + bool + GetMuted(ErrorResult& aRv) const; + + void + SetMuted(bool aMuted, ErrorResult& aRv); + + bool + GetSpeakerEnabled(ErrorResult& aRv) const; + + void + SetSpeakerEnabled(bool aEnabled, ErrorResult& aRv); + + already_AddRefed + GetActive() const; + + already_AddRefed + Calls() const; + + void + StartTone(const nsAString& aDTMF, ErrorResult& aRv); + + void + StopTone(ErrorResult& aRv); + + IMPL_EVENT_HANDLER(incoming) + IMPL_EVENT_HANDLER(callschanged) + IMPL_EVENT_HANDLER(remoteheld) + IMPL_EVENT_HANDLER(remoteresumed) static already_AddRefed Create(nsPIDOMWindow* aOwner, ErrorResult& aRv); static bool CheckPermission(nsPIDOMWindow* aOwner); - nsISupports* - ToISupports() - { - return static_cast(this); - } - void AddCall(TelephonyCall* aCall) { NS_ASSERTION(!mCalls.Contains(aCall), "Already know about this one!"); mCalls.AppendElement(aCall); - mCallsArray = nullptr; NotifyCallsChanged(aCall); } @@ -83,7 +112,6 @@ public: { NS_ASSERTION(mCalls.Contains(aCall), "Didn't know about this one!"); mCalls.RemoveElement(aCall); - mCallsArray = nullptr; NotifyCallsChanged(aCall); } @@ -93,6 +121,12 @@ public: return mProvider; } + const nsTArray >& + CallsArray() const + { + return mCalls; + } + virtual void EventListenerAdded(nsIAtom* aType) MOZ_OVERRIDE; private: @@ -108,14 +142,14 @@ private: nsresult NotifyCallsChanged(TelephonyCall* aCall); - nsresult + already_AddRefed DialInternal(bool isEmergency, const nsAString& aNumber, - nsIDOMTelephonyCall** aResult); + ErrorResult& aRv); nsresult DispatchCallEvent(const nsAString& aType, - nsIDOMTelephonyCall* aCall); + TelephonyCall* aCall); void EnqueueEnumerationAck(); diff --git a/dom/telephony/TelephonyCall.cpp b/dom/telephony/TelephonyCall.cpp index d4785cb6b296..0f376fe2bdab 100644 --- a/dom/telephony/TelephonyCall.cpp +++ b/dom/telephony/TelephonyCall.cpp @@ -5,16 +5,15 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "TelephonyCall.h" - -#include "nsIDOMCallEvent.h" +#include "mozilla/dom/TelephonyCallBinding.h" #include "mozilla/dom/DOMError.h" -#include "GeneratedEvents.h" -#include "nsDOMClassInfo.h" + +#include "CallEvent.h" #include "Telephony.h" -#include "nsITelephonyProvider.h" USING_TELEPHONY_NAMESPACE +using namespace mozilla::dom; // static already_AddRefed @@ -46,6 +45,17 @@ TelephonyCall::TelephonyCall() mLive(false), mOutgoing(false) { + SetIsDOMBinding(); +} + +TelephonyCall::~TelephonyCall() +{ +} + +JSObject* +TelephonyCall::WrapObject(JSContext* aCx, JS::Handle aScope) +{ + return TelephonyCallBinding::Wrap(aCx, aScope, this); } void @@ -124,20 +134,13 @@ TelephonyCall::ChangeStateInternal(uint16_t aCallState, bool aFireEvents) nsresult TelephonyCall::DispatchCallEvent(const nsAString& aType, - nsIDOMTelephonyCall* aCall) + TelephonyCall* aCall) { MOZ_ASSERT(aCall); - nsCOMPtr event; - NS_NewDOMCallEvent(getter_AddRefs(event), this, nullptr, nullptr); - NS_ASSERTION(event, "This should never fail!"); + nsRefPtr event = CallEvent::Create(this, aType, aCall, false, false); - nsCOMPtr callEvent = do_QueryInterface(event); - MOZ_ASSERT(callEvent); - nsresult rv = callEvent->InitCallEvent(aType, false, false, aCall); - NS_ENSURE_SUCCESS(rv, rv); - - return DispatchTrustedEvent(callEvent); + return DispatchTrustedEvent(event); } void @@ -163,114 +166,87 @@ NS_IMPL_CYCLE_COLLECTION_INHERITED_2(TelephonyCall, mError); NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(TelephonyCall) - NS_INTERFACE_MAP_ENTRY(nsIDOMTelephonyCall) - NS_DOM_INTERFACE_MAP_ENTRY_CLASSINFO(TelephonyCall) NS_INTERFACE_MAP_END_INHERITING(nsDOMEventTargetHelper) NS_IMPL_ADDREF_INHERITED(TelephonyCall, nsDOMEventTargetHelper) NS_IMPL_RELEASE_INHERITED(TelephonyCall, nsDOMEventTargetHelper) -DOMCI_DATA(TelephonyCall, TelephonyCall) +// TelephonyCall WebIDL -NS_IMETHODIMP -TelephonyCall::GetNumber(nsAString& aNumber) +already_AddRefed +TelephonyCall::GetError() const { - aNumber.Assign(mNumber); - return NS_OK; + nsRefPtr error = mError; + return error.forget(); } -NS_IMETHODIMP -TelephonyCall::GetState(nsAString& aState) -{ - aState.Assign(mState); - return NS_OK; -} - -NS_IMETHODIMP -TelephonyCall::GetEmergency(bool* aEmergency) -{ - *aEmergency = mEmergency; - return NS_OK; -} - -NS_IMETHODIMP -TelephonyCall::GetError(nsISupports** aError) -{ - NS_IF_ADDREF(*aError = mError); - return NS_OK; -} - -NS_IMETHODIMP -TelephonyCall::Answer() +void +TelephonyCall::Answer(ErrorResult& aRv) { if (mCallState != nsITelephonyProvider::CALL_STATE_INCOMING) { NS_WARNING("Answer on non-incoming call ignored!"); - return NS_OK; + return; } nsresult rv = mTelephony->Provider()->AnswerCall(mCallIndex); - NS_ENSURE_SUCCESS(rv, rv); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return; + } ChangeStateInternal(nsITelephonyProvider::CALL_STATE_CONNECTING, true); - return NS_OK; } -NS_IMETHODIMP -TelephonyCall::HangUp() +void +TelephonyCall::HangUp(ErrorResult& aRv) { if (mCallState == nsITelephonyProvider::CALL_STATE_DISCONNECTING || mCallState == nsITelephonyProvider::CALL_STATE_DISCONNECTED) { NS_WARNING("HangUp on previously disconnected call ignored!"); - return NS_OK; + return; } nsresult rv = mCallState == nsITelephonyProvider::CALL_STATE_INCOMING ? mTelephony->Provider()->RejectCall(mCallIndex) : mTelephony->Provider()->HangUp(mCallIndex); - NS_ENSURE_SUCCESS(rv, rv); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return; + } ChangeStateInternal(nsITelephonyProvider::CALL_STATE_DISCONNECTING, true); - return NS_OK; } -NS_IMETHODIMP -TelephonyCall::Hold() +void +TelephonyCall::Hold(ErrorResult& aRv) { if (mCallState != nsITelephonyProvider::CALL_STATE_CONNECTED) { NS_WARNING("Hold non-connected call ignored!"); - return NS_OK; + return; } nsresult rv = mTelephony->Provider()->HoldCall(mCallIndex); - NS_ENSURE_SUCCESS(rv,rv); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return; + } ChangeStateInternal(nsITelephonyProvider::CALL_STATE_HOLDING, true); - return NS_OK; } -NS_IMETHODIMP -TelephonyCall::Resume() +void +TelephonyCall::Resume(ErrorResult& aRv) { if (mCallState != nsITelephonyProvider::CALL_STATE_HELD) { NS_WARNING("Resume non-held call ignored!"); - return NS_OK; + return; } nsresult rv = mTelephony->Provider()->ResumeCall(mCallIndex); - NS_ENSURE_SUCCESS(rv,rv); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return; + } ChangeStateInternal(nsITelephonyProvider::CALL_STATE_RESUMING, true); - return NS_OK; } - -NS_IMPL_EVENT_HANDLER(TelephonyCall, statechange) -NS_IMPL_EVENT_HANDLER(TelephonyCall, dialing) -NS_IMPL_EVENT_HANDLER(TelephonyCall, alerting) -NS_IMPL_EVENT_HANDLER(TelephonyCall, connecting) -NS_IMPL_EVENT_HANDLER(TelephonyCall, connected) -NS_IMPL_EVENT_HANDLER(TelephonyCall, disconnecting) -NS_IMPL_EVENT_HANDLER(TelephonyCall, disconnected) -NS_IMPL_EVENT_HANDLER(TelephonyCall, holding) -NS_IMPL_EVENT_HANDLER(TelephonyCall, held) -NS_IMPL_EVENT_HANDLER(TelephonyCall, resuming) -NS_IMPL_EVENT_HANDLER(TelephonyCall, error) diff --git a/dom/telephony/TelephonyCall.h b/dom/telephony/TelephonyCall.h index f244b6acb88c..9b213de05e5a 100644 --- a/dom/telephony/TelephonyCall.h +++ b/dom/telephony/TelephonyCall.h @@ -9,19 +9,18 @@ #include "TelephonyCommon.h" -#include "nsIDOMTelephonyCall.h" #include "mozilla/dom/DOMError.h" class nsPIDOMWindow; BEGIN_TELEPHONY_NAMESPACE -class TelephonyCall : public nsDOMEventTargetHelper, - public nsIDOMTelephonyCall +class TelephonyCall MOZ_FINAL : public nsDOMEventTargetHelper { nsRefPtr mTelephony; nsString mNumber; + nsString mSecondNumber; nsString mState; bool mEmergency; nsRefPtr mError; @@ -33,22 +32,77 @@ class TelephonyCall : public nsDOMEventTargetHelper, public: NS_DECL_ISUPPORTS_INHERITED - NS_DECL_NSIDOMTELEPHONYCALL NS_REALLY_FORWARD_NSIDOMEVENTTARGET(nsDOMEventTargetHelper) NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(TelephonyCall, nsDOMEventTargetHelper) + nsPIDOMWindow* + GetParentObject() const + { + return GetOwner(); + } + + // WrapperCache + virtual JSObject* + WrapObject(JSContext* aCx, JS::Handle aScope) MOZ_OVERRIDE; + + // WebIDL + void + GetNumber(nsString& aNumber) const + { + aNumber.Assign(mNumber); + } + + void + GetSecondNumber(nsString& aSecondNumber) const + { + aSecondNumber.Assign(mSecondNumber); + } + + void + GetState(nsString& aState) const + { + aState.Assign(mState); + } + + bool + Emergency() const + { + return mEmergency; + } + + already_AddRefed + GetError() const; + + void + Answer(ErrorResult& aRv); + + void + HangUp(ErrorResult& aRv); + + void + Hold(ErrorResult& aRv); + + void + Resume(ErrorResult& aRv); + + IMPL_EVENT_HANDLER(statechange) + IMPL_EVENT_HANDLER(dialing) + IMPL_EVENT_HANDLER(alerting) + IMPL_EVENT_HANDLER(connecting) + IMPL_EVENT_HANDLER(connected) + IMPL_EVENT_HANDLER(disconnecting) + IMPL_EVENT_HANDLER(disconnected) + IMPL_EVENT_HANDLER(holding) + IMPL_EVENT_HANDLER(held) + IMPL_EVENT_HANDLER(resuming) + IMPL_EVENT_HANDLER(error) + static already_AddRefed Create(Telephony* aTelephony, const nsAString& aNumber, uint16_t aCallState, uint32_t aCallIndex = kOutgoingPlaceholderCallIndex, bool aEmergency = false); - nsISupports* - ToISupports() - { - return static_cast(this); - } - void ChangeState(uint16_t aCallState) { @@ -81,6 +135,12 @@ public: mEmergency = aEmergency; } + void + UpdateSecondNumber(const nsAString& aNumber) + { + mSecondNumber = aNumber; + } + bool IsOutgoing() const { @@ -93,15 +153,14 @@ public: private: TelephonyCall(); - ~TelephonyCall() - { } + ~TelephonyCall(); void ChangeStateInternal(uint16_t aCallState, bool aFireEvents); nsresult DispatchCallEvent(const nsAString& aType, - nsIDOMTelephonyCall* aCall); + TelephonyCall* aCall); }; END_TELEPHONY_NAMESPACE diff --git a/dom/telephony/TelephonyCommon.h b/dom/telephony/TelephonyCommon.h index 41e7467da1d2..cbd4a7f9dd4f 100644 --- a/dom/telephony/TelephonyCommon.h +++ b/dom/telephony/TelephonyCommon.h @@ -7,6 +7,8 @@ #ifndef mozilla_dom_telephony_telephonycommon_h__ #define mozilla_dom_telephony_telephonycommon_h__ +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" #include "nsAutoPtr.h" #include "nsCOMPtr.h" #include "nsCycleCollectionParticipant.h" @@ -22,15 +24,13 @@ #define USING_TELEPHONY_NAMESPACE \ using namespace mozilla::dom::telephony; -class nsIDOMTelephony; -class nsIDOMTelephonyCall; - BEGIN_TELEPHONY_NAMESPACE enum { kOutgoingPlaceholderCallIndex = UINT32_MAX }; +class CallsList; class Telephony; class TelephonyCall; diff --git a/dom/telephony/moz.build b/dom/telephony/moz.build index 21262d97bcc0..8a06b1bbcc26 100644 --- a/dom/telephony/moz.build +++ b/dom/telephony/moz.build @@ -5,9 +5,6 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. XPIDL_SOURCES += [ - 'nsIDOMCallEvent.idl', - 'nsIDOMTelephony.idl', - 'nsIDOMTelephonyCall.idl', 'nsITelephonyProvider.idl', ] @@ -22,6 +19,8 @@ XPIDL_FLAGS += [ MODULE = 'dom' CPP_SOURCES += [ + 'CallEvent.cpp', + 'CallsList.cpp', 'Telephony.cpp', 'TelephonyCall.cpp', ] diff --git a/dom/telephony/nsIDOMCallEvent.idl b/dom/telephony/nsIDOMCallEvent.idl deleted file mode 100644 index 1ae8185482f8..000000000000 --- a/dom/telephony/nsIDOMCallEvent.idl +++ /dev/null @@ -1,24 +0,0 @@ -/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ -/* vim: set ts=2 et sw=2 tw=40: */ -/* 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/. */ - -#include "nsIDOMEvent.idl" - -interface nsIDOMTelephonyCall; - -[scriptable, builtinclass, uuid(476aacec-661e-44ec-80b4-4b7292b927b5)] -interface nsIDOMCallEvent : nsIDOMEvent -{ - readonly attribute nsIDOMTelephonyCall call; - [noscript] void initCallEvent(in DOMString aType, - in boolean aCanBubble, - in boolean aCancelable, - in nsIDOMTelephonyCall aCall); -}; - -dictionary CallEventInit : EventInit -{ - nsIDOMTelephonyCall call; -}; diff --git a/dom/telephony/nsIDOMTelephony.idl b/dom/telephony/nsIDOMTelephony.idl deleted file mode 100644 index 334059550270..000000000000 --- a/dom/telephony/nsIDOMTelephony.idl +++ /dev/null @@ -1,35 +0,0 @@ -/* -*- Mode: IDL; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* vim: set ts=8 sts=2 et sw=2 tw=80: */ -/* 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/. */ - -#include "nsIDOMEventTarget.idl" - -interface nsIDOMEventListener; -interface nsIDOMTelephonyCall; - -[scriptable, builtinclass, uuid(5ad8bf8b-958c-447b-9e1a-e6cf598b680f)] -interface nsIDOMTelephony : nsIDOMEventTarget -{ - nsIDOMTelephonyCall dial(in DOMString number); - nsIDOMTelephonyCall dialEmergency(in DOMString number); - - attribute boolean muted; - attribute boolean speakerEnabled; - - // The call that is "active", i.e. receives microphone input and tones - // generated via startTone. - readonly attribute nsIDOMTelephonyCall active; - - // Array of all calls that are currently connected. - readonly attribute jsval calls; - - void startTone(in DOMString tone); - void stopTone(); - - [implicit_jscontext] attribute jsval onincoming; - [implicit_jscontext] attribute jsval oncallschanged; - [implicit_jscontext] attribute jsval onremoteheld; - [implicit_jscontext] attribute jsval onremoteresumed; -}; diff --git a/dom/telephony/nsIDOMTelephonyCall.idl b/dom/telephony/nsIDOMTelephonyCall.idl deleted file mode 100644 index 3b6ccd3b5d30..000000000000 --- a/dom/telephony/nsIDOMTelephonyCall.idl +++ /dev/null @@ -1,44 +0,0 @@ -/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ -/* vim: set ts=2 et sw=2 tw=40: */ -/* 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/. */ - -#include "nsIDOMEventTarget.idl" - -interface nsIDOMEventListener; - -[scriptable, builtinclass, uuid(74d240f5-a379-4ac0-a085-a7f714189a27)] -interface nsIDOMTelephonyCall : nsIDOMEventTarget -{ - readonly attribute DOMString number; - - readonly attribute DOMString state; - - // The property "emergency" indicate whether the call number is an emergency - // number. Only the outgoing call could have a value with true and it is - // available after dialing state. - readonly attribute boolean emergency; - - // This is a DOMError - readonly attribute nsISupports error; - - void answer(); - void hangUp(); - void hold(); - void resume(); - - [implicit_jscontext] attribute jsval onstatechange; - - [implicit_jscontext] attribute jsval ondialing; - [implicit_jscontext] attribute jsval onalerting; - [implicit_jscontext] attribute jsval onconnecting; - [implicit_jscontext] attribute jsval onconnected; - [implicit_jscontext] attribute jsval ondisconnecting; - [implicit_jscontext] attribute jsval ondisconnected; - [implicit_jscontext] attribute jsval onholding; - [implicit_jscontext] attribute jsval onheld; - [implicit_jscontext] attribute jsval onresuming; - - [implicit_jscontext] attribute jsval onerror; -}; diff --git a/dom/telephony/nsITelephonyProvider.idl b/dom/telephony/nsITelephonyProvider.idl index 7435f8e4d0be..f21afd1de9d2 100644 --- a/dom/telephony/nsITelephonyProvider.idl +++ b/dom/telephony/nsITelephonyProvider.idl @@ -4,7 +4,7 @@ #include "nsISupports.idl" -[scriptable, uuid(a1e9fdd9-7901-4a0f-8b6b-6ee0fa8f9d81)] +[scriptable, uuid(3fb573c3-6fc4-41d3-80c1-f0e60662691e)] interface nsITelephonyListener : nsISupports { /** @@ -82,6 +82,14 @@ interface nsITelephonyListener : nsISupports */ void notifyError(in long callIndex, in AString error); + + /** + * Called when a waiting call comes in CDMA networks. + * + * @param number + * Number of the other party. + */ + void notifyCdmaCallWaiting(in AString number); }; /** diff --git a/dom/wappush/src/gonk/WapPushManager.js b/dom/wappush/src/gonk/WapPushManager.js index 217bb989b60c..ccbf728e1d76 100644 --- a/dom/wappush/src/gonk/WapPushManager.js +++ b/dom/wappush/src/gonk/WapPushManager.js @@ -8,7 +8,7 @@ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); - +Cu.import("resource://gre/modules/PhoneNumberUtils.jsm"); Cu.import("resource://gre/modules/WspPduHelper.jsm", this); const DEBUG = false; // set to true to see debug messages @@ -109,7 +109,14 @@ this.WapPushManager = { }; } + let sender = PhoneNumberUtils.normalize(options.sourceAddress, false); + let parsedSender = PhoneNumberUtils.parse(sender); + if (parsedSender && parsedSender.internationalNumber) { + sender = parsedSender.internationalNumber; + } + gSystemMessenger.broadcastMessage("wappush-received", { + sender: sender, contentType: msg.contentType, content: msg.content }); diff --git a/dom/webidl/CallEvent.webidl b/dom/webidl/CallEvent.webidl index 83db7db030ca..4de26c9ad2f6 100644 --- a/dom/webidl/CallEvent.webidl +++ b/dom/webidl/CallEvent.webidl @@ -3,9 +3,8 @@ * 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/. */ -interface TelephonyCall; -[Constructor(DOMString type, optional CallEventInit eventInitDict), HeaderFile="GeneratedEventClasses.h"] +[Constructor(DOMString type, optional CallEventInit eventInitDict)] interface CallEvent : Event { readonly attribute TelephonyCall? call; diff --git a/dom/webidl/CallsList.webidl b/dom/webidl/CallsList.webidl new file mode 100644 index 000000000000..251a0b91d5f4 --- /dev/null +++ b/dom/webidl/CallsList.webidl @@ -0,0 +1,11 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. + */ + +[ArrayClass, NoInterfaceObject] +interface CallsList { + getter TelephonyCall item(unsigned long index); + readonly attribute unsigned long length; +}; diff --git a/dom/webidl/Navigator.webidl b/dom/webidl/Navigator.webidl index c9f224ef1e9d..419e61b6e829 100644 --- a/dom/webidl/Navigator.webidl +++ b/dom/webidl/Navigator.webidl @@ -252,11 +252,9 @@ partial interface Navigator { }; #ifdef MOZ_B2G_RIL -interface MozTelephony; -// nsIDOMNavigatorTelephony partial interface Navigator { [Throws, Func="Navigator::HasTelephonySupport"] - readonly attribute MozTelephony? mozTelephony; + readonly attribute Telephony? mozTelephony; }; // nsIMozNavigatorMobileConnection diff --git a/dom/webidl/Telephony.webidl b/dom/webidl/Telephony.webidl new file mode 100644 index 000000000000..2d5f705a6bd8 --- /dev/null +++ b/dom/webidl/Telephony.webidl @@ -0,0 +1,35 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. + */ + +interface Telephony : EventTarget { + [Throws] + TelephonyCall dial(DOMString number); + [Throws] + TelephonyCall dialEmergency(DOMString number); + + [Throws] + attribute boolean muted; + [Throws] + attribute boolean speakerEnabled; + + readonly attribute TelephonyCall? active; + + readonly attribute CallsList calls; + + [Throws] + void startTone(DOMString tone); + [Throws] + void stopTone(); + + [SetterThrows] + attribute EventHandler onincoming; + [SetterThrows] + attribute EventHandler oncallschanged; + [SetterThrows] + attribute EventHandler onremoteheld; + [SetterThrows] + attribute EventHandler onremoteresumed; +}; diff --git a/dom/webidl/TelephonyCall.webidl b/dom/webidl/TelephonyCall.webidl new file mode 100644 index 000000000000..2a900cd96c88 --- /dev/null +++ b/dom/webidl/TelephonyCall.webidl @@ -0,0 +1,54 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. + */ + +interface TelephonyCall : EventTarget { + readonly attribute DOMString number; + + // In CDMA networks, the 2nd waiting call shares the connection with the 1st + // call. We need an additional attribute for the 2nd number. + readonly attribute DOMString? secondNumber; + + readonly attribute DOMString state; + + // The property "emergency" indicates whether the call number is an emergency + // number. Only the outgoing call could have a value with true and it is + // available after dialing state. + readonly attribute boolean emergency; + + readonly attribute DOMError? error; + + [Throws] + void answer(); + [Throws] + void hangUp(); + [Throws] + void hold(); + [Throws] + void resume(); + + [SetterThrows] + attribute EventHandler onstatechange; + [SetterThrows] + attribute EventHandler ondialing; + [SetterThrows] + attribute EventHandler onalerting; + [SetterThrows] + attribute EventHandler onconnecting; + [SetterThrows] + attribute EventHandler onconnected; + [SetterThrows] + attribute EventHandler ondisconnecting; + [SetterThrows] + attribute EventHandler ondisconnected; + [SetterThrows] + attribute EventHandler onholding; + [SetterThrows] + attribute EventHandler onheld; + [SetterThrows] + attribute EventHandler onresuming; + [SetterThrows] + attribute EventHandler onerror; +}; diff --git a/dom/webidl/WebIDL.mk b/dom/webidl/WebIDL.mk index 8d8554f35388..24af47647ac2 100644 --- a/dom/webidl/WebIDL.mk +++ b/dom/webidl/WebIDL.mk @@ -437,7 +437,10 @@ endif ifdef MOZ_B2G_RIL webidl_files += \ + CallsList.webidl \ MozStkCommandEvent.webidl \ + Telephony.webidl \ + TelephonyCall.webidl \ $(NULL) endif diff --git a/dom/wifi/WifiWorker.js b/dom/wifi/WifiWorker.js index b32ed7d54f6e..cd6c74a1970d 100644 --- a/dom/wifi/WifiWorker.js +++ b/dom/wifi/WifiWorker.js @@ -843,6 +843,7 @@ var WifiManager = (function() { if (ok === 0) { // Tell the event worker to start waiting for events. retryTimer = null; + connectTries = 0; didConnectSupplicant(function(){}); return; } @@ -858,6 +859,7 @@ var WifiManager = (function() { } retryTimer = null; + connectTries = 0; notify("supplicantlost", { success: false }); } diff --git a/js/src/jit/IonBuilder.cpp b/js/src/jit/IonBuilder.cpp index dddebf2952bf..4b8a5c82767b 100644 --- a/js/src/jit/IonBuilder.cpp +++ b/js/src/jit/IonBuilder.cpp @@ -5073,6 +5073,9 @@ ArgumentTypesMatch(MDefinition *def, types::StackTypeSet *calleeTypes) if (def->type() == MIRType_Value) return false; + if (def->type() == MIRType_Object) + return calleeTypes->unknownObject(); + return calleeTypes->mightBeType(ValueTypeFromMIRType(def->type())); } diff --git a/js/xpconnect/src/event_impl_gen.conf.in b/js/xpconnect/src/event_impl_gen.conf.in index f17fd06e1e1b..7b88abfc6506 100644 --- a/js/xpconnect/src/event_impl_gen.conf.in +++ b/js/xpconnect/src/event_impl_gen.conf.in @@ -32,7 +32,6 @@ simple_events = [ 'BluetoothStatusChangedEvent', #endif #ifdef MOZ_B2G_RIL - 'CallEvent', 'CFStateChangeEvent', 'DataErrorEvent', 'IccCardLockErrorEvent', diff --git a/layout/tools/reftest/reftest-cmdline.js b/layout/tools/reftest/reftest-cmdline.js index 3005a38374ea..c2a615c89ae7 100644 --- a/layout/tools/reftest/reftest-cmdline.js +++ b/layout/tools/reftest/reftest-cmdline.js @@ -106,7 +106,7 @@ RefTestCmdLineHandler.prototype = // Setting this pref makes tests run much faster there. branch.setBoolPref("security.fileuri.strict_origin_policy", false); // Disable the thumbnailing service - branch.setBoolPref("browser.pagethumbnails.capturing_disabled", false); + branch.setBoolPref("browser.pagethumbnails.capturing_disabled", true); var wwatch = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] .getService(nsIWindowWatcher); diff --git a/mach b/mach index 9aba2825387e..dbbd8fca40e2 100755 --- a/mach +++ b/mach @@ -33,8 +33,7 @@ for dir_path in ancestors(os.getcwd()): # to look for a config file at the path in $MOZCONFIG rather than # its default locations. # - # Note: subprocess requires native strings in os.environ Python - # 2.7.2 and earlier on Windows. + # Note: subprocess requires native strings in os.environ on Windows os.environ[b"MOZCONFIG"] = str(info["mozconfig"]) if "topsrcdir" in info: diff --git a/mobile/android/app/mobile.js b/mobile/android/app/mobile.js index 965510a93c8c..695a0cd6bd02 100644 --- a/mobile/android/app/mobile.js +++ b/mobile/android/app/mobile.js @@ -478,10 +478,8 @@ pref("security.alternate_certificate_error_page", "certerror"); pref("security.warn_viewing_mixed", false); // Warning is disabled. See Bug 616712. -#ifdef NIGHTLY_BUILD // Block insecure active content on https pages pref("security.mixed_content.block_active_content", true); -#endif // Override some named colors to avoid inverse OS themes pref("ui.-moz-dialog", "#efebe7"); diff --git a/mobile/android/base/AppConstants.java.in b/mobile/android/base/AppConstants.java.in index 120089390ae3..fa315cd1326f 100644 --- a/mobile/android/base/AppConstants.java.in +++ b/mobile/android/base/AppConstants.java.in @@ -136,4 +136,13 @@ public class AppConstants { #else false; #endif + + // See this wiki page for more details about channel specific build defines: + // https://wiki.mozilla.org/Platform/Channel-specific_build_defines + public static final boolean RELEASE_BUILD = +#ifdef RELEASE_BUILD + true; +#else + false; +#endif } diff --git a/mobile/android/base/BrowserApp.java b/mobile/android/base/BrowserApp.java index 1b68db52ce48..05594149b022 100644 --- a/mobile/android/base/BrowserApp.java +++ b/mobile/android/base/BrowserApp.java @@ -1988,8 +1988,7 @@ abstract public class BrowserApp extends GeckoApp * @return true if update UI was launched. */ protected boolean handleUpdaterLaunch() { - if ("release".equals(AppConstants.MOZ_UPDATE_CHANNEL) || - "beta".equals(AppConstants.MOZ_UPDATE_CHANNEL)) { + if (AppConstants.RELEASE_BUILD) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse("market://details?id=" + getPackageName())); startActivity(intent); diff --git a/mobile/android/base/BrowserToolbar.java b/mobile/android/base/BrowserToolbar.java index 23d97123b6c9..b9ab84e8fc98 100644 --- a/mobile/android/base/BrowserToolbar.java +++ b/mobile/android/base/BrowserToolbar.java @@ -50,6 +50,7 @@ import android.view.Window; import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.AccelerateInterpolator; import android.view.animation.Animation; +import android.view.animation.AnimationUtils; import android.view.animation.AlphaAnimation; import android.view.animation.Interpolator; import android.view.animation.TranslateAnimation; @@ -89,7 +90,7 @@ public class BrowserToolbar extends GeckoRelativeLayout public ImageButton mStop; public ImageButton mSiteSecurity; public PageActionLayout mPageActionLayout; - private AnimationDrawable mProgressSpinner; + private Animation mProgressSpinner; private TabCounter mTabsCounter; private ImageView mShadow; private GeckoImageButton mMenu; @@ -103,6 +104,7 @@ public class BrowserToolbar extends GeckoRelativeLayout private boolean mShowSiteSecurity; private boolean mShowReader; + private boolean mSpinnerVisible; private boolean mAnimatingEntry; @@ -221,7 +223,7 @@ public class BrowserToolbar extends GeckoRelativeLayout mSiteSecurityVisible = (mSiteSecurity.getVisibility() == View.VISIBLE); mActivity.getSiteIdentityPopup().setAnchor(mSiteSecurity); - mProgressSpinner = (AnimationDrawable) res.getDrawable(R.drawable.progress_spinner); + mProgressSpinner = AnimationUtils.loadAnimation(mActivity, R.anim.progress_spinner); mStop = (ImageButton) findViewById(R.id.stop); mShadow = (ImageView) findViewById(R.id.shadow); @@ -787,16 +789,26 @@ public class BrowserToolbar extends GeckoRelativeLayout // are needed by S1/S2 tests (http://mrcote.info/phonedash/#). // See discussion in Bug 804457. Bug 805124 tracks paring these down. if (visible) { - mFavicon.setImageDrawable(mProgressSpinner); - mProgressSpinner.start(); - setPageActionVisibility(true); + mFavicon.setImageResource(R.drawable.progress_spinner); + //To stop the glitch caused by mutiple start() calls. + if (!mSpinnerVisible) { + setPageActionVisibility(true); + mFavicon.setAnimation(mProgressSpinner); + mProgressSpinner.start(); + mSpinnerVisible = true; + } Log.i(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - Throbber start"); } else { - mProgressSpinner.stop(); - setPageActionVisibility(false); Tab selectedTab = Tabs.getInstance().getSelectedTab(); if (selectedTab != null) setFavicon(selectedTab.getFavicon()); + + if (mSpinnerVisible) { + setPageActionVisibility(false); + mFavicon.setAnimation(null); + mProgressSpinner.cancel(); + mSpinnerVisible = false; + } Log.i(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - Throbber stop"); } } diff --git a/mobile/android/base/GeckoApp.java b/mobile/android/base/GeckoApp.java index 207d0c8df6a9..dad91700a854 100644 --- a/mobile/android/base/GeckoApp.java +++ b/mobile/android/base/GeckoApp.java @@ -721,6 +721,21 @@ abstract public class GeckoApp // something went wrong. Log.e(LOGTAG, "Received Contact:Add message with no email nor phone number"); } + } else if (event.equals("Intent:GetHandlers")) { + Intent intent = GeckoAppShell.getOpenURIIntent(sAppContext, message.optString("url"), + message.optString("mime"), message.optString("action"), message.optString("title")); + String[] handlers = GeckoAppShell.getHandlersForIntent(intent); + ArrayList appList = new ArrayList(handlers.length); + for (int i = 0; i < handlers.length; i++) { + appList.add(handlers[i]); + } + JSONObject handlersJSON = new JSONObject(); + handlersJSON.put("apps", new JSONArray(appList)); + mCurrentResponse = handlersJSON.toString(); + } else if (event.equals("Intent:Open")) { + GeckoAppShell.openUriExternal(message.optString("url"), + message.optString("mime"), message.optString("packageName"), + message.optString("className"), message.optString("action"), message.optString("title")); } } catch (Exception e) { Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e); @@ -1507,6 +1522,8 @@ abstract public class GeckoApp registerEventListener("Update:Install"); registerEventListener("PrivateBrowsing:Data"); registerEventListener("Contact:Add"); + registerEventListener("Intent:Open"); + registerEventListener("Intent:GetHandlers"); if (SmsManager.getInstance() != null) { SmsManager.getInstance().start(); @@ -2061,6 +2078,8 @@ abstract public class GeckoApp unregisterEventListener("Update:Install"); unregisterEventListener("PrivateBrowsing:Data"); unregisterEventListener("Contact:Add"); + unregisterEventListener("Intent:Open"); + unregisterEventListener("Intent:GetHandlers"); deleteTempFiles(); diff --git a/mobile/android/base/Makefile.in b/mobile/android/base/Makefile.in index ccce5d4d012d..b54c3ad4b30b 100644 --- a/mobile/android/base/Makefile.in +++ b/mobile/android/base/Makefile.in @@ -597,6 +597,7 @@ RES_ANIM = \ res/anim/awesomebar_hold_still.xml \ res/anim/grow_fade_in.xml \ res/anim/grow_fade_in_center.xml \ + res/anim/progress_spinner.xml \ res/anim/shrink_fade_out.xml \ $(NULL) @@ -647,18 +648,7 @@ RES_DRAWABLE_MDPI = \ res/drawable-mdpi/ic_menu_reload.png \ res/drawable-mdpi/ic_status_logo.png \ res/drawable-mdpi/icon_pageaction.png \ - res/drawable-mdpi/progress_spinner_1.png \ - res/drawable-mdpi/progress_spinner_2.png \ - res/drawable-mdpi/progress_spinner_3.png \ - res/drawable-mdpi/progress_spinner_4.png \ - res/drawable-mdpi/progress_spinner_5.png \ - res/drawable-mdpi/progress_spinner_6.png \ - res/drawable-mdpi/progress_spinner_7.png \ - res/drawable-mdpi/progress_spinner_8.png \ - res/drawable-mdpi/progress_spinner_9.png \ - res/drawable-mdpi/progress_spinner_10.png \ - res/drawable-mdpi/progress_spinner_11.png \ - res/drawable-mdpi/progress_spinner_12.png \ + res/drawable-mdpi/progress_spinner.png \ res/drawable-mdpi/tab_indicator_divider.9.png \ res/drawable-mdpi/tab_indicator_selected.9.png \ res/drawable-mdpi/tab_indicator_selected_focused.9.png \ @@ -1118,7 +1108,6 @@ RES_DRAWABLE += \ res/drawable/ic_menu_quit.xml \ res/drawable/menu_item_state.xml \ res/drawable/menu_level.xml \ - res/drawable/progress_spinner.xml \ res/drawable/remote_tabs_child_divider.xml \ res/drawable/shaped_button.xml \ res/drawable/site_security_level.xml \ diff --git a/mobile/android/base/UpdateService.java b/mobile/android/base/UpdateService.java index b90a64b49eea..73936435e061 100644 --- a/mobile/android/base/UpdateService.java +++ b/mobile/android/base/UpdateService.java @@ -138,8 +138,7 @@ public class UpdateService extends IntentService { int interval; if (isRetry) { interval = INTERVAL_RETRY; - } else if (AppConstants.MOZ_UPDATE_CHANNEL.equals("nightly") || - AppConstants.MOZ_UPDATE_CHANNEL.equals("aurora")) { + } else if (!AppConstants.RELEASE_BUILD) { interval = INTERVAL_SHORT; } else { interval = INTERVAL_LONG; diff --git a/mobile/android/base/background/healthreport/HealthReportDatabaseStorage.java b/mobile/android/base/background/healthreport/HealthReportDatabaseStorage.java index 69625c1e2d14..9ac4117e2dcd 100644 --- a/mobile/android/base/background/healthreport/HealthReportDatabaseStorage.java +++ b/mobile/android/base/background/healthreport/HealthReportDatabaseStorage.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.ContextWrapper; import android.database.Cursor; import android.database.SQLException; +import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.os.Build; @@ -186,7 +187,7 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { protected final HealthReportSQLiteOpenHelper helper; public static class HealthReportSQLiteOpenHelper extends SQLiteOpenHelper { - public static final int CURRENT_VERSION = 4; + public static final int CURRENT_VERSION = 5; public static final String LOG_TAG = "HealthReportSQL"; /** @@ -227,11 +228,16 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { public static boolean CAN_USE_ABSOLUTE_DB_PATH = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO); public HealthReportSQLiteOpenHelper(Context context, File profileDirectory, String name) { + this(context, profileDirectory, name, CURRENT_VERSION); + } + + // For testing DBs of different versions. + public HealthReportSQLiteOpenHelper(Context context, File profileDirectory, String name, int version) { super( (CAN_USE_ABSOLUTE_DB_PATH ? context : new AbsolutePathContext(context, profileDirectory)), (CAN_USE_ABSOLUTE_DB_PATH ? getAbsolutePath(profileDirectory, name) : name), null, - CURRENT_VERSION); + version); if (CAN_USE_ABSOLUTE_DB_PATH) { Logger.pii(LOG_TAG, "Opening: " + getAbsolutePath(profileDirectory, name)); @@ -347,6 +353,13 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { } } + @Override + public void onOpen(SQLiteDatabase db) { + if (!db.isReadOnly()) { + db.execSQL("PRAGMA foreign_keys=ON;"); + } + } + private void createAddonsEnvironmentsView(SQLiteDatabase db) { db.execSQL("CREATE VIEW environments_with_addons AS " + "SELECT e.id AS id, " + @@ -394,6 +407,22 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { " WHERE measurement IN (SELECT id FROM measurements WHERE name = 'org.mozilla.searches.counts')"); } + private void upgradeDatabaseFrom4to5(SQLiteDatabase db) { + // Delete NULL in addons.body, which appeared as a result of Bug 886156. Note that the + // foreign key constraint, "ON DELETE RESTRICT", may be violated, but since onOpen() is + // called after this method, foreign keys are not yet enabled and constraints can be broken. + db.delete("addons", "body IS NULL", null); + + // Purge any data inconsistent with foreign key references (which may have appeared before + // foreign keys were enabled in Bug 900289). + db.delete("fields", "measurement NOT IN (SELECT id FROM measurements)", null); + db.delete("environments", "addonsID NOT IN (SELECT id from addons)", null); + db.delete(EVENTS_INTEGER, "env NOT IN (SELECT id FROM environments)", null); + db.delete(EVENTS_TEXTUAL, "env NOT IN (SELECT id FROM environments)", null); + db.delete(EVENTS_INTEGER, "field NOT IN (SELECT id FROM fields)", null); + db.delete(EVENTS_TEXTUAL, "field NOT IN (SELECT id FROM fields)", null); + } + @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion >= newVersion) { @@ -408,6 +437,8 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { upgradeDatabaseFrom2To3(db); case 3: upgradeDatabaseFrom3To4(db); + case 4: + upgradeDatabaseFrom4to5(db); } db.setTransactionSuccessful(); } catch (Exception e) { @@ -1031,7 +1062,11 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { v.put("env", env); v.put("field", field); v.put("date", day); - db.insert(table, null, v); + try { + db.insertOrThrow(table, null, v); + } catch (SQLiteConstraintException e) { + throw new IllegalStateException("Event did not reference existing an environment or field.", e); + } } } @@ -1063,7 +1098,11 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { final SQLiteDatabase db = this.helper.getWritableDatabase(); putValue(v, value); - db.insert(table, null, v); + try { + db.insertOrThrow(table, null, v); + } catch (SQLiteConstraintException e) { + throw new IllegalStateException("Event did not reference existing an environment or field.", e); + } } @Override @@ -1133,7 +1172,11 @@ public class HealthReportDatabaseStorage implements HealthReportStorage { v.put("value", by); v.put("field", field); v.put("date", day); - db.insert(EVENTS_INTEGER, null, v); + try { + db.insertOrThrow(EVENTS_INTEGER, null, v); + } catch (SQLiteConstraintException e) { + throw new IllegalStateException("Event did not reference existing an environment or field.", e); + } } } diff --git a/mobile/android/base/resources/anim/progress_spinner.xml b/mobile/android/base/resources/anim/progress_spinner.xml new file mode 100644 index 000000000000..f3eb6e01bcee --- /dev/null +++ b/mobile/android/base/resources/anim/progress_spinner.xml @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/mobile/android/base/resources/drawable-mdpi/progress_spinner_10.png b/mobile/android/base/resources/drawable-mdpi/progress_spinner.png similarity index 100% rename from mobile/android/base/resources/drawable-mdpi/progress_spinner_10.png rename to mobile/android/base/resources/drawable-mdpi/progress_spinner.png diff --git a/mobile/android/base/resources/drawable-mdpi/progress_spinner_1.png b/mobile/android/base/resources/drawable-mdpi/progress_spinner_1.png deleted file mode 100644 index 6b543a96b659..000000000000 Binary files a/mobile/android/base/resources/drawable-mdpi/progress_spinner_1.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-mdpi/progress_spinner_11.png b/mobile/android/base/resources/drawable-mdpi/progress_spinner_11.png deleted file mode 100644 index 7f3d12ddeb92..000000000000 Binary files a/mobile/android/base/resources/drawable-mdpi/progress_spinner_11.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-mdpi/progress_spinner_12.png b/mobile/android/base/resources/drawable-mdpi/progress_spinner_12.png deleted file mode 100644 index c7b271592af6..000000000000 Binary files a/mobile/android/base/resources/drawable-mdpi/progress_spinner_12.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-mdpi/progress_spinner_2.png b/mobile/android/base/resources/drawable-mdpi/progress_spinner_2.png deleted file mode 100644 index 709c9fa2f21c..000000000000 Binary files a/mobile/android/base/resources/drawable-mdpi/progress_spinner_2.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-mdpi/progress_spinner_3.png b/mobile/android/base/resources/drawable-mdpi/progress_spinner_3.png deleted file mode 100644 index d5ded1e253f1..000000000000 Binary files a/mobile/android/base/resources/drawable-mdpi/progress_spinner_3.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-mdpi/progress_spinner_4.png b/mobile/android/base/resources/drawable-mdpi/progress_spinner_4.png deleted file mode 100644 index f3e1dd9b9b8f..000000000000 Binary files a/mobile/android/base/resources/drawable-mdpi/progress_spinner_4.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-mdpi/progress_spinner_5.png b/mobile/android/base/resources/drawable-mdpi/progress_spinner_5.png deleted file mode 100644 index f2283efbf977..000000000000 Binary files a/mobile/android/base/resources/drawable-mdpi/progress_spinner_5.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-mdpi/progress_spinner_6.png b/mobile/android/base/resources/drawable-mdpi/progress_spinner_6.png deleted file mode 100644 index 413fe939e5f5..000000000000 Binary files a/mobile/android/base/resources/drawable-mdpi/progress_spinner_6.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-mdpi/progress_spinner_7.png b/mobile/android/base/resources/drawable-mdpi/progress_spinner_7.png deleted file mode 100644 index 5386d592a9de..000000000000 Binary files a/mobile/android/base/resources/drawable-mdpi/progress_spinner_7.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-mdpi/progress_spinner_8.png b/mobile/android/base/resources/drawable-mdpi/progress_spinner_8.png deleted file mode 100644 index db7d794f8ba5..000000000000 Binary files a/mobile/android/base/resources/drawable-mdpi/progress_spinner_8.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-mdpi/progress_spinner_9.png b/mobile/android/base/resources/drawable-mdpi/progress_spinner_9.png deleted file mode 100644 index 617a77c05bc0..000000000000 Binary files a/mobile/android/base/resources/drawable-mdpi/progress_spinner_9.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable/progress_spinner.xml b/mobile/android/base/resources/drawable/progress_spinner.xml deleted file mode 100644 index 937cbfab0fe0..000000000000 --- a/mobile/android/base/resources/drawable/progress_spinner.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml b/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml index dba52d77d562..02f2ebbbecc5 100644 --- a/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml +++ b/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml @@ -75,8 +75,9 @@ android:layout_width="@dimen/browser_toolbar_favicon_size" android:layout_height="fill_parent" android:scaleType="fitCenter" - android:paddingLeft="8dip" - android:layout_marginRight="4dip" + android:layout_marginLeft="4dip" + android:paddingLeft="4dip" + android:paddingRight="4dip" android:layout_gravity="center_vertical" android:src="@drawable/favicon"/> diff --git a/mobile/android/base/resources/layout/browser_toolbar.xml b/mobile/android/base/resources/layout/browser_toolbar.xml index 50340b0e0dd1..3dd5634d1117 100644 --- a/mobile/android/base/resources/layout/browser_toolbar.xml +++ b/mobile/android/base/resources/layout/browser_toolbar.xml @@ -101,8 +101,9 @@ android:layout_width="@dimen/browser_toolbar_favicon_size" android:layout_height="fill_parent" android:scaleType="fitCenter" - android:paddingLeft="12dip" - android:layout_marginRight="4dip" + android:layout_marginLeft="8dip" + android:paddingLeft="4dip" + android:paddingRight="4dip" android:layout_gravity="center_vertical" android:src="@drawable/favicon"/> diff --git a/mobile/android/base/resources/layout/search_engine_row.xml b/mobile/android/base/resources/layout/search_engine_row.xml index a794c402a921..31f607648605 100644 --- a/mobile/android/base/resources/layout/search_engine_row.xml +++ b/mobile/android/base/resources/layout/search_engine_row.xml @@ -17,8 +17,9 @@ + android:layout_height="wrap_content" + android:layout_centerVertical="true" + android:duplicateParentState="true"> diff --git a/mobile/android/base/resources/values/dimens.xml b/mobile/android/base/resources/values/dimens.xml index 681fe0f566e3..c19067d93a60 100644 --- a/mobile/android/base/resources/values/dimens.xml +++ b/mobile/android/base/resources/values/dimens.xml @@ -30,7 +30,7 @@ 12dp 48dp 20dp - 29.33dip + 25.33dip 32dp 1dp diff --git a/mobile/android/chrome/content/HelperApps.js b/mobile/android/chrome/content/HelperApps.js index a7ea014ded7b..b2488630b02b 100644 --- a/mobile/android/chrome/content/HelperApps.js +++ b/mobile/android/chrome/content/HelperApps.js @@ -3,6 +3,19 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; +XPCOMUtils.defineLazyGetter(this, "ContentAreaUtils", function() { + let ContentAreaUtils = {}; + Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", ContentAreaUtils); + return ContentAreaUtils; +}); + +function getBridge() { + return Cc["@mozilla.org/android/bridge;1"].getService(Ci.nsIAndroidBridge); +} + +function sendMessageToJava(aMessage) { + return getBridge().handleGeckoMessage(JSON.stringify(aMessage)); +} var HelperApps = { get defaultHttpHandlers() { @@ -34,28 +47,54 @@ var HelperApps = { let handlerInfoProto = this.protoSvc.getProtocolHandlerInfoFromOS(uri, {}); return handlerInfoProto.possibleApplicationHandlers; }, - + getAppsForUri: function getAppsFor(uri) { let found = []; - let handlerInfoProto = this.urlHandlerService.getURLHandlerInfoFromOS(uri, {}); - let urlHandlers = handlerInfoProto.possibleApplicationHandlers; - for (var i = 0; i < urlHandlers.length; i++) { - let urlApp = urlHandlers.queryElementAt(i, Ci.nsIHandlerApp); - if (!this.defaultHttpHandlers[urlApp.name]) { - found.push(urlApp); - } + let mimeType = ContentAreaUtils.getMIMETypeForURI(uri) || ""; + // empty action string defaults to android.intent.action.VIEW + let msg = { + type: "Intent:GetHandlers", + mime: mimeType, + action: "", + url: uri.spec, + packageName: "", + className: "" + }; + let apps = this._parseApps(JSON.parse(sendMessageToJava(msg))); + for (let i = 0; i < apps.length; i++) { + let appName = apps[i].name; + if (appName.length > 0 && !this.defaultHttpHandlers[appName]) + found.push(apps[i]); } return found; }, - + openUriInApp: function openUriInApp(uri) { - var possibleHandlers = this.getAppsForUri(uri); - if (possibleHandlers.length == 1) { - possibleHandlers[0].launchWithURI(uri); - } else if (possibleHandlers.length > 0) { - let handlerInfoProto = this.urlHandlerService.getURLHandlerInfoFromOS(uri, {}); - handlerInfoProto.preferredApplicationHandler.launchWithURI(uri); + let mimeType = ContentAreaUtils.getMIMETypeForURI(uri) || ""; + let msg = { + type: "Intent:Open", + mime: mimeType, + action: "", + url: uri.spec, + packageName: "", + className: "" + }; + sendMessageToJava(msg); + }, + + _parseApps: function _parseApps(aJSON) { + // aJSON -> {apps: [app1Label, app1Default, app1PackageName, app1ActivityName, app2Label, app2Defaut, ...]} + // see GeckoAppShell.java getHandlersForIntent function for details + let appInfo = aJSON.apps; + const numAttr = 4; // 4 elements per ResolveInfo: label, default, package name, activity name. + let apps = []; + for (let i = 0; i < appInfo.length; i += numAttr) { + apps.push({"name" : appInfo[i], + "isDefault" : appInfo[i+1], + "packageName" : appInfo[i+2], + "activityName" : appInfo[i+3]}); } + return apps; }, showDoorhanger: function showDoorhanger(aUri, aCallback) { @@ -80,7 +119,7 @@ var HelperApps = { message = strings.formatStringFromName("helperapps.openWithApp2", [apps[0].name], 1); else message = strings.GetStringFromName("helperapps.openWithList2"); - + let buttons = [{ label: strings.GetStringFromName("helperapps.open"), callback: function(aChecked) { diff --git a/mobile/android/chrome/content/aboutAddons.js b/mobile/android/chrome/content/aboutAddons.js index 9c27dc56abbb..027215005ac1 100644 --- a/mobile/android/chrome/content/aboutAddons.js +++ b/mobile/android/chrome/content/aboutAddons.js @@ -41,7 +41,6 @@ var ContextMenus = { document.getElementById("contextmenu-enable").setAttribute("hidden", "true"); document.getElementById("contextmenu-disable").setAttribute("hidden", "true"); document.getElementById("contextmenu-uninstall").setAttribute("hidden", "true"); - document.getElementById("contextmenu-default").setAttribute("hidden", "true"); return; } @@ -60,8 +59,6 @@ var ContextMenus = { document.getElementById("contextmenu-enable").removeAttribute("hidden"); document.getElementById("contextmenu-disable").setAttribute("hidden", "true"); } - - document.getElementById("contextmenu-default").setAttribute("hidden", "true"); }, enable: function(event) { @@ -253,37 +250,6 @@ var Addons = { list.appendChild(item); } - // Load the search engines - let defaults = Services.search.getDefaultEngines({ }).map(function (e) e.name); - function isDefault(aEngine) - defaults.indexOf(aEngine.name) != -1 - - let defaultDescription = gStringBundle.GetStringFromName("addonsSearchEngine.description"); - - let engines = Services.search.getEngines({ }); - for (let e = 0; e < engines.length; e++) { - let engine = engines[e]; - let addon = {}; - addon.id = engine.name; - addon.type = "search"; - addon.name = engine.name; - addon.version = ""; - addon.description = engine.description || defaultDescription; - addon.iconURL = engine.iconURI ? engine.iconURI.spec : ""; - addon.optionsURL = ""; - addon.appDisabled = false; - addon.scope = isDefault(engine) ? AddonManager.SCOPE_APPLICATION : AddonManager.SCOPE_PROFILE; - addon.engine = engine; - - let item = self._createItem(addon); - item.setAttribute("isDisabled", engine.hidden); - item.setAttribute("updateable", "false"); - item.setAttribute("opType", ""); - item.setAttribute("optionsURL", ""); - item.addon = addon; - list.appendChild(item); - } - // Add a "Browse all Firefox Add-ons" item to the bottom of the list. let browseItem = self._createBrowseItem(); list.appendChild(browseItem); @@ -348,9 +314,6 @@ var Addons = { else uninstallBtn.removeAttribute("disabled"); - let defaultButton = document.getElementById("default-btn"); - defaultButton.setAttribute("hidden", "true"); - let box = document.querySelector("#addons-details > .addon-item .options-box"); box.innerHTML = ""; diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index fd135012f4c6..bbd04edf795e 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -7627,12 +7627,26 @@ let Reader = { var ExternalApps = { _contextMenuId: -1, + // extend _getLink to pickup html5 media links. + _getMediaLink: function(aElement) { + let uri = NativeWindow.contextmenus._getLink(aElement); + if (uri == null) { + if (aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE && (aElement instanceof Ci.nsIDOMHTMLMediaElement && mediaSrc)) { + try { + let mediaSrc = aElement.currentSrc || aElement.src; + uri = ContentAreaUtils.makeURI(mediaSrc, null, null); + } catch (e) {} + } + } + return uri; + }, + init: function helper_init() { this._contextMenuId = NativeWindow.contextmenus.add(function(aElement) { let uri = null; var node = aElement; while (node && !uri) { - uri = NativeWindow.contextmenus._getLink(node); + uri = ExternalApps._getMediaLink(node); node = node.parentNode; } let apps = []; @@ -7650,7 +7664,7 @@ var ExternalApps = { filter: { matches: function(aElement) { - let uri = NativeWindow.contextmenus._getLink(aElement); + let uri = ExternalApps._getMediaLink(aElement); let apps = []; if (uri) { apps = HelperApps.getAppsForUri(uri); @@ -7660,7 +7674,7 @@ var ExternalApps = { }, openExternal: function(aElement) { - let uri = NativeWindow.contextmenus._getLink(aElement); + let uri = ExternalApps._getMediaLink(aElement); HelperApps.openUriInApp(uri); } }; diff --git a/python/mach_commands.py b/python/mach_commands.py index 50286ba232f9..3d920b0e6c1a 100644 --- a/python/mach_commands.py +++ b/python/mach_commands.py @@ -59,7 +59,8 @@ class MachCommands(MachCommandBase): return self.run_process([self.python_executable] + args, pass_thru=True, # Allow user to run Python interactively. ensure_exit_code=False, # Don't throw on non-zero exit code. - append_env={'PYTHONDONTWRITEBYTECODE': '1'}) + # Note: subprocess requires native strings in os.environ on Windows + append_env={b'PYTHONDONTWRITEBYTECODE': str('1')}) @Command('python-test', category='testing', description='Run Python unit tests.') @@ -109,7 +110,8 @@ class MachCommands(MachCommandBase): [self.python_executable, file], ensure_exit_code=False, # Don't throw on non-zero exit code. log_name='python-test', - append_env={'PYTHONDONTWRITEBYTECODE': '1'}, + # subprocess requires native strings in os.environ on Windows + append_env={b'PYTHONDONTWRITEBYTECODE': str('1')}, line_handler=_line_handler) return_code += inner_return_code diff --git a/testing/mochitest/b2g.json b/testing/mochitest/b2g.json index 29d8679c219d..4a5d2dfc9979 100644 --- a/testing/mochitest/b2g.json +++ b/testing/mochitest/b2g.json @@ -21,6 +21,7 @@ "content/media/test/test_can_play_type.html":"timed out", "content/media/test/test_can_play_type_mpeg.html":"7 failures out of 27", "content/media/test/test_can_play_type_no_dash.html":"", + "content/media/test/test_mediarecorder_record_stopms.html":"", "content/media/test/test_can_play_type_ogg.html":"", "content/media/test/test_chaining.html": "timed out", "content/media/test/test_delay_load.html": "6 failures", diff --git a/testing/mochitest/mach_commands.py b/testing/mochitest/mach_commands.py index 5e14e278ad17..7ec1afc08b8c 100644 --- a/testing/mochitest/mach_commands.py +++ b/testing/mochitest/mach_commands.py @@ -149,7 +149,6 @@ class MochitestRunner(MozbuildObject): return 1 options.testPath = test_path - env = {'TEST_PATH': test_path} if rerun_failures: options.testManifest = failure_file_path diff --git a/testing/profiles/prefs_general.js b/testing/profiles/prefs_general.js index 4e1d5d1323bd..2a3dc23518f7 100644 --- a/testing/profiles/prefs_general.js +++ b/testing/profiles/prefs_general.js @@ -147,4 +147,4 @@ user_pref("geo.provider.testing", true); // Background thumbnails in particular cause grief, and disabling thumbnails // in general can't hurt - we re-enable them when tests need them. -user_pref("browser.pagethumbnails.capturing_disabled", false); +user_pref("browser.pagethumbnails.capturing_disabled", true); diff --git a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm index fd22e29ed5b7..cbf48f0454c4 100644 --- a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm +++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm @@ -479,5 +479,181 @@ this.DownloadIntegration = { */ _getDirectory: function DI_getDirectory(aName) { return Services.dirsvc.get(this.testMode ? "TmpD" : aName, Ci.nsIFile); + }, + + /** + * Register the downloads interruption observers. + * + * @param aList + * The public or private downloads list. + * @param aIsPrivate + * True if the list is private, false otherwise. + * + * @return {Promise} + * @resolves When the views and observers are added. + */ + addListObservers: function DI_addListObservers(aList, aIsPrivate) { + DownloadObserver.registerView(aList, aIsPrivate); + if (!DownloadObserver.observersAdded) { + DownloadObserver.observersAdded = true; + Services.obs.addObserver(DownloadObserver, "quit-application-requested", true); + Services.obs.addObserver(DownloadObserver, "offline-requested", true); + Services.obs.addObserver(DownloadObserver, "last-pb-context-exiting", true); + } + return Promise.resolve(); } }; + +let DownloadObserver = { + /** + * Flag to determine if the observers have been added previously. + */ + observersAdded: false, + + /** + * Set that contains the in progress publics downloads. + * It's keep updated when a public download is added, removed or change its + * properties. + */ + _publicInProgressDownloads: new Set(), + + /** + * Set that contains the in progress private downloads. + * It's keep updated when a private download is added, removed or change its + * properties. + */ + _privateInProgressDownloads: new Set(), + + /** + * Registers a view that updates the corresponding downloads state set, based + * on the aIsPrivate argument. The set is updated when a download is added, + * removed or changed its properties. + * + * @param aList + * The public or private downloads list. + * @param aIsPrivate + * True if the list is private, false otherwise. + */ + registerView: function DO_registerView(aList, aIsPrivate) { + let downloadsSet = aIsPrivate ? this._privateInProgressDownloads + : this._publicInProgressDownloads; + let downloadsView = { + onDownloadAdded: function DO_V_onDownloadAdded(aDownload) { + if (!aDownload.stopped) { + downloadsSet.add(aDownload); + } + }, + onDownloadChanged: function DO_V_onDownloadChanged(aDownload) { + if (aDownload.stopped) { + downloadsSet.delete(aDownload); + } else { + downloadsSet.add(aDownload); + } + }, + onDownloadRemoved: function DO_V_onDownloadRemoved(aDownload) { + downloadsSet.delete(aDownload); + } + }; + + aList.addView(downloadsView); + }, + + /** + * Shows the confirm cancel downloads dialog. + * + * @param aCancel + * The observer notification subject. + * @param aDownloadsCount + * The current downloads count. + * @param aIdTitle + * The string bundle id for the dialog title. + * @param aIdMessageSingle + * The string bundle id for the single download message. + * @param aIdMessageMultiple + * The string bundle id for the multiple downloads message. + * @param aIdButton + * The string bundle id for the don't cancel button text. + */ + _confirmCancelDownloads: function DO_confirmCancelDownload( + aCancel, aDownloadsCount, aIdTitle, aIdMessageSingle, aIdMessageMultiple, aIdButton) { + // If user has already dismissed the request, then do nothing. + if ((aCancel instanceof Ci.nsISupportsPRBool) && aCancel.data) { + return; + } + // If there are no active downloads, then do nothing. + if (aDownloadsCount <= 0) { + return; + } + + let win = Services.wm.getMostRecentWindow("navigator:browser"); + let buttonFlags = (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) + + (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1); + let title = gStringBundle.GetStringFromName(aIdTitle); + let dontQuitButton = gStringBundle.GetStringFromName(aIdButton); + let quitButton; + let message; + + if (aDownloadsCount > 1) { + message = gStringBundle.formatStringFromName(aIdMessageMultiple, + [aDownloadsCount], 1); + quitButton = gStringBundle.formatStringFromName("cancelDownloadsOKTextMultiple", + [aDownloadsCount], 1); + } else { + message = gStringBundle.GetStringFromName(aIdMessageSingle); + quitButton = gStringBundle.GetStringFromName("cancelDownloadsOKText"); + } + + let rv = Services.prompt.confirmEx(win, title, message, buttonFlags, + quitButton, dontQuitButton, null, null, {}); + aCancel.data = (rv == 1); + }, + + //////////////////////////////////////////////////////////////////////////// + //// nsIObserver + + observe: function DO_observe(aSubject, aTopic, aData) { + let downloadsCount; + switch (aTopic) { + case "quit-application-requested": + downloadsCount = this._publicInProgressDownloads.size + + this._privateInProgressDownloads.size; +#ifndef XP_MACOSX + this._confirmCancelDownloads(aSubject, downloadsCount, + "quitCancelDownloadsAlertTitle", + "quitCancelDownloadsAlertMsg", + "quitCancelDownloadsAlertMsgMultiple", + "dontQuitButtonWin"); +#else + this._confirmCancelDownloads(aSubject, downloadsCount, + "quitCancelDownloadsAlertTitle", + "quitCancelDownloadsAlertMsgMac", + "quitCancelDownloadsAlertMsgMacMultiple", + "dontQuitButtonMac"); +#endif + break; + case "offline-requested": + downloadsCount = this._publicInProgressDownloads.size + + this._privateInProgressDownloads.size; + this._confirmCancelDownloads(aSubject, downloadsCount, + "offlineCancelDownloadsAlertTitle", + "offlineCancelDownloadsAlertMsg", + "offlineCancelDownloadsAlertMsgMultiple", + "dontGoOfflineButton"); + break; + case "last-pb-context-exiting": + this._confirmCancelDownloads(aSubject, + this._privateInProgressDownloads.size, + "leavePrivateBrowsingCancelDownloadsAlertTitle", + "leavePrivateBrowsingWindowsCancelDownloadsAlertMsg", + "leavePrivateBrowsingWindowsCancelDownloadsAlertMsgMultiple", + "dontLeavePrivateBrowsingButton"); + break; + } + }, + + //////////////////////////////////////////////////////////////////////////// + //// nsISupports + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]) +}; diff --git a/toolkit/components/jsdownloads/src/Downloads.jsm b/toolkit/components/jsdownloads/src/Downloads.jsm index 735835209fe1..a31c77669c20 100644 --- a/toolkit/components/jsdownloads/src/Downloads.jsm +++ b/toolkit/components/jsdownloads/src/Downloads.jsm @@ -147,6 +147,7 @@ this.Downloads = { function task_D_getPublicDownloadList() { let list = new DownloadList(true); try { + yield DownloadIntegration.addListObservers(list, false); yield DownloadIntegration.loadPersistent(list); } catch (ex) { Cu.reportError(ex); @@ -176,12 +177,27 @@ this.Downloads = { */ getPrivateDownloadList: function D_getPrivateDownloadList() { - if (!this._privateDownloadList) { - this._privateDownloadList = new DownloadList(false); + if (!this._promisePrivateDownloadList) { + this._promisePrivateDownloadList = Task.spawn( + function task_D_getPublicDownloadList() { + let list = new DownloadList(false); + try { + yield DownloadIntegration.addListObservers(list, true); + } catch (ex) { + Cu.reportError(ex); + } + throw new Task.Result(list); + }); } - return Promise.resolve(this._privateDownloadList); + return this._promisePrivateDownloadList; }, - _privateDownloadList: null, + + /** + * This promise is resolved with a reference to a DownloadList object that + * represents private downloads. This property is null before the list of + * downloads is requested for the first time. + */ + _promisePrivateDownloadList: null, /** * Returns the system downloads directory asynchronously. diff --git a/toolkit/components/jsdownloads/test/unit/head.js b/toolkit/components/jsdownloads/test/unit/head.js index a7bed79d8f5e..fb057a697802 100644 --- a/toolkit/components/jsdownloads/test/unit/head.js +++ b/toolkit/components/jsdownloads/test/unit/head.js @@ -389,7 +389,7 @@ function promiseNewDownloadList() { */ function promiseNewPrivateDownloadList() { // Force the creation of a new public download list. - Downloads._privateDownloadList = null; + Downloads._promisePrivateDownloadList = null; return Downloads.getPrivateDownloadList(); } diff --git a/toolkit/components/osfile/modules/osfile_async_front.jsm b/toolkit/components/osfile/modules/osfile_async_front.jsm index 5d2c0935b74b..de55eeddf993 100644 --- a/toolkit/components/osfile/modules/osfile_async_front.jsm +++ b/toolkit/components/osfile/modules/osfile_async_front.jsm @@ -23,6 +23,7 @@ this.EXPORTED_SYMBOLS = ["OS"]; let SharedAll = {}; Components.utils.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", SharedAll); +Components.utils.import("resource://gre/modules/Deprecated.jsm", this); // Boilerplate, to simplify the transition to require() let OS = SharedAll.OS; @@ -725,7 +726,14 @@ File.writeAtomic = function writeAtomic(path, buffer, options = {}) { * @constructor */ File.Info = function Info(value) { - return value; + // Note that we can't just do this[k] = value[k] because our + // prototype defines getters for all of these fields. + for (let k in value) { + if (k != "creationDate") { + Object.defineProperty(this, k, {value: value[k]}); + } + } + Object.defineProperty(this, "_deprecatedCreationDate", {value: value["creationDate"]}); }; if (OS.Constants.Win) { File.Info.prototype = Object.create(OS.Shared.Win.AbstractInfo.prototype); @@ -735,6 +743,14 @@ if (OS.Constants.Win) { throw new Error("I am neither under Windows nor under a Posix system"); } +// Deprecated +Object.defineProperty(File.Info.prototype, "creationDate", { + get: function creationDate() { + Deprecated.warning("Field 'creationDate' is deprecated.", "https://developer.mozilla.org/en-US/docs/JavaScript_OS.File/OS.File.Info#Cross-platform_Attributes"); + return this._deprecatedCreationDate; + } +}); + File.Info.fromMsg = function fromMsg(value) { return new File.Info(value); }; diff --git a/toolkit/components/osfile/tests/xpcshell/test_creationDate.js b/toolkit/components/osfile/tests/xpcshell/test_creationDate.js new file mode 100644 index 000000000000..0d93b597c86a --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_creationDate.js @@ -0,0 +1,39 @@ +"use strict"; + +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/osfile.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js"); + +function run_test() { + do_test_pending(); + run_next_test(); +} + +/** + * Test to ensure that deprecation warning is issued on use + * of creationDate. + */ +add_task(function test_deprecatedCreationDate () { + let deferred = Promise.defer(); + let consoleListener = { + observe: function (aMessage) { + if(aMessage.message.indexOf("Field 'creationDate' is deprecated.") > -1) { + do_print("Deprecation message printed"); + do_check_true(true); + Services.console.unregisterListener(consoleListener); + deferred.resolve(); + } + } + }; + let currentDir = yield OS.File.getCurrentDirectory(); + let path = OS.Path.join(currentDir, "test_creationDate.js"); + + Services.console.registerListener(consoleListener); + (yield OS.File.stat(path)).creationDate; +}); + +add_task(do_test_finished); diff --git a/toolkit/components/osfile/tests/xpcshell/xpcshell.ini b/toolkit/components/osfile/tests/xpcshell/xpcshell.ini index 67646caac72c..c42f704342d3 100644 --- a/toolkit/components/osfile/tests/xpcshell/xpcshell.ini +++ b/toolkit/components/osfile/tests/xpcshell/xpcshell.ini @@ -7,5 +7,6 @@ tail = [test_osfile_async.js] [test_profiledir.js] [test_logging.js] +[test_creationDate.js] # bug 845190 - thread pool wasn't shutdown assertions skip-if = (os == "win" || "linux") && debug diff --git a/toolkit/components/telemetry/TelemetryFile.jsm b/toolkit/components/telemetry/TelemetryFile.jsm new file mode 100644 index 000000000000..7a21d5c2fd4e --- /dev/null +++ b/toolkit/components/telemetry/TelemetryFile.jsm @@ -0,0 +1,286 @@ +"use strict"; + +this.EXPORTED_SYMBOLS = ["TelemetryFile"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +let imports = {}; +Cu.import("resource://gre/modules/Services.jsm", imports); +Cu.import("resource://gre/modules/Deprecated.jsm", imports); +Cu.import("resource://gre/modules/NetUtil.jsm", imports); + +let {Services, Deprecated, NetUtil} = imports; + +// Constants from prio.h for nsIFileOutputStream.init +const PR_WRONLY = 0x2; +const PR_CREATE_FILE = 0x8; +const PR_TRUNCATE = 0x20; +const PR_EXCL = 0x80; +const RW_OWNER = parseInt("0600", 8); +const RWX_OWNER = parseInt("0700", 8); + +// Delete ping files that have been lying around for longer than this. +const MAX_PING_FILE_AGE = 7 * 24 * 60 * 60 * 1000; // 1 week + +// The number of outstanding saved pings that we have issued loading +// requests for. +let pingsLoaded = 0; + +// The number of those requests that have actually completed. +let pingLoadsCompleted = 0; + +// If |true|, send notifications "telemetry-test-save-complete" +// and "telemetry-test-load-complete" once save/load is complete. +let shouldNotifyUponSave = false; + +// Data that has neither been saved nor sent by ping +let pendingPings = []; + +this.TelemetryFile = { + + /** + * Save a single ping to a file. + * + * @param {object} ping The content of the ping to save. + * @param {nsIFile} file The destination file. + * @param {bool} sync If |true|, write synchronously. Deprecated. + * This argument should be |false|. + * @param {bool} overwrite If |true|, the file will be overwritten + * if it exists. + */ + savePingToFile: function(ping, file, sync, overwrite) { + let pingString = JSON.stringify(ping); + + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + + let ostream = Cc["@mozilla.org/network/file-output-stream;1"] + .createInstance(Ci.nsIFileOutputStream); + let initFlags = PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE; + if (!overwrite) { + initFlags |= PR_EXCL; + } + try { + ostream.init(file, initFlags, RW_OWNER, 0); + } catch (e) { + // Probably due to PR_EXCL. + return; + } + + if (sync) { + let utf8String = converter.ConvertFromUnicode(pingString); + utf8String += converter.Finish(); + let success = false; + try { + let amount = ostream.write(utf8String, utf8String.length); + success = amount == utf8String.length; + } catch (e) { + } + finishTelemetrySave(success, ostream); + } else { + let istream = converter.convertToInputStream(pingString); + let self = this; + NetUtil.asyncCopy(istream, ostream, + function(result) { + finishTelemetrySave(Components.isSuccessCode(result), + ostream); + }); + } + }, + + /** + * Save a ping to its file, synchronously. + * + * @param {object} ping The content of the ping to save. + * @param {bool} overwrite If |true|, the file will be overwritten + * if it exists. + */ + savePing: function(ping, overwrite) { + this.savePingToFile(ping, + getSaveFileForPing(ping), true, overwrite); + }, + + /** + * Save all pending pings, synchronously. + * + * @param {object} sessionPing The additional session ping. + */ + savePendingPings: function(sessionPing) { + this.savePing(sessionPing, true); + pendingPings.forEach(function sppcb(e, i, a) { + this.savePing(e, false); + }, this); + pendingPings = []; + }, + + /** + * Remove the file for a ping + * + * @param {object} ping The ping. + */ + cleanupPingFile: function(ping) { + // FIXME: We shouldn't create the directory just to remove the file. + let file = getSaveFileForPing(ping); + try { + file.remove(true); // FIXME: Should be |false|, isn't it? + } catch(e) { + } + }, + + /** + * Load all saved pings. + * + * Once loaded, the saved pings can be accessed (destructively only) + * through |popPendingPings|. + * + * @param {bool} sync If |true|, loading takes place synchronously. + * @param {function*} onLoad A function called upon loading of each + * ping. It is passed |true| in case of success, |false| in case of + * format error. + */ + loadSavedPings: function(sync, onLoad = null) { + let directory = ensurePingDirectory(); + let entries = directory.directoryEntries + .QueryInterface(Ci.nsIDirectoryEnumerator); + pingsLoaded = 0; + pingLoadsCompleted = 0; + try { + while (entries.hasMoreElements()) { + this.loadHistograms(entries.nextFile, sync, onLoad); + } + } finally { + entries.close(); + } + }, + + /** + * Load the histograms from a file. + * + * Once loaded, the saved pings can be accessed (destructively only) + * through |popPendingPings|. + * + * @param {nsIFile} file The file to load. + * @param {bool} sync If |true|, loading takes place synchronously. + * @param {function*} onLoad A function called upon loading of the + * ping. It is passed |true| in case of success, |false| in case of + * format error. + */ + loadHistograms: function loadHistograms(file, sync, onLoad = null) { + let now = new Date(); + if (now - file.lastModifiedTime > MAX_PING_FILE_AGE) { + // We haven't had much luck in sending this file; delete it. + file.remove(true); + return; + } + + pingsLoaded++; + if (sync) { + let stream = Cc["@mozilla.org/network/file-input-stream;1"] + .createInstance(Ci.nsIFileInputStream); + stream.init(file, -1, -1, 0); + addToPendingPings(file, stream, onLoad); + } else { + let channel = NetUtil.newChannel(file); + channel.contentType = "application/json"; + + NetUtil.asyncFetch(channel, (function(stream, result) { + if (!Components.isSuccessCode(result)) { + return; + } + addToPendingPings(file, stream, onLoad); + }).bind(this)); + } + }, + + /** + * The number of pings loaded since the beginning of time. + */ + get pingsLoaded() { + return pingsLoaded; + }, + + /** + * Iterate destructively through the pending pings. + * + * @return {iterator} + */ + popPendingPings: function(reason) { + while (pendingPings.length > 0) { + let data = pendingPings.pop(); + // Send persisted pings to the test URL too. + if (reason == "test-ping") { + data.reason = reason; + } + yield data; + } + }, + + set shouldNotifyUponSave(value) { + shouldNotifyUponSave = value; + }, + + testLoadHistograms: function(file, sync, onLoad) { + pingsLoaded = 0; + pingLoadsCompleted = 0; + this.loadHistograms(file, sync, onLoad); + } +}; + +///// Utility functions + +function getSaveFileForPing(ping) { + let file = ensurePingDirectory(); + file.append(ping.slug); + return file; +}; + +function ensurePingDirectory() { + let directory = Services.dirsvc.get("ProfD", Ci.nsILocalFile).clone(); + directory.append("saved-telemetry-pings"); + try { + directory.create(Ci.nsIFile.DIRECTORY_TYPE, RWX_OWNER); + } catch (e) { + // Already exists, just ignore this. + } + return directory; +}; + +function addToPendingPings(file, stream, onLoad) { + let success = false; + + try { + let string = NetUtil.readInputStreamToString(stream, stream.available(), + { charset: "UTF-8" }); + stream.close(); + let ping = JSON.parse(string); + // The ping's payload used to be stringified JSON. Deal with that. + if (typeof(ping.payload) == "string") { + ping.payload = JSON.parse(ping.payload); + } + pingLoadsCompleted++; + pendingPings.push(ping); + if (shouldNotifyUponSave && + pingLoadsCompleted == pingsLoaded) { + Services.obs.notifyObservers(null, "telemetry-test-load-complete", null); + } + success = true; + } catch (e) { + // An error reading the file, or an error parsing the contents. + stream.close(); // close is idempotent. + file.remove(true); // FIXME: Should be false, isn't it? + } + if (onLoad) { + onLoad(success); + } +}; + +function finishTelemetrySave(ok, stream) { + stream.close(); + if (shouldNotifyUponSave && ok) { + Services.obs.notifyObservers(null, "telemetry-test-save-complete", null); + } +}; diff --git a/toolkit/components/telemetry/TelemetryPing.js b/toolkit/components/telemetry/TelemetryPing.js index fdeada468496..59d80026224d 100644 --- a/toolkit/components/telemetry/TelemetryPing.js +++ b/toolkit/components/telemetry/TelemetryPing.js @@ -10,12 +10,12 @@ const Cu = Components.utils; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/NetUtil.jsm"); #ifndef MOZ_WIDGET_GONK Cu.import("resource://gre/modules/LightweightThemeManager.jsm"); #endif Cu.import("resource://gre/modules/ctypes.jsm"); Cu.import("resource://gre/modules/ThirdPartyCookieProbe.jsm"); +Cu.import("resource://gre/modules/TelemetryFile.jsm"); // When modifying the payload in incompatible ways, please bump this version number const PAYLOAD_VERSION = 1; @@ -37,15 +37,6 @@ const PREF_PREVIOUS_BUILDID = PREF_BRANCH + "previousBuildID"; const TELEMETRY_INTERVAL = 60000; // Delay before intializing telemetry (ms) const TELEMETRY_DELAY = 60000; -// Delete ping files that have been lying around for longer than this. -const MAX_PING_FILE_AGE = 7 * 24 * 60 * 60 * 1000; // 1 week -// Constants from prio.h for nsIFileOutputStream.init -const PR_WRONLY = 0x2; -const PR_CREATE_FILE = 0x8; -const PR_TRUNCATE = 0x20; -const PR_EXCL = 0x80; -const RW_OWNER = 0600; -const RWX_OWNER = 0700; // MEM_HISTOGRAMS lists the memory reporters we turn into histograms. // @@ -172,15 +163,7 @@ TelemetryPing.prototype = { _prevSession: null, _hasWindowRestoredObserver: false, _hasXulWindowVisibleObserver: false, - _pendingPings: [], - _doLoadSaveNotifications: false, _startupIO : {}, - // The number of outstanding saved pings that we have issued loading - // requests for. - _pingsLoaded: 0, - // The number of those requests that have actually completed. - _pingLoadsCompleted: 0, - _savedProfileDirectory: null, // The previous build ID, if this is the first run with a new build. // Undefined if this is not the first run, or the previous build ID is unknown. _previousBuildID: undefined, @@ -258,7 +241,7 @@ TelemetryPing.prototype = { } catch(e) { } if (!forSavedSession || hasPingBeenSent) { - ret.savedPings = this._pingsLoaded; + ret.savedPings = TelemetryFile.pingsLoaded; } return ret; @@ -593,16 +576,11 @@ TelemetryPing.prototype = { return this.assemblePing(this.getSessionPayload(reason), reason); }, - getPayloads: function getPayloads(reason) { + popPayloads: function popPayloads(reason) { function payloadIter() { yield this.getSessionPayloadAndSlug(reason); - - while (this._pendingPings.length > 0) { - let data = this._pendingPings.pop(); - // Send persisted pings to the test URL too. - if (reason == "test-ping") { - data.reason = reason; - } + let iterator = TelemetryFile.popPendingPings(reason); + for (let data of iterator) { yield data; } } @@ -618,7 +596,7 @@ TelemetryPing.prototype = { // populate histograms one last time this.gatherMemory(); this.sendPingsFromIterator(server, reason, - Iterator(this.getPayloads(reason))); + Iterator(this.popPayloads(reason))); }, /** @@ -651,7 +629,7 @@ TelemetryPing.prototype = { this.sendPingsFromIterator(server, reason, i); } function onError() { - this.savePing(data, true); + TelemetryFile.savePing(data, true); // Notify that testing is complete, even if we didn't send everything. finishPings(reason); } @@ -667,11 +645,7 @@ TelemetryPing.prototype = { hping.add(new Date() - startTime); if (success) { - let file = this.saveFileForPing(ping); - try { - file.remove(true); - } catch(e) { - } + TelemetryFile.cleanupPingFile(ping); } }, @@ -820,7 +794,11 @@ TelemetryPing.prototype = { this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); function timerCallback() { this._initialized = true; - this.loadSavedPings(false); + TelemetryFile.loadSavedPings(false, (success => + { + let success_histogram = Telemetry.getHistogramById("READ_SAVED_PING_SUCCESS"); + success_histogram.add(success); + })); this.attachObservers(); this.gatherMemory(); @@ -832,128 +810,12 @@ TelemetryPing.prototype = { Ci.nsITimer.TYPE_ONE_SHOT); }, - addToPendingPings: function addToPendingPings(file, stream) { - let success = false; - - try { - let string = NetUtil.readInputStreamToString(stream, stream.available(), { charset: "UTF-8" }); - stream.close(); - let ping = JSON.parse(string); - // The ping's payload used to be stringified JSON. Deal with that. - if (typeof(ping.payload) == "string") { - ping.payload = JSON.parse(ping.payload); - } - this._pingLoadsCompleted++; - this._pendingPings.push(ping); - if (this._doLoadSaveNotifications && - this._pingLoadsCompleted == this._pingsLoaded) { - Services.obs.notifyObservers(null, "telemetry-test-load-complete", null); - } - success = true; - } catch (e) { - // An error reading the file, or an error parsing the contents. - stream.close(); // close is idempotent. - file.remove(true); - } - let success_histogram = Telemetry.getHistogramById("READ_SAVED_PING_SUCCESS"); - success_histogram.add(success); - }, - - loadHistograms: function loadHistograms(file, sync) { - let now = new Date(); - if (now - file.lastModifiedTime > MAX_PING_FILE_AGE) { - // We haven't had much luck in sending this file; delete it. - file.remove(true); - return; - } - - this._pingsLoaded++; - if (sync) { - let stream = Cc["@mozilla.org/network/file-input-stream;1"] - .createInstance(Ci.nsIFileInputStream); - stream.init(file, -1, -1, 0); - this.addToPendingPings(file, stream); - } else { - let channel = NetUtil.newChannel(file); - channel.contentType = "application/json" - - NetUtil.asyncFetch(channel, (function(stream, result) { - if (!Components.isSuccessCode(result)) { - return; - } - this.addToPendingPings(file, stream); - }).bind(this)); - } - }, - testLoadHistograms: function testLoadHistograms(file, sync) { - this._pingsLoaded = 0; - this._pingLoadsCompleted = 0; - this.loadHistograms(file, sync); - }, - - loadSavedPings: function loadSavedPings(sync) { - let directory = this.ensurePingDirectory(); - let entries = directory.directoryEntries - .QueryInterface(Ci.nsIDirectoryEnumerator); - this._pingsLoaded = 0; - this._pingLoadsCompleted = 0; - try { - while (entries.hasMoreElements()) { - this.loadHistograms(entries.nextFile, sync); - } - } - finally { - entries.close(); - } - }, - - finishTelemetrySave: function finishTelemetrySave(ok, stream) { - stream.close(); - if (this._doLoadSaveNotifications && ok) { - Services.obs.notifyObservers(null, "telemetry-test-save-complete", null); - } - }, - - savePingToFile: function savePingToFile(ping, file, sync, overwrite) { - let pingString = JSON.stringify(ping); - - let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] - .createInstance(Ci.nsIScriptableUnicodeConverter); - converter.charset = "UTF-8"; - - let ostream = Cc["@mozilla.org/network/file-output-stream;1"] - .createInstance(Ci.nsIFileOutputStream); - let initFlags = PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE; - if (!overwrite) { - initFlags |= PR_EXCL; - } - try { - ostream.init(file, initFlags, RW_OWNER, 0); - } catch (e) { - // Probably due to PR_EXCL. - return; - } - - if (sync) { - let utf8String = converter.ConvertFromUnicode(pingString); - utf8String += converter.Finish(); - let success = false; - try { - let amount = ostream.write(utf8String, utf8String.length); - success = amount == utf8String.length; - } catch (e) { - } - this.finishTelemetrySave(success, ostream); - } else { - let istream = converter.convertToInputStream(pingString) - let self = this; - NetUtil.asyncCopy(istream, ostream, - function(result) { - self.finishTelemetrySave(Components.isSuccessCode(result), - ostream); - }); - } + TelemetryFile.testLoadHistograms(file, sync, (success => + { + let success_histogram = Telemetry.getHistogramById("READ_SAVED_PING_SUCCESS"); + success_histogram.add(success); + })); }, getFlashVersion: function getFlashVersion() { @@ -968,39 +830,15 @@ TelemetryPing.prototype = { return null; }, - ensurePingDirectory: function ensurePingDirectory() { - let directory = this._savedProfileDirectory.clone(); - directory.append("saved-telemetry-pings"); - try { - directory.create(Ci.nsIFile.DIRECTORY_TYPE, RWX_OWNER); - } catch (e) { - // Already exists, just ignore this. - } - return directory; - }, - - saveFileForPing: function saveFileForPing(ping) { - let file = this.ensurePingDirectory(); - file.append(ping.slug); - return file; - }, - - savePing: function savePing(ping, overwrite) { - this.savePingToFile(ping, this.saveFileForPing(ping), true, overwrite); - }, - savePendingPings: function savePendingPings() { let sessionPing = this.getSessionPayloadAndSlug("saved-session"); - this.savePing(sessionPing, true); - this._pendingPings.forEach(function sppcb(e, i, a) { - this.savePing(e, false); - }, this); - this._pendingPings = []; + TelemetryFile.savePendingPings(sessionPing); }, saveHistograms: function saveHistograms(file, sync) { - this.savePingToFile(this.getSessionPayloadAndSlug("saved-session"), - file, sync, true); + TelemetryFile.savePingToFile( + this.getSessionPayloadAndSlug("saved-session"), + file, sync, true); }, /** @@ -1045,7 +883,7 @@ TelemetryPing.prototype = { }, enableLoadSaveNotifications: function enableLoadSaveNotifications() { - this._doLoadSaveNotifications = true; + TelemetryFile.shouldNotifyUponSave = true; }, setAddOns: function setAddOns(aAddOns) { @@ -1069,7 +907,8 @@ TelemetryPing.prototype = { }, cacheProfileDirectory: function cacheProfileDirectory() { - this._savedProfileDirectory = Services.dirsvc.get("ProfD", Ci.nsILocalFile); + // This method doesn't do anything anymore + return; }, /** @@ -1079,7 +918,6 @@ TelemetryPing.prototype = { switch (aTopic) { case "profile-after-change": this.setup(); - this.cacheProfileDirectory(); break; case "cycle-collector-begin": let now = new Date(); @@ -1146,7 +984,7 @@ TelemetryPing.prototype = { case "application-background": if (Telemetry.canSend) { let ping = this.getSessionPayloadAndSlug("saved-session"); - this.savePing(ping, true); + TelemetryFile.savePing(ping, true); } break; #endif diff --git a/toolkit/components/telemetry/moz.build b/toolkit/components/telemetry/moz.build index ae6c0404bb05..c79dc5008302 100644 --- a/toolkit/components/telemetry/moz.build +++ b/toolkit/components/telemetry/moz.build @@ -32,6 +32,7 @@ EXTRA_PP_COMPONENTS += [ ] EXTRA_JS_MODULES += [ + 'TelemetryFile.jsm', 'TelemetryStopwatch.jsm', 'ThirdPartyCookieProbe.jsm', ] diff --git a/toolkit/components/thumbnails/PageThumbs.jsm b/toolkit/components/thumbnails/PageThumbs.jsm index b7aee14c4d14..647ab27e1911 100644 --- a/toolkit/components/thumbnails/PageThumbs.jsm +++ b/toolkit/components/thumbnails/PageThumbs.jsm @@ -465,7 +465,7 @@ this.PageThumbs = { _prefEnabled: function PageThumbs_prefEnabled() { try { - return Services.prefs.getBoolPref("browser.pagethumbnails.capturing_disabled"); + return !Services.prefs.getBoolPref("browser.pagethumbnails.capturing_disabled"); } catch (e) { return true; diff --git a/toolkit/components/thumbnails/test/head.js b/toolkit/components/thumbnails/test/head.js index d0688135e689..77047a38b051 100644 --- a/toolkit/components/thumbnails/test/head.js +++ b/toolkit/components/thumbnails/test/head.js @@ -11,7 +11,7 @@ let {PageThumbs, PageThumbsStorage, SessionStore, FileUtils, OS} = tmp; Cu.import("resource://gre/modules/PlacesUtils.jsm"); let oldEnabledPref = Services.prefs.getBoolPref("browser.pagethumbnails.capturing_disabled"); -Services.prefs.setBoolPref("browser.pagethumbnails.capturing_disabled", true); +Services.prefs.setBoolPref("browser.pagethumbnails.capturing_disabled", false); registerCleanupFunction(function () { while (gBrowser.tabs.length > 1) diff --git a/toolkit/content/jar.mn b/toolkit/content/jar.mn index 9e9bb1cfd815..5c011ed6ac7c 100644 --- a/toolkit/content/jar.mn +++ b/toolkit/content/jar.mn @@ -62,6 +62,7 @@ toolkit.jar: content/global/bindings/expander.xml (widgets/expander.xml) * content/global/bindings/filefield.xml (widgets/filefield.xml) *+ content/global/bindings/findbar.xml (widgets/findbar.xml) + content/global/bindings/findbar.css (widgets/findbar.css) content/global/bindings/general.xml (widgets/general.xml) content/global/bindings/groupbox.xml (widgets/groupbox.xml) *+ content/global/bindings/listbox.xml (widgets/listbox.xml) diff --git a/toolkit/content/widgets/findbar.css b/toolkit/content/widgets/findbar.css new file mode 100644 index 000000000000..de083ee33961 --- /dev/null +++ b/toolkit/content/widgets/findbar.css @@ -0,0 +1,41 @@ +/* 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/. */ + +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); + +findbar { + transition-property: transform, opacity, visibility; + transition-duration: 120ms, 120ms, 0s; + transition-timing-function: ease-in-out, ease-in-out, linear; + + /* The following positioning properties only take an effect during findbar + * transitions. The findbar binding sets position:absolute during that time + * on the findbar. + */ + left: 0; + right: 0; + bottom: 0; +} + +findbar[position="top"] { + top: 0; + bottom: auto; +} + +findbar > hbox { + width: 100%; +} + +findbar[hidden] { + /* Override display:none to make the transition work. */ + display: -moz-box; + visibility: collapse; + opacity: 0; + transition-delay: 0s, 0s, 120ms; + transform: translateY(2em); +} + +findbar[position="top"][hidden] { + transform: translateY(-2em); +} diff --git a/toolkit/content/widgets/findbar.xml b/toolkit/content/widgets/findbar.xml index 8f5965dd1b08..c8e4c8f36aba 100644 --- a/toolkit/content/widgets/findbar.xml +++ b/toolkit/content/widgets/findbar.xml @@ -173,6 +173,7 @@ + @@ -241,6 +242,8 @@ 0 6 + 0 + @@ -1134,8 +1137,30 @@ this._updateFindUI(); if (this.hidden) { + // Use position:absolute during the transition. + this.style.position = "absolute"; + this.parentNode.style.position = "relative"; + + // Apparently a flush is necessary after setting position:relative + // on our parentNode, otherwise setting hidden to false won't + // animate the transform change. + this.getBoundingClientRect(); + this.hidden = false; + // Set a height on the findbar that's at least as much as the + // current height, but guaranteed to be an integer number of + // screen pixels. + // This way, reapplying position:static on the findbar after the + // fade in animation won't cause the browser contents to wiggle. + let [chromeOffset, contentScrollOffset] = this._findOffsets(); + this.style.height = chromeOffset + "px"; + this._contentScrollOffset = contentScrollOffset; + + // Wait for the findbar appearance animation to end before + // changing the browser size. + this.addEventListener("transitionend", this); + this._updateStatusUI(this.nsITypeAheadFind.FIND_FOUND); let event = document.createEvent("Events"); @@ -1178,6 +1203,16 @@ } this.hidden = true; + + this.addEventListener("transitionend", this); + + // Revert browser scroll shift + findbar static positioning. + if (this.getAttribute("position") == "top" && + this.style.position != "absolute") { + this._browser.contentWindow.scrollBy(0, -this._contentScrollOffset); + } + this.style.position = "absolute"; + var fastFind = this.browser.fastFind; fastFind.setSelectionModeAndRepaint (this.nsISelectionController.SELECTION_ON); @@ -1479,10 +1514,58 @@ case "keypress": this._onBrowserKeypress(aEvent); break; + case "transitionend": + if (aEvent.target == this && + aEvent.propertyName == "transform") { + this.removeEventListener("transitionend", this); + + // Change the browser size in such a way that the region that's + // overlapped by the findbar can be scrolled to, but try to + // avoid a visual shift of the browser contents. + this.style.removeProperty("position"); + if (this.getAttribute("position") == "top" && + !this.hidden) { + this._browser.contentWindow.scrollBy(0, this._contentScrollOffset); + } + + // We'd like to remove position:relative from this.parentNode, + // but that unfortunately causes unnecessary repainting. + } + break; } ]]>
+ + + + + + + + + + { - return this._unknownError( - "error occurred while processing '" + aPacket.type, - e); - }); + let pendingResponse = this._actorResponses.get(actor.actorID) || resolve(null); + let response = pendingResponse.then(() => { + return ret; + }).then(aResponse => { + if (!aResponse.from) { + aResponse.from = aPacket.to; + } + this.transport.send(aResponse); + }).then(null, (e) => { + let errorPacket = this._unknownError( + "error occurred while processing '" + aPacket.type, + e); + errorPacket.from = aPacket.to; + this.transport.send(errorPacket); + }); + + this._actorResponses.set(actor.actorID, response); }, /** diff --git a/toolkit/devtools/server/protocol.js b/toolkit/devtools/server/protocol.js index b1d55e54b9bc..c53d56e9cc04 100644 --- a/toolkit/devtools/server/protocol.js +++ b/toolkit/devtools/server/protocol.js @@ -824,6 +824,12 @@ let Actor = Class({ message: err.toString() }); }, + + _queueResponse: function(create) { + let pending = this._pendingResponse || promise.resolve(null); + let response = create(pending); + this._pendingResponse = response; + } }); exports.Actor = Actor; @@ -915,14 +921,16 @@ let actorProto = function(actorProto) { conn.send(response); }; - if (ret && ret.then) { - ret.then(sendReturn).then(null, this.writeError.bind(this)); - } else { - sendReturn(ret); - } - + this._queueResponse(p => { + return p + .then(() => ret) + .then(sendReturn) + .then(null, this.writeError.bind(this)); + }) } catch(e) { - this.writeError(e); + this._queueResponse(p => { + return p.then(() => this.writeError(e)); + }); } }; diff --git a/toolkit/devtools/server/tests/unit/test_protocol_async.js b/toolkit/devtools/server/tests/unit/test_protocol_async.js new file mode 100644 index 000000000000..dfbaca38b7e9 --- /dev/null +++ b/toolkit/devtools/server/tests/unit/test_protocol_async.js @@ -0,0 +1,165 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Make sure we get replies in the same order that we sent their + * requests even when earlier requests take several event ticks to + * complete. + */ + +let protocol = devtools.require("devtools/server/protocol"); +let {method, Arg, Option, RetVal} = protocol; +let promise = devtools.require("sdk/core/promise"); +let events = devtools.require("sdk/event/core"); + +function simpleHello() { + return { + from: "root", + applicationType: "xpcshell-tests", + traits: [], + } +} + +let RootActor = protocol.ActorClass({ + typeName: "root", + initialize: function(conn) { + protocol.Actor.prototype.initialize.call(this, conn); + // Root actor owns itself. + this.manage(this); + this.actorID = "root"; + this.sequence = 0; + }, + + sayHello: simpleHello, + + simpleReturn: method(function() { + return this.sequence++; + }, { + response: { value: RetVal() }, + }), + + promiseReturn: method(function(toWait) { + // Guarantee that this resolves after simpleReturn returns. + let deferred = promise.defer(); + let sequence = this.sequence++; + + // Wait until the number of requests specified by toWait have + // happened, to test queuing. + let check = () => { + if ((this.sequence - sequence) < toWait) { + do_execute_soon(check); + return; + } + deferred.resolve(sequence); + } + do_execute_soon(check); + + return deferred.promise; + }, { + request: { toWait: Arg(0, "number") }, + response: { value: RetVal("number") }, + }), + + simpleThrow: method(function() { + throw new Error(this.sequence++); + }, { + response: { value: RetVal("number") } + }), + + promiseThrow: method(function() { + // Guarantee that this resolves after simpleReturn returns. + let deferred = promise.defer(); + let sequence = this.sequence++; + // This should be enough to force a failure if the code is broken. + do_timeout(150, () => { + deferred.reject(sequence++); + }); + return deferred.promise; + }, { + response: { value: RetVal("number") }, + }) +}); + +let RootFront = protocol.FrontClass(RootActor, { + initialize: function(client) { + this.actorID = "root"; + protocol.Front.prototype.initialize.call(this, client); + // Root owns itself. + this.manage(this); + } +}); + +function run_test() +{ + DebuggerServer.createRootActor = RootActor; + DebuggerServer.init(() => true); + + let trace = connectPipeTracing(); + let client = new DebuggerClient(trace); + let rootClient; + + client.connect((applicationType, traits) => { + rootClient = RootFront(client); + + let calls = []; + let sequence = 0; + + // Execute a call that won't finish processing until 2 + // more calls have happened + calls.push(rootClient.promiseReturn(2).then(ret => { + do_check_eq(sequence, 0); // Check right return order + do_check_eq(ret, sequence++); // Check request handling order + })); + + // Put a few requests into the backlog + + calls.push(rootClient.simpleReturn().then(ret => { + do_check_eq(sequence, 1); // Check right return order + do_check_eq(ret, sequence++); // Check request handling order + })); + + calls.push(rootClient.simpleReturn().then(ret => { + do_check_eq(sequence, 2); // Check right return order + do_check_eq(ret, sequence++); // Check request handling order + })); + + calls.push(rootClient.simpleThrow().then(() => { + do_check_true(false, "simpleThrow shouldn't succeed!"); + }, error => { + do_check_eq(sequence++, 3); // Check right return order + return promise.resolve(null); + })); + + calls.push(rootClient.promiseThrow().then(() => { + do_check_true(false, "promiseThrow shouldn't succeed!"); + }, error => { + do_check_eq(sequence++, 4); // Check right return order + do_check_true(true, "simple throw should throw"); + return promise.resolve(null); + })); + + calls.push(rootClient.simpleReturn().then(ret => { + do_check_eq(sequence, 5); // Check right return order + do_check_eq(ret, sequence++); // Check request handling order + })); + + // Break up the backlog with a long request that waits + // for another simpleReturn before completing + calls.push(rootClient.promiseReturn(1).then(ret => { + do_check_eq(sequence, 6); // Check right return order + do_check_eq(ret, sequence++); // Check request handling order + })); + + calls.push(rootClient.simpleReturn().then(ret => { + do_check_eq(sequence, 7); // Check right return order + do_check_eq(ret, sequence++); // Check request handling order + })); + + promise.all.apply(null, calls).then(() => { + client.close(() => { + do_test_finished(); + }); + }) + }); + do_test_pending(); +} diff --git a/toolkit/devtools/server/tests/unit/xpcshell.ini b/toolkit/devtools/server/tests/unit/xpcshell.ini index 56ba4f6d6257..be4b67a58be8 100644 --- a/toolkit/devtools/server/tests/unit/xpcshell.ini +++ b/toolkit/devtools/server/tests/unit/xpcshell.ini @@ -42,6 +42,7 @@ reason = bug 821285 [test_eval-03.js] [test_eval-04.js] [test_eval-05.js] +[test_protocol_async.js] [test_protocol_simple.js] [test_protocol_longstring.js] [test_protocol_children.js] diff --git a/browser/devtools/styleinspector/css-logic.js b/toolkit/devtools/styleinspector/css-logic.js similarity index 100% rename from browser/devtools/styleinspector/css-logic.js rename to toolkit/devtools/styleinspector/css-logic.js diff --git a/toolkit/devtools/styleinspector/moz.build b/toolkit/devtools/styleinspector/moz.build new file mode 100644 index 000000000000..90ba481f4bdc --- /dev/null +++ b/toolkit/devtools/styleinspector/moz.build @@ -0,0 +1,11 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +JS_MODULES_PATH = 'modules/devtools/styleinspector' + +EXTRA_JS_MODULES += [ + 'css-logic.js' +] diff --git a/toolkit/themes/linux/global/findBar.css b/toolkit/themes/linux/global/findBar.css index bdb5d082f587..02b0eed59da1 100644 --- a/toolkit/themes/linux/global/findBar.css +++ b/toolkit/themes/linux/global/findBar.css @@ -9,29 +9,11 @@ findbar { border-top: 2px solid; -moz-border-top-colors: ThreeDShadow ThreeDHighlight; min-width: 1px; - transition-property: margin-bottom, opacity, visibility; - transition-duration: 150ms, 150ms, 0s; - transition-timing-function: ease-in-out, ease-in-out, linear; } findbar[position="top"] { border-top-style: none; border-bottom: 1px solid ThreeDShadow; - transition-property: margin-top, opacity, visibility; -} - -findbar[hidden] { - /* Override display:none to make the transition work. */ - display: -moz-box; - visibility: collapse; - margin-bottom: -1em; - opacity: 0; - transition-delay: 0s, 0s, 150ms; -} - -findbar[position="top"][hidden] { - margin-bottom: auto; - margin-top: -1em; } .findbar-closebutton { diff --git a/toolkit/themes/osx/global/findBar.css b/toolkit/themes/osx/global/findBar.css index ca7fed380e14..429135fde877 100644 --- a/toolkit/themes/osx/global/findBar.css +++ b/toolkit/themes/osx/global/findBar.css @@ -10,29 +10,11 @@ findbar { border-top: @scopeBarSeparatorBorder@; min-width: 1px; padding: 4px 2px; - transition-property: margin-bottom, opacity, visibility; - transition-duration: 150ms, 150ms, 0s; - transition-timing-function: ease-in-out, ease-in-out, linear; } findbar[position="top"] { border-top: none; border-bottom: @scopeBarSeparatorBorder@; - transition-property: margin-top, opacity, visibility; -} - -findbar[hidden] { - /* Override display:none to make the transition work. */ - display: -moz-box; - visibility: collapse; - margin-bottom: -1em; - opacity: 0; - transition-delay: 0s, 0s, 150ms; -} - -findbar[position="top"][hidden] { - margin-bottom: auto; - margin-top: -1em; } findbar:-moz-lwtheme { diff --git a/toolkit/themes/windows/global/findBar.css b/toolkit/themes/windows/global/findBar.css index 450a6812d8f6..d5dbf4d68b4a 100644 --- a/toolkit/themes/windows/global/findBar.css +++ b/toolkit/themes/windows/global/findBar.css @@ -11,29 +11,11 @@ findbar { background-size: 100% 2px; background-repeat: no-repeat; min-width: 1px; - transition-property: margin-bottom, opacity, visibility; - transition-duration: 150ms, 150ms, 0s; - transition-timing-function: ease-in-out, ease-in-out, linear; } findbar[position="top"] { background-image: none; box-shadow: 0 -1px 0 rgba(0,0,0,.1) inset; - transition-property: margin-top, opacity, visibility; -} - -findbar[hidden] { - /* Override display:none to make the transition work. */ - display: -moz-box; - visibility: collapse; - margin-bottom: -1em; - opacity: 0; - transition-delay: 0s, 0s, 150ms; -} - -findbar[position="top"][hidden] { - margin-bottom: auto; - margin-top: -1em; } .findbar-closebutton { diff --git a/toolkit/webapps/WebappsInstaller.jsm b/toolkit/webapps/WebappsInstaller.jsm index 8cd0a2041541..6c15f474606d 100644 --- a/toolkit/webapps/WebappsInstaller.jsm +++ b/toolkit/webapps/WebappsInstaller.jsm @@ -15,6 +15,7 @@ Cu.import("resource://gre/modules/NetUtil.jsm"); Cu.import("resource://gre/modules/osfile.jsm"); Cu.import("resource://gre/modules/WebappOSUtils.jsm"); Cu.import("resource://gre/modules/AppsUtils.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); this.WebappsInstaller = { shell: null, @@ -639,6 +640,7 @@ MacNativeApp.prototype = { writer.setString("Webapp", "Name", this.appName); writer.setString("Webapp", "Profile", this.appProfileDir.leafName); writer.writeFile(); + applicationINI.permissions = FileUtils.PERMS_FILE; // ${InstallDir}/Contents/Info.plist let infoPListContent = '\n\ @@ -956,9 +958,12 @@ LinuxNativeApp.prototype = { * @param aData a string with the data to be written */ function writeToFile(aFile, aData) { - let path = aFile.path; - let data = new TextEncoder().encode(aData); - return OS.File.writeAtomic(path, data, { tmpPath: path + ".tmp" }); + return Task.spawn(function() { + let data = new TextEncoder().encode(aData); + let file = yield OS.File.open(aFile.path, { truncate: true }, { unixMode: FileUtils.PERMS_FILE }); + yield file.write(data); + yield file.close(); + }); } /** diff --git a/webapprt/content/webapp.js b/webapprt/content/webapp.js index 75fd31f799e2..90c05bf57631 100644 --- a/webapprt/content/webapp.js +++ b/webapprt/content/webapp.js @@ -128,15 +128,22 @@ function updateMenuItems() { #endif } +#ifndef XP_MACOSX +let gEditUIVisible = true; +#endif + function updateEditUIVisibility() { #ifndef XP_MACOSX let editMenuPopupState = document.getElementById("menu_EditPopup").state; + let contextMenuPopupState = document.getElementById("contentAreaContextMenu").state; // The UI is visible if the Edit menu is opening or open, if the context menu // is open, or if the toolbar has been customized to include the Cut, Copy, // or Paste toolbar buttons. gEditUIVisible = editMenuPopupState == "showing" || - editMenuPopupState == "open"; + editMenuPopupState == "open" || + contextMenuPopupState == "showing" || + contextMenuPopupState == "open"; // If UI is visible, update the edit commands' enabled state to reflect // whether or not they are actually enabled for the current focus/selection. @@ -177,3 +184,60 @@ function updateCrashReportURL(aURI) { gCrashReporter.annotateCrashReport("URL", uri.spec); #endif } + +// Context menu handling code. +// At the moment there isn't any built-in menu, we only support HTML5 custom +// menus. + +let gContextMenu = null; + +XPCOMUtils.defineLazyGetter(this, "PageMenu", function() { + let tmp = {}; + Cu.import("resource://gre/modules/PageMenu.jsm", tmp); + return new tmp.PageMenu(); +}); + +function showContextMenu(aEvent, aXULMenu) { + if (aEvent.target != aXULMenu) { + return true; + } + + gContextMenu = new nsContextMenu(aXULMenu); + if (gContextMenu.shouldDisplay) { + updateEditUIVisibility(); + } + + return gContextMenu.shouldDisplay; +} + +function hideContextMenu(aEvent, aXULMenu) { + if (aEvent.target != aXULMenu) { + return; + } + + gContextMenu = null; + + updateEditUIVisibility(); +} + +function nsContextMenu(aXULMenu) { + this.initMenu(aXULMenu); +} + +nsContextMenu.prototype = { + initMenu: function(aXULMenu) { + this.hasPageMenu = PageMenu.maybeBuildAndAttachMenu(document.popupNode, + aXULMenu); + this.shouldDisplay = this.hasPageMenu; + + this.showItem("page-menu-separator", this.hasPageMenu); + }, + + showItem: function(aItemOrID, aShow) { + let item = aItemOrID.constructor == String ? + document.getElementById(aItemOrID) : aItemOrID; + if (item) { + item.hidden = !aShow; + } + } +}; diff --git a/webapprt/content/webapp.xul b/webapprt/content/webapp.xul index 5d5ae0af7098..cf8424158ce9 100644 --- a/webapprt/content/webapp.xul +++ b/webapprt/content/webapp.xul @@ -154,6 +154,14 @@ - + + + + + + + diff --git a/webapprt/mac/webapprt.mm b/webapprt/mac/webapprt.mm index 2e829864defc..74b07334e56c 100644 --- a/webapprt/mac/webapprt.mm +++ b/webapprt/mac/webapprt.mm @@ -331,14 +331,12 @@ NSString @"org.mozilla.aurora", @"org.mozilla.firefox", nil]; - //if they provided a manual override, use that. If they made an error, it will fail to launch + // If they provided a binary ID, use that. if (alternateBinaryID != nil && ([alternateBinaryID length] > 0)) { binaryPath = [[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier:alternateBinaryID]; - if (binaryPath == nil || [binaryPath length] == 0) { - @throw MakeException(@"Web Runtime Not Found", - [NSString stringWithFormat:@"Failed to locate specified override Web Runtime with signature '%@'", alternateBinaryID]); + if (binaryPath && [binaryPath length] > 0) { + return binaryPath; } - return binaryPath; } //No override found, loop through the various flavors of firefox we have diff --git a/widget/windows/WinMouseScrollHandler.cpp b/widget/windows/WinMouseScrollHandler.cpp index e4f1228a4a6e..ee75ffe87f37 100644 --- a/widget/windows/WinMouseScrollHandler.cpp +++ b/widget/windows/WinMouseScrollHandler.cpp @@ -149,7 +149,7 @@ MouseScrollHandler::~MouseScrollHandler() /* static */ bool -MouseScrollHandler::ProcessMessage(nsWindow* aWindow, UINT msg, +MouseScrollHandler::ProcessMessage(nsWindowBase* aWidget, UINT msg, WPARAM wParam, LPARAM lParam, MSGResult& aResult) { @@ -169,7 +169,7 @@ MouseScrollHandler::ProcessMessage(nsWindow* aWindow, UINT msg, case WM_MOUSEWHEEL: case WM_MOUSEHWHEEL: GetInstance()-> - ProcessNativeMouseWheelMessage(aWindow, msg, wParam, lParam); + ProcessNativeMouseWheelMessage(aWidget, msg, wParam, lParam); sInstance->mSynthesizingEvent->NotifyNativeMessageHandlingFinished(); // We don't need to call next wndproc for WM_MOUSEWHEEL and // WM_MOUSEHWHEEL. We should consume them always. If the messages @@ -182,14 +182,14 @@ MouseScrollHandler::ProcessMessage(nsWindow* aWindow, UINT msg, case WM_HSCROLL: case WM_VSCROLL: aResult.mConsumed = - GetInstance()->ProcessNativeScrollMessage(aWindow, msg, wParam, lParam); + GetInstance()->ProcessNativeScrollMessage(aWidget, msg, wParam, lParam); sInstance->mSynthesizingEvent->NotifyNativeMessageHandlingFinished(); aResult.mResult = 0; return true; case MOZ_WM_MOUSEVWHEEL: case MOZ_WM_MOUSEHWHEEL: - GetInstance()->HandleMouseWheelMessage(aWindow, msg, wParam, lParam); + GetInstance()->HandleMouseWheelMessage(aWidget, msg, wParam, lParam); sInstance->mSynthesizingEvent->NotifyInternalMessageHandlingFinished(); // Doesn't need to call next wndproc for internal wheel message. aResult.mConsumed = true; @@ -198,7 +198,7 @@ MouseScrollHandler::ProcessMessage(nsWindow* aWindow, UINT msg, case MOZ_WM_HSCROLL: case MOZ_WM_VSCROLL: GetInstance()-> - HandleScrollMessageAsMouseWheelMessage(aWindow, msg, wParam, lParam); + HandleScrollMessageAsMouseWheelMessage(aWidget, msg, wParam, lParam); sInstance->mSynthesizingEvent->NotifyInternalMessageHandlingFinished(); // Doesn't need to call next wndproc for internal scroll message. aResult.mConsumed = true; @@ -207,13 +207,13 @@ MouseScrollHandler::ProcessMessage(nsWindow* aWindow, UINT msg, case WM_KEYDOWN: case WM_KEYUP: PR_LOG(gMouseScrollLog, PR_LOG_ALWAYS, - ("MouseScroll::ProcessMessage(): aWindow=%p, " + ("MouseScroll::ProcessMessage(): aWidget=%p, " "msg=%s(0x%04X), wParam=0x%02X, ::GetMessageTime()=%d", - aWindow, msg == WM_KEYDOWN ? "WM_KEYDOWN" : + aWidget, msg == WM_KEYDOWN ? "WM_KEYDOWN" : msg == WM_KEYUP ? "WM_KEYUP" : "Unknown", msg, wParam, ::GetMessageTime())); LOG_KEYSTATE(); - if (Device::Elantech::HandleKeyMessage(aWindow, msg, wParam)) { + if (Device::Elantech::HandleKeyMessage(aWidget, msg, wParam)) { aResult.mResult = 0; aResult.mConsumed = true; return true; @@ -227,7 +227,7 @@ MouseScrollHandler::ProcessMessage(nsWindow* aWindow, UINT msg, /* static */ nsresult -MouseScrollHandler::SynthesizeNativeMouseScrollEvent(nsWindow* aWindow, +MouseScrollHandler::SynthesizeNativeMouseScrollEvent(nsWindowBase* aWidget, const nsIntPoint& aPoint, uint32_t aNativeMessage, int32_t aDelta, @@ -302,18 +302,18 @@ MouseScrollHandler::SynthesizeNativeMouseScrollEvent(nsWindow* aWindow, /* static */ bool -MouseScrollHandler::DispatchEvent(nsWindow* aWindow, nsGUIEvent& aEvent) +MouseScrollHandler::DispatchEvent(nsWindowBase* aWidget, nsGUIEvent& aEvent) { - return aWindow->DispatchWindowEvent(&aEvent); + return aWidget->DispatchWindowEvent(&aEvent); } /* static */ void -MouseScrollHandler::InitEvent(nsWindow* aWindow, +MouseScrollHandler::InitEvent(nsWindowBase* aWidget, nsGUIEvent& aEvent, nsIntPoint* aPoint) { - NS_ENSURE_TRUE_VOID(aWindow); + NS_ENSURE_TRUE_VOID(aWidget); nsIntPoint point; if (aPoint) { point = *aPoint; @@ -322,11 +322,11 @@ MouseScrollHandler::InitEvent(nsWindow* aWindow, POINT pt; pt.x = pts.x; pt.y = pts.y; - ::ScreenToClient(aWindow->GetWindowHandle(), &pt); + ::ScreenToClient(aWidget->GetWindowHandle(), &pt); point.x = pt.x; point.y = pt.y; } - aWindow->InitEvent(aEvent, &point); + aWidget->InitEvent(aEvent, &point); } /* static */ @@ -364,22 +364,22 @@ MouseScrollHandler::ComputeMessagePos(UINT aMessage, } void -MouseScrollHandler::ProcessNativeMouseWheelMessage(nsWindow* aWindow, +MouseScrollHandler::ProcessNativeMouseWheelMessage(nsWindowBase* aWidget, UINT aMessage, WPARAM aWParam, LPARAM aLParam) { if (SynthesizingEvent::IsSynthesizing()) { - mSynthesizingEvent->NativeMessageReceived(aWindow, aMessage, + mSynthesizingEvent->NativeMessageReceived(aWidget, aMessage, aWParam, aLParam); } POINT point = ComputeMessagePos(aMessage, aWParam, aLParam); PR_LOG(gMouseScrollLog, PR_LOG_ALWAYS, - ("MouseScroll::ProcessNativeMouseWheelMessage: aWindow=%p, " + ("MouseScroll::ProcessNativeMouseWheelMessage: aWidget=%p, " "aMessage=%s, wParam=0x%08X, lParam=0x%08X, point: { x=%d, y=%d }", - aWindow, aMessage == WM_MOUSEWHEEL ? "WM_MOUSEWHEEL" : + aWidget, aMessage == WM_MOUSEWHEEL ? "WM_MOUSEWHEEL" : aMessage == WM_MOUSEHWHEEL ? "WM_MOUSEHWHEEL" : aMessage == WM_VSCROLL ? "WM_VSCROLL" : "WM_HSCROLL", aWParam, aLParam, point.x, point.y)); @@ -412,14 +412,14 @@ MouseScrollHandler::ProcessNativeMouseWheelMessage(nsWindow* aWindow, // except plugin window (MozillaWindowClass), we should handle the message // on the window. if (WinUtils::IsOurProcessWindow(underCursorWnd)) { - nsWindow* destWindow = WinUtils::GetNSWindowPtr(underCursorWnd); + nsWindowBase* destWindow = WinUtils::GetNSWindowBasePtr(underCursorWnd); if (!destWindow) { PR_LOG(gMouseScrollLog, PR_LOG_ALWAYS, ("MouseScroll::ProcessNativeMouseWheelMessage: " "Found window under the cursor isn't managed by nsWindow...")); HWND wnd = ::GetParent(underCursorWnd); for (; wnd; wnd = ::GetParent(wnd)) { - destWindow = WinUtils::GetNSWindowPtr(wnd); + destWindow = WinUtils::GetNSWindowBasePtr(wnd); if (destWindow) { break; } @@ -440,7 +440,7 @@ MouseScrollHandler::ProcessNativeMouseWheelMessage(nsWindow* aWindow, // cause accessing the plugin. Therefore, we should unlock the plugin // process by using PostMessage(). if (destWindow->GetWindowType() == eWindowType_plugin) { - destWindow = destWindow->GetParentWindow(false); + destWindow = destWindow->GetParentWindowBase(false); if (!destWindow) { PR_LOG(gMouseScrollLog, PR_LOG_ALWAYS, ("MouseScroll::ProcessNativeMouseWheelMessage: " @@ -478,9 +478,9 @@ MouseScrollHandler::ProcessNativeMouseWheelMessage(nsWindow* aWindow, // it on parent window. However, note that the DOM event may cause accessing // the plugin. Therefore, we should unlock the plugin process by using // PostMessage(). - if (aWindow->GetWindowType() == eWindowType_plugin && - aWindow->GetWindowHandle() == pluginWnd) { - nsWindow* destWindow = aWindow->GetParentWindow(false); + if (aWidget->GetWindowType() == eWindowType_plugin && + aWidget->GetWindowHandle() == pluginWnd) { + nsWindowBase* destWindow = aWidget->GetParentWindowBase(false); if (!destWindow) { PR_LOG(gMouseScrollLog, PR_LOG_ALWAYS, ("MouseScroll::ProcessNativeMouseWheelMessage: Our normal window which " @@ -507,7 +507,7 @@ MouseScrollHandler::ProcessNativeMouseWheelMessage(nsWindow* aWindow, } bool -MouseScrollHandler::ProcessNativeScrollMessage(nsWindow* aWindow, +MouseScrollHandler::ProcessNativeScrollMessage(nsWindowBase* aWidget, UINT aMessage, WPARAM aWParam, LPARAM aLParam) @@ -515,25 +515,25 @@ MouseScrollHandler::ProcessNativeScrollMessage(nsWindow* aWindow, if (aLParam || mUserPrefs.IsScrollMessageHandledAsWheelMessage()) { // Scroll message generated by Thinkpad Trackpoint Driver or similar // Treat as a mousewheel message and scroll appropriately - ProcessNativeMouseWheelMessage(aWindow, aMessage, aWParam, aLParam); + ProcessNativeMouseWheelMessage(aWidget, aMessage, aWParam, aLParam); // Always consume the scroll message if we try to emulate mouse wheel // action. return true; } if (SynthesizingEvent::IsSynthesizing()) { - mSynthesizingEvent->NativeMessageReceived(aWindow, aMessage, + mSynthesizingEvent->NativeMessageReceived(aWidget, aMessage, aWParam, aLParam); } PR_LOG(gMouseScrollLog, PR_LOG_ALWAYS, - ("MouseScroll::ProcessNativeScrollMessage: aWindow=%p, " + ("MouseScroll::ProcessNativeScrollMessage: aWidget=%p, " "aMessage=%s, wParam=0x%08X, lParam=0x%08X", - aWindow, aMessage == WM_VSCROLL ? "WM_VSCROLL" : "WM_HSCROLL", + aWidget, aMessage == WM_VSCROLL ? "WM_VSCROLL" : "WM_HSCROLL", aWParam, aLParam)); // Scroll message generated by external application - nsContentCommandEvent commandEvent(true, NS_CONTENT_COMMAND_SCROLL, aWindow); + nsContentCommandEvent commandEvent(true, NS_CONTENT_COMMAND_SCROLL, aWidget); commandEvent.mScroll.mIsHorizontal = (aMessage == WM_HSCROLL); @@ -567,12 +567,12 @@ MouseScrollHandler::ProcessNativeScrollMessage(nsWindow* aWindow, } // XXX If this is a plugin window, we should dispatch the event from // parent window. - DispatchEvent(aWindow, commandEvent); + DispatchEvent(aWidget, commandEvent); return true; } void -MouseScrollHandler::HandleMouseWheelMessage(nsWindow* aWindow, +MouseScrollHandler::HandleMouseWheelMessage(nsWindowBase* aWidget, UINT aMessage, WPARAM aWParam, LPARAM aLParam) @@ -583,14 +583,14 @@ MouseScrollHandler::HandleMouseWheelMessage(nsWindow* aWindow, "MOZ_WM_MOUSEVWHEEL or MOZ_WM_MOUSEHWHEEL"); PR_LOG(gMouseScrollLog, PR_LOG_ALWAYS, - ("MouseScroll::HandleMouseWheelMessage: aWindow=%p, " + ("MouseScroll::HandleMouseWheelMessage: aWidget=%p, " "aMessage=MOZ_WM_MOUSE%sWHEEL, aWParam=0x%08X, aLParam=0x%08X", - aWindow, aMessage == MOZ_WM_MOUSEVWHEEL ? "V" : "H", + aWidget, aMessage == MOZ_WM_MOUSEVWHEEL ? "V" : "H", aWParam, aLParam)); mIsWaitingInternalMessage = false; - EventInfo eventInfo(aWindow, WinUtils::GetNativeMessage(aMessage), + EventInfo eventInfo(aWidget, WinUtils::GetNativeMessage(aMessage), aWParam, aLParam); if (!eventInfo.CanDispatchWheelEvent()) { PR_LOG(gMouseScrollLog, PR_LOG_ALWAYS, @@ -611,15 +611,15 @@ MouseScrollHandler::HandleMouseWheelMessage(nsWindow* aWindow, ModifierKeyState modKeyState = GetModifierKeyState(aMessage); // Grab the widget, it might be destroyed by a DOM event handler. - nsRefPtr kungFuDethGrip(aWindow); + nsRefPtr kungFuDethGrip(aWidget); - WheelEvent wheelEvent(true, NS_WHEEL_WHEEL, aWindow); - if (mLastEventInfo.InitWheelEvent(aWindow, wheelEvent, modKeyState)) { + WheelEvent wheelEvent(true, NS_WHEEL_WHEEL, aWidget); + if (mLastEventInfo.InitWheelEvent(aWidget, wheelEvent, modKeyState)) { PR_LOG(gMouseScrollLog, PR_LOG_ALWAYS, ("MouseScroll::HandleMouseWheelMessage: dispatching " "NS_WHEEL_WHEEL event")); - DispatchEvent(aWindow, wheelEvent); - if (aWindow->Destroyed()) { + DispatchEvent(aWidget, wheelEvent); + if (aWidget->Destroyed()) { PR_LOG(gMouseScrollLog, PR_LOG_ALWAYS, ("MouseScroll::HandleMouseWheelMessage: The window was destroyed " "by NS_WHEEL_WHEEL event")); @@ -637,7 +637,7 @@ MouseScrollHandler::HandleMouseWheelMessage(nsWindow* aWindow, } void -MouseScrollHandler::HandleScrollMessageAsMouseWheelMessage(nsWindow* aWindow, +MouseScrollHandler::HandleScrollMessageAsMouseWheelMessage(nsWindowBase* aWidget, UINT aMessage, WPARAM aWParam, LPARAM aLParam) @@ -651,7 +651,7 @@ MouseScrollHandler::HandleScrollMessageAsMouseWheelMessage(nsWindow* aWindow, ModifierKeyState modKeyState = GetModifierKeyState(aMessage); - WheelEvent wheelEvent(true, NS_WHEEL_WHEEL, aWindow); + WheelEvent wheelEvent(true, NS_WHEEL_WHEEL, aWidget); double& delta = (aMessage == MOZ_WM_VSCROLL) ? wheelEvent.deltaY : wheelEvent.deltaX; int32_t& lineOrPageDelta = @@ -683,15 +683,15 @@ MouseScrollHandler::HandleScrollMessageAsMouseWheelMessage(nsWindow* aWindow, // XXX Current mouse position may not be same as when the original message // is received. We need to know the actual mouse cursor position when // the original message was received. - InitEvent(aWindow, wheelEvent); + InitEvent(aWidget, wheelEvent); PR_LOG(gMouseScrollLog, PR_LOG_ALWAYS, - ("MouseScroll::HandleScrollMessageAsMouseWheelMessage: aWindow=%p, " + ("MouseScroll::HandleScrollMessageAsMouseWheelMessage: aWidget=%p, " "aMessage=MOZ_WM_%sSCROLL, aWParam=0x%08X, aLParam=0x%08X, " "wheelEvent { refPoint: { x: %d, y: %d }, deltaX: %f, deltaY: %f, " "lineOrPageDeltaX: %d, lineOrPageDeltaY: %d, " "isShift: %s, isControl: %s, isAlt: %s, isMeta: %s }", - aWindow, (aMessage == MOZ_WM_VSCROLL) ? "V" : "H", aWParam, aLParam, + aWidget, (aMessage == MOZ_WM_VSCROLL) ? "V" : "H", aWParam, aLParam, wheelEvent.refPoint.x, wheelEvent.refPoint.y, wheelEvent.deltaX, wheelEvent.deltaY, wheelEvent.lineOrPageDeltaX, wheelEvent.lineOrPageDeltaY, @@ -700,7 +700,7 @@ MouseScrollHandler::HandleScrollMessageAsMouseWheelMessage(nsWindow* aWindow, GetBoolName(wheelEvent.IsAlt()), GetBoolName(wheelEvent.IsMeta()))); - DispatchEvent(aWindow, wheelEvent); + DispatchEvent(aWidget, wheelEvent); } /****************************************************************************** @@ -709,7 +709,7 @@ MouseScrollHandler::HandleScrollMessageAsMouseWheelMessage(nsWindow* aWindow, * ******************************************************************************/ -MouseScrollHandler::EventInfo::EventInfo(nsWindow* aWindow, +MouseScrollHandler::EventInfo::EventInfo(nsWindowBase* aWidget, UINT aMessage, WPARAM aWParam, LPARAM aLParam) { @@ -722,7 +722,7 @@ MouseScrollHandler::EventInfo::EventInfo(nsWindow* aWindow, mIsPage = MouseScrollHandler::sInstance-> mSystemSettings.IsPageScroll(mIsVertical); mDelta = (short)HIWORD(aWParam); - mWnd = aWindow->GetWindowHandle(); + mWnd = aWidget->GetWindowHandle(); mTimeStamp = TimeStamp::Now(); } @@ -804,7 +804,7 @@ MouseScrollHandler::LastEventInfo::RoundDelta(double aDelta) bool MouseScrollHandler::LastEventInfo::InitWheelEvent( - nsWindow* aWindow, + nsWindowBase* aWidget, WheelEvent& aWheelEvent, const ModifierKeyState& aModKeyState) { @@ -813,7 +813,7 @@ MouseScrollHandler::LastEventInfo::InitWheelEvent( // XXX Why don't we use lParam value? We should use lParam value because // our internal message is always posted by original message handler. // So, GetMessagePos() may return different cursor position. - InitEvent(aWindow, aWheelEvent); + InitEvent(aWidget, aWheelEvent); aModKeyState.InitInputEvent(aWheelEvent); @@ -841,12 +841,12 @@ MouseScrollHandler::LastEventInfo::InitWheelEvent( lineOrPageDelta * orienter * RoundDelta(nativeDeltaPerUnit); PR_LOG(gMouseScrollLog, PR_LOG_ALWAYS, - ("MouseScroll::LastEventInfo::InitWheelEvent: aWindow=%p, " + ("MouseScroll::LastEventInfo::InitWheelEvent: aWidget=%p, " "aWheelEvent { refPoint: { x: %d, y: %d }, deltaX: %f, deltaY: %f, " "lineOrPageDeltaX: %d, lineOrPageDeltaY: %d, " "isShift: %s, isControl: %s, isAlt: %s, isMeta: %s }, " "mAccumulatedDelta: %d", - aWindow, aWheelEvent.refPoint.x, aWheelEvent.refPoint.y, + aWidget, aWheelEvent.refPoint.x, aWheelEvent.refPoint.y, aWheelEvent.deltaX, aWheelEvent.deltaY, aWheelEvent.lineOrPageDeltaX, aWheelEvent.lineOrPageDeltaY, GetBoolName(aWheelEvent.IsShift()), @@ -1065,6 +1065,11 @@ MouseScrollHandler::Device::GetWorkaroundPref(const char* aPrefName, void MouseScrollHandler::Device::Init() { + // Not supported in metro mode. + if (XRE_GetWindowsEnvironment() == WindowsEnvironmentType_Metro) { + return; + } + sFakeScrollableWindowNeeded = GetWorkaroundPref("ui.trackpoint_hack.enabled", (TrackPoint::IsDriverInstalled() || @@ -1170,7 +1175,7 @@ MouseScrollHandler::Device::Elantech::IsHelperWindow(HWND aWnd) /* static */ bool -MouseScrollHandler::Device::Elantech::HandleKeyMessage(nsWindow* aWindow, +MouseScrollHandler::Device::Elantech::HandleKeyMessage(nsWindowBase* aWidget, UINT aMsg, WPARAM aWParam) { @@ -1204,9 +1209,9 @@ MouseScrollHandler::Device::Elantech::HandleKeyMessage(nsWindow* aWindow, aWParam == VK_NEXT ? "Forward" : "Back")); nsCommandEvent commandEvent(true, nsGkAtoms::onAppCommand, - (aWParam == VK_NEXT) ? nsGkAtoms::Forward : nsGkAtoms::Back, aWindow); - InitEvent(aWindow, commandEvent); - MouseScrollHandler::DispatchEvent(aWindow, commandEvent); + (aWParam == VK_NEXT) ? nsGkAtoms::Forward : nsGkAtoms::Back, aWidget); + InitEvent(aWidget, commandEvent); + MouseScrollHandler::DispatchEvent(aWidget, commandEvent); } #ifdef PR_LOGGING else { @@ -1484,7 +1489,7 @@ MouseScrollHandler::SynthesizingEvent::Synthesize(const POINTS& aCursorPoint, } void -MouseScrollHandler::SynthesizingEvent::NativeMessageReceived(nsWindow* aWindow, +MouseScrollHandler::SynthesizingEvent::NativeMessageReceived(nsWindowBase* aWidget, UINT aMessage, WPARAM aWParam, LPARAM aLParam) @@ -1492,13 +1497,13 @@ MouseScrollHandler::SynthesizingEvent::NativeMessageReceived(nsWindow* aWindow, if (mStatus == SENDING_MESSAGE && mMessage == aMessage && mWParam == aWParam && mLParam == aLParam) { mStatus = NATIVE_MESSAGE_RECEIVED; - if (aWindow && aWindow->GetWindowHandle() == mWnd) { + if (aWidget && aWidget->GetWindowHandle() == mWnd) { return; } // If the target window is not ours and received window is our plugin // window, it comes from child window of the plugin. - if (aWindow && aWindow->GetWindowType() == eWindowType_plugin && - !WinUtils::GetNSWindowPtr(mWnd)) { + if (aWidget && aWidget->GetWindowType() == eWindowType_plugin && + !WinUtils::GetNSWindowBasePtr(mWnd)) { return; } // Otherwise, the message may not be sent by us. @@ -1506,9 +1511,9 @@ MouseScrollHandler::SynthesizingEvent::NativeMessageReceived(nsWindow* aWindow, PR_LOG(gMouseScrollLog, PR_LOG_ALWAYS, ("MouseScrollHandler::SynthesizingEvent::NativeMessageReceived(): " - "aWindow=%p, aWindow->GetWindowHandle()=0x%X, mWnd=0x%X, " + "aWidget=%p, aWidget->GetWindowHandle()=0x%X, mWnd=0x%X, " "aMessage=0x%04X, aWParam=0x%08X, aLParam=0x%08X, mStatus=%s", - aWindow, aWindow ? aWindow->GetWindowHandle() : 0, mWnd, + aWidget, aWidget ? aWidget->GetWindowHandle() : 0, mWnd, aMessage, aWParam, aLParam, GetStatusName())); // We failed to receive our sent message, we failed to do the job. diff --git a/widget/windows/WinMouseScrollHandler.h b/widget/windows/WinMouseScrollHandler.h index 5588a6403084..457cceff9a3e 100644 --- a/widget/windows/WinMouseScrollHandler.h +++ b/widget/windows/WinMouseScrollHandler.h @@ -13,7 +13,7 @@ #include "mozilla/TimeStamp.h" #include -class nsWindow; +class nsWindowBase; class nsGUIEvent; struct nsIntPoint; @@ -32,7 +32,7 @@ public: static void Initialize(); static void Shutdown(); - static bool ProcessMessage(nsWindow* aWindow, + static bool ProcessMessage(nsWindowBase* aWidget, UINT msg, WPARAM wParam, LPARAM lParam, @@ -42,7 +42,7 @@ public: * See nsIWidget::SynthesizeNativeMouseScrollEvent() for the detail about * this method. */ - static nsresult SynthesizeNativeMouseScrollEvent(nsWindow* aWindow, + static nsresult SynthesizeNativeMouseScrollEvent(nsWindowBase* aWidget, const nsIntPoint& aPoint, uint32_t aNativeMessage, int32_t aDelta, @@ -68,17 +68,17 @@ private: static MouseScrollHandler* sInstance; /** - * DispatchEvent() dispatches aEvent on aWindow. + * DispatchEvent() dispatches aEvent on aWidget. * * @return TRUE if the event was consumed. Otherwise, FALSE. */ - static bool DispatchEvent(nsWindow* aWindow, nsGUIEvent& aEvent); + static bool DispatchEvent(nsWindowBase* aWidget, nsGUIEvent& aEvent); /** * InitEvent() initializes the aEvent. If aPoint is null, the result of * GetCurrentMessagePos() will be used. */ - static void InitEvent(nsWindow* aWindow, + static void InitEvent(nsWindowBase* aWidget, nsGUIEvent& aEvent, nsIntPoint* aPoint = nullptr); @@ -107,13 +107,13 @@ private: * MOZ_WM_VSCROLL or MOZ_WM_HSCROLL if we need to dispatch mouse scroll * events. That avoids deadlock with plugin process. * - * @param aWindow A window which receives the message. + * @param aWidget A window which receives the message. * @param aMessage WM_MOUSEWHEEL, WM_MOUSEHWHEEL, WM_VSCROLL or * WM_HSCROLL. * @param aWParam The wParam value of the message. * @param aLParam The lParam value of the message. */ - void ProcessNativeMouseWheelMessage(nsWindow* aWindow, + void ProcessNativeMouseWheelMessage(nsWindowBase* aWidget, UINT aMessage, WPARAM aWParam, LPARAM aLParam); @@ -124,13 +124,13 @@ private: * processed as mouse wheel message. Otherwise, dispatches a content * command event. * - * @param aWindow A window which receives the message. + * @param aWidget A window which receives the message. * @param aMessage WM_VSCROLL or WM_HSCROLL. * @param aWParam The wParam value of the message. * @param aLParam The lParam value of the message. * @return TRUE if the message is processed. Otherwise, FALSE. */ - bool ProcessNativeScrollMessage(nsWindow* aWindow, + bool ProcessNativeScrollMessage(nsWindowBase* aWidget, UINT aMessage, WPARAM aWParam, LPARAM aLParam); @@ -140,12 +140,12 @@ private: * MOZ_WM_MOUSEHWHEEL which are posted when one of our windows received * WM_MOUSEWHEEL or WM_MOUSEHWHEEL for avoiding deadlock with OOPP. * - * @param aWindow A window which receives the wheel message. + * @param aWidget A window which receives the wheel message. * @param aMessage MOZ_WM_MOUSEWHEEL or MOZ_WM_MOUSEHWHEEL. * @param aWParam The wParam value of the original message. * @param aLParam The lParam value of the original message. */ - void HandleMouseWheelMessage(nsWindow* aWindow, + void HandleMouseWheelMessage(nsWindowBase* aWidget, UINT aMessage, WPARAM aWParam, LPARAM aLParam); @@ -156,12 +156,12 @@ private: * WM_VSCROLL or WM_HSCROLL and user wants them to emulate mouse wheel * message's behavior. * - * @param aWindow A window which receives the scroll message. + * @param aWidget A window which receives the scroll message. * @param aMessage MOZ_WM_VSCROLL or MOZ_WM_HSCROLL. * @param aWParam The wParam value of the original message. * @param aLParam The lParam value of the original message. */ - void HandleScrollMessageAsMouseWheelMessage(nsWindow* aWindow, + void HandleScrollMessageAsMouseWheelMessage(nsWindowBase* aWidget, UINT aMessage, WPARAM aWParam, LPARAM aLParam); @@ -184,10 +184,10 @@ private: class EventInfo { public: /** - * @param aWindow An nsWindow which is handling the event. + * @param aWidget An nsWindow which is handling the event. * @param aMessage Must be WM_MOUSEWHEEL or WM_MOUSEHWHEEL. */ - EventInfo(nsWindow* aWindow, UINT aMessage, WPARAM aWParam, LPARAM aLParam); + EventInfo(nsWindowBase* aWidget, UINT aMessage, WPARAM aWParam, LPARAM aLParam); bool CanDispatchWheelEvent() const; @@ -252,14 +252,14 @@ private: * This must be called only once during handling a message and after * RecordEvent() is called. * - * @param aWindow A window which will dispatch the event. + * @param aWidget A window which will dispatch the event. * @param aWheelEvent An NS_WHEEL_WHEEL event, this will be * initialized. * @param aModKeyState Current modifier key state. * @return TRUE if the event is ready to dispatch. * Otherwise, FALSE. */ - bool InitWheelEvent(nsWindow* aWindow, + bool InitWheelEvent(nsWindowBase* aWidget, WheelEvent& aWheelEvent, const ModifierKeyState& aModKeyState); @@ -365,7 +365,7 @@ private: UINT aMessage, WPARAM aWParam, LPARAM aLParam, const BYTE (&aKeyStates)[256]); - void NativeMessageReceived(nsWindow* aWindow, UINT aMessage, + void NativeMessageReceived(nsWindowBase* aWidget, UINT aMessage, WPARAM aWParam, LPARAM aLParam); void NotifyNativeMessageHandlingFinished(); @@ -435,7 +435,7 @@ public: * Key message handler for Elantech's hack. Returns TRUE if the message * is consumed by this handler. Otherwise, FALSE. */ - static bool HandleKeyMessage(nsWindow* aWindow, + static bool HandleKeyMessage(nsWindowBase* aWidget, UINT aMsg, WPARAM aWParam); diff --git a/widget/windows/WinUtils.cpp b/widget/windows/WinUtils.cpp index 1da311bd0e33..01ab26afbeee 100644 --- a/widget/windows/WinUtils.cpp +++ b/widget/windows/WinUtils.cpp @@ -252,13 +252,20 @@ GetNSWindowPropName() /* static */ bool -WinUtils::SetNSWindowPtr(HWND aWnd, nsWindow* aWindow) +WinUtils::SetNSWindowBasePtr(HWND aWnd, nsWindowBase* aWidget) { - if (!aWindow) { + if (!aWidget) { ::RemovePropW(aWnd, GetNSWindowPropName()); return true; } - return ::SetPropW(aWnd, GetNSWindowPropName(), (HANDLE)aWindow); + return ::SetPropW(aWnd, GetNSWindowPropName(), (HANDLE)aWidget); +} + +/* static */ +nsWindowBase* +WinUtils::GetNSWindowBasePtr(HWND aWnd) +{ + return static_cast(::GetPropW(aWnd, GetNSWindowPropName())); } /* static */ diff --git a/widget/windows/WinUtils.h b/widget/windows/WinUtils.h index 0d74e5bf9ab8..cc1badc1bd6c 100644 --- a/widget/windows/WinUtils.h +++ b/widget/windows/WinUtils.h @@ -26,6 +26,7 @@ #include "mozilla/Attributes.h" class nsWindow; +class nsWindowBase; struct KeyPair; namespace mozilla { @@ -134,12 +135,15 @@ public: bool aStopIfNotPopup = true); /** - * SetNSWindowPtr() associates an nsWindow to aWnd. If aWindow is NULL, - * it dissociate any nsWindow pointer from aWnd. - * GetNSWindowPtr() returns an nsWindow pointer which was associated by - * SetNSWindowPtr(). + * SetNSWindowBasePtr() associates an nsWindowBase to aWnd. If aWidget is NULL, + * it dissociate any nsBaseWidget pointer from aWnd. + * GetNSWindowBasePtr() returns an nsWindowBase pointer which was associated by + * SetNSWindowBasePtr(). + * GetNSWindowPtr() is a legacy api for win32 nsWindow and should be avoided + * outside of nsWindow src. */ - static bool SetNSWindowPtr(HWND aWnd, nsWindow* aWindow); + static bool SetNSWindowBasePtr(HWND aWnd, nsWindowBase* aWidget); + static nsWindowBase* GetNSWindowBasePtr(HWND aWnd); static nsWindow* GetNSWindowPtr(HWND aWnd); /** diff --git a/widget/windows/nsNativeThemeWin.cpp b/widget/windows/nsNativeThemeWin.cpp index fe9880545174..ca3bed42867e 100644 --- a/widget/windows/nsNativeThemeWin.cpp +++ b/widget/windows/nsNativeThemeWin.cpp @@ -64,11 +64,11 @@ GetTopLevelWindowActiveState(nsIFrame *aFrame) // Get the widget. nsIFrame's GetNearestWidget walks up the view chain // until it finds a real window. nsIWidget* widget = aFrame->GetNearestWidget(); - nsWindow * window = static_cast(widget); + nsWindowBase * window = static_cast(widget); if (!window) return mozilla::widget::themeconst::FS_INACTIVE; if (widget && !window->IsTopLevelWidget() && - !(window = window->GetParentWindow(false))) + !(window = window->GetParentWindowBase(false))) return mozilla::widget::themeconst::FS_INACTIVE; if (window->GetWindowHandle() == ::GetActiveWindow()) diff --git a/widget/windows/nsWindow.cpp b/widget/windows/nsWindow.cpp index ab9143880428..388da10256ec 100644 --- a/widget/windows/nsWindow.cpp +++ b/widget/windows/nsWindow.cpp @@ -886,7 +886,7 @@ void nsWindow::SubclassWindow(BOOL bState) } NS_ASSERTION(mPrevWndProc, "Null standard window procedure"); // connect the this pointer to the nsWindow handle - WinUtils::SetNSWindowPtr(mWnd, this); + WinUtils::SetNSWindowBasePtr(mWnd, this); } else { if (IsWindow(mWnd)) { if (mUnicodeWidget) { @@ -899,7 +899,7 @@ void nsWindow::SubclassWindow(BOOL bState) reinterpret_cast(mPrevWndProc)); } } - WinUtils::SetNSWindowPtr(mWnd, NULL); + WinUtils::SetNSWindowBasePtr(mWnd, NULL); mPrevWndProc = NULL; } } @@ -978,7 +978,14 @@ double nsWindow::GetDefaultScaleInternal() return gfxWindowsPlatform::GetPlatform()->GetDPIScale(); } -nsWindow* nsWindow::GetParentWindow(bool aIncludeOwner) +nsWindow* +nsWindow::GetParentWindow(bool aIncludeOwner) +{ + return static_cast(GetParentWindowBase(aIncludeOwner)); +} + +nsWindowBase* +nsWindow::GetParentWindowBase(bool aIncludeOwner) { if (mIsTopWidgetWindow) { // Must use a flag instead of mWindowType to tell if the window is the @@ -1017,7 +1024,7 @@ nsWindow* nsWindow::GetParentWindow(bool aIncludeOwner) } } - return widget; + return static_cast(widget); } BOOL CALLBACK diff --git a/widget/windows/nsWindow.h b/widget/windows/nsWindow.h index b53f81c3f021..b309e8a9a99b 100644 --- a/widget/windows/nsWindow.h +++ b/widget/windows/nsWindow.h @@ -85,6 +85,8 @@ public: // nsWindowBase virtual void InitEvent(nsGUIEvent& aEvent, nsIntPoint* aPoint = nullptr) MOZ_OVERRIDE; virtual bool DispatchWindowEvent(nsGUIEvent* aEvent) MOZ_OVERRIDE; + virtual nsWindowBase* GetParentWindowBase(bool aIncludeOwner) MOZ_OVERRIDE; + virtual bool IsTopLevelWidget() MOZ_OVERRIDE { return mIsTopWidgetWindow; } // nsIWidget interface NS_IMETHOD Create(nsIWidget *aParent, @@ -227,7 +229,7 @@ public: * Misc. */ virtual bool AutoErase(HDC dc); - bool IsTopLevelWidget() { return mIsTopWidgetWindow; } + /** * Start allowing Direct3D9 to be used by widgets when GetLayerManager is * called. diff --git a/widget/windows/nsWindowBase.h b/widget/windows/nsWindowBase.h index 454a06c957c3..b66f9b8cdb16 100644 --- a/widget/windows/nsWindowBase.h +++ b/widget/windows/nsWindowBase.h @@ -15,7 +15,6 @@ * nsWindowBase - Base class of common methods other classes need to access * in both win32 and winrt window classes. */ - class nsWindowBase : public nsBaseWidget { public: @@ -26,6 +25,16 @@ public: return static_cast(GetNativeData(NS_NATIVE_WINDOW)); } + /* + * Return the parent window, if it exists. + */ + virtual nsWindowBase* GetParentWindowBase(bool aIncludeOwner) = 0; + + /* + * Return true if this is a top level widget. + */ + virtual bool IsTopLevelWidget() = 0; + /* * Init a standard gecko event for this widget. */ diff --git a/widget/windows/winrt/MetroInput.cpp b/widget/windows/winrt/MetroInput.cpp index 08a4ae62887e..2747897e3407 100644 --- a/widget/windows/winrt/MetroInput.cpp +++ b/widget/windows/winrt/MetroInput.cpp @@ -31,8 +31,6 @@ namespace { const double SWIPE_MIN_DISTANCE = 5.0; const double SWIPE_MIN_VELOCITY = 5.0; - const double WHEEL_DELTA_DOUBLE = static_cast(WHEEL_DELTA); - // Convenience typedefs for event handler types typedef Foundation::__FITypedEventHandler_2_Windows__CUI__CInput__CEdgeGesture_Windows__CUI__CInput__CEdgeGestureEventArgs_t EdgeGestureHandler; typedef Foundation::__FITypedEventHandler_2_Windows__CUI__CCore__CCoreDispatcher_Windows__CUI__CCore__CAcceleratorKeyEventArgs_t AcceleratorKeyActivatedHandler; @@ -173,7 +171,6 @@ MetroInput::MetroInput(MetroWidget* aWidget, mTokenPointerMoved.value = 0; mTokenPointerEntered.value = 0; mTokenPointerExited.value = 0; - mTokenPointerWheelChanged.value = 0; mTokenEdgeStarted.value = 0; mTokenEdgeCanceled.value = 0; mTokenEdgeCompleted.value = 0; @@ -297,79 +294,6 @@ MetroInput::OnEdgeGestureCompleted(UI::Input::IEdgeGesture* sender, return S_OK; } -// This event is received when the user rotates a mouse wheel. MSDN does not -// seem to indicate that this event can be triggered from other types of input -// (i.e. pen, touch). -HRESULT -MetroInput::OnPointerWheelChanged(UI::Core::ICoreWindow* aSender, - UI::Core::IPointerEventArgs* aArgs) -{ -#ifdef DEBUG_INPUT - LogFunction(); -#endif - WRL::ComPtr currentPoint; - WRL::ComPtr props; - Foundation::Point position; - uint64_t timestamp; - float pressure; - boolean horzEvent; - int32_t delta; - - aArgs->get_CurrentPoint(currentPoint.GetAddressOf()); - currentPoint->get_Position(&position); - currentPoint->get_Timestamp(×tamp); - currentPoint->get_Properties(props.GetAddressOf()); - props->get_Pressure(&pressure); - props->get_IsHorizontalMouseWheel(&horzEvent); - props->get_MouseWheelDelta(&delta); - - WheelEvent wheelEvent(true, NS_WHEEL_WHEEL, mWidget.Get()); - mModifierKeyState.Update(); - mModifierKeyState.InitInputEvent(wheelEvent); - wheelEvent.refPoint = LayoutDeviceIntPoint::FromUntyped(MetroUtils::LogToPhys(position)); - wheelEvent.time = timestamp; - wheelEvent.inputSource = nsIDOMMouseEvent::MOZ_SOURCE_MOUSE; - wheelEvent.pressure = pressure; - wheelEvent.deltaMode = nsIDOMWheelEvent::DOM_DELTA_LINE; - - static int previousVertLeftOverDelta = 0; - static int previousHorLeftOverDelta = 0; - // Since we have chosen DOM_DELTA_LINE as our deltaMode, deltaX or deltaY - // should be the number of lines that we want to scroll. Windows has given - // us delta, which is a more precise value, and the constant WHEEL_DELTA, - // which defines the threshold of wheel movement before an action should - // be taken. - if (horzEvent) { - wheelEvent.deltaX = delta / WHEEL_DELTA_DOUBLE; - if ((delta > 0 && previousHorLeftOverDelta < 0) - || (delta < 0 && previousHorLeftOverDelta > 0)) { - previousHorLeftOverDelta = 0; - } - previousHorLeftOverDelta += delta; - wheelEvent.lineOrPageDeltaX = previousHorLeftOverDelta / WHEEL_DELTA; - previousHorLeftOverDelta %= WHEEL_DELTA; - } else { - int mouseWheelDelta = -1 * delta; - wheelEvent.deltaY = mouseWheelDelta / WHEEL_DELTA_DOUBLE; - if ((mouseWheelDelta > 0 && previousVertLeftOverDelta < 0) - || (mouseWheelDelta < 0 && previousVertLeftOverDelta > 0)) { - previousVertLeftOverDelta = 0; - } - previousVertLeftOverDelta += mouseWheelDelta; - wheelEvent.lineOrPageDeltaY = previousVertLeftOverDelta / WHEEL_DELTA; - previousVertLeftOverDelta %= WHEEL_DELTA; - } - - DispatchEventIgnoreStatus(&wheelEvent); - - WRL::ComPtr point; - aArgs->get_CurrentPoint(point.GetAddressOf()); - mGestureRecognizer->ProcessMouseWheelEvent(point.Get(), - wheelEvent.IsShift(), - wheelEvent.IsControl()); - return S_OK; -} - /** * This helper function is used by our processing of PointerPressed, * PointerReleased, and PointerMoved events. @@ -1149,7 +1073,6 @@ MetroInput::UnregisterInputEvents() { mWindow->remove_PointerMoved(mTokenPointerMoved); mWindow->remove_PointerEntered(mTokenPointerEntered); mWindow->remove_PointerExited(mTokenPointerExited); - mWindow->remove_PointerWheelChanged(mTokenPointerWheelChanged); // Unregistering from the gesture recognizer events probably isn't as // necessary since we're about to destroy the gesture recognizer, but @@ -1241,12 +1164,6 @@ MetroInput::RegisterInputEvents() &MetroInput::OnPointerExited).Get(), &mTokenPointerExited); - mWindow->add_PointerWheelChanged( - WRL::Callback( - this, - &MetroInput::OnPointerWheelChanged).Get(), - &mTokenPointerWheelChanged); - // Register for the events raised by our Gesture Recognizer mGestureRecognizer->add_Tapped( WRL::Callback( diff --git a/widget/windows/winrt/MetroInput.h b/widget/windows/winrt/MetroInput.h index e6d21c9f5dba..735dfa5ee98d 100644 --- a/widget/windows/winrt/MetroInput.h +++ b/widget/windows/winrt/MetroInput.h @@ -114,8 +114,6 @@ public: // by sending gecko events and forwarding these input events to its // GestureRecognizer to be processed into more complex input events // (tap, rightTap, rotate, etc) - HRESULT OnPointerWheelChanged(ICoreWindow* aSender, - IPointerEventArgs* aArgs); HRESULT OnPointerPressed(ICoreWindow* aSender, IPointerEventArgs* aArgs); HRESULT OnPointerReleased(ICoreWindow* aSender, @@ -243,7 +241,6 @@ private: EventRegistrationToken mTokenPointerMoved; EventRegistrationToken mTokenPointerEntered; EventRegistrationToken mTokenPointerExited; - EventRegistrationToken mTokenPointerWheelChanged; // When we register ourselves to handle edge gestures, we receive a // token. To we unregister ourselves, we must use the token we received. diff --git a/widget/windows/winrt/MetroWidget.cpp b/widget/windows/winrt/MetroWidget.cpp index b8836ec1ba66..3c78acb24df1 100644 --- a/widget/windows/winrt/MetroWidget.cpp +++ b/widget/windows/winrt/MetroWidget.cpp @@ -30,6 +30,7 @@ #include "nsExceptionHandler.h" #endif #include "UIABridgePrivate.h" +#include "WinMouseScrollHandler.h" using namespace Microsoft::WRL; using namespace Microsoft::WRL::Wrappers; @@ -168,6 +169,7 @@ MetroWidget::MetroWidget() : if (!gInstanceCount) { UserActivity(); nsTextStore::Initialize(); + MouseScrollHandler::Initialize(); KeyboardLayout::GetInstance()->OnLayoutChange(::GetKeyboardLayout(0)); } // !gInstanceCount gInstanceCount++; @@ -241,6 +243,7 @@ MetroWidget::Create(nsIWidget *aParent, // the main widget gets created first gTopLevelAssigned = true; MetroApp::SetBaseWidget(this); + WinUtils::SetNSWindowBasePtr(mWnd, this); if (mWidgetListener) { mWidgetListener->WindowActivated(); @@ -278,6 +281,7 @@ MetroWidget::Destroy() // Release references to children, device context, toolkit, and app shell. nsBaseWidget::Destroy(); nsBaseWidget::OnDestroy(); + WinUtils::SetNSWindowBasePtr(mWnd, nullptr); if (mLayerManager) { mLayerManager->Destroy(); @@ -524,43 +528,18 @@ MetroWidget::SynthesizeNativeMouseEvent(nsIntPoint aPoint, nsresult MetroWidget::SynthesizeNativeMouseScrollEvent(nsIntPoint aPoint, - uint32_t aNativeMessage, - double aDeltaX, - double aDeltaY, - double aDeltaZ, - uint32_t aModifierFlags, - uint32_t aAdditionalFlags) + uint32_t aNativeMessage, + double aDeltaX, + double aDeltaY, + double aDeltaZ, + uint32_t aModifierFlags, + uint32_t aAdditionalFlags) { - Log("ENTERED SynthesizeNativeMouseScrollEvent"); - - int32_t mouseData = 0; - if (aNativeMessage == MOUSEEVENTF_WHEEL) { - mouseData = static_cast(aDeltaY); - Log(" Vertical scroll, delta %d", mouseData); - } else if (aNativeMessage == MOUSEEVENTF_HWHEEL) { - mouseData = static_cast(aDeltaX); - Log(" Horizontal scroll, delta %d", mouseData); - } else { - Log("ERROR Unrecognized scroll event"); - return NS_ERROR_INVALID_ARG; - } - - INPUT inputs[2]; - memset(inputs, 0, 2*sizeof(INPUT)); - inputs[0].type = inputs[1].type = INPUT_MOUSE; - inputs[0].mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE; - // Inexplicably, the x and y coordinates that we want to move the mouse to - // are specified as values in the range (0, 65535). (0,0) represents the - // top left of the primary monitor and (65535, 65535) represents the - // bottom right of the primary monitor. - inputs[0].mi.dx = (aPoint.x * 65535) / ::GetSystemMetrics(SM_CXSCREEN); - inputs[0].mi.dy = (aPoint.y * 65535) / ::GetSystemMetrics(SM_CYSCREEN); - inputs[1].mi.dwFlags = aNativeMessage; - inputs[1].mi.mouseData = mouseData; - SendInputs(aModifierFlags, inputs, 2); - - Log("EXITING SynthesizeNativeMouseScrollEvent"); - return NS_OK; + return MouseScrollHandler::SynthesizeNativeMouseScrollEvent( + this, aPoint, aNativeMessage, + (aNativeMessage == WM_MOUSEWHEEL || aNativeMessage == WM_VSCROLL) ? + static_cast(aDeltaY) : static_cast(aDeltaX), + aModifierFlags, aAdditionalFlags); } static void @@ -597,9 +576,16 @@ MetroWidget::WindowProcedure(HWND aWnd, UINT aMsg, WPARAM aWParam, LPARAM aLPara // Indicates if we should hand messages to the default windows // procedure for processing. bool processDefault = true; + // The result returned if we do not do default processing. LRESULT processResult = 0; + MSGResult msgResult(&processResult); + MouseScrollHandler::ProcessMessage(this, aMsg, aWParam, aLParam, msgResult); + if (msgResult.mConsumed) { + return processResult; + } + switch (aMsg) { case WM_PAINT: { diff --git a/widget/windows/winrt/MetroWidget.h b/widget/windows/winrt/MetroWidget.h index 7aa27491865b..24e6ea17b2a8 100644 --- a/widget/windows/winrt/MetroWidget.h +++ b/widget/windows/winrt/MetroWidget.h @@ -71,6 +71,8 @@ public: // nsWindowBase virtual void InitEvent(nsGUIEvent& aEvent, nsIntPoint* aPoint = nullptr) MOZ_OVERRIDE; virtual bool DispatchWindowEvent(nsGUIEvent* aEvent) MOZ_OVERRIDE; + virtual bool IsTopLevelWidget() MOZ_OVERRIDE { return true; } + virtual nsWindowBase* GetParentWindowBase(bool aIncludeOwner) MOZ_OVERRIDE { return nullptr; } // nsBaseWidget virtual CompositorParent* NewCompositorParent(int aSurfaceWidth, int aSurfaceHeight);