Merge m-c to inbound, a=merge

MozReview-Commit-ID: LKkCpwt95EJ
This commit is contained in:
Wes Kocher 2016-03-15 17:32:53 -07:00
commit ad6f84a9ee
103 changed files with 18494 additions and 1199 deletions

View File

@ -68,7 +68,13 @@
width: 16px;
min-height: 16px;
-moz-margin-end: 3px;
list-style-image: url("chrome://global/skin/icons/loading_16.png");
list-style-image: url("chrome://global/skin/icons/loading.png");
}
@media (min-resolution: 1.1dppx) {
.update-throbber {
list-style-image: url("chrome://global/skin/icons/loading@2x.png");
}
}
.text-link,

View File

@ -152,8 +152,16 @@ function ensureSnippetsMapThen(aCallback)
}
let cache = new Map();
let cursorRequest = db.transaction(SNIPPETS_OBJECTSTORE_NAME)
.objectStore(SNIPPETS_OBJECTSTORE_NAME).openCursor();
let cursorRequest;
try {
cursorRequest = db.transaction(SNIPPETS_OBJECTSTORE_NAME)
.objectStore(SNIPPETS_OBJECTSTORE_NAME).openCursor();
} catch(ex) {
console.error(ex);
invokeCallbacks();
return;
}
cursorRequest.onerror = function (event) {
invokeCallbacks();
}

View File

@ -452,14 +452,12 @@ ContentSearchUIController.prototype = {
_currentEngineIndex: -1,
_cycleCurrentEngine: function (aReverse) {
if ((this._currentEngineIndex == this._oneOffButtons.length - 1 && !aReverse) ||
(this._currentEngineIndex < 0 && aReverse)) {
if ((this._currentEngineIndex == this._engines.length - 1 && !aReverse) ||
(this._currentEngineIndex == 0 && aReverse)) {
return;
}
this._currentEngineIndex += aReverse ? -1 : 1;
let engineName = this._currentEngineIndex > -1 ?
this._oneOffButtons[this._currentEngineIndex].engineName :
this._originalDefaultEngine.name;
let engineName = this._engines[this._currentEngineIndex].name;
this._sendMsg("SetCurrentEngine", engineName);
},
@ -572,6 +570,8 @@ ContentSearchUIController.prototype = {
this._setUpOneOffButtons();
delete this._pendingOneOffRefresh;
}
this._currentEngineIndex =
this._engines.findIndex(aEngine => aEngine.name == this.defaultEngine.name);
this._table.hidden = false;
this.input.setAttribute("aria-expanded", "true");
this._originalDefaultEngine = {
@ -829,7 +829,8 @@ ContentSearchUIController.prototype = {
this._oneOffButtons = [];
let engines = this._engines.filter(aEngine => aEngine.name != this.defaultEngine.name);
let engines = this._engines.filter(aEngine => aEngine.name != this.defaultEngine.name)
.filter(aEngine => !aEngine.hidden);
if (!engines.length) {
this._oneOffsTable.hidden = true;
return;

View File

@ -134,7 +134,6 @@ support-files =
[browser_aboutHealthReport.js]
skip-if = os == "linux" # Bug 924307
[browser_aboutHome.js]
skip-if = e10s # Bug 1093153 - no about:home support yet
[browser_aboutHome_wrapsCorrectly.js]
[browser_action_keyword.js]
[browser_action_keyword_override.js]

File diff suppressed because it is too large Load Diff

View File

@ -1152,6 +1152,9 @@
event.preventDefault();
return;
}
document.getAnonymousElementByAttribute(this, "anonid", "search-one-offs-context-set-default")
.setAttribute("disabled", target.engine == Services.search.currentEngine);
this._contextEngine = target.engine;
]]></handler>

View File

@ -1111,13 +1111,17 @@ var MozLoopServiceInternal = {
}, callback);
if (!chatboxInstance) {
resolve(null);
// It's common for unit tests to overload Chat.open.
// It's common for unit tests to overload Chat.open, so check if we actually
// got a DOM node back.
} else if (chatboxInstance.setAttribute) {
// Set properties that influence visual appearance of the chatbox right
// away to circumvent glitches.
chatboxInstance.setAttribute("customSize", "loopDefault");
chatboxInstance.parentNode.setAttribute("customSize", "loopDefault");
Chat.loadButtonSet(chatboxInstance, "minimize,swap," + kChatboxHangupButton.id);
// Final fall-through in case a unit test overloaded Chat.open. Here we can
// immediately resolve the promise.
} else {
resolve(windowId);
}
});

View File

@ -472,13 +472,11 @@ this.ContentSearch = {
let pref = Services.prefs.getCharPref("browser.search.hiddenOneOffs");
let hiddenList = pref ? pref.split(",") : [];
for (let engine of Services.search.getVisibleEngines()) {
if (hiddenList.indexOf(engine.name) != -1) {
continue;
}
let uri = engine.getIconURLBySize(16, 16);
state.engines.push({
name: engine.name,
iconBuffer: yield this._arrayBufferFromDataURI(uri),
hidden: hiddenList.indexOf(engine.name) != -1,
});
}
return state;

View File

@ -366,6 +366,7 @@ var currentStateObj = Task.async(function* () {
state.engines.push({
name: engine.name,
iconBuffer: yield arrayBufferFromDataURI(uri),
hidden: false,
});
}
return state;

View File

@ -1600,7 +1600,7 @@ richlistitem[type~="action"][actiontype$="tab"] > .ac-url-box > .ac-action-icon
}
.alltabs-item[busy] > .menu-iconic-left > .menu-iconic-icon {
list-style-image: url("chrome://global/skin/icons/loading_16.png");
list-style-image: url("chrome://global/skin/icons/loading.png");
}
.alltabs-item[tabIsVisible] {
@ -1626,7 +1626,7 @@ menuitem:hover > hbox > .alltabs-endimage[soundplaying] {
/* Sidebar */
#sidebar-throbber[loading="true"] {
list-style-image: url("chrome://global/skin/icons/loading_16.png");
list-style-image: url("chrome://global/skin/icons/loading.png");
-moz-margin-end: 4px;
}

View File

@ -101,7 +101,6 @@ browser.jar:
skin/classic/browser/tabbrowser/alltabs.png (tabbrowser/alltabs.png)
skin/classic/browser/tabbrowser/alltabs-inverted.png (tabbrowser/alltabs-inverted.png)
skin/classic/browser/tabbrowser/connecting.png (tabbrowser/connecting.png)
skin/classic/browser/tabbrowser/loading.png (tabbrowser/loading.png)
skin/classic/browser/tabbrowser/tab-active-middle.png (tabbrowser/tab-active-middle.png)
skin/classic/browser/tabbrowser/tab-arrow-left.png (tabbrowser/tab-arrow-left.png)
skin/classic/browser/tabbrowser/tab-arrow-left-inverted.png (tabbrowser/tab-arrow-left-inverted.png)

View File

@ -14,7 +14,7 @@
}
.statusIcon[status="active"] {
list-style-image: url("chrome://global/skin/icons/loading_16.png");
list-style-image: url("chrome://global/skin/icons/loading.png");
}
.statusIcon[status="error"] {

View File

@ -112,9 +112,15 @@ description > .text-link:focus {
width: 0.5em;
}
#pairDeviceThrobber,
#login-throbber {
-moz-box-align: center;
}
#pairDeviceThrobber > image,
#login-throbber > image {
list-style-image: url("chrome://global/skin/icons/loading_16.png");
width: 16px;
list-style-image: url("chrome://global/skin/icons/loading.png");
}
#captchaFeedback {

View File

@ -2383,7 +2383,15 @@ sidebarheader {
.sidebar-throbber[loading="true"],
#sidebar-throbber[loading="true"] {
list-style-image: url("chrome://global/skin/icons/loading_16.png");
list-style-image: url("chrome://global/skin/icons/loading.png");
}
@media (min-resolution: 2dppx) {
.sidebar-throbber[loading="true"],
#sidebar-throbber[loading="true"] {
list-style-image: url("chrome://global/skin/icons/loading@2x.png");
width: 16px;
}
}
/* ----- CONTENT ----- */
@ -2472,7 +2480,7 @@ toolbarbutton.chevron > .toolbarbutton-menu-dropmarker {
}
.tab-throbber[progress] {
list-style-image: url("chrome://browser/skin/tabbrowser/loading@2x.png");
list-style-image: url("chrome://global/skin/icons/loading@2x.png");
}
}
@ -2869,14 +2877,18 @@ toolbarbutton.chevron > .toolbarbutton-menu-dropmarker {
list-style-image: url("chrome://mozapps/skin/places/defaultFavicon.png");
}
.alltabs-item[busy] > .menu-iconic-left > .menu-iconic-icon {
list-style-image: url("chrome://global/skin/icons/loading.png") !important;
}
@media (min-resolution: 2dppx) {
.alltabs-item > .menu-iconic-left > .menu-iconic-icon {
list-style-image: url("chrome://mozapps/skin/places/defaultFavicon@2x.png");
}
}
.alltabs-item[busy] > .menu-iconic-left > .menu-iconic-icon {
list-style-image: url("chrome://global/skin/icons/loading_16.png") !important;
.alltabs-item[busy] > .menu-iconic-left > .menu-iconic-icon {
list-style-image: url("chrome://global/skin/icons/loading@2x.png") !important;
}
}
.alltabs-item[tabIsVisible] {

View File

@ -163,8 +163,6 @@ browser.jar:
skin/classic/browser/tabbrowser/newtab-inverted@2x.png (tabbrowser/newtab-inverted@2x.png)
skin/classic/browser/tabbrowser/connecting.png (tabbrowser/connecting.png)
skin/classic/browser/tabbrowser/connecting@2x.png (tabbrowser/connecting@2x.png)
skin/classic/browser/tabbrowser/loading.png (tabbrowser/loading.png)
skin/classic/browser/tabbrowser/loading@2x.png (tabbrowser/loading@2x.png)
skin/classic/browser/tabbrowser/tab-active-middle.png (tabbrowser/tab-active-middle.png)
skin/classic/browser/tabbrowser/tab-active-middle@2x.png (tabbrowser/tab-active-middle@2x.png)
skin/classic/browser/tabbrowser/tab-arrow-left.png (tabbrowser/tab-arrow-left.png)

View File

@ -14,7 +14,13 @@
}
.statusIcon[status="active"] {
list-style-image: url("chrome://global/skin/icons/loading_16.png");
list-style-image: url("chrome://global/skin/icons/loading.png");
}
@media (min-resolution: 2dppx) {
.statusIcon[status="active"] {
list-style-image: url("chrome://global/skin/icons/loading@2x.png");
}
}
.statusIcon[status="error"] {

View File

@ -111,9 +111,22 @@ description > .text-link:focus {
width: 0.5em;
}
#pairDeviceThrobber,
#login-throbber {
-moz-box-align: center;
}
#pairDeviceThrobber > image,
#login-throbber > image {
list-style-image: url("chrome://global/skin/icons/loading_16.png");
width: 16px;
list-style-image: url("chrome://global/skin/icons/loading.png");
}
@media (min-resolution: 2dppx) {
#pairDeviceThrobber > image,
#login-throbber > image {
list-style-image: url("chrome://global/skin/icons/loading@2x.png");
}
}
#captchaFeedback {

View File

@ -31,7 +31,7 @@
}
#social-sidebar-button[loading="true"] {
list-style-image: url("chrome://global/skin/icons/loading_16.png");
list-style-image: url("chrome://global/skin/icons/loading.png");
}
#social-sidebar-favico {

View File

@ -131,7 +131,7 @@
}
.tab-throbber[progress] {
list-style-image: url("chrome://browser/skin/tabbrowser/loading.png");
list-style-image: url("chrome://global/skin/icons/loading.png");
}
.tab-label {

View File

@ -1954,7 +1954,7 @@ richlistitem[type~="action"][actiontype$="tab"] > .ac-url-box > .ac-action-icon
}
.tab-throbber[progress] {
list-style-image: url("chrome://browser/skin/tabbrowser/loading@2x.png");
list-style-image: url("chrome://global/skin/icons/loading@2x.png");
}
}
@ -2118,7 +2118,13 @@ richlistitem[type~="action"][actiontype$="tab"] > .ac-url-box > .ac-action-icon
}
.alltabs-item[busy] > .menu-iconic-left > .menu-iconic-icon {
list-style-image: url("chrome://global/skin/icons/loading_16.png");
list-style-image: url("chrome://global/skin/icons/loading.png");
}
@media (min-resolution: 1.1dppx) {
.alltabs-item[busy] > .menu-iconic-left > .menu-iconic-icon {
list-style-image: url("chrome://global/skin/icons/loading@2x.png");
}
}
.alltabs-item[tabIsVisible] {
@ -2164,10 +2170,17 @@ toolbarbutton.chevron > .toolbarbutton-icon {
}
#sidebar-throbber[loading="true"] {
list-style-image: url("chrome://global/skin/icons/loading_16.png");
list-style-image: url("chrome://global/skin/icons/loading.png");
-moz-margin-end: 4px;
}
@media (min-resolution: 1.1dppx) {
#sidebar-throbber[loading="true"] {
list-style-image: url("chrome://global/skin/icons/loading@2x.png");
width: 16px;
}
}
/* Bookmarks toolbar */
#PlacesToolbarDropIndicator {
list-style-image: url(chrome://browser/skin/places/toolbarDropMarker.png);

View File

@ -171,8 +171,6 @@ browser.jar:
skin/classic/browser/tabbrowser/newtab-inverted-XPVista7.svg (tabbrowser/newtab-inverted-XPVista7.svg)
skin/classic/browser/tabbrowser/connecting.png (tabbrowser/connecting.png)
skin/classic/browser/tabbrowser/connecting@2x.png (tabbrowser/connecting@2x.png)
skin/classic/browser/tabbrowser/loading.png (tabbrowser/loading.png)
skin/classic/browser/tabbrowser/loading@2x.png (tabbrowser/loading@2x.png)
skin/classic/browser/tabbrowser/tab-active-middle.png (tabbrowser/tab-active-middle.png)
skin/classic/browser/tabbrowser/tab-active-middle@2x.png (tabbrowser/tab-active-middle@2x.png)
skin/classic/browser/tabbrowser/tab-arrow-left.svg (tabbrowser/tab-arrow-left.svg)

View File

@ -14,7 +14,13 @@
}
.statusIcon[status="active"] {
list-style-image: url("chrome://global/skin/icons/loading_16.png");
list-style-image: url("chrome://global/skin/icons/loading.png");
}
@media (min-resolution: 1.1dppx) {
.statusIcon[status="active"] {
list-style-image: url("chrome://global/skin/icons/loading@2x.png");
}
}
.statusIcon[status="error"] {

View File

@ -112,9 +112,22 @@ description > .text-link:focus {
width: 0.5em;
}
#pairDeviceThrobber,
#login-throbber {
-moz-box-align: center;
}
#pairDeviceThrobber > image,
#login-throbber > image {
list-style-image: url("chrome://global/skin/icons/loading_16.png");
width: 16px;
list-style-image: url("chrome://global/skin/icons/loading.png");
}
@media (min-resolution: 1.1dppx) {
#pairDeviceThrobber > image,
#login-throbber > image {
list-style-image: url("chrome://global/skin/icons/loading@2x.png");
}
}
#captchaFeedback {

View File

@ -469,7 +469,6 @@ html, body, #app, #memory-tool {
.heap-tree-percent,
.heap-tree-item-name {
white-space: nowrap;
overflow: hidden;
}
.heap-tree-number {

View File

@ -444,7 +444,6 @@ let SourceActor = ActorClass({
* Handler for the "prettyPrint" packet.
*/
prettyPrint: method(function (indent) {
dump("EN IS HET EEN KEER NIET KUT " + JSON.stringify(indent) + "\n");
this.threadActor.sources.prettyPrint(this.url, indent);
return this._getSourceText()
.then(this._sendToPrettyPrintWorker(indent))

View File

@ -138,6 +138,9 @@ android {
exclude 'org/mozilla/gecko/push/**/*.java'
}
}
resources {
srcDir "${topsrcdir}/mobile/android/tests/background/junit4/resources"
}
}
androidTest {

View File

@ -341,6 +341,11 @@
android:name="org.mozilla.gecko.dlc.DownloadContentService">
</service>
<service
android:exported="false"
android:name="org.mozilla.gecko.feeds.FeedService">
</service>
<service
android:name="org.mozilla.gecko.telemetry.TelemetryUploadService"
android:exported="false"/>

View File

@ -0,0 +1,110 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.feeds;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.mozilla.gecko.feeds.parser.Feed;
import org.mozilla.gecko.feeds.parser.SimpleFeedParser;
import org.mozilla.gecko.util.IOUtils;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import ch.boye.httpclientandroidlib.util.TextUtils;
/**
* Helper class for fetching and parsing a feed.
*/
public class FeedFetcher {
private static final int CONNECT_TIMEOUT = 15000;
private static final int READ_TIMEOUT = 15000;
public static class FeedResponse {
public final Feed feed;
public final String etag;
public final String lastModified;
public FeedResponse(Feed feed, String etag, String lastModified) {
this.feed = feed;
this.etag = etag;
this.lastModified = lastModified;
}
}
/**
* Fetch and parse a feed from the given URL. Will return null if fetching or parsing failed.
*/
public static FeedResponse fetchAndParseFeed(String url) {
return fetchAndParseFeedIfModified(url, null, null);
}
/**
* Fetch and parse a feed from the given URL using the given ETag and "Last modified" value.
*
* Will return null if fetching or parsing failed. Will also return null if the feed has not
* changed (ETag / Last-Modified-Since).
*
* @param eTag The ETag from the last fetch or null if no ETag is available (will always fetch feed)
* @param lastModified The "Last modified" header from the last time the feed has been fetch or
* null if no value is available (will always fetch feed)
* @return A FeedResponse or null if no feed could be fetched (error or no new version available)
*/
@Nullable
public static FeedResponse fetchAndParseFeedIfModified(@NonNull String url, @Nullable String eTag, @Nullable String lastModified) {
HttpURLConnection connection = null;
InputStream stream = null;
try {
connection = (HttpURLConnection) new URL(url).openConnection();
connection.setInstanceFollowRedirects(true);
connection.setConnectTimeout(CONNECT_TIMEOUT);
connection.setReadTimeout(READ_TIMEOUT);
if (!TextUtils.isEmpty(eTag)) {
connection.setRequestProperty("If-None-Match", eTag);
}
if (!TextUtils.isEmpty(lastModified)) {
connection.setRequestProperty("If-Modified-Since", lastModified);
}
final int statusCode = connection.getResponseCode();
if (statusCode != HttpURLConnection.HTTP_OK) {
return null;
}
String responseEtag = connection.getHeaderField("ETag");
if (!TextUtils.isEmpty(responseEtag) && responseEtag.startsWith("W/")) {
// Weak ETag, get actual ETag value
responseEtag = responseEtag.substring(2);
}
final String updatedLastModified = connection.getHeaderField("Last-Modified");
stream = new BufferedInputStream(connection.getInputStream());
final SimpleFeedParser parser = new SimpleFeedParser();
final Feed feed = parser.parse(stream);
return new FeedResponse(feed, responseEtag, updatedLastModified);
} catch (IOException e) {
return null;
} catch (SimpleFeedParser.ParserException e) {
return null;
} finally {
if (connection != null) {
connection.disconnect();
}
IOUtils.safeStreamClose(stream);
}
}
}

View File

@ -0,0 +1,62 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.feeds;
import android.app.IntentService;
import android.content.Intent;
import android.util.Log;
import com.keepsafe.switchboard.SwitchBoard;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.feeds.action.CheckAction;
import org.mozilla.gecko.feeds.subscriptions.SubscriptionStorage;
import org.mozilla.gecko.util.Experiments;
/**
* Background service for subscribing to and checking website feeds to notify the user about updates.
*/
public class FeedService extends IntentService {
private static final String LOGTAG = "GeckoFeedService";
public static final String ACTION_CHECK = AppConstants.ANDROID_PACKAGE_NAME + ".FEEDS.CHECK";
private SubscriptionStorage storage;
public FeedService() {
super(LOGTAG);
}
@Override
public void onCreate() {
super.onCreate();
storage = new SubscriptionStorage(getApplicationContext());
}
@Override
protected void onHandleIntent(Intent intent) {
if (intent == null) {
return;
}
if (!SwitchBoard.isInExperiment(this, Experiments.CONTENT_NOTIFICATIONS)) {
Log.d(LOGTAG, "Not in content notifications experiment. Skipping.");
return;
}
switch (intent.getAction()) {
case ACTION_CHECK:
new CheckAction(this, storage).perform();
break;
default:
Log.e(LOGTAG, "Unknown action: " + intent.getAction());
}
storage.persistChanges();
}
}

View File

@ -0,0 +1,97 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.feeds.action;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import org.mozilla.gecko.BrowserApp;
import org.mozilla.gecko.R;
import org.mozilla.gecko.feeds.FeedFetcher;
import org.mozilla.gecko.feeds.parser.Feed;
import org.mozilla.gecko.feeds.subscriptions.FeedSubscription;
import org.mozilla.gecko.feeds.subscriptions.SubscriptionStorage;
import java.util.List;
/**
* CheckAction: Check if feeds we subscribed to have new content available.
*/
public class CheckAction {
private static final String LOGTAG = "FeedCheckAction";
private Context context;
private SubscriptionStorage storage;
public CheckAction(Context context, SubscriptionStorage storage) {
this.context = context;
this.storage = storage;
}
public void perform() {
final List<FeedSubscription> subscriptions = storage.getSubscriptions();
Log.d(LOGTAG, "Checking feeds for updates (" + subscriptions.size() + " feeds) ..");
for (FeedSubscription subscription : subscriptions) {
Log.i(LOGTAG, "Checking feed: " + subscription.getFeedTitle());
FeedFetcher.FeedResponse response = fetchFeed(subscription);
if (response == null) {
continue;
}
if (subscription.isNewer(response)) {
Log.d(LOGTAG, "* Feed has changed. New item: " + response.feed.getLastItem().getTitle());
storage.updateSubscription(subscription, response);
notify(response.feed);
}
}
}
private void notify(Feed feed) {
NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle()
.bigText(feed.getLastItem().getTitle())
.setBigContentTitle(feed.getTitle())
.setSummaryText(feed.getLastItem().getURL());
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setComponent(new ComponentName(context, BrowserApp.class));
intent.setData(Uri.parse(feed.getLastItem().getURL()));
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
Notification notification = new NotificationCompat.Builder(context)
.setSmallIcon(R.drawable.ic_status_logo)
.setContentTitle(feed.getTitle())
.setContentText(feed.getLastItem().getTitle())
.setStyle(style)
.setColor(ContextCompat.getColor(context, R.color.link_blue))
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build();
NotificationManagerCompat.from(context).notify(R.id.websiteContentNotification, notification);
}
private FeedFetcher.FeedResponse fetchFeed(FeedSubscription subscription) {
return FeedFetcher.fetchAndParseFeedIfModified(
subscription.getFeedUrl(),
subscription.getETag(),
subscription.getLastModified()
);
}
}

View File

@ -0,0 +1,98 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.feeds.parser;
import ch.boye.httpclientandroidlib.util.TextUtils;
public class Feed {
private String title;
private String websiteURL;
private String feedURL;
private Item lastItem;
public static Feed create(String title, String websiteURL, String feedURL, Item lastItem) {
Feed feed = new Feed();
feed.setTitle(title);
feed.setWebsiteURL(websiteURL);
feed.setFeedURL(feedURL);
feed.setLastItem(lastItem);
return feed;
}
/* package-private */ Feed() {}
/* package-private */ void setTitle(String title) {
this.title = title;
}
/* package-private */ void setWebsiteURL(String websiteURL) {
this.websiteURL = websiteURL;
}
/* package-private */ void setFeedURL(String feedURL) {
this.feedURL = feedURL;
}
/* package-private */ void setLastItem(Item lastItem) {
this.lastItem = lastItem;
}
/**
* Is this feed object sufficiently complete so that we can use it?
*/
/* package-private */ boolean isSufficientlyComplete() {
return !TextUtils.isEmpty(title) &&
lastItem != null &&
!TextUtils.isEmpty(lastItem.getURL()) &&
!TextUtils.isEmpty(lastItem.getTitle());
}
/**
* Guesstimate if the given feed is a newer representation of this feed.
*/
public boolean hasBeenUpdated(Feed newFeed) {
final Item otherItem = newFeed.getLastItem();
if (lastItem.getTimestamp() > otherItem.getTimestamp()) {
// The timestamp is from a newer date so we expect that this item is a new item. But this
// could also mean that the timestamp of an already existing item has been updated. We
// accept that and assume that the content will have changed too in this case.
return true;
}
if (lastItem.getTimestamp() == otherItem.getTimestamp() && lastItem.getTimestamp() != 0) {
// We have a timestamp that is not zero and this item has still the timestamp: It's very
// likely that we are looking at the same item. We assume this is not new content.
return false;
}
if (!lastItem.getURL().equals(otherItem.getURL())) {
// The URL changed: It is very likely that this is a new item. At least it has been updated
// in a way that we just treat it as new content here.
return true;
}
return false;
}
public String getTitle() {
return title;
}
public String getWebsiteURL() {
return websiteURL;
}
public String getFeedURL() {
return feedURL;
}
public Item getLastItem() {
return lastItem;
}
}

View File

@ -0,0 +1,49 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.feeds.parser;
public class Item {
private String title;
private String url;
private long timestamp;
public static Item create(String title, String url, long timestamp) {
Item item = new Item();
item.setTitle(title);
item.setURL(url);
item.setTimestamp(timestamp);
return item;
}
/* package-private */ void setTitle(String title) {
this.title = title;
}
/* package-private */ void setURL(String url) {
this.url = url;
}
/* package-private */ void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
public String getTitle() {
return title;
}
public String getURL() {
return url;
}
/**
* @return the number of milliseconds since Jan. 1, 1970, midnight GMT.
*/
public long getTimestamp() {
return timestamp;
}
}

View File

@ -0,0 +1,357 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.feeds.parser;
import android.util.Log;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import ch.boye.httpclientandroidlib.util.TextUtils;
/**
* A super simple feed parser written for implementing "content notifications". This XML Pull Parser
* can read ATOM and RSS feeds and returns an object describing the feed and the latest entry.
*/
public class SimpleFeedParser {
/**
* Generic exception that's thrown by the parser whenever a stream cannot be parsed.
*/
public static class ParserException extends Exception {
private static final long serialVersionUID = -6119538440219805603L;
public ParserException(Throwable cause) {
super(cause);
}
public ParserException(String message) {
super(message);
}
}
private static final String LOGTAG = "Gecko/FeedParser";
private static final String TAG_RSS = "rss";
private static final String TAG_FEED = "feed";
private static final String TAG_RDF = "RDF";
private static final String TAG_TITLE = "title";
private static final String TAG_ITEM = "item";
private static final String TAG_LINK = "link";
private static final String TAG_ENTRY = "entry";
private static final String TAG_PUBDATE = "pubDate";
private static final String TAG_UPDATED = "updated";
private static final String TAG_DATE = "date";
private static final String TAG_SOURCE = "source";
private static final String TAG_IMAGE = "image";
private class ParserState {
public Feed feed;
public Item currentItem;
public boolean isRSS;
public boolean isATOM;
public boolean inSource;
public boolean inImage;
}
public Feed parse(InputStream in) throws ParserException, IOException {
final ParserState state = new ParserState();
try {
final XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
factory.setNamespaceAware(true);
XmlPullParser parser = factory.newPullParser();
parser.setInput(in, null);
int eventType = parser.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
switch (eventType) {
case XmlPullParser.START_DOCUMENT:
handleStartDocument(state);
break;
case XmlPullParser.START_TAG:
handleStartTag(parser, state);
break;
case XmlPullParser.END_TAG:
handleEndTag(parser, state);
break;
}
eventType = parser.next();
}
} catch (XmlPullParserException e) {
throw new ParserException(e);
}
if (!state.feed.isSufficientlyComplete()) {
throw new ParserException("Feed is not sufficiently complete");
}
return state.feed;
}
private void handleStartDocument(ParserState state) {
state.feed = new Feed();
}
private void handleStartTag(XmlPullParser parser, ParserState state) throws IOException, XmlPullParserException {
switch (parser.getName()) {
case TAG_RSS:
state.isRSS = true;
break;
case TAG_FEED:
state.isATOM = true;
break;
case TAG_RDF:
// This is a RSS 1.0 feed
state.isRSS = true;
break;
case TAG_ITEM:
case TAG_ENTRY:
state.currentItem = new Item();
break;
case TAG_TITLE:
handleTitleStartTag(parser, state);
break;
case TAG_LINK:
handleLinkStartTag(parser, state);
break;
case TAG_PUBDATE:
handlePubDateStartTag(parser, state);
break;
case TAG_UPDATED:
handleUpdatedStartTag(parser, state);
break;
case TAG_DATE:
handleDateStartTag(parser, state);
break;
case TAG_SOURCE:
state.inSource = true;
break;
case TAG_IMAGE:
state.inImage = true;
break;
}
}
private void handleEndTag(XmlPullParser parser, ParserState state) {
switch (parser.getName()) {
case TAG_ITEM:
case TAG_ENTRY:
handleItemOrEntryREndTag(state);
break;
case TAG_SOURCE:
state.inSource = false;
break;
case TAG_IMAGE:
state.inImage = false;
break;
}
}
private void handleTitleStartTag(XmlPullParser parser, ParserState state) throws IOException, XmlPullParserException {
if (state.inSource || state.inImage) {
// We do not care about titles in <source> or <image> tags.
return;
}
String title = getTextUntilEndTag(parser, TAG_TITLE);
title = title.replaceAll("[\r\n]", " ");
title = title.replaceAll(" +", " ");
if (state.currentItem != null) {
state.currentItem.setTitle(title);
} else {
state.feed.setTitle(title);
}
}
private void handleLinkStartTag(XmlPullParser parser, ParserState state) throws IOException, XmlPullParserException {
if (state.inSource || state.inImage) {
// We do not care about links in <source> or <image> tags.
return;
}
Map<String, String> attributes = fetchAttributes(parser);
if (attributes.size() > 0) {
String rel = attributes.get("rel");
if (state.currentItem == null && "self".equals(rel)) {
state.feed.setFeedURL(attributes.get("href"));
return;
}
if (rel == null || "alternate".equals(rel)) {
String type = attributes.get("type");
if (type == null || type.equals("text/html")) {
String link = attributes.get("href");
if (TextUtils.isEmpty(link)) {
return;
}
if (state.currentItem != null) {
state.currentItem.setURL(link);
} else {
state.feed.setWebsiteURL(link);
}
return;
}
}
}
if (state.isRSS) {
String link = getTextUntilEndTag(parser, TAG_LINK);
if (TextUtils.isEmpty(link)) {
return;
}
if (state.currentItem != null) {
state.currentItem.setURL(link);
} else {
state.feed.setWebsiteURL(link);
}
}
}
private void handleItemOrEntryREndTag(ParserState state) {
if (state.feed.getLastItem() == null || state.feed.getLastItem().getTimestamp() < state.currentItem.getTimestamp()) {
// Only set this item as "last item" if we do not have an item yet or this item is newer.
state.feed.setLastItem(state.currentItem);
}
state.currentItem = null;
}
private void handlePubDateStartTag(XmlPullParser parser, ParserState state) throws IOException, XmlPullParserException {
if (state.currentItem == null) {
return;
}
String pubDate = getTextUntilEndTag(parser, TAG_PUBDATE);
if (TextUtils.isEmpty(pubDate)) {
return;
}
// RFC-822
SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
updateCurrentItemTimestamp(state, pubDate, format);
}
private void handleUpdatedStartTag(XmlPullParser parser, ParserState state) throws IOException, XmlPullParserException {
if (state.inSource) {
// We do not care about stuff in <source> tags.
return;
}
if (state.currentItem == null) {
// We are only interested in <updated> values of feed items.
return;
}
String updated = getTextUntilEndTag(parser, TAG_UPDATED);
if (TextUtils.isEmpty(updated)) {
return;
}
SimpleDateFormat[] formats = new SimpleDateFormat[] {
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US),
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US)
};
// Fix timezones SimpleDateFormat can't parse:
// 2016-01-26T18:56:54Z -> 2016-01-26T18:56:54+0000 (Timezone: Z -> +0000)
updated = updated.replaceFirst("Z$", "+0000");
// 2016-01-26T18:56:54+01:00 -> 2016-01-26T18:56:54+0100 (Timezone: +01:00 -> +0100)
updated = updated.replaceFirst("([0-9]{2})([\\+\\-])([0-9]{2}):([0-9]{2})$", "$1$2$3$4");
updateCurrentItemTimestamp(state, updated, formats);
}
private void handleDateStartTag(XmlPullParser parser, ParserState state) throws IOException, XmlPullParserException {
if (state.currentItem == null) {
// We are only interested in <updated> values of feed items.
return;
}
String text = getTextUntilEndTag(parser, TAG_DATE);
if (TextUtils.isEmpty(text)) {
return;
}
// Fix timezones SimpleDateFormat can't parse:
// 2016-01-26T18:56:54+00:00 -> 2016-01-26T18:56:54+0000
text = text.replaceFirst("([0-9]{2})([\\+\\-])([0-9]{2}):([0-9]{2})$", "$1$2$3$4");
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US);
updateCurrentItemTimestamp(state, text, format);
}
private void updateCurrentItemTimestamp(ParserState state, String text, SimpleDateFormat... formats) {
for (SimpleDateFormat format : formats) {
try {
Date date = format.parse(text);
state.currentItem.setTimestamp(date.getTime());
return;
} catch (ParseException e) {
Log.w(LOGTAG, "Could not parse 'updated': " + text);
}
}
}
private Map<String, String> fetchAttributes(XmlPullParser parser) {
Map<String, String> attributes = new HashMap<>();
for (int i = 0; i < parser.getAttributeCount(); i++) {
attributes.put(parser.getAttributeName(i), parser.getAttributeValue(i));
}
return attributes;
}
private String getTextUntilEndTag(XmlPullParser parser, String tag) throws IOException, XmlPullParserException {
StringBuilder builder = new StringBuilder();
while (parser.next() != XmlPullParser.END_DOCUMENT) {
if (parser.getEventType() == XmlPullParser.TEXT) {
builder.append(parser.getText());
} else if (parser.getEventType() == XmlPullParser.END_TAG && tag.equals(parser.getName())) {
break;
}
}
return builder.toString().trim();
}
}

View File

@ -0,0 +1,161 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.feeds.subscriptions;
import android.text.TextUtils;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.feeds.FeedFetcher;
import org.mozilla.gecko.feeds.parser.Item;
/**
* An object describing a subscription and containing some meta data about the last time we fetched
* the feed.
*/
public class FeedSubscription {
private static final String JSON_KEY_FEED_URL = "feed_url";
private static final String JSON_KEY_FEED_TITLE = "feed_title";
private static final String JSON_KEY_WEBSITE_URL = "website_url";
private static final String JSON_KEY_LAST_ITEM_TITLE = "last_item_title";
private static final String JSON_KEY_LAST_ITEM_URL = "last_item_url";
private static final String JSON_KEY_LAST_ITEM_TIMESTAMP = "last_item_timestamp";
private static final String JSON_KEY_ETAG = "etag";
private static final String JSON_KEY_LAST_MODIFIED = "last_modified";
private static final String JSON_KEY_BOOKMARK_GUID = "bookmark_guid";
private String bookmarkGuid; // Currently a subscription is linked to a bookmark
private String feedUrl;
private String feedTitle;
private String websiteUrl;
private String lastItemTitle;
private String lastItemUrl;
private long lastItemTimestamp;
private String etag;
private String lastModified;
public static FeedSubscription create(String bookmarkGuid, String url, FeedFetcher.FeedResponse response) {
FeedSubscription subscription = new FeedSubscription();
subscription.bookmarkGuid = bookmarkGuid;
subscription.feedUrl = url;
subscription.update(response);
return subscription;
}
public static FeedSubscription fromJSON(JSONObject object) throws JSONException {
FeedSubscription subscription = new FeedSubscription();
subscription.feedUrl = object.getString(JSON_KEY_FEED_URL);
subscription.feedTitle = object.getString(JSON_KEY_FEED_TITLE);
subscription.websiteUrl = object.getString(JSON_KEY_WEBSITE_URL);
subscription.lastItemTitle = object.getString(JSON_KEY_LAST_ITEM_TITLE);
subscription.lastItemUrl = object.getString(JSON_KEY_LAST_ITEM_URL);
subscription.lastItemTimestamp = object.getLong(JSON_KEY_LAST_ITEM_TIMESTAMP);
subscription.etag = object.getString(JSON_KEY_ETAG);
subscription.lastModified = object.getString(JSON_KEY_LAST_MODIFIED);
subscription.bookmarkGuid = object.getString(JSON_KEY_BOOKMARK_GUID);
return subscription;
}
/* package-private */ void update(FeedFetcher.FeedResponse response) {
final String feedUrl = response.feed.getFeedURL();
if (!TextUtils.isEmpty(feedUrl)) {
// Prefer to use the URL we get from the feed for further requests
this.feedUrl = feedUrl;
}
feedTitle = response.feed.getTitle();
websiteUrl = response.feed.getWebsiteURL();
lastItemTitle = response.feed.getLastItem().getTitle();
lastItemUrl = response.feed.getLastItem().getURL();
lastItemTimestamp = response.feed.getLastItem().getTimestamp();
etag = response.etag;
lastModified = response.lastModified;
}
/**
* Guesstimate if this response is a newer representation of the feed.
*/
public boolean isNewer(FeedFetcher.FeedResponse response) {
final Item otherItem = response.feed.getLastItem();
if (lastItemTimestamp > otherItem.getTimestamp()) {
return true; // How to detect if this same item and it only has been updated?
}
if (lastItemTimestamp == otherItem.getTimestamp() &&
lastItemTimestamp != 0) {
return false;
}
if (lastItemUrl == null || !lastItemUrl.equals(otherItem.getURL())) {
// URL changed: Probably a different item
return true;
}
return false;
}
public String getFeedUrl() {
return feedUrl;
}
public String getFeedTitle() {
return feedTitle;
}
public String getWebsiteUrl() {
return websiteUrl;
}
public String getLastItemTitle() {
return lastItemTitle;
}
public String getLastItemUrl() {
return lastItemUrl;
}
public long getLastItemTimestamp() {
return lastItemTimestamp;
}
public String getETag() {
return etag;
}
public String getLastModified() {
return lastModified;
}
public String getBookmarkGUID() {
return bookmarkGuid;
}
public boolean isForTheSameBookmarkAs(FeedSubscription other) {
return TextUtils.equals(bookmarkGuid, other.bookmarkGuid);
}
public JSONObject toJSON() throws JSONException {
JSONObject object = new JSONObject();
object.put(JSON_KEY_FEED_URL, feedUrl);
object.put(JSON_KEY_FEED_TITLE, feedTitle);
object.put(JSON_KEY_WEBSITE_URL, websiteUrl);
object.put(JSON_KEY_LAST_ITEM_TITLE, lastItemTitle);
object.put(JSON_KEY_LAST_ITEM_URL, lastItemUrl);
object.put(JSON_KEY_LAST_ITEM_TIMESTAMP, lastItemTimestamp);
object.put(JSON_KEY_ETAG, etag);
object.put(JSON_KEY_LAST_MODIFIED, lastModified);
object.put(JSON_KEY_BOOKMARK_GUID, bookmarkGuid);
return object;
}
}

View File

@ -0,0 +1,220 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.feeds.subscriptions;
import android.content.Context;
import android.support.v4.util.AtomicFile;
import android.text.TextUtils;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.feeds.FeedFetcher;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Storage for feed subscriptions. This is just using a plain JSON file on disk.
*
* TODO: Store this data in the url metadata tablet instead (See bug 1250707)
*/
public class SubscriptionStorage {
private static final String LOGTAG = "FeedStorage";
private static final String FILE_NAME = "feed_subscriptions";
private static final String JSON_KEY_SUBSCRIPTIONS = "subscriptions";
private final AtomicFile file; // Guarded by 'file'
private List<FeedSubscription> subscriptions;
private boolean hasLoadedSubscriptions;
private boolean hasChanged;
public SubscriptionStorage(Context context) {
this(new AtomicFile(new File(context.getApplicationInfo().dataDir, FILE_NAME)));
startLoadFromDisk();
}
// For injecting mocked AtomicFile objects during test
protected SubscriptionStorage(AtomicFile file) {
this.subscriptions = new ArrayList<>();
this.file = file;
}
public synchronized void addSubscription(FeedSubscription subscription) {
awaitLoadingSubscriptionsLocked();
subscriptions.add(subscription);
hasChanged = true;
}
public synchronized void removeSubscription(FeedSubscription subscription) {
awaitLoadingSubscriptionsLocked();
Iterator<FeedSubscription> iterator = subscriptions.iterator();
while (iterator.hasNext()) {
if (subscription.isForTheSameBookmarkAs(iterator.next())) {
iterator.remove();
hasChanged = true;
return;
}
}
}
public synchronized List<FeedSubscription> getSubscriptions() {
awaitLoadingSubscriptionsLocked();
return new ArrayList<>(subscriptions);
}
public synchronized void updateSubscription(FeedSubscription subscription, FeedFetcher.FeedResponse response) {
awaitLoadingSubscriptionsLocked();
subscription.update(response);
for (int i = 0; i < subscriptions.size(); i++) {
if (subscriptions.get(i).isForTheSameBookmarkAs(subscription)) {
subscriptions.set(i, subscription);
hasChanged = true;
return;
}
}
}
public synchronized boolean hasSubscriptionForBookmark(String guid) {
awaitLoadingSubscriptionsLocked();
for (int i = 0; i < subscriptions.size(); i++) {
if (TextUtils.equals(guid, subscriptions.get(i).getBookmarkGUID())) {
return true;
}
}
return false;
}
private void awaitLoadingSubscriptionsLocked() {
while (!hasLoadedSubscriptions) {
try {
Log.v(LOGTAG, "Waiting for subscriptions to be loaded");
wait();
} catch (InterruptedException e) {
// Ignore
}
}
}
public void persistChanges() {
new Thread(LOGTAG + "-Persist") {
public void run() {
writeToDisk();
}
}.start();
}
private void startLoadFromDisk() {
new Thread(LOGTAG + "-Load") {
public void run() {
loadFromDisk();
}
}.start();
}
protected synchronized void loadFromDisk() {
Log.d(LOGTAG, "Loading from disk");
if (hasLoadedSubscriptions) {
return;
}
List<FeedSubscription> subscriptions = new ArrayList<>();
try {
JSONObject data;
synchronized (file) {
data = new JSONObject(new String(file.readFully(), "UTF-8"));
}
JSONArray array = data.getJSONArray(JSON_KEY_SUBSCRIPTIONS);
for (int i = 0; i < array.length(); i++) {
subscriptions.add(FeedSubscription.fromJSON(array.getJSONObject(i)));
}
} catch (FileNotFoundException e) {
Log.d(LOGTAG, "No subscriptions yet.");
} catch (JSONException e) {
Log.w(LOGTAG, "Unable to parse subscriptions JSON. Using empty list.", e);
} catch (UnsupportedEncodingException e) {
AssertionError error = new AssertionError("Should not happen: This device does not speak UTF-8");
error.initCause(e);
throw error;
} catch (IOException e) {
Log.d(LOGTAG, "Can't read subscriptions due to IOException", e);
}
onSubscriptionsLoaded(subscriptions);
notifyAll();
Log.d(LOGTAG, "Loaded " + subscriptions.size() + " elements");
}
protected void onSubscriptionsLoaded(List<FeedSubscription> subscriptions) {
this.subscriptions = subscriptions;
this.hasLoadedSubscriptions = true;
}
protected synchronized void writeToDisk() {
if (!hasChanged) {
Log.v(LOGTAG, "Not persisting: Subscriptions have not changed");
return;
}
Log.d(LOGTAG, "Writing to disk");
FileOutputStream outputStream = null;
synchronized (file) {
try {
outputStream = file.startWrite();
JSONArray array = new JSONArray();
for (FeedSubscription subscription : this.subscriptions) {
array.put(subscription.toJSON());
}
JSONObject catalog = new JSONObject();
catalog.put(JSON_KEY_SUBSCRIPTIONS, array);
outputStream.write(catalog.toString().getBytes("UTF-8"));
file.finishWrite(outputStream);
hasChanged = false;
} catch (UnsupportedEncodingException e) {
AssertionError error = new AssertionError("Should not happen: This device does not speak UTF-8");
error.initCause(e);
throw error;
} catch (IOException | JSONException e) {
Log.e(LOGTAG, "IOException during writing catalog", e);
if (outputStream != null) {
file.failWrite(outputStream);
}
}
}
}
}

View File

@ -401,7 +401,6 @@ public class DynamicToolbarAnimator {
float prevDir = mLastTouch - mTouchStart.y;
float newDir = event.getRawY() - mLastTouch;
if (prevDir != 0 && newDir != 0 && ((prevDir < 0) != (newDir < 0))) {
Log.v(LOGTAG, "Direction changed: " + mTouchStart.y + " -> " + mLastTouch + " -> " + event.getRawY());
// If the direction of movement changed, reset the travel
// distance properties.
mTouchStart = null;
@ -428,7 +427,6 @@ public class DynamicToolbarAnimator {
}
float translation = decideTranslation(deltaY, metrics, travelDistance);
Log.v(LOGTAG, "Got vertical translation " + translation);
float oldToolbarTranslation = mToolbarTranslation;
float oldLayerViewTranslation = mLayerViewTranslation;

View File

@ -23,6 +23,21 @@ import android.content.Context;
import android.database.Cursor;
import android.support.v4.content.AsyncTaskLoader;
/**
* A copy of the framework's {@link android.content.CursorLoader} that
* instead allows the caller to load the Cursor themselves via the abstract
* {@link #loadCursor()} method, rather than calling out to a ContentProvider via
* class methods.
*
* For new code, prefer {@link android.content.CursorLoader} (see @deprecated).
*
* This was originally created to re-use existing code which loaded Cursors manually.
*
* @deprecated since the framework provides an implementation, we'd like to eventually remove
* this class to reduce maintenance burden. Originally planned for bug 1239491, but
* it'd be more efficient to do this over time, rather than all at once.
*/
@Deprecated
abstract class SimpleCursorLoader extends AsyncTaskLoader<Cursor> {
final ForceLoadContentObserver mObserver;
Cursor mCursor;

View File

@ -31,6 +31,9 @@ public class Experiments {
// Show a system notification linking to a "What's New" page on app update.
public static final String WHATSNEW_NOTIFICATION = "whatsnew-notification";
// Subscribe to known, bookmarked sites and show a notification if new content is available.
public static final String CONTENT_NOTIFICATIONS = "content-notifications";
// Onboarding: "Features and Story". These experiments are determined
// on the client, they are not part of the server config.
public static final String ONBOARDING2_A = "onboarding2-a"; // Control: Single (blue) welcome screen

View File

@ -273,6 +273,14 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
'favicons/LoadFaviconTask.java',
'favicons/OnFaviconLoadedListener.java',
'favicons/RemoteFavicon.java',
'feeds/action/CheckAction.java',
'feeds/FeedFetcher.java',
'feeds/FeedService.java',
'feeds/parser/Feed.java',
'feeds/parser/Item.java',
'feeds/parser/SimpleFeedParser.java',
'feeds/subscriptions/FeedSubscription.java',
'feeds/subscriptions/SubscriptionStorage.java',
'FilePicker.java',
'FilePickerResultHandler.java',
'FindInPageBar.java',

View File

@ -17,5 +17,6 @@
<item type="id" name="pref_header_privacy"/>
<item type="id" name="pref_header_search"/>
<item type="id" name="updateServicePermissionNotification" />
<item type="id" name="websiteContentNotification" />
</resources>

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://en.wikipedia.org/wiki/Atom_%28standard%29#Example_of_an_Atom_1.0_feed
-->
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Example Feed</title>
<subtitle>A subtitle.</subtitle>
<link href="http://example.org/feed/" rel="self" />
<link href="http://example.org/" />
<id>urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6</id>
<updated>2003-12-13T18:30:02Z</updated>
<entry>
<title>Atom-Powered Robots Run Amok</title>
<link href="http://example.org/2003/12/13/atom03" />
<link rel="alternate" type="text/html" href="http://example.org/2003/12/13/atom03.html"/>
<link rel="edit" href="http://example.org/2003/12/13/atom03/edit"/>
<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
<updated>2003-12-13T18:30:02Z</updated>
<summary>Some text.</summary>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml">
<p>This is the entry content.</p>
</div>
</content>
<author>
<name>John Doe</name>
<email>johndoe@example.com</email>
</author>
</entry>
</feed>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Snapshot from https://medium.com/feed/@Antlam/
-->
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
<channel>
<title><![CDATA[Anthony Lam on Medium]]></title>
<description><![CDATA[Latest posts by Anthony Lam on Medium]]></description>
<link>https://medium.com/@antlam?source=rss-59f49b9e4b19------2</link>
<image>
<url>https://d262ilb51hltx0.cloudfront.net/fit/c/150/150/1*BNfAhhQ8TybWsu_gMMixWw.jpeg</url>
<title>Anthony Lam on Medium</title>
<link>https://medium.com/@antlam?source=rss-59f49b9e4b19------2</link>
</image>
<generator>RSS for Node</generator>
<lastBuildDate>Tue, 26 Jan 2016 17:06:09 GMT</lastBuildDate>
<atom:link href="https://medium.com/feed/@antlam" rel="self" type="application/rss+xml"/>
<webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
<atom:link href="http://medium.superfeedr.com" rel="hub"/>
<item>
<title><![CDATA[UX thoughts for 2016]]></title>
<description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@antlam/ux-thoughts-for-2016-1fc1d6e515e8?source=rss-59f49b9e4b19------2"><img src="https://d262ilb51hltx0.cloudfront.net/fit/c/600/200/1*mOiPSWfvCBoLUrfQlIrdVQ.png" width="600" height="200"></a></p><p class="medium-feed-snippet">And just like that, another year.</p><p class="medium-feed-link"><a href="https://medium.com/@antlam/ux-thoughts-for-2016-1fc1d6e515e8?source=rss-59f49b9e4b19------2">Continue reading on Medium »</a></p></div>]]></description>
<link>https://medium.com/@antlam/ux-thoughts-for-2016-1fc1d6e515e8?source=rss-59f49b9e4b19------2</link>
<guid isPermaLink="false">https://medium.com/p/1fc1d6e515e8</guid>
<dc:creator><![CDATA[Anthony Lam]]></dc:creator>
<pubDate>Mon, 11 Jan 2016 18:43:58 GMT</pubDate>
</item>
<item>
<title><![CDATA[A New Mobile Tabs tray]]></title>
<description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@antlam/a-new-mobile-tabs-tray-327ac262eacb?source=rss-59f49b9e4b19------2"><img src="https://d262ilb51hltx0.cloudfront.net/fit/c/600/200/1*scpLRAL_zy9CcW6BLGZHng.png" width="600" height="200"></a></p><p class="medium-feed-snippet">Why we&#8217;re giving it an update in Firefox for Android</p><p class="medium-feed-link"><a href="https://medium.com/@antlam/a-new-mobile-tabs-tray-327ac262eacb?source=rss-59f49b9e4b19------2">Continue reading on Medium »</a></p></div>]]></description>
<link>https://medium.com/@antlam/a-new-mobile-tabs-tray-327ac262eacb?source=rss-59f49b9e4b19------2</link>
<guid isPermaLink="false">https://medium.com/p/327ac262eacb</guid>
<dc:creator><![CDATA[Anthony Lam]]></dc:creator>
<pubDate>Fri, 06 Nov 2015 17:30:55 GMT</pubDate>
</item>
<item>
<title><![CDATA[Quick search]]></title>
<description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@antlam/quick-search-bdd374257e75?source=rss-59f49b9e4b19------2"><img src="https://d262ilb51hltx0.cloudfront.net/fit/c/600/200/1*DyBNARNADZsexPnxkMaEgQ.png" width="600" height="200"></a></p><p class="medium-feed-snippet">Instantly search with any of your search providers</p><p class="medium-feed-link"><a href="https://medium.com/@antlam/quick-search-bdd374257e75?source=rss-59f49b9e4b19------2">Continue reading on Medium »</a></p></div>]]></description>
<link>https://medium.com/@antlam/quick-search-bdd374257e75?source=rss-59f49b9e4b19------2</link>
<guid isPermaLink="false">https://medium.com/p/bdd374257e75</guid>
<dc:creator><![CDATA[Anthony Lam]]></dc:creator>
<pubDate>Tue, 01 Sep 2015 17:36:32 GMT</pubDate>
</item>
<item>
<title><![CDATA[Designing helpfulness]]></title>
<description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@antlam/designing-helpfulness-c1777727faf?source=rss-59f49b9e4b19------2"><img src="https://d262ilb51hltx0.cloudfront.net/fit/c/600/200/1*Cg8wkgjwCH7A1Aotec1Okw.png" width="600" height="200"></a></p><p class="medium-feed-snippet">Being there, without being annoying</p><p class="medium-feed-link"><a href="https://medium.com/@antlam/designing-helpfulness-c1777727faf?source=rss-59f49b9e4b19------2">Continue reading on Medium »</a></p></div>]]></description>
<link>https://medium.com/@antlam/designing-helpfulness-c1777727faf?source=rss-59f49b9e4b19------2</link>
<guid isPermaLink="false">https://medium.com/p/c1777727faf</guid>
<dc:creator><![CDATA[Anthony Lam]]></dc:creator>
<pubDate>Tue, 11 Aug 2015 20:59:13 GMT</pubDate>
</item>
<item>
<title><![CDATA[Share to… Firefox?]]></title>
<description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@antlam/share-to-firefox-245984b2da33?source=rss-59f49b9e4b19------2"><img src="https://d262ilb51hltx0.cloudfront.net/fit/c/600/200/1*H5wztlFTvbE1EM_5MRkuzg.png" width="600" height="200"></a></p><p class="medium-feed-snippet">You may remember this from such share intents as &#8220;Add to Firefox&#8221;</p><p class="medium-feed-link"><a href="https://medium.com/@antlam/share-to-firefox-245984b2da33?source=rss-59f49b9e4b19------2">Continue reading on Medium »</a></p></div>]]></description>
<link>https://medium.com/@antlam/share-to-firefox-245984b2da33?source=rss-59f49b9e4b19------2</link>
<guid isPermaLink="false">https://medium.com/p/245984b2da33</guid>
<dc:creator><![CDATA[Anthony Lam]]></dc:creator>
<pubDate>Fri, 08 May 2015 20:18:18 GMT</pubDate>
</item>
<item>
<title><![CDATA[Open multiple links]]></title>
<description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@antlam/open-multiple-links-1ce475c47de3?source=rss-59f49b9e4b19------2"><img src="https://d262ilb51hltx0.cloudfront.net/fit/c/600/200/1*yEQ5DjomHZIgVzUN-FKx2A.jpeg" width="600" height="200"></a></p><p class="medium-feed-snippet">Queue links in Firefox instead of switching applications each time.</p><p class="medium-feed-link"><a href="https://medium.com/@antlam/open-multiple-links-1ce475c47de3?source=rss-59f49b9e4b19------2">Continue reading on Medium »</a></p></div>]]></description>
<link>https://medium.com/@antlam/open-multiple-links-1ce475c47de3?source=rss-59f49b9e4b19------2</link>
<guid isPermaLink="false">https://medium.com/p/1ce475c47de3</guid>
<dc:creator><![CDATA[Anthony Lam]]></dc:creator>
<pubDate>Tue, 14 Apr 2015 18:44:59 GMT</pubDate>
</item>
<item>
<title><![CDATA[Firefox for Android on Tablets]]></title>
<description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@antlam/firefox-for-android-on-tablets-f67edc83dd46?source=rss-59f49b9e4b19------2"><img src="https://d262ilb51hltx0.cloudfront.net/fit/c/600/200/1*H5p3YQ1NGpfGPDryd22Ujg.png" width="600" height="200"></a></p><p class="medium-feed-snippet">Redesigning the browser interface&#8202;&#8212;&#8202;Part two</p><p class="medium-feed-link"><a href="https://medium.com/@antlam/firefox-for-android-on-tablets-f67edc83dd46?source=rss-59f49b9e4b19------2">Continue reading on Medium »</a></p></div>]]></description>
<link>https://medium.com/@antlam/firefox-for-android-on-tablets-f67edc83dd46?source=rss-59f49b9e4b19------2</link>
<guid isPermaLink="false">https://medium.com/p/f67edc83dd46</guid>
<dc:creator><![CDATA[Anthony Lam]]></dc:creator>
<pubDate>Tue, 03 Feb 2015 20:29:18 GMT</pubDate>
</item>
<item>
<title><![CDATA[Are big phones back?]]></title>
<description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@antlam/are-big-phones-back-59550ba0f24e?source=rss-59f49b9e4b19------2"><img src="https://d262ilb51hltx0.cloudfront.net/fit/c/600/200/1*XArwkY9ZcmtCkEhDwl0aKA.jpeg" width="600" height="200"></a></p><p class="medium-feed-snippet">Some thoughts and impressions</p><p class="medium-feed-link"><a href="https://medium.com/@antlam/are-big-phones-back-59550ba0f24e?source=rss-59f49b9e4b19------2">Continue reading on Medium »</a></p></div>]]></description>
<link>https://medium.com/@antlam/are-big-phones-back-59550ba0f24e?source=rss-59f49b9e4b19------2</link>
<guid isPermaLink="false">https://medium.com/p/59550ba0f24e</guid>
<dc:creator><![CDATA[Anthony Lam]]></dc:creator>
<pubDate>Tue, 23 Dec 2014 20:05:36 GMT</pubDate>
</item>
<item>
<title><![CDATA[Firefox for Android looks a bit different]]></title>
<description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@antlam/firefox-for-android-looks-a-bit-different-8ae8eba41b1f?source=rss-59f49b9e4b19------2"><img src="https://d262ilb51hltx0.cloudfront.net/fit/c/600/200/1*Lk31G3Qt2fs5WcOtBQQVgw.png" width="600" height="200"></a></p><p class="medium-feed-snippet">Redesigning the browser interface</p><p class="medium-feed-link"><a href="https://medium.com/@antlam/firefox-for-android-looks-a-bit-different-8ae8eba41b1f?source=rss-59f49b9e4b19------2">Continue reading on Medium »</a></p></div>]]></description>
<link>https://medium.com/@antlam/firefox-for-android-looks-a-bit-different-8ae8eba41b1f?source=rss-59f49b9e4b19------2</link>
<guid isPermaLink="false">https://medium.com/p/8ae8eba41b1f</guid>
<dc:creator><![CDATA[Anthony Lam]]></dc:creator>
<pubDate>Fri, 29 Aug 2014 23:38:07 GMT</pubDate>
</item>
<item>
<title><![CDATA[My fancy new watch]]></title>
<description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@antlam/my-fancy-new-watch-4856162890a3?source=rss-59f49b9e4b19------2"><img src="https://d262ilb51hltx0.cloudfront.net/fit/c/600/200/1*_gsBD-Vw7qevrwLxXZe8jA.jpeg" width="600" height="200"></a></p><p class="medium-feed-snippet">Early thoughts</p><p class="medium-feed-link"><a href="https://medium.com/@antlam/my-fancy-new-watch-4856162890a3?source=rss-59f49b9e4b19------2">Continue reading on Medium »</a></p></div>]]></description>
<link>https://medium.com/@antlam/my-fancy-new-watch-4856162890a3?source=rss-59f49b9e4b19------2</link>
<guid isPermaLink="false">https://medium.com/p/4856162890a3</guid>
<dc:creator><![CDATA[Anthony Lam]]></dc:creator>
<pubDate>Tue, 22 Jul 2014 01:36:52 GMT</pubDate>
</item>
</channel>

View File

@ -0,0 +1,314 @@
<?xml version="1.0" encoding="ISO-8859-1" standalone="yes"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
<channel>
<title>SPIEGEL ONLINE - Schlagzeilen</title>
<link>http://www.spiegel.de</link>
<description>Deutschlands führende Nachrichtenseite. Alles Wichtige aus Politik, Wirtschaft, Sport, Kultur, Wissenschaft, Technik und mehr.</description>
<language>de</language>
<pubDate>Wed, 27 Jan 2016 18:20:31 +0100</pubDate>
<lastBuildDate>Wed, 27 Jan 2016 18:20:31 +0100</lastBuildDate>
<image>
<title>SPIEGEL ONLINE</title>
<link>http://www.spiegel.de</link>
<url>http://www.spiegel.de/static/sys/logo_120x61.gif</url>
</image>
<item>
<title>Angebliche Vergewaltigung einer 13-Jährigen: Steinmeier kanzelt russischen Minister Lawrow ab</title>
<link>http://www.spiegel.de/politik/ausland/steinmeier-kanzelt-lawrow-ab-aerger-um-angebliche-vergewaltigung-a-1074292.html#ref=rss</link>
<description>Der Ton wird scharf zwischen Berlin und Moskau: Frank-Walter Steinmeier wirft dem russischen Außenminister Lawrow politische Propaganda vor - es geht um die angebliche Vergewaltigung einer 13-Jährigen in Berlin.</description>
<category>Politik</category>
<pubDate>Wed, 27 Jan 2016 18:16:16 +0100</pubDate>
<guid>http://www.spiegel.de/politik/ausland/steinmeier-kanzelt-lawrow-ab-aerger-um-angebliche-vergewaltigung-a-1074292.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949492-thumbsmall-wtgq.jpg" hspace="5" align="left" >Der Ton wird scharf zwischen Berlin und Moskau: Frank-Walter Steinmeier wirft dem russischen Außenminister Lawrow politische Propaganda vor - es geht um die angebliche Vergewaltigung einer 13-Jährigen in Berlin.]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949492-thumbsmall-wtgq.jpg"/>
</item>
<item>
<title>Grafischer Überblick: Hier gibt es die wenigsten Herzinfarkte in Deutschland</title>
<link>http://www.spiegel.de/gesundheit/diagnose/ostdeutsche-sterben-deutlich-haeufiger-an-einem-herzinfarkt-a-1074231.html#ref=rss</link>
<description>Nirgendwo arbeiten mehr Kardiologen als in Hamburg - auch deshalb gibt es dort weniger Herzinfarkte als in anderen Bundesländern. Wie sieht es in Ihrer Gegend aus, wo ist die Sterblichkeit am höchsten? Der Überblick.</description>
<category>Gesundheit</category>
<pubDate>Wed, 27 Jan 2016 18:08:00 +0100</pubDate>
<guid>http://www.spiegel.de/gesundheit/diagnose/ostdeutsche-sterben-deutlich-haeufiger-an-einem-herzinfarkt-a-1074231.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-566602-thumbsmall-bxcg.jpg" hspace="5" align="left" >Nirgendwo arbeiten mehr Kardiologen als in Hamburg - auch deshalb gibt es dort weniger Herzinfarkte als in anderen Bundesländern. Wie sieht es in Ihrer Gegend aus, wo ist die Sterblichkeit am höchsten? Der Überblick.]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-566602-thumbsmall-bxcg.jpg"/>
</item>
<item>
<title>Nachhilfe für gute Schüler: Lasst Eure Kinder auch das Scheitern lernen</title>
<link>http://www.spiegel.de/schulspiegel/nachhilfe-entspannte-schueler-brauchen-entspannte-eltern-a-1074197.html#ref=rss</link>
<description>Selbst gute Schüler werden von ihren Eltern zur Nachhilfe geschickt. Das schadet mehr, als es nützt - denn die Kinder lernen dabei vor allem eines: Dass sie nicht scheitern dürfen.</description>
<category>SchulSPIEGEL</category>
<pubDate>Wed, 27 Jan 2016 18:02:00 +0100</pubDate>
<guid>http://www.spiegel.de/schulspiegel/nachhilfe-entspannte-schueler-brauchen-entspannte-eltern-a-1074197.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-396678-thumbsmall-dhws.jpg" hspace="5" align="left" >Selbst gute Schüler werden von ihren Eltern zur Nachhilfe geschickt. Das schadet mehr, als es nützt - denn die Kinder lernen dabei vor allem eines: Dass sie nicht scheitern dürfen.]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-396678-thumbsmall-dhws.jpg"/>
</item>
<item>
<title>Germanwings-Katastrophe: Arbeitsgruppe regt Schleuse zwischen Kabine und Cockpit an</title>
<link>http://www.spiegel.de/panorama/germanwings-katastrophe-arbeitsgruppe-legt-abschlussbericht-vor-a-1074294.html#ref=rss</link>
<description>Wie sicher muss ein Flugzeugcockpit sein? Nach dem Germanwings-Absturz mit 150 Toten hat sich eine Arbeitsgruppe mit dieser Frage befasst - und nun ihren Abschlussbericht vorgelegt.</description>
<category>Panorama</category>
<pubDate>Wed, 27 Jan 2016 17:52:00 +0100</pubDate>
<guid>http://www.spiegel.de/panorama/germanwings-katastrophe-arbeitsgruppe-legt-abschlussbericht-vor-a-1074294.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-828832-thumbsmall-gjvf.jpg" hspace="5" align="left" >Wie sicher muss ein Flugzeugcockpit sein? Nach dem Germanwings-Absturz mit 150 Toten hat sich eine Arbeitsgruppe mit dieser Frage befasst - und nun ihren Abschlussbericht vorgelegt.]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-828832-thumbsmall-gjvf.jpg"/>
</item>
<item>
<title>Polen: Händler wehren sich gegen Supermarktsteuer</title>
<link>http://www.spiegel.de/wirtschaft/soziales/polen-will-internationale-einzelhaendler-hoeher-besteuern-a-1074101.html#ref=rss</link>
<description>Polens Regierung plant, Einzelhändler höher zu besteuern - und will mit den Einnahmen soziale Wohltaten finanzieren. Besonders trifft es ausländische Unternehmen wie Kaufland und Metro.</description>
<category>Wirtschaft</category>
<pubDate>Wed, 27 Jan 2016 17:46:10 +0100</pubDate>
<guid>http://www.spiegel.de/wirtschaft/soziales/polen-will-internationale-einzelhaendler-hoeher-besteuern-a-1074101.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949308-thumbsmall-wwsr.jpg" hspace="5" align="left" >Polens Regierung plant, Einzelhändler höher zu besteuern - und will mit den Einnahmen soziale Wohltaten finanzieren. Besonders trifft es ausländische Unternehmen wie Kaufland und Metro.]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949308-thumbsmall-wwsr.jpg"/>
</item>
<item>
<title>Übergriffe in Köln: Polizei erteilt Silvester-Verdächtigen Karnevalverbot</title>
<link>http://www.spiegel.de/panorama/justiz/koeln-polizei-erteilt-silvester-verdaechtigen-verbote-fuer-karneval-a-1074274.html#ref=rss</link>
<description>Kölns Polizei bereitet sich auf die Karnevalstage vor: Tatverdächtige aus der Silvesternacht sollen mit Zutrittsverboten von bestimmten Orten ferngehalten werden. Der Polizeipräsident forderte die Narren zudem auf, keine Spielzeugwaffen zu tragen.</description>
<category>Panorama</category>
<pubDate>Wed, 27 Jan 2016 17:23:00 +0100</pubDate>
<guid>http://www.spiegel.de/panorama/justiz/koeln-polizei-erteilt-silvester-verdaechtigen-verbote-fuer-karneval-a-1074274.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-942482-thumbsmall-qyge.jpg" hspace="5" align="left" >Kölns Polizei bereitet sich auf die Karnevalstage vor: Tatverdächtige aus der Silvesternacht sollen mit Zutrittsverboten von bestimmten Orten ferngehalten werden. Der Polizeipräsident forderte die Narren zudem auf, keine Spielzeugwaffen zu tragen.]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-942482-thumbsmall-qyge.jpg"/>
</item>
<item>
<title>"The Hateful 8"-Stars im Interview: "Wahnsinn, was da alles passiert!"</title>
<link>http://www.spiegel.de/kultur/kino/the-hateful-8-quentin-tarantino-und-jennifer-jason-leigh-im-interview-a-1074202.html#ref=rss</link>
<description>Sieben Männer und eine Frau - in seinem Western "The Hateful 8" entwirft Quentin Tarantino ein brutales Abbild der US-Gesellschaft. Hier erklären der Regisseur und seine Hauptdarstellerin, warum es sich lohnt, den Film gleich mehrmals anzusehen.</description>
<category>Kultur</category>
<pubDate>Wed, 27 Jan 2016 17:08:00 +0100</pubDate>
<guid>http://www.spiegel.de/kultur/kino/the-hateful-8-quentin-tarantino-und-jennifer-jason-leigh-im-interview-a-1074202.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949256-thumbsmall-hbrj.jpg" hspace="5" align="left" >Sieben Männer und eine Frau - in seinem Western "The Hateful 8" entwirft Quentin Tarantino ein brutales Abbild der US-Gesellschaft. Hier erklären der Regisseur und seine Hauptdarstellerin, warum es sich lohnt, den Film gleich mehrmals anzusehen.]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949256-thumbsmall-hbrj.jpg"/>
</item>
<item>
<title>Drama auf zugefrorenem Teich: Baby in akuter Lebensgefahr - Messer gefunden</title>
<link>http://www.spiegel.de/panorama/justiz/hamburg-vater-mit-baby-in-zugefrorenem-teich-eingebrochen-messer-gefunden-a-1074258.html#ref=rss</link>
<description>Der Fall gibt Rätsel auf: In Hamburg wird ein Vater mit seinem Baby aus einem zugefrorenen Teich gerettet - zwei Männer hätten ihn überfallen, sagt der 24-Jährige. Nun haben Ermittler in der Nähe des Gewässers ein Messer gefunden.</description>
<category>Panorama</category>
<pubDate>Wed, 27 Jan 2016 16:32:00 +0100</pubDate>
<guid>http://www.spiegel.de/panorama/justiz/hamburg-vater-mit-baby-in-zugefrorenem-teich-eingebrochen-messer-gefunden-a-1074258.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949367-thumbsmall-vpsq.jpg" hspace="5" align="left" >Der Fall gibt Rätsel auf: In Hamburg wird ein Vater mit seinem Baby aus einem zugefrorenen Teich gerettet - zwei Männer hätten ihn überfallen, sagt der 24-Jährige. Nun haben Ermittler in der Nähe des Gewässers ein Messer gefunden.]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949367-thumbsmall-vpsq.jpg"/>
</item>
<item>
<title>Rheinland-Pfalz: Elefantenrunde soll mit sechs Parteien stattfinden</title>
<link>http://www.spiegel.de/politik/deutschland/swr-fernsehdebatte-jetzt-mit-sechs-parteien-a-1074262.html#ref=rss</link>
<description>Erst drei, dann eine, jetzt sechs Parteien: Die TV-Diskussion im Südwestrundfunk zur rheinland-pfälzischen Landtagswahl findet nun in ganz großer Runde statt.</description>
<category>Politik</category>
<pubDate>Wed, 27 Jan 2016 16:29:00 +0100</pubDate>
<guid>http://www.spiegel.de/politik/deutschland/swr-fernsehdebatte-jetzt-mit-sechs-parteien-a-1074262.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-946925-thumbsmall-icoq.jpg" hspace="5" align="left" >Erst drei, dann eine, jetzt sechs Parteien: Die TV-Diskussion im Südwestrundfunk zur rheinland-pfälzischen Landtagswahl findet nun in ganz großer Runde statt.]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-946925-thumbsmall-icoq.jpg"/>
</item>
<item>
<title>Blauer Brief: Britische Schulleiterin mahnt Pyjama-Eltern ab</title>
<link>http://www.spiegel.de/schulspiegel/direktorin-appelliert-an-eltern-bringt-eure-kinder-nicht-im-schlafanzug-zur-schule-a-1074167.html#ref=rss</link>
<description>Rektorin Kate Chisholm hat genug: Ständig beobachtet sie Eltern, die noch im Schlafanzug stecken, wenn sie ihre Kinder an der Schule absetzen. Nun schrieb sie einen Brandbrief, die Elternschaft reagiert gespalten.</description>
<category>SchulSPIEGEL</category>
<pubDate>Wed, 27 Jan 2016 16:26:00 +0100</pubDate>
<guid>http://www.spiegel.de/schulspiegel/direktorin-appelliert-an-eltern-bringt-eure-kinder-nicht-im-schlafanzug-zur-schule-a-1074167.html</guid>
<content:encoded><![CDATA[Rektorin Kate Chisholm hat genug: Ständig beobachtet sie Eltern, die noch im Schlafanzug stecken, wenn sie ihre Kinder an der Schule absetzen. Nun schrieb sie einen Brandbrief, die Elternschaft reagiert gespalten.]]></content:encoded>
</item>
<item>
<title>Zwischenruf bei Merkel-Besuch: Hochschule verwirft juristische Schritte gegen Stör-Professor</title>
<link>http://www.spiegel.de/unispiegel/studium/zwischenruf-bei-merkel-besuch-hochschule-verwirft-juristische-schritte-gegen-professor-a-1074208.html#ref=rss</link>
<description>Seine politische Stör-Aktion während einer Rede von Kanzlerin Merkel ist für einen Merseburger Professor glimpflich ausgegangen. Die Hochschule sieht von juristischen Schritten ab. </description>
<category>UniSPIEGEL</category>
<pubDate>Wed, 27 Jan 2016 16:08:00 +0100</pubDate>
<guid>http://www.spiegel.de/unispiegel/studium/zwischenruf-bei-merkel-besuch-hochschule-verwirft-juristische-schritte-gegen-professor-a-1074208.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949364-thumbsmall-nnis.jpg" hspace="5" align="left" >Seine politische Stör-Aktion während einer Rede von Kanzlerin Merkel ist für einen Merseburger Professor glimpflich ausgegangen. Die Hochschule sieht von juristischen Schritten ab. ]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949364-thumbsmall-nnis.jpg"/>
</item>
<item>
<title>Bundesautobahngesellschaft: Verkehrsminister wehren sich gegen "Mammutbehörde"</title>
<link>http://www.spiegel.de/auto/aktuell/bundesautobahngesellschaft-widerstand-der-verkehrsminister-a-1074198.html#ref=rss</link>
<description>Der Bund könnte die Gründung einer Bundesautobahngesellschaft vorantreiben. Mehrere Verkehrsminister der Länder stellen sich dagegen - auch aus Furcht vor privaten Investoren beim Fernstraßenbau. </description>
<category>Auto</category>
<pubDate>Wed, 27 Jan 2016 15:54:00 +0100</pubDate>
<guid>http://www.spiegel.de/auto/aktuell/bundesautobahngesellschaft-widerstand-der-verkehrsminister-a-1074198.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949241-thumbsmall-cksv.jpg" hspace="5" align="left" >Der Bund könnte die Gründung einer Bundesautobahngesellschaft vorantreiben. Mehrere Verkehrsminister der Länder stellen sich dagegen - auch aus Furcht vor privaten Investoren beim Fernstraßenbau. ]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949241-thumbsmall-cksv.jpg"/>
</item>
<item>
<title>Glasfaser-Ausbau: Dobrindt will schnelles Netz unter die Autobahnen legen</title>
<link>http://www.spiegel.de/netzwelt/netzpolitik/alexander-dobrindt-will-schnelles-netz-unter-die-autobahnen-legen-a-1074151.html#ref=rss</link>
<description>Jede Baustelle soll Bandbreite bringen: Die Bundesregierung beschließt, dass künftig beim Straßenbau automatisch Glasfaserkabel verlegt werden müssen. Kommt Deutschland so schneller ins Netz?</description>
<category>Netzwelt</category>
<pubDate>Wed, 27 Jan 2016 15:52:00 +0100</pubDate>
<guid>http://www.spiegel.de/netzwelt/netzpolitik/alexander-dobrindt-will-schnelles-netz-unter-die-autobahnen-legen-a-1074151.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949166-thumbsmall-xrfd.jpg" hspace="5" align="left" >Jede Baustelle soll Bandbreite bringen: Die Bundesregierung beschließt, dass künftig beim Straßenbau automatisch Glasfaserkabel verlegt werden müssen. Kommt Deutschland so schneller ins Netz?]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949166-thumbsmall-xrfd.jpg"/>
</item>
<item>
<title>Handball-Insolvenz: Flensburg will den HSV verklagen</title>
<link>http://www.spiegel.de/sport/sonst/handball-bundesliga-sg-flensburg-handewitt-klagt-gegen-hsv-a-1074260.html#ref=rss</link>
<description>Einem nackten Mann kann man nicht in die Tasche greifen? Die SG Flensburg-Handewitt will den insolventen HSV Handball verklagen. Der Traditionsverein will Schadenersatz aus Hamburg haben.</description>
<category>Sport</category>
<pubDate>Wed, 27 Jan 2016 15:49:00 +0100</pubDate>
<guid>http://www.spiegel.de/sport/sonst/handball-bundesliga-sg-flensburg-handewitt-klagt-gegen-hsv-a-1074260.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-948459-thumbsmall-banl.jpg" hspace="5" align="left" >Einem nackten Mann kann man nicht in die Tasche greifen? Die SG Flensburg-Handewitt will den insolventen HSV Handball verklagen. Der Traditionsverein will Schadenersatz aus Hamburg haben.]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-948459-thumbsmall-banl.jpg"/>
</item>
<item>
<title>Lage am Lageso: Berliner Senatsverwaltung bestreitet Tod eines Flüchtlings</title>
<link>http://www.spiegel.de/politik/deutschland/berlin-fluechtling-vom-lageso-tot-senat-widerspricht-a-1074255.html#ref=rss</link>
<description>Ist in Berlin ein syrischer Flüchtling gestorben, nachdem er lange vor dem Lageso warten musste? Der Helfer, der darüber berichtete, ist abgetaucht - die Senatsverwaltung widerspricht der Darstellung über den Todesfall.</description>
<category>Politik</category>
<pubDate>Wed, 27 Jan 2016 15:45:00 +0100</pubDate>
<guid>http://www.spiegel.de/politik/deutschland/berlin-fluechtling-vom-lageso-tot-senat-widerspricht-a-1074255.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949380-thumbsmall-umdf.jpg" hspace="5" align="left" >Ist in Berlin ein syrischer Flüchtling gestorben, nachdem er lange vor dem Lageso warten musste? Der Helfer, der darüber berichtete, ist abgetaucht - die Senatsverwaltung widerspricht der Darstellung über den Todesfall.]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949380-thumbsmall-umdf.jpg"/>
</item>
<item>
<title>Tierarzt-Ikone: Evan Antin, der "heißeste Veterinär der Welt" </title>
<link>http://www.spiegel.de/panorama/leute/tierarzt-evan-antin-ist-der-sexiest-veterinaer-ever-laut-people-a-1074229.html#ref=rss</link>
<description>Evan Antin ist Tierarzt, Model und Personal Trainer. So irritierend gutaussehend, dass er zum "Sexiest Tierbetörer" avancierte. Trotz seiner Vorliebe für blutiges Gedärm und anatomische Anomalien.</description>
<category>Panorama</category>
<pubDate>Wed, 27 Jan 2016 15:31:00 +0100</pubDate>
<guid>http://www.spiegel.de/panorama/leute/tierarzt-evan-antin-ist-der-sexiest-veterinaer-ever-laut-people-a-1074229.html</guid>
<content:encoded><![CDATA[Evan Antin ist Tierarzt, Model und Personal Trainer. So irritierend gutaussehend, dass er zum "Sexiest Tierbetörer" avancierte. Trotz seiner Vorliebe für blutiges Gedärm und anatomische Anomalien.]]></content:encoded>
</item>
<item>
<title>Verteidigungshaushalt: Schäuble will mehr Geld für Bundeswehr ausgeben</title>
<link>http://www.spiegel.de/politik/deutschland/wolfgang-schaeuble-einverstanden-mit-mehr-geld-fuer-ruestung-a-1074242.html#ref=rss</link>
<description>Drei Milliarden Euro mehr pro Jahr für Waffen: Finanzminister Schäuble sieht die Aufrüstungspläne der Verteidigungsministerin von der Leyen positiv.</description>
<category>Politik</category>
<pubDate>Wed, 27 Jan 2016 15:29:00 +0100</pubDate>
<guid>http://www.spiegel.de/politik/deutschland/wolfgang-schaeuble-einverstanden-mit-mehr-geld-fuer-ruestung-a-1074242.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-947025-thumbsmall-qecb.jpg" hspace="5" align="left" >Drei Milliarden Euro mehr pro Jahr für Waffen: Finanzminister Schäuble sieht die Aufrüstungspläne der Verteidigungsministerin von der Leyen positiv.]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-947025-thumbsmall-qecb.jpg"/>
</item>
<item>
<title>Doping-Vorwürfe: NFL leitet Untersuchung gegen Manning ein</title>
<link>http://www.spiegel.de/sport/sonst/nfl-peyton-manning-unter-doping-verdacht-a-1074230.html#ref=rss</link>
<description>Die NFL prüft Vorwürfe gegen einen Superstar: Die Football-Liga geht jetzt offiziell den Doping-Gerüchten um Peyton Manning nach. Der Quarterback soll über seine Frau Hormone geordert haben.</description>
<category>Sport</category>
<pubDate>Wed, 27 Jan 2016 15:23:00 +0100</pubDate>
<guid>http://www.spiegel.de/sport/sonst/nfl-peyton-manning-unter-doping-verdacht-a-1074230.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949287-thumbsmall-cwxj.jpg" hspace="5" align="left" >Die NFL prüft Vorwürfe gegen einen Superstar: Die Football-Liga geht jetzt offiziell den Doping-Gerüchten um Peyton Manning nach. Der Quarterback soll über seine Frau Hormone geordert haben.]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949287-thumbsmall-cwxj.jpg"/>
</item>
<item>
<title>Posse um SWR-Elefantenrunde: Feigheit vor dem Feind</title>
<link>http://www.spiegel.de/politik/deutschland/spd-und-swr-posse-um-tv-auftritt-in-rheinland-pfalz-kommentar-a-1074235.html#ref=rss</link>
<description>Die Sozialdemokraten weigern sich, an einer TV-Debatte mit der AfD teilzunehmen, jetzt geht das Spektakel in eine neue Runde. Statt der Ministerpräsidentin soll der SPD-Landeschef ran. Wie absurd ist das denn? </description>
<category>Politik</category>
<pubDate>Wed, 27 Jan 2016 15:23:00 +0100</pubDate>
<guid>http://www.spiegel.de/politik/deutschland/spd-und-swr-posse-um-tv-auftritt-in-rheinland-pfalz-kommentar-a-1074235.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949309-thumbsmall-zhac.jpg" hspace="5" align="left" >Die Sozialdemokraten weigern sich, an einer TV-Debatte mit der AfD teilzunehmen, jetzt geht das Spektakel in eine neue Runde. Statt der Ministerpräsidentin soll der SPD-Landeschef ran. Wie absurd ist das denn? ]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949309-thumbsmall-zhac.jpg"/>
</item>
<item>
<title>Apple-Browser: Und plötzlich stürzt Safari ab</title>
<link>http://www.spiegel.de/netzwelt/apps/apple-safari-browser-stuerzt-ploetzlich-ab-a-1074217.html#ref=rss</link>
<description>Seit einigen Stunden haben Apple-Nutzer Probleme mit dem Safari-Browser. Wodurch die Abstürze ausgelöst werden, ist unklar. Doch mit einem kleinen Trick kann man sich helfen.</description>
<category>Netzwelt</category>
<pubDate>Wed, 27 Jan 2016 15:18:00 +0100</pubDate>
<guid>http://www.spiegel.de/netzwelt/apps/apple-safari-browser-stuerzt-ploetzlich-ab-a-1074217.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949312-thumbsmall-jqlt.jpg" hspace="5" align="left" >Seit einigen Stunden haben Apple-Nutzer Probleme mit dem Safari-Browser. Wodurch die Abstürze ausgelöst werden, ist unklar. Doch mit einem kleinen Trick kann man sich helfen.]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949312-thumbsmall-jqlt.jpg"/>
</item>
<item>
<title>EU-Bericht zu Grenzsicherung: Griechenland droht Schengen-Rauswurf</title>
<link>http://www.spiegel.de/politik/ausland/griechenland-eu-kommission-droht-mit-schengen-rauswurf-a-1074201.html#ref=rss</link>
<description>Griechenland gerät in der Flüchtlingskrise unter massiven Druck der EU: Die Kommission will Athen Forderungen zum Grenzschutz schicken. Werden die nicht binnen drei Monaten erfüllt, droht der Ausschluss aus dem Schengen-Raum.</description>
<category>Politik</category>
<pubDate>Wed, 27 Jan 2016 15:04:00 +0100</pubDate>
<guid>http://www.spiegel.de/politik/ausland/griechenland-eu-kommission-droht-mit-schengen-rauswurf-a-1074201.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949347-thumbsmall-vbmi.jpg" hspace="5" align="left" >Griechenland gerät in der Flüchtlingskrise unter massiven Druck der EU: Die Kommission will Athen Forderungen zum Grenzschutz schicken. Werden die nicht binnen drei Monaten erfüllt, droht der Ausschluss aus dem Schengen-Raum.]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949347-thumbsmall-vbmi.jpg"/>
</item>
<item>
<title>Protest gegen dänisches Asylrecht: Ai Weiwei schließt Ausstellung in Kopenhagen</title>
<link>http://www.spiegel.de/kultur/gesellschaft/protest-gegen-daenisches-asylrecht-ai-weiwei-schliesst-ausstellung-a-1074241.html#ref=rss</link>
<description>Ai Weiwei, Chinas wohl bekanntester Künstler, beendet seine Ausstellung in Kopenhagen vorzeitig. Grund ist eine Verschärfung der Asylregeln in Dänemark.</description>
<category>Kultur</category>
<pubDate>Wed, 27 Jan 2016 15:01:00 +0100</pubDate>
<guid>http://www.spiegel.de/kultur/gesellschaft/protest-gegen-daenisches-asylrecht-ai-weiwei-schliesst-ausstellung-a-1074241.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949292-thumbsmall-uhox.jpg" hspace="5" align="left" >Ai Weiwei, Chinas wohl bekanntester Künstler, beendet seine Ausstellung in Kopenhagen vorzeitig. Grund ist eine Verschärfung der Asylregeln in Dänemark.]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949292-thumbsmall-uhox.jpg"/>
</item>
<item>
<title>Deutsche Handballer gegen Dänemark: Jetzt muss es schmutzig werden</title>
<link>http://www.spiegel.de/sport/sonst/handball-em-2016-jetzt-muss-es-schmutzig-werden-a-1074123.html#ref=rss</link>
<description>Noch ein Sieg bis zum Halbfinale: Die deutsche Handball-Nationalmannschaft hat bei der EM einen Lauf, sie ist gierig auf den Titel. Jetzt geht es gegen einen großen Favoriten - und Dänemark wirkt angeschlagen. </description>
<category>Sport</category>
<pubDate>Wed, 27 Jan 2016 14:59:00 +0100</pubDate>
<guid>http://www.spiegel.de/sport/sonst/handball-em-2016-jetzt-muss-es-schmutzig-werden-a-1074123.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949116-thumbsmall-fpcj.jpg" hspace="5" align="left" >Noch ein Sieg bis zum Halbfinale: Die deutsche Handball-Nationalmannschaft hat bei der EM einen Lauf, sie ist gierig auf den Titel. Jetzt geht es gegen einen großen Favoriten - und Dänemark wirkt angeschlagen. ]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949116-thumbsmall-fpcj.jpg"/>
</item>
<item>
<title>Sicherheitslage nach Anschlag: Russland spricht Reisewarnung für Türkei aus</title>
<link>http://www.spiegel.de/reise/aktuell/russland-spricht-reisewarnung-fuer-tuerkei-aus-a-1074225.html#ref=rss</link>
<description>Die russische Regierung warnt ihre Bürger vor Reisen in die Türkei. Damit schränkt sie den Tourismus in das Land weiter ein. Nach dem Anschlag in Istanbul sind auch die Buchungen von deutschen Urlaubern eingebrochen.</description>
<category>Reise</category>
<pubDate>Wed, 27 Jan 2016 14:58:00 +0100</pubDate>
<guid>http://www.spiegel.de/reise/aktuell/russland-spricht-reisewarnung-fuer-tuerkei-aus-a-1074225.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-378624-thumbsmall-xtnj.jpg" hspace="5" align="left" >Die russische Regierung warnt ihre Bürger vor Reisen in die Türkei. Damit schränkt sie den Tourismus in das Land weiter ein. Nach dem Anschlag in Istanbul sind auch die Buchungen von deutschen Urlaubern eingebrochen.]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-378624-thumbsmall-xtnj.jpg"/>
</item>
<item>
<title>Bayern-Deal mit Katar: Scheich Di!</title>
<link>http://www.spiegel.de/sport/fussball/fc-bayern-muenchen-und-der-katar-deal-in-der-pflicht-kommentar-a-1074187.html#ref=rss</link>
<description>Der FC Bayern schließt einen Sponsoren-Deal mit Katar ab, die Kritik darüber ist ebenso berechtigt wie vorhersehbar. Dabei darf es nicht bleiben. Nehmen wir den Rekordmeister doch beim Wort.</description>
<category>Sport</category>
<pubDate>Wed, 27 Jan 2016 14:52:00 +0100</pubDate>
<guid>http://www.spiegel.de/sport/fussball/fc-bayern-muenchen-und-der-katar-deal-in-der-pflicht-kommentar-a-1074187.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949236-thumbsmall-hyyw.jpg" hspace="5" align="left" >Der FC Bayern schließt einen Sponsoren-Deal mit Katar ab, die Kritik darüber ist ebenso berechtigt wie vorhersehbar. Dabei darf es nicht bleiben. Nehmen wir den Rekordmeister doch beim Wort.]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949236-thumbsmall-hyyw.jpg"/>
</item>
<item>
<title>Flüchtlingskrise: EU wirft Griechenland schwere Mängel bei Grenzkontrolle vor</title>
<link>http://www.spiegel.de/politik/ausland/fluechtlinge-eu-wirft-griechenland-maengel-bei-grenzkontrolle-vor-a-1074219.html#ref=rss</link>
<description>Die griechische Regierung verletzt ihre Pflichten bei der Grenzsicherung - zu diesem Schluss kommt ein Untersuchungsbericht der EU-Kommission. Brüssel spricht nun eine klare Drohung gegen Athen aus.</description>
<category>Politik</category>
<pubDate>Wed, 27 Jan 2016 14:47:00 +0100</pubDate>
<guid>http://www.spiegel.de/politik/ausland/fluechtlinge-eu-wirft-griechenland-maengel-bei-grenzkontrolle-vor-a-1074219.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949273-thumbsmall-ifxg.jpg" hspace="5" align="left" >Die griechische Regierung verletzt ihre Pflichten bei der Grenzsicherung - zu diesem Schluss kommt ein Untersuchungsbericht der EU-Kommission. Brüssel spricht nun eine klare Drohung gegen Athen aus.]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949273-thumbsmall-ifxg.jpg"/>
</item>
<item>
<title>Abgasaffäre: Brüssel will Autokonzerne bestrafen können</title>
<link>http://www.spiegel.de/wirtschaft/soziales/abgasaffaere-bruessel-will-autokonzerne-bestrafen-koennen-a-1074228.html#ref=rss</link>
<description>Im Abgas-Skandal ist der VW-Konzern Schuldiger - aber auch die Aufseher in Brüssel sind blamiert. Denn US-Behörden enthüllten, was ihnen jahrelang durch die Lappen ging. Die EU-Kommission will jetzt dafür sorgen, dass sich das nicht wiederholt.</description>
<category>Wirtschaft</category>
<pubDate>Wed, 27 Jan 2016 14:46:00 +0100</pubDate>
<guid>http://www.spiegel.de/wirtschaft/soziales/abgasaffaere-bruessel-will-autokonzerne-bestrafen-koennen-a-1074228.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-938136-thumbsmall-hxbc.jpg" hspace="5" align="left" >Im Abgas-Skandal ist der VW-Konzern Schuldiger - aber auch die Aufseher in Brüssel sind blamiert. Denn US-Behörden enthüllten, was ihnen jahrelang durch die Lappen ging. Die EU-Kommission will jetzt dafür sorgen, dass sich das nicht wiederholt.]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-938136-thumbsmall-hxbc.jpg"/>
</item>
<item>
<title>Bundesverfassungsgericht: Wer alles gegen die Vorratsdatenspeicherung klagt</title>
<link>http://www.spiegel.de/netzwelt/netzpolitik/vorratsdatenspeicherung-wer-klagt-vor-dem-bundesverfassungsgericht-a-1074152.html#ref=rss</link>
<description>Widerstand gegen Vorratsdatenspeicherung: Die FDP hat Klage in Karlsruhe eingereicht, und auch weitere Gegner versuchen, das Gesetz vor Gericht noch zu kippen. Der Überblick. </description>
<category>Netzwelt</category>
<pubDate>Wed, 27 Jan 2016 14:42:00 +0100</pubDate>
<guid>http://www.spiegel.de/netzwelt/netzpolitik/vorratsdatenspeicherung-wer-klagt-vor-dem-bundesverfassungsgericht-a-1074152.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-925932-thumbsmall-zbbn.jpg" hspace="5" align="left" >Widerstand gegen Vorratsdatenspeicherung: Die FDP hat Klage in Karlsruhe eingereicht, und auch weitere Gegner versuchen, das Gesetz vor Gericht noch zu kippen. Der Überblick. ]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-925932-thumbsmall-zbbn.jpg"/>
</item>
<item>
<title>Berliner Flüchtlingsamt: Das Scheitern des Lageso - das Protokoll</title>
<link>http://www.spiegel.de/politik/deutschland/berlin-das-scheitern-des-lageso-eine-chronik-a-1074186.html#ref=rss</link>
<description>Tagelanges Warten, keine Auszahlung von Essensgeld, Gewalt - jetzt angeblich ein Toter: Seit Monaten steht das Berliner Flüchtlingsamt Lageso in den Schlagzeilen. Die Chronologie des Scheiterns. </description>
<category>Politik</category>
<pubDate>Wed, 27 Jan 2016 14:22:00 +0100</pubDate>
<guid>http://www.spiegel.de/politik/deutschland/berlin-das-scheitern-des-lageso-eine-chronik-a-1074186.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949137-thumbsmall-pgoa.jpg" hspace="5" align="left" >Tagelanges Warten, keine Auszahlung von Essensgeld, Gewalt - jetzt angeblich ein Toter: Seit Monaten steht das Berliner Flüchtlingsamt Lageso in den Schlagzeilen. Die Chronologie des Scheiterns. ]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949137-thumbsmall-pgoa.jpg"/>
</item>
<item>
<title>Apple-Gerüchte: So könnte das neue iPhone aussehen. Aber was wünschen Sie sich?</title>
<link>http://www.spiegel.de/netzwelt/gadgets/apple-so-gut-muss-das-iphone-7-sein-a-1074158.html#ref=rss</link>
<description>Ist der iPhone-Boom vorbei? Nicht unbedingt - das nächste Gerät könnte starke neue Features haben. Hier der Stand der Gerüchte. Und Sie können abstimmen: Was brauchen Sie wirklich?</description>
<category>Netzwelt</category>
<pubDate>Wed, 27 Jan 2016 14:17:00 +0100</pubDate>
<guid>http://www.spiegel.de/netzwelt/gadgets/apple-so-gut-muss-das-iphone-7-sein-a-1074158.html</guid>
<content:encoded><![CDATA[<img src="http://www.spiegel.de/images/image-949140-thumbsmall-bdof.jpg" hspace="5" align="left" >Ist der iPhone-Boom vorbei? Nicht unbedingt - das nächste Gerät könnte starke neue Features haben. Hier der Stand der Gerüchte. Und Sie können abstimmen: Was brauchen Sie wirklich?]]></content:encoded>
<enclosure type="image/jpeg" url="http://www.spiegel.de/images/image-949140-thumbsmall-bdof.jpg"/>
</item>
</channel>
</rss>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
https://en.wikipedia.org/wiki/RSS#Example
-->
<rss version="2.0">
<channel>
<title>RSS Title</title>
<description>This is an example of an RSS feed</description>
<link>http://www.example.com/main.html</link>
<lastBuildDate>Mon, 06 Sep 2010 00:01:00 +0000 </lastBuildDate>
<pubDate>Sun, 06 Sep 2009 16:20:00 +0000</pubDate>
<ttl>1800</ttl>
<item>
<title>Example entry</title>
<description>Here is some text containing an interesting description.</description>
<link>http://www.example.com/blog/post/1</link>
<guid isPermaLink="true">7bd204c6-1655-4c27-aeee-53f933c5395f</guid>
<pubDate>Sun, 06 Sep 2009 16:20:00 +0000</pubDate>
</item>
</channel>
</rss>

View File

@ -0,0 +1,276 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.feeds.parser;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mozilla.gecko.background.testhelpers.TestRunner;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.net.URL;
@RunWith(TestRunner.class)
public class TestSimpleFeedParser {
/**
* Parse and verify the RSS example from Wikipedia:
* https://en.wikipedia.org/wiki/RSS#Example
*/
@Test
public void testRSSExample() throws Exception {
InputStream stream = openFeed("feed_rss_wikipedia.xml");
SimpleFeedParser parser = new SimpleFeedParser();
Feed feed = parser.parse(stream);
Assert.assertNotNull(feed);
Assert.assertEquals("RSS Title", feed.getTitle());
Assert.assertEquals("http://www.example.com/main.html", feed.getWebsiteURL());
Assert.assertNull(feed.getFeedURL());
Assert.assertTrue(feed.isSufficientlyComplete());
Item item = feed.getLastItem();
Assert.assertNotNull(item);
Assert.assertEquals("Example entry", item.getTitle());
Assert.assertEquals("http://www.example.com/blog/post/1", item.getURL());
Assert.assertEquals(1252254000000L, item.getTimestamp());
}
/**
* Parse and verify the ATOM example from Wikipedia:
* https://en.wikipedia.org/wiki/Atom_%28standard%29#Example_of_an_Atom_1.0_feed
*/
@Test
public void testATOMExample() throws Exception {
InputStream stream = openFeed("feed_atom_wikipedia.xml");
SimpleFeedParser parser = new SimpleFeedParser();
Feed feed = parser.parse(stream);
Assert.assertNotNull(feed);
Assert.assertEquals("Example Feed", feed.getTitle());
Assert.assertEquals("http://example.org/", feed.getWebsiteURL());
Assert.assertEquals("http://example.org/feed/", feed.getFeedURL());
Assert.assertTrue(feed.isSufficientlyComplete());
Item item = feed.getLastItem();
Assert.assertNotNull(item);
Assert.assertEquals("Atom-Powered Robots Run Amok", item.getTitle());
Assert.assertEquals("http://example.org/2003/12/13/atom03.html", item.getURL());
Assert.assertEquals(1071340202000L, item.getTimestamp());
}
/**
* Parse and verify a snapshot of a Medium feed.
*/
@Test
public void testMediumFeed() throws Exception {
InputStream stream = openFeed("feed_rss_medium.xml");
SimpleFeedParser parser = new SimpleFeedParser();
Feed feed = parser.parse(stream);
Assert.assertNotNull(feed);
Assert.assertEquals("Anthony Lam on Medium", feed.getTitle());
Assert.assertEquals("https://medium.com/@antlam?source=rss-59f49b9e4b19------2", feed.getWebsiteURL());
Assert.assertEquals("https://medium.com/feed/@antlam", feed.getFeedURL());
Assert.assertTrue(feed.isSufficientlyComplete());
Item item = feed.getLastItem();
Assert.assertNotNull(item);
Assert.assertEquals("UX thoughts for 2016", item.getTitle());
Assert.assertEquals("https://medium.com/@antlam/ux-thoughts-for-2016-1fc1d6e515e8?source=rss-59f49b9e4b19------2", item.getURL());
Assert.assertEquals(1452537838000L, item.getTimestamp());
}
/**
* Parse and verify a snapshot of planet.mozilla.org ATOM feed.
*/
@Test
public void testPlanetMozillaATOMFeed() throws Exception {
InputStream stream = openFeed("feed_atom_planetmozilla.xml");
SimpleFeedParser parser = new SimpleFeedParser();
Feed feed = parser.parse(stream);
Assert.assertNotNull(feed);
Assert.assertEquals("Planet Mozilla", feed.getTitle());
Assert.assertEquals("http://planet.mozilla.org/", feed.getWebsiteURL());
Assert.assertEquals("http://planet.mozilla.org/atom.xml", feed.getFeedURL());
Assert.assertTrue(feed.isSufficientlyComplete());
Item item = feed.getLastItem();
Assert.assertNotNull(item);
Assert.assertEquals("Firefox 45.0 Beta 3 Testday, February 5th", item.getTitle());
Assert.assertEquals("https://quality.mozilla.org/2016/01/firefox-45-0-beta-3-testday-february-5th/", item.getURL());
Assert.assertEquals(1453819255000L, item.getTimestamp());
}
/**
* Parse and verify a snapshot of planet.mozilla.org RSS 2.0 feed.
*/
@Test
public void testPlanetMozillaRSS20Feed() throws Exception {
InputStream stream = openFeed("feed_rss20_planetmozilla.xml");
SimpleFeedParser parser = new SimpleFeedParser();
Feed feed = parser.parse(stream);
Assert.assertNotNull(feed);
Assert.assertEquals("Planet Mozilla", feed.getTitle());
Assert.assertEquals("http://planet.mozilla.org/", feed.getWebsiteURL());
Assert.assertEquals("http://planet.mozilla.org/rss20.xml", feed.getFeedURL());
Assert.assertTrue(feed.isSufficientlyComplete());
Item item = feed.getLastItem();
Assert.assertNotNull(item);
Assert.assertEquals("Aaron Klotz: Announcing Mozdbgext", item.getTitle());
Assert.assertEquals("http://dblohm7.ca/blog/2016/01/26/announcing-mozdbgext/", item.getURL());
Assert.assertEquals(1453837500000L, item.getTimestamp());
}
/**
* Parse and verify a snapshot of planet.mozilla.org RSS 1.0 feed.
*/
@Test
public void testPlanetMozillaRSS10Feed() throws Exception {
InputStream stream = openFeed("feed_rss10_planetmozilla.xml");
SimpleFeedParser parser = new SimpleFeedParser();
Feed feed = parser.parse(stream);
Assert.assertNotNull(feed);
Assert.assertEquals("Planet Mozilla", feed.getTitle());
Assert.assertEquals("http://planet.mozilla.org/", feed.getWebsiteURL());
Assert.assertEquals("http://planet.mozilla.org/rss10.xml", feed.getFeedURL());
Assert.assertTrue(feed.isSufficientlyComplete());
Item item = feed.getLastItem();
Assert.assertNotNull(item);
Assert.assertEquals("Aaron Klotz: Announcing Mozdbgext", item.getTitle());
Assert.assertEquals("http://dblohm7.ca/blog/2016/01/26/announcing-mozdbgext/", item.getURL());
Assert.assertEquals(1453837500000L, item.getTimestamp());
}
/**
* Parse an verify a snapshot of a feedburner ATOM feed.
*/
@Test
public void testFeedburnerAtomFeed() throws Exception {
InputStream stream = openFeed("feed_atom_feedburner.xml");
SimpleFeedParser parser = new SimpleFeedParser();
Feed feed = parser.parse(stream);
Assert.assertNotNull(feed);
Assert.assertEquals("Android Zeitgeist", feed.getTitle());
Assert.assertEquals("http://www.androidzeitgeist.com/", feed.getWebsiteURL());
Assert.assertEquals("http://feeds.feedburner.com/AndroidZeitgeist", feed.getFeedURL());
Assert.assertTrue(feed.isSufficientlyComplete());
Item item = feed.getLastItem();
Assert.assertNotNull(item);
Assert.assertEquals("Support for restricted profiles in Firefox 42", item.getTitle());
Assert.assertEquals("http://feedproxy.google.com/~r/AndroidZeitgeist/~3/xaSicfGuwOU/support-restricted-profiles-firefox.html", item.getURL());
Assert.assertEquals(1442511968239L, item.getTimestamp());
}
/**
* Parse and verify a snapshot of a Tumblr RSS feed.
*/
@Test
public void testTumblrRssFeed() throws Exception {
InputStream stream = openFeed("feed_rss_tumblr.xml");
SimpleFeedParser parser = new SimpleFeedParser();
Feed feed = parser.parse(stream);
Assert.assertNotNull(feed);
Assert.assertEquals("Tumblr Staff", feed.getTitle());
Assert.assertEquals("http://staff.tumblr.com/", feed.getWebsiteURL());
Assert.assertNull(feed.getFeedURL());
Assert.assertTrue(feed.isSufficientlyComplete());
Item item = feed.getLastItem();
Assert.assertNotNull(item);
Assert.assertEquals("hardyboyscovers: Can Nancy Drew see things through and solve...", item.getTitle());
Assert.assertEquals("http://staff.tumblr.com/post/138124026275", item.getURL());
Assert.assertEquals(1453861812000L, item.getTimestamp());
}
/**
* Parse and verify a snapshot of a Spiegel (German news magazine) RSS feed.
*/
@Test
public void testSpiegelRssFeed() throws Exception {
InputStream stream = openFeed("feed_rss_spon.xml");
SimpleFeedParser parser = new SimpleFeedParser();
Feed feed = parser.parse(stream);
Assert.assertNotNull(feed);
Assert.assertEquals("SPIEGEL ONLINE - Schlagzeilen", feed.getTitle());
Assert.assertEquals("http://www.spiegel.de", feed.getWebsiteURL());
Assert.assertNull(feed.getFeedURL());
Assert.assertTrue(feed.isSufficientlyComplete());
Item item = feed.getLastItem();
Assert.assertNotNull(item);
Assert.assertEquals("Angebliche Vergewaltigung einer 13-Jährigen: Steinmeier kanzelt russischen Minister Lawrow ab", item.getTitle());
Assert.assertEquals("http://www.spiegel.de/politik/ausland/steinmeier-kanzelt-lawrow-ab-aerger-um-angebliche-vergewaltigung-a-1074292.html#ref=rss", item.getURL());
Assert.assertEquals(1453914976000L, item.getTimestamp());
}
/**
* Parse and verify a snapshot of a Heise (German tech news) RSS feed.
*/
@Test
public void testHeiseRssFeed() throws Exception {
InputStream stream = openFeed("feed_rss_heise.xml");
SimpleFeedParser parser = new SimpleFeedParser();
Feed feed = parser.parse(stream);
Assert.assertNotNull(feed);
Assert.assertEquals("heise online News", feed.getTitle());
Assert.assertEquals("http://www.heise.de/newsticker/", feed.getWebsiteURL());
Assert.assertNull(feed.getFeedURL());
Assert.assertTrue(feed.isSufficientlyComplete());
Item item = feed.getLastItem();
Assert.assertNotNull(item);
Assert.assertEquals("Google: “Dramatische Verbesserungen” für Chrome in iOS", item.getTitle());
Assert.assertEquals("http://www.heise.de/newsticker/meldung/Google-Dramatische-Verbesserungen-fuer-Chrome-in-iOS-3085808.html?wt_mc=rss.ho.beitrag.atom", item.getURL());
Assert.assertEquals(1453915920000L, item.getTimestamp());
}
private InputStream openFeed(String fileName) throws URISyntaxException, FileNotFoundException, UnsupportedEncodingException {
URL url = getClass().getResource("/" + fileName);
if (url == null) {
throw new FileNotFoundException(fileName);
}
return new BufferedInputStream(new FileInputStream(url.getPath()));
}
}

View File

@ -103,8 +103,8 @@ PrefStore.prototype = {
let values = {};
for (let pref of this._getSyncPrefs()) {
if (this._isSynced(pref)) {
// Missing prefs get the null value.
values[pref] = this._prefs.get(pref, null);
// Missing and default prefs get the null value.
values[pref] = this._prefs.isSet(pref) ? this._prefs.get(pref, null) : null;
}
}
return values;

View File

@ -0,0 +1,25 @@
// This is a "preferences" file used by test_prefs_store.js
// The prefs that control what should be synced.
// Most of these are "default" prefs, so the value itself will not sync.
pref("services.sync.prefs.sync.testing.int", true);
pref("services.sync.prefs.sync.testing.string", true);
pref("services.sync.prefs.sync.testing.bool", true);
pref("services.sync.prefs.sync.testing.dont.change", true);
// this one is a user pref, so it *will* sync.
user_pref("services.sync.prefs.sync.testing.turned.off", false);
pref("services.sync.prefs.sync.testing.nonexistent", true);
pref("services.sync.prefs.sync.testing.default", true);
// The preference values - these are all user_prefs, otherwise their value
// will not be synced.
user_pref("testing.int", 123);
user_pref("testing.string", "ohai");
user_pref("testing.bool", true);
user_pref("testing.dont.change", "Please don't change me.");
user_pref("testing.turned.off", "I won't get synced.");
user_pref("testing.not.turned.on", "I won't get synced either!");
// A pref that exists but still has the default value - will be synced with
// null as the value.
pref("testing.default", "I'm the default value");

View File

@ -23,25 +23,22 @@ function makePersona(id) {
}
function run_test() {
_("Test fixtures.");
// read our custom prefs file before doing anything.
Services.prefs.readUserPrefs(do_get_file("prefs_test_prefs_store.js"));
// Now we've read from this file, any writes the pref service makes will be
// back to this prefs_test_prefs_store.js directly in the obj dir. This
// upsets things in confusing ways :) We avoid this by explicitly telling the
// pref service to use a file in our profile dir.
let prefFile = do_get_profile();
prefFile.append("prefs.js");
Services.prefs.savePrefFile(prefFile);
Services.prefs.readUserPrefs(prefFile);
let store = Service.engineManager.get("prefs")._store;
let prefs = new Preferences();
try {
_("Test fixtures.");
Svc.Prefs.set("prefs.sync.testing.int", true);
Svc.Prefs.set("prefs.sync.testing.string", true);
Svc.Prefs.set("prefs.sync.testing.bool", true);
Svc.Prefs.set("prefs.sync.testing.dont.change", true);
Svc.Prefs.set("prefs.sync.testing.turned.off", false);
Svc.Prefs.set("prefs.sync.testing.nonexistent", true);
prefs.set("testing.int", 123);
prefs.set("testing.string", "ohai");
prefs.set("testing.bool", true);
prefs.set("testing.dont.change", "Please don't change me.");
prefs.set("testing.turned.off", "I won't get synced.");
prefs.set("testing.not.turned.on", "I won't get synced either!");
_("The GUID corresponds to XUL App ID.");
let allIDs = store.getAllIDs();
let ids = Object.keys(allIDs);
@ -61,17 +58,22 @@ function run_test() {
do_check_eq(record.value["testing.int"], 123);
do_check_eq(record.value["testing.string"], "ohai");
do_check_eq(record.value["testing.bool"], true);
// non-existing prefs get null as the value
do_check_eq(record.value["testing.nonexistent"], null);
// as do prefs that have a default value.
do_check_eq(record.value["testing.default"], null);
do_check_false("testing.turned.off" in record.value);
do_check_false("testing.not.turned.on" in record.value);
_("Prefs record contains pref sync prefs too.");
do_check_eq(record.value["services.sync.prefs.sync.testing.int"], true);
do_check_eq(record.value["services.sync.prefs.sync.testing.string"], true);
do_check_eq(record.value["services.sync.prefs.sync.testing.bool"], true);
do_check_eq(record.value["services.sync.prefs.sync.testing.dont.change"], true);
_("Prefs record contains non-default pref sync prefs too.");
do_check_eq(record.value["services.sync.prefs.sync.testing.int"], null);
do_check_eq(record.value["services.sync.prefs.sync.testing.string"], null);
do_check_eq(record.value["services.sync.prefs.sync.testing.bool"], null);
do_check_eq(record.value["services.sync.prefs.sync.testing.dont.change"], null);
// but this one is a user_pref so *will* be synced.
do_check_eq(record.value["services.sync.prefs.sync.testing.turned.off"], false);
do_check_eq(record.value["services.sync.prefs.sync.testing.nonexistent"], true);
do_check_eq(record.value["services.sync.prefs.sync.testing.nonexistent"], null);
do_check_eq(record.value["services.sync.prefs.sync.testing.default"], null);
_("Update some prefs, including one that's to be reset/deleted.");
Svc.Prefs.set("testing.deleteme", "I'm going to be deleted!");

View File

@ -169,6 +169,7 @@ skip-if = debug
# Too many intermittent "ASSERTION: thread pool wasn't shutdown: '!mPool'" (bug 804479)
skip-if = debug
[test_prefs_store.js]
support-files = prefs_test_prefs_store.js
[test_prefs_tracker.js]
[test_tab_engine.js]
[test_tab_store.js]

View File

@ -38,6 +38,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
Cu.permitCPOWsInScope(this);
var gSendCharCount = 0;
var gSynthesizeKeyCount = 0;
var gSynthesizeCompositionCount = 0;
var gSynthesizeCompositionChangeCount = 0;
this.BrowserTestUtils = {
/**
@ -827,7 +830,7 @@ this.BrowserTestUtils = {
/**
* Version of EventUtils' `sendChar` function; it will synthesize a keypress
* event in a child process and returns a Promise that will result when the
* event in a child process and returns a Promise that will resolve when the
* event was fired. Instead of a Window, a Browser object is required to be
* passed to this function.
*
@ -849,7 +852,7 @@ this.BrowserTestUtils = {
return;
mm.removeMessageListener("Test:SendCharDone", charMsg);
resolve(message.data.sendCharResult);
resolve(message.data.result);
});
mm.sendAsyncMessage("Test:SendChar", {
@ -859,6 +862,99 @@ this.BrowserTestUtils = {
});
},
/**
* Version of EventUtils' `synthesizeKey` function; it will synthesize a key
* event in a child process and returns a Promise that will resolve when the
* event was fired. Instead of a Window, a Browser object is required to be
* passed to this function.
*
* @param {String} key
* See the documentation available for EventUtils#synthesizeKey.
* @param {Object} event
* See the documentation available for EventUtils#synthesizeKey.
* @param {Browser} browser
* Browser element, must not be null.
*
* @returns {Promise}
*/
synthesizeKey(key, event, browser) {
return new Promise(resolve => {
let seq = ++gSynthesizeKeyCount;
let mm = browser.messageManager;
mm.addMessageListener("Test:SynthesizeKeyDone", function keyMsg(message) {
if (message.data.seq != seq)
return;
mm.removeMessageListener("Test:SynthesizeKeyDone", keyMsg);
resolve();
});
mm.sendAsyncMessage("Test:SynthesizeKey", { key, event, seq });
});
},
/**
* Version of EventUtils' `synthesizeComposition` function; it will synthesize
* a composition event in a child process and returns a Promise that will
* resolve when the event was fired. Instead of a Window, a Browser object is
* required to be passed to this function.
*
* @param {Object} event
* See the documentation available for EventUtils#synthesizeComposition.
* @param {Browser} browser
* Browser element, must not be null.
*
* @returns {Promise}
* @resolves False if the composition event could not be synthesized.
*/
synthesizeComposition(event, browser) {
return new Promise(resolve => {
let seq = ++gSynthesizeCompositionCount;
let mm = browser.messageManager;
mm.addMessageListener("Test:SynthesizeCompositionDone", function compMsg(message) {
if (message.data.seq != seq)
return;
mm.removeMessageListener("Test:SynthesizeCompositionDone", compMsg);
resolve(message.data.result);
});
mm.sendAsyncMessage("Test:SynthesizeComposition", { event, seq });
});
},
/**
* Version of EventUtils' `synthesizeCompositionChange` function; it will
* synthesize a compositionchange event in a child process and returns a
* Promise that will resolve when the event was fired. Instead of a Window, a
* Browser object is required to be passed to this function.
*
* @param {Object} event
* See the documentation available for EventUtils#synthesizeCompositionChange.
* @param {Browser} browser
* Browser element, must not be null.
*
* @returns {Promise}
*/
synthesizeCompositionChange(event, browser) {
return new Promise(resolve => {
let seq = ++gSynthesizeCompositionChangeCount;
let mm = browser.messageManager;
mm.addMessageListener("Test:SynthesizeCompositionChangeDone", function compMsg(message) {
if (message.data.seq != seq)
return;
mm.removeMessageListener("Test:SynthesizeCompositionChangeDone", compMsg);
resolve();
});
mm.sendAsyncMessage("Test:SynthesizeCompositionChange", { event, seq });
});
},
/**
* Will poll a condition function until it returns true.
*

View File

@ -58,8 +58,20 @@ addMessageListener("Test:SynthesizeMouse", (message) => {
addMessageListener("Test:SendChar", message => {
let result = EventUtils.sendChar(message.data.char, content);
sendAsyncMessage("Test:SendCharDone", {
sendCharResult: result,
seq: message.data.seq
});
sendAsyncMessage("Test:SendCharDone", { result, seq: message.data.seq });
});
addMessageListener("Test:SynthesizeKey", message => {
EventUtils.synthesizeKey(message.data.key, message.data.event || {}, content);
sendAsyncMessage("Test:SynthesizeKeyDone", { seq: message.data.seq });
});
addMessageListener("Test:SynthesizeComposition", message => {
let result = EventUtils.synthesizeComposition(message.data.event, content);
sendAsyncMessage("Test:SynthesizeCompositionDone", { result, seq: message.data.seq });
});
addMessageListener("Test:SynthesizeCompositionChange", message => {
EventUtils.synthesizeCompositionChange(message.data.event, content);
sendAsyncMessage("Test:SynthesizeCompositionChangeDone", { seq: message.data.seq });
});

View File

@ -43,6 +43,40 @@ window.__defineGetter__('_EU_Cu', function() {
return c.value && !c.writable ? Components.utils : SpecialPowers.Cu;
});
window.__defineGetter__("_EU_OS", function() {
delete this._EU_OS;
try {
this._EU_OS = this._EU_Cu.import("resource://gre/modules/AppConstants.jsm", {}).platform;
} catch (ex) {
this._EU_OS = null;
}
return this._EU_OS;
});
function _EU_isMac(aWindow = window) {
if (window._EU_OS) {
return window._EU_OS == "macosx";
}
if (aWindow) {
try {
return aWindow.navigator.platform.indexOf("Mac") > -1;
} catch (ex) {}
}
return navigator.platform.indexOf("Mac") > -1;
}
function _EU_isWin(aWindow = window) {
if (window._EU_OS) {
return window._EU_OS == "win";
}
if (aWindow) {
try {
return aWindow.navigator.platform.indexOf("Win") > -1;
} catch (ex) {}
}
return navigator.platform.indexOf("Win") > -1;
}
/**
* Send a mouse event to the node aTarget (aTarget can be an id, or an
* actual node) . The "event" passed in to aEvent is just a JavaScript
@ -237,7 +271,7 @@ function _parseModifiers(aEvent, aWindow = window)
mval |= nsIDOMWindowUtils.MODIFIER_META;
}
if (aEvent.accelKey) {
mval |= (navigator.platform.indexOf("Mac") >= 0) ?
mval |= _EU_isMac(aWindow) ?
nsIDOMWindowUtils.MODIFIER_META : nsIDOMWindowUtils.MODIFIER_CONTROL;
}
if (aEvent.altGrKey) {
@ -789,16 +823,13 @@ function _parseNativeModifiers(aModifiers, aWindow = window)
}
if (aModifiers.accelKey) {
modifiers |=
(navigator.platform.indexOf("Mac") == 0) ? 0x00004000 : 0x00000400;
modifiers |= _EU_isMac(aWindow) ? 0x00004000 : 0x00000400;
}
if (aModifiers.accelRightKey) {
modifiers |=
(navigator.platform.indexOf("Mac") == 0) ? 0x00008000 : 0x00000800;
modifiers |= _EU_isMac(aWindow) ? 0x00008000 : 0x00000800;
}
if (aModifiers.altGrKey) {
modifiers |=
(navigator.platform.indexOf("Win") == 0) ? 0x00002800 : 0x00001000;
modifiers |= _EU_isWin(aWindow) ? 0x00002800 : 0x00001000;
}
return modifiers;
}
@ -873,9 +904,9 @@ function synthesizeNativeKey(aKeyboardLayout, aNativeKeyCode, aModifiers,
}
var navigator = _getNavigator(aWindow);
var nativeKeyboardLayout = null;
if (navigator.platform.indexOf("Mac") == 0) {
if (_EU_isMac(aWindow)) {
nativeKeyboardLayout = aKeyboardLayout.Mac;
} else if (navigator.platform.indexOf("Win") == 0) {
} else if (_EU_isWin(aWindow)) {
nativeKeyboardLayout = aKeyboardLayout.Win;
}
if (nativeKeyboardLayout === null) {
@ -1063,7 +1094,15 @@ function _getTIP(aWindow, aCallback)
function _getKeyboardEvent(aWindow = window)
{
if (typeof KeyboardEvent != "undefined") {
return KeyboardEvent;
try {
// See if the object can be instantiated; sometimes this yields
// 'TypeError: can't access dead object' or 'KeyboardEvent is not a constructor'.
new KeyboardEvent("", {});
return KeyboardEvent;
} catch (ex) {}
}
if (typeof content != "undefined" && ("KeyboardEvent" in content)) {
return content.KeyboardEvent;
}
return aWindow.KeyboardEvent;
}
@ -1282,7 +1321,7 @@ function _emulateToActivateModifiers(aTIP, aKeyEvent, aWindow = window)
{ key: "OS", attr: "osKey" },
{ key: "Shift", attr: "shiftKey" },
{ key: "Symbol", attr: "symbolKey" },
{ key: (navigator.platform.indexOf("Mac") >= 0) ? "Meta" : "Control",
{ key: _EU_isMac(aWindow) ? "Meta" : "Control",
attr: "accelKey" },
],
lockable: [

View File

@ -268,8 +268,18 @@ var add_task = (function () {
// script finishes.
setTimeout(function () {
spawn_task(function* () {
for (var task of task_list) {
yield task();
// We stop the entire test file at the first exception because this
// may mean that the state of subsequent tests may be corrupt.
try {
for (var task of task_list) {
yield task();
}
} catch (ex) {
try {
ok(false, "" + ex);
} catch (ex2) {
ok(false, "(The exception cannot be converted to string.)");
}
}
// All tasks are finished.
SimpleTest.finish();

View File

@ -331,7 +331,8 @@ extensions.registerSchemaAPI("cookies", "cookies", (extension, context) => {
notify(false, subject, "explicit");
break;
case "changed":
notify(false, subject, "overwrite");
notify(true, subject, "overwrite");
notify(false, subject, "explicit");
break;
case "batch-deleted":
subject.QueryInterface(Ci.nsIArray);

View File

@ -26,11 +26,17 @@ const {
const DOWNLOAD_ITEM_FIELDS = ["id", "url", "referrer", "filename", "incognito",
"danger", "mime", "startTime", "endTime",
"estimatedEndTime", "state", "canResume",
"error", "bytesReceived", "totalBytes",
"estimatedEndTime", "state",
"paused", "canResume", "error",
"bytesReceived", "totalBytes",
"fileSize", "exists",
"byExtensionId", "byExtensionName"];
// Fields that we generate onChanged events for.
const DOWNLOAD_ITEM_CHANGE_FIELDS = ["endTime", "state", "paused", "canResume",
"error", "exists"];
class DownloadItem {
constructor(id, download, extension) {
this.id = id;
@ -52,13 +58,17 @@ class DownloadItem {
if (this.download.succeeded) {
return "complete";
}
if (this.download.stopped) {
if (this.download.canceled) {
return "interrupted";
}
return "in_progress";
}
get paused() {
return this.download.canceled && this.download.hasPartialData && !this.download.error;
}
get canResume() {
return this.download.stopped && this.download.hasPartialData;
return (this.download.stopped || this.download.canceled) &&
this.download.hasPartialData && !this.download.error;
}
get error() {
if (!this.download.stopped || this.download.succeeded) {
@ -114,7 +124,7 @@ class DownloadItem {
// After all handlers have been invoked, this gets called to store the
// current values of all fields ahead of the next event.
_change() {
for (let field of DOWNLOAD_ITEM_FIELDS) {
for (let field of DOWNLOAD_ITEM_CHANGE_FIELDS) {
this.prechange[field] = this[field];
}
}
@ -158,7 +168,11 @@ const DownloadMap = {
if (item == null) {
Cu.reportError("Got onDownloadChanged for unknown download object");
} else {
self.emit("change", item);
// We get the first one of these when the download is started.
// In this case, don't emit anything, just initialize prechange.
if (Object.keys(item.prechange).length > 0) {
self.emit("change", item);
}
item._change();
}
},
@ -303,8 +317,9 @@ function downloadQuery(query) {
return false;
}
// todo: include danger, paused, error
// todo: include danger
const SIMPLE_ITEMS = ["id", "mime", "startTime", "endTime", "state",
"paused", "error",
"bytesReceived", "totalBytes", "fileSize", "exists"];
for (let field of SIMPLE_ITEMS) {
if (query[field] != null && item[field] != query[field]) {
@ -377,7 +392,10 @@ extensions.registerSchemaAPI("downloads", "downloads", (extension, context) => {
.then(downloadsDir => createTarget(downloadsDir))
.then(target => Downloads.createDownload({
source: options.url,
target: target,
target: {
path: target,
partFilePath: target + ".part",
},
})).then(dl => {
download = dl;
return DownloadMap.getDownloadList();
@ -446,6 +464,47 @@ extensions.registerSchemaAPI("downloads", "downloads", (extension, context) => {
});
},
pause(id) {
return DownloadMap.lazyInit().then(() => {
let item = DownloadMap.fromId(id);
if (!item) {
return Promise.reject({message: `Invalid download id ${id}`});
}
if (item.state != "in_progress") {
return Promise.reject({message: `Download ${id} cannot be paused since it is in state ${item.state}`});
}
return item.download.cancel();
});
},
resume(id) {
return DownloadMap.lazyInit().then(() => {
let item = DownloadMap.fromId(id);
if (!item) {
return Promise.reject({message: `Invalid download id ${id}`});
}
if (!item.canResume) {
return Promise.reject({message: `Download ${id} cannot be resumed`});
}
return item.download.start();
});
},
cancel(id) {
return DownloadMap.lazyInit().then(() => {
let item = DownloadMap.fromId(id);
if (!item) {
return Promise.reject({message: `Invalid download id ${id}`});
}
if (item.download.succeeded) {
return Promise.reject({message: `Download ${id} is already complete`});
}
return item.download.finalize(true);
});
},
showDefaultFolder() {
Downloads.getPreferredDownloadsDirectory().then(dir => {
let dirobj = new FileUtils.File(dir);
@ -469,14 +528,19 @@ extensions.registerSchemaAPI("downloads", "downloads", (extension, context) => {
onChanged: new SingletonEventManager(context, "downloads.onChanged", fire => {
const handler = (what, item) => {
if (item.state != item.prechange.state) {
runSafeSync(context, fire, {
id: item.id,
state: {
previous: item.prechange.state || null,
current: item.state,
},
});
let changes = {};
const noundef = val => (val === undefined) ? null : val;
DOWNLOAD_ITEM_CHANGE_FIELDS.forEach(fld => {
if (item[fld] != item.prechange[fld]) {
changes[fld] = {
previous: noundef(item.prechange[fld]),
current: noundef(item[fld]),
};
}
});
if (Object.keys(changes).length > 0) {
changes.id = item.id;
runSafeSync(context, fire, changes);
}
};

View File

@ -458,7 +458,7 @@
{
"name": "pause",
"type": "function",
"unsupported": true,
"async": "callback",
"description": "Pause the download. If the request was successful the download is in a paused state. Otherwise <a href='extension.html#property-lastError'>chrome.extension.lastError</a> contains an error message. The request will fail if the download is not active.",
"parameters": [
{
@ -477,7 +477,7 @@
{
"name": "resume",
"type": "function",
"unsupported": true,
"async": "callback",
"description": "Resume a paused download. If the request was successful the download is in progress and unpaused. Otherwise <a href='extension.html#property-lastError'>chrome.extension.lastError</a> contains an error message. The request will fail if the download is not active.",
"parameters": [
{
@ -496,7 +496,7 @@
{
"name": "cancel",
"type": "function",
"unsupported": true,
"async": "callback",
"description": "Cancel a download. When <code>callback</code> is run, the download is cancelled, completed, interrupted or doesn't exist anymore.",
"parameters": [
{

View File

@ -3,6 +3,7 @@ skip-if = os == 'android'
support-files =
file_download.html
file_download.txt
interruptible.sjs
[test_chrome_ext_downloads_download.html]
[test_chrome_ext_downloads_misc.html]

View File

@ -0,0 +1,38 @@
const TEST_DATA = "This is 31 bytes of sample data";
const TOTAL_LEN = TEST_DATA.length;
const PARTIAL_LEN = 15;
// A handler to let us systematically test pausing/resuming/canceling
// of downloads. This target represents a small text file but a simple
// GET will stall after sending part of the data, to give the test code
// a chance to pause or do other operations on an in-progress download.
// A resumed download (ie, a GET with a Range: header) will allow the
// download to complete.
function handleRequest(request, response) {
response.setHeader("Content-Type", "text/plain", false);
if (request.hasHeader("Range")) {
let start, end;
let matches = request.getHeader("Range")
.match(/^\s*bytes=(\d+)?-(\d+)?\s*$/);
if (matches != null) {
start = matches[1] ? parseInt(matches[1], 10) : 0;
end = matches[2] ? pareInt(matchs[2], 10) : (TOTAL_LEN - 1);
}
if (end == undefined || end >= TOTAL_LEN) {
response.setStatusLine(request.httpVersion, 416, "Requested Range Not Satisfiable");
response.setHeader("Content-Range", `*/${TOTAL_LEN}`, false);
response.finish();
return;
}
response.setStatusLine(request.httpVersion, 206, "Partial Content");
response.setHeader("Content-Range", `${start}-${end}/${TOTAL_LEN}`, false);
response.write(TEST_DATA.slice(start, end + 1));
} else {
response.processAsync();
response.setHeader("Content-Length", `${TOTAL_LEN}`, false);
response.write(TEST_DATA.slice(0, PARTIAL_LEN));
}
}

View File

@ -25,6 +25,10 @@ Cu.import("resource://gre/modules/Downloads.jsm");
const BASE = "http://mochi.test:8888/chrome/toolkit/components/extensions/test/mochitest";
const TXT_FILE = "file_download.txt";
const TXT_URL = BASE + "/" + TXT_FILE;
const INTERRUPTIBLE_URL = BASE + "/interruptible.sjs";
// Keep these in sync with code in interruptible.sjs
const INT_PARTIAL_LEN = 15;
const INT_TOTAL_LEN = 31;
function backgroundScript() {
let events = [];
@ -47,6 +51,9 @@ function backgroundScript() {
function waitForEvents(expected) {
function compare(received, expected) {
if (typeof expected == "object" && expected != null) {
if (typeof received != "object") {
return false;
}
return Object.keys(expected).every(fld => compare(received[fld], expected[fld]));
}
return (received == expected);
@ -75,20 +82,27 @@ function backgroundScript() {
});
}
browser.test.onMessage.addListener(function(msg) {
// extension functions throw on bad arguments, we can remove the extra
// promise when bug 1250223 is fixed.
if (msg == "download.request") {
Promise.resolve().then(() => browser.downloads.download(arguments[1]))
.then(id => {
browser.test.sendMessage("download.done", {status: "success", id});
})
.catch(error => {
browser.test.sendMessage("download.done", {status: "error", errmsg: error.message});
});
} else if (msg == "waitForEvents.request") {
browser.test.onMessage.addListener(function(msg, ...args) {
let match = msg.match(/(\w+).request$/);
if (!match) {
return;
}
let what = match[1];
if (what == "waitForEvents") {
waitForEvents(arguments[1]).then(() => {
browser.test.sendMessage("waitForEvents.done", {status: "success"});
}).catch(error => {
browser.test.sendMessage("waitForEvents.done", {status: "error", errmsg: error.message});
});
} else {
// extension functions throw on bad arguments, we can remove the extra
// promise when bug 1250223 is fixed.
Promise.resolve().then(() => {
return browser.downloads[what](...args);
}).then(result => {
browser.test.sendMessage(`${what}.done`, {status: "success", result});
}).catch(error => {
browser.test.sendMessage(`${what}.done`, {status: "error", errmsg: error.message});
});
}
});
@ -108,7 +122,31 @@ function clearDownloads(callback) {
});
}
function* setup(backgroundScript) {
function runInExtension(what, args) {
extension.sendMessage(`${what}.request`, args);
return extension.awaitMessage(`${what}.done`);
}
// This is pretty simplistic, it looks for a progress update for a
// download of the given url in which the total bytes are exactly equal
// to the given value. Unless you know exactly how data will arrive from
// the server (eg see interruptible.sjs), it probably isn't very useful.
function waitForProgress(url, bytes) {
return Downloads.getList(Downloads.ALL)
.then(list => new Promise(resolve => {
const view = {
onDownloadChanged(download) {
if (download.source.url == url && download.currentBytes == bytes) {
list.removeView(view);
resolve();
}
},
};
list.addView(view);
}));
}
add_task(function* setup() {
const nsIFile = Ci.nsIFile;
downloadDir = FileUtils.getDir("TmpD", ["downloads"]);
downloadDir.createUnique(nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
@ -138,20 +176,12 @@ function* setup(backgroundScript) {
yield extension.startup();
yield extension.awaitMessage("ready");
info("extension started");
}
function runInExtension(what, args) {
extension.sendMessage(`${what}.request`, args);
return extension.awaitMessage(`${what}.done`);
}
add_task(function* test_misc() {
yield setup(backgroundScript);
});
add_task(function* test_events() {
let msg = yield runInExtension("download", {url: TXT_URL});
is(msg.status, "success", "downoad succeeded");
const id = msg.id;
is(msg.status, "success", "download() succeeded");
const id = msg.result;
msg = yield runInExtension("waitForEvents", [
{type: "onCreated", data: {id, url: TXT_URL}},
@ -160,9 +190,160 @@ add_task(function* test_misc() {
data: {
id,
state: {
previous: "in_progress",
current: "complete",
},
},
},
]);
is(msg.status, "success", "got onCreated and onChanged events");
});
add_task(function* test_cancel() {
let msg = yield runInExtension("download", {url: INTERRUPTIBLE_URL});
is(msg.status, "success", "download() succeeded");
const id = msg.result;
let progressPromise = waitForProgress(INTERRUPTIBLE_URL, INT_PARTIAL_LEN);
msg = yield runInExtension("waitForEvents", [
{type: "onCreated", data: {id}},
]);
is(msg.status, "success", "got created and changed events");
yield progressPromise;
info(`download reached ${INT_PARTIAL_LEN} bytes`);
msg = yield runInExtension("cancel", id);
is(msg.status, "success", "cancel() succeeded");
// This sequence of events is bogus (bug 1256243)
msg = yield runInExtension("waitForEvents", [
{
type: "onChanged",
data: {
state: {
previous: "in_progress",
current: "interrupted",
},
paused: {
previous: false,
current: true,
},
},
}, {
type: "onChanged",
data: {
id,
error: {
previous: null,
current: "USER_CANCELED",
},
},
}]);
is(msg.status, "success", "got onChanged event corresponding to pause");
msg = yield runInExtension("search", {error: "USER_CANCELED"});
is(msg.status, "success", "search() succeeded");
is(msg.result.length, 1, "search() found 1 download");
is(msg.result[0].id, id, "download.id is correct");
is(msg.result[0].state, "interrupted", "download.state is correct");
is(msg.result[0].paused, false, "download.paused is correct");
is(msg.result[0].canResume, false, "download.canResume is correct");
is(msg.result[0].error, "USER_CANCELED", "download.error is correct");
is(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
is(msg.result[0].exists, false, "download.exists is correct");
msg = yield runInExtension("pause", id);
is(msg.status, "error", "cannot pause a canceled download");
msg = yield runInExtension("resume", id);
is(msg.status, "error", "cannot resume a canceled download");
});
add_task(function* test_pauseresume() {
let msg = yield runInExtension("download", {url: INTERRUPTIBLE_URL});
is(msg.status, "success", "download() succeeded");
const id = msg.result;
let progressPromise = waitForProgress(INTERRUPTIBLE_URL, INT_PARTIAL_LEN);
msg = yield runInExtension("waitForEvents", [
{type: "onCreated", data: {id}},
]);
is(msg.status, "success", "got created and changed events");
yield progressPromise;
info(`download reached ${INT_PARTIAL_LEN} bytes`);
msg = yield runInExtension("pause", id);
is(msg.status, "success", "pause() succeeded");
msg = yield runInExtension("waitForEvents", [
{
type: "onChanged",
data: {
id,
state: {
previous: "in_progress",
current: "interrupted",
},
paused: {
previous: false,
current: true,
},
canResume: {
previous: false,
current: true,
},
},
}]);
is(msg.status, "success", "got onChanged event corresponding to pause");
msg = yield runInExtension("search", {paused: true});
is(msg.status, "success", "search() succeeded");
is(msg.result.length, 1, "search() found 1 download");
is(msg.result[0].id, id, "download.id is correct");
is(msg.result[0].state, "interrupted", "download.state is correct");
is(msg.result[0].paused, true, "download.paused is correct");
is(msg.result[0].canResume, true, "download.canResume is correct");
is(msg.result[0].error, "USER_CANCELED", "download.error is correct");
is(msg.result[0].bytesReceived, INT_PARTIAL_LEN, "download.bytesReceived is correct");
is(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
is(msg.result[0].exists, false, "download.exists is correct");
msg = yield runInExtension("search", {error: "USER_CANCELED"});
is(msg.status, "success", "search() succeeded");
let found = msg.result.filter(item => item.id == id);
is(found.length, 1, "search() by error found the paused download");
msg = yield runInExtension("pause", id);
is(msg.status, "error", "cannot pause an already paused download");
msg = yield runInExtension("resume", id);
is(msg.status, "success", "resume() succeeded");
msg = yield runInExtension("waitForEvents", [
{
type: "onChanged",
data: {
id,
state: {
previous: "interrupted",
current: "in_progress",
},
paused: {
previous: true,
current: false,
},
canResume: {
previous: true,
current: false,
},
error: {
previous: "USER_CANCELED",
current: null,
},
},
},
{
@ -176,8 +357,115 @@ add_task(function* test_misc() {
},
},
]);
is(msg.status, "success", "got onCreated and onChanged events");
is(msg.status, "success", "got onChanged events for resume and complete");
msg = yield runInExtension("search", {id});
is(msg.status, "success", "search() succeeded");
is(msg.result.length, 1, "search() found 1 download");
is(msg.result[0].state, "complete", "download.state is correct");
is(msg.result[0].paused, false, "download.paused is correct");
is(msg.result[0].canResume, false, "download.canResume is correct");
is(msg.result[0].error, null, "download.error is correct");
is(msg.result[0].bytesReceived, INT_TOTAL_LEN, "download.bytesReceived is correct");
is(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
is(msg.result[0].exists, true, "download.exists is correct");
msg = yield runInExtension("pause", id);
is(msg.status, "error", "cannot pause a completed download");
msg = yield runInExtension("resume", id);
is(msg.status, "error", "cannot resume a completed download");
});
add_task(function* test_pausecancel() {
let msg = yield runInExtension("download", {url: INTERRUPTIBLE_URL});
is(msg.status, "success", "download() succeeded");
const id = msg.result;
let progressPromise = waitForProgress(INTERRUPTIBLE_URL, INT_PARTIAL_LEN);
msg = yield runInExtension("waitForEvents", [
{type: "onCreated", data: {id}},
]);
is(msg.status, "success", "got created and changed events");
yield progressPromise;
info(`download reached ${INT_PARTIAL_LEN} bytes`);
msg = yield runInExtension("pause", id);
is(msg.status, "success", "pause() succeeded");
msg = yield runInExtension("waitForEvents", [
{
type: "onChanged",
data: {
id,
state: {
previous: "in_progress",
current: "interrupted",
},
paused: {
previous: false,
current: true,
},
canResume: {
previous: false,
current: true,
},
},
}]);
is(msg.status, "success", "got onChanged event corresponding to pause");
msg = yield runInExtension("search", {paused: true});
is(msg.status, "success", "search() succeeded");
is(msg.result.length, 1, "search() found 1 download");
is(msg.result[0].id, id, "download.id is correct");
is(msg.result[0].state, "interrupted", "download.state is correct");
is(msg.result[0].paused, true, "download.paused is correct");
is(msg.result[0].canResume, true, "download.canResume is correct");
is(msg.result[0].error, "USER_CANCELED", "download.error is correct");
is(msg.result[0].bytesReceived, INT_PARTIAL_LEN, "download.bytesReceived is correct");
is(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
is(msg.result[0].exists, false, "download.exists is correct");
msg = yield runInExtension("search", {error: "USER_CANCELED"});
is(msg.status, "success", "search() succeeded");
let found = msg.result.filter(item => item.id == id);
is(found.length, 1, "search() by error found the paused download");
msg = yield runInExtension("cancel", id);
is(msg.status, "success", "cancel() succeeded");
msg = yield runInExtension("waitForEvents", [
{
type: "onChanged",
data: {
id,
paused: {
previous: true,
current: false,
},
canResume: {
previous: true,
current: false,
},
},
},
]);
is(msg.status, "success", "got onChanged event for cancel");
msg = yield runInExtension("search", {id});
is(msg.status, "success", "search() succeeded");
is(msg.result.length, 1, "search() found 1 download");
is(msg.result[0].state, "interrupted", "download.state is correct");
is(msg.result[0].paused, false, "download.paused is correct");
is(msg.result[0].canResume, false, "download.canResume is correct");
is(msg.result[0].error, "USER_CANCELED", "download.error is correct");
is(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
is(msg.result[0].exists, false, "download.exists is correct");
});
add_task(function* cleanup() {
yield extension.unload();
});

View File

@ -73,7 +73,7 @@ function* testCookies(options) {
changed.splice(evicted, 1);
}
browser.test.assertEq("x:explicit,x:overwrite,x:explicit,foo:overwrite,bar:explicit,deleted:explicit",
browser.test.assertEq("x:explicit,x:overwrite,x:explicit,x:explicit,foo:overwrite,foo:explicit,bar:explicit,deleted:explicit",
changed.join(","), "expected changes");
} else {
browser.test.assertEq("", changed.join(","), "expected no changes");

View File

@ -1,5 +1,5 @@
[DEFAULT]
skip-if = toolkit == 'android' || buildapp == 'b2g' || os == 'linux' # linux - bug 947531
skip-if = toolkit == 'android' || buildapp == 'b2g' || os == 'linux' # linux - bug 1022386
support-files =
satchel_common.js
subtst_form_submission_1.html
@ -7,13 +7,12 @@ support-files =
parent_utils.js
[test_bug_511615.html]
skip-if = e10s # bug 1162338 (needs refactoring to talk to the autocomplete popup)
[test_bug_787624.html]
skip-if = e10s # bug 1162338 (needs refactoring to talk to the autocomplete popup)
[test_datalist_with_caching.html]
[test_form_autocomplete.html]
[test_form_autocomplete_with_list.html]
[test_form_submission.html]
[test_form_submission_cap.html]
[test_form_submission_cap2.html]
[test_popup_direction.html]
[test_popup_enter_event.html]

View File

@ -80,6 +80,23 @@ var ParentUtils = {
});
},
checkSelectedIndex(expectedIndex) {
ContentTaskUtils.waitForCondition(() => {
return gAutocompletePopup.popupOpen &&
gAutocompletePopup.selectedIndex === expectedIndex;
}).then(() => {
sendAsyncMessage("gotSelectedIndex");
});
},
getPopupState() {
sendAsyncMessage("gotPopupState", {
open: gAutocompletePopup.popupOpen,
selectedIndex: gAutocompletePopup.selectedIndex,
direction: gAutocompletePopup.style.direction,
});
},
observe(subject, topic, data) {
assert.ok(topic === "satchel-storage-changed");
sendAsyncMessage("satchel-storage-changed", { subject: null, topic, data });
@ -108,6 +125,14 @@ addMessageListener("waitForMenuChange", ({ expectedCount, expectedFirstValue })
ParentUtils.checkRowCount(expectedCount, expectedFirstValue);
});
addMessageListener("waitForSelectedIndex", ({ expectedIndex }) => {
ParentUtils.checkSelectedIndex(expectedIndex);
});
addMessageListener("getPopupState", () => {
ParentUtils.getPopupState();
});
addMessageListener("addObserver", () => {
Services.obs.addObserver(ParentUtils, "satchel-storage-changed", false);
});

View File

@ -125,23 +125,6 @@ function checkForSave(name, value, message) {
checkObserver.verifyStack.push({ name : name, value: value, message: message });
}
function NonE10SgetAutocompletePopup() {
var Ci = SpecialPowers.Ci;
chromeWin = SpecialPowers.wrap(window)
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem)
.rootTreeItem
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow)
.QueryInterface(Ci.nsIDOMChromeWindow);
autocompleteMenu = chromeWin.document.getElementById("PopupAutoComplete");
ok(autocompleteMenu, "Got autocomplete popup");
return autocompleteMenu;
}
function getFormSubmitButton(formNum) {
var form = $("form" + formNum); // by id, not name
ok(form != null, "getting form " + formNum);
@ -197,6 +180,22 @@ function notifyMenuChanged(expectedCount, expectedFirstValue, then) {
});
}
function notifySelectedIndex(expectedIndex, then) {
script.sendAsyncMessage("waitForSelectedIndex", { expectedIndex });
script.addMessageListener("gotSelectedIndex", function changed() {
script.removeMessageListener("gotSelectedIndex", changed);
then();
});
}
function getPopupState(then) {
script.sendAsyncMessage("getPopupState");
script.addMessageListener("gotPopupState", function listener(state) {
script.removeMessageListener("gotPopupState", listener);
then(state);
});
}
var chromeURL = SimpleTest.getTestFileURL("parent_utils.js");
var script = SpecialPowers.loadChromeScript(chromeURL);
script.addMessageListener("onpopupshown", ({ results }) => {

View File

@ -1,23 +1,19 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test for Form History Autocomplete</title>
<title>Test for Form History Autocomplete Untrusted Events: Bug 511615</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
<script type="text/javascript" src="satchel_common.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
Form History test: form field autocomplete
Test for Form History Autocomplete Untrusted Events: Bug 511615
<p id="display"></p>
<!-- we presumably can't hide the content for this test. -->
<div id="content" style="direction: rtl;">
<!-- unused -->
<form id="unused" onsubmit="return false;">
<input type="text" name="field1" value="unused">
</form>
<div id="content">
<!-- normal, basic form -->
<form id="form1" onsubmit="return false;">
<input type="text" name="field1">
@ -28,15 +24,101 @@ Form History test: form field autocomplete
<pre id="test">
<script class="testbody" type="text/javascript">
/** Test for Form History autocomplete **/
var autocompletePopup = NonE10SgetAutocompletePopup();
autocompletePopup.style.direction = "ltr";
var resolvePopupShownListener;
registerPopupShownListener(() => resolvePopupShownListener());
var input = $_(1, "field1");
function waitForNextPopup() {
return new Promise(resolve => { resolvePopupShownListener = resolve; });
}
// Get the form history service
function setupFormHistory(aCallback) {
updateFormHistory([
/**
* Indicates the time to wait before checking that the state of the autocomplete
* popup, including whether it is open, has not changed in response to events.
*
* Manual testing on a fast machine revealed that 80ms was still unreliable,
* while 100ms detected a simulated failure reliably. Unfortunately, this means
* that to take into account slower machines we should use a larger value.
*
* Note that if a machine takes more than this time to show the popup, this
* would not cause a failure, conversely the machine would not be able to detect
* whether the test should have failed. In other words, this use of timeouts is
* never expected to cause intermittent failures with test automation.
*/
const POPUP_RESPONSE_WAIT_TIME_MS = 200;
SimpleTest.requestFlakyTimeout("Must ensure that an event does not happen.");
/**
* Checks that the popup does not open in response to the given function.
*/
function expectPopupDoesNotOpen(triggerFn) {
let popupShown = waitForNextPopup();
triggerFn();
return Promise.race([
popupShown.then(() => Promise.reject("Popup opened unexpectedly.")),
new Promise(resolve => setTimeout(resolve, POPUP_RESPONSE_WAIT_TIME_MS)),
]);
}
/**
* Checks that the selected index in the popup still matches the given value.
*/
function checkSelectedIndexAfterResponseTime(expectedIndex) {
return new Promise(resolve => {
setTimeout(() => getPopupState(resolve), POPUP_RESPONSE_WAIT_TIME_MS);
}).then(popupState => {
is(popupState.open, true, "Popup should still be open.");
is(popupState.selectedIndex, expectedIndex, "Selected index should match.");
});
}
function doKeyUnprivileged(key) {
let keyName = "DOM_VK_" + key.toUpperCase();
let keycode, charcode, alwaysVal;
if (key.length == 1) {
keycode = 0;
charcode = key.charCodeAt(0);
alwaysval = charcode;
} else {
keycode = KeyEvent[keyName];
if (!keycode)
throw "invalid keyname in test";
charcode = 0;
alwaysval = keycode;
}
let dnEvent = document.createEvent('KeyboardEvent');
let prEvent = document.createEvent('KeyboardEvent');
let upEvent = document.createEvent('KeyboardEvent');
dnEvent.initKeyEvent("keydown", true, true, null, false, false, false, false, alwaysval, 0);
prEvent.initKeyEvent("keypress", true, true, null, false, false, false, false, keycode, charcode);
upEvent.initKeyEvent("keyup", true, true, null, false, false, false, false, alwaysval, 0);
input.dispatchEvent(dnEvent);
input.dispatchEvent(prEvent);
input.dispatchEvent(upEvent);
}
function doClickWithMouseEventUnprivileged() {
let dnEvent = document.createEvent('MouseEvent');
let upEvent = document.createEvent('MouseEvent');
let ckEvent = document.createEvent('MouseEvent');
dnEvent.initMouseEvent("mousedown", true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
upEvent.initMouseEvent("mouseup", true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
ckEvent.initMouseEvent("mouseclick", true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
input.dispatchEvent(dnEvent);
input.dispatchEvent(upEvent);
input.dispatchEvent(ckEvent);
}
let input = $_(1, "field1");
add_task(function* test_initialize() {
yield new Promise(resolve => updateFormHistory([
{ op : "remove" },
{ op : "add", fieldname : "field1", value : "value1" },
{ op : "add", fieldname : "field1", value : "value2" },
@ -47,342 +129,65 @@ function setupFormHistory(aCallback) {
{ op : "add", fieldname : "field1", value : "value7" },
{ op : "add", fieldname : "field1", value : "value8" },
{ op : "add", fieldname : "field1", value : "value9" },
], aCallback);
}
], resolve));
});
function checkForm(expectedValue) {
var formID = input.parentNode.id;
is(input.value, expectedValue, "Checking " + formID + " input");
}
function checkPopupOpen(isOpen, expectedIndex) {
var actuallyOpen = autocompletePopup.popupOpen;
var actualIndex = autocompletePopup.selectedIndex;
is(actuallyOpen, isOpen, "popup should be " + (isOpen ? "open" : "closed"));
if (isOpen)
is(actualIndex, expectedIndex, "checking selected index");
// Correct state if needed, so following tests run as expected.
if (actuallyOpen != isOpen) {
if (isOpen)
autocompletePopup.openPopup();
else
autocompletePopup.closePopup();
}
if (isOpen && actualIndex != expectedIndex)
autocompletePopup.selectedIndex = expectedIndex;
}
function doKeyUnprivileged(key) {
var keyName = "DOM_VK_" + key.toUpperCase();
var keycode, charcode, alwaysVal;
if (key.length == 1) {
keycode = 0;
charcode = key.charCodeAt(0);
alwaysval = charcode;
} else {
keycode = KeyEvent[keyName];
if (!keycode)
throw "invalid keyname in test";
charcode = 0;
alwaysval = keycode;
}
var dnEvent = document.createEvent('KeyboardEvent');
var prEvent = document.createEvent('KeyboardEvent');
var upEvent = document.createEvent('KeyboardEvent');
dnEvent.initKeyEvent("keydown", true, true, null, false, false, false, false, alwaysval, 0);
prEvent.initKeyEvent("keypress", true, true, null, false, false, false, false, keycode, charcode);
upEvent.initKeyEvent("keyup", true, true, null, false, false, false, false, alwaysval, 0);
input.dispatchEvent(dnEvent);
input.dispatchEvent(prEvent);
input.dispatchEvent(upEvent);
}
function doClickUnprivileged() {
var dnEvent = document.createEvent('MouseEvent');
var upEvent = document.createEvent('MouseEvent');
var ckEvent = document.createEvent('MouseEvent');
dnEvent.initMouseEvent("mousedown", true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
upEvent.initMouseEvent("mouseup", true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
ckEvent.initMouseEvent("mouseclick", true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
input.dispatchEvent(dnEvent);
input.dispatchEvent(upEvent);
input.dispatchEvent(ckEvent);
}
var testNum = 0;
var expectingPopup = false;
function expectPopup()
{
info("expecting popup for test " + testNum);
expectingPopup = true;
}
function popupShownListener()
{
info("popup shown for test " + testNum);
if (expectingPopup) {
expectingPopup = false;
SimpleTest.executeSoon(runTest);
}
else {
ok(false, "Autocomplete popup not expected" + testNum);
}
}
SpecialPowers.addAutoCompletePopupEventListener(window, "popupshown", popupShownListener);
/*
* Main section of test...
*
* This is a bit hacky, because the events are either being sent or
* processes asynchronously, so we need to interrupt our flow with lots of
* setTimeout() calls. The case statements are executed in order, one per
* timeout.
*/
function runTest() {
testNum++;
ok(true, "Starting test #" + testNum);
switch(testNum) {
//
// Check initial state
//
case 1:
input.value = "";
checkForm("");
is(autocompletePopup.popupOpen, false, "popup should be initially closed");
break;
//
// Try to open the autocomplete popup from untrusted events.
//
// try a focus()
case 2:
input.focus();
break;
case 3:
checkPopupOpen(false);
checkForm("");
break;
// try a click()
case 4:
input.click();
break;
case 5:
checkPopupOpen(false);
checkForm("");
break;
// try a mouseclick event
case 6:
doClickUnprivileged();
break;
case 7:
checkPopupOpen(false);
checkForm("");
break;
// try a down-arrow
case 8:
doKeyUnprivileged("down");
break;
case 9:
checkPopupOpen(false);
checkForm("");
break;
// try a page-down
case 10:
doKeyUnprivileged("page_down");
break;
case 11:
checkPopupOpen(false);
checkForm("");
break;
// try a return
case 12:
// XXX this causes later tests to fail for some reason.
// doKeyUnprivileged("return"); // not "enter"!
break;
case 13:
checkPopupOpen(false);
checkForm("");
break;
// try a keypress
case 14:
doKeyUnprivileged('v');
break;
case 15:
checkPopupOpen(false);
checkForm("");
break;
// try a space
case 16:
doKeyUnprivileged(" ");
break;
case 17:
checkPopupOpen(false);
checkForm("");
break;
// backspace
case 18:
doKeyUnprivileged("back_space");
break;
case 19:
checkPopupOpen(false);
checkForm("");
break;
case 20:
// We're privileged for this test, so open the popup.
checkPopupOpen(false);
checkForm("");
expectPopup();
doKey("down");
return;
break;
case 21:
checkPopupOpen(true, -1);
checkForm("");
testNum = 99;
break;
//
// Try to change the selected autocomplete item from untrusted events
//
// try a down-arrow
case 100:
doKeyUnprivileged("down");
break;
case 101:
checkPopupOpen(true, -1);
checkForm("");
break;
// try a page-down
case 102:
doKeyUnprivileged("page_down");
break;
case 103:
checkPopupOpen(true, -1);
checkForm("");
break;
// really adjust the index
case 104:
// (We're privileged for this test.) Try a privileged down-arrow.
doKey("down");
break;
case 105:
checkPopupOpen(true, 0);
checkForm("");
break;
// try a down-arrow
case 106:
doKeyUnprivileged("down");
break;
case 107:
checkPopupOpen(true, 0);
checkForm("");
break;
// try a page-down
case 108:
doKeyUnprivileged("page_down");
break;
case 109:
checkPopupOpen(true, 0);
checkForm("");
// try a keypress
case 110:
// XXX this causes the popup to close, and makes the value "vaa" (sic)
//doKeyUnprivileged('a');
break;
case 111:
checkPopupOpen(true, 0);
checkForm("");
testNum = 199;
break;
//
// Try to use the selected autocomplete item from untrusted events
//
// try a right-arrow
case 200:
doKeyUnprivileged("right");
break;
case 201:
checkPopupOpen(true, 0);
checkForm("");
break;
// try a space
case 202:
doKeyUnprivileged(" ");
break;
case 203:
// XXX we should ignore this input while popup is open?
checkPopupOpen(true, 0);
checkForm("");
break;
// backspace
case 204:
doKeyUnprivileged("back_space");
break;
case 205:
// XXX we should ignore this input while popup is open?
checkPopupOpen(true, 0);
checkForm("");
break;
case 206:
// (this space intentionally left blank)
break;
case 207:
checkPopupOpen(true, 0);
checkForm("");
break;
// try a return
case 208:
// XXX this seems to cause problems with reliably closing the popup
// doKeyUnprivileged("return"); // not "enter"!
break;
case 209:
checkPopupOpen(true, 0);
checkForm("");
break;
// Send a real Escape to ensure popup closed at end of test.
case 210:
// Need to use doKey(), even though this test is not privileged.
doKey("escape");
break;
case 211:
checkPopupOpen(false);
checkForm("");
is(autocompletePopup.style.direction, "rtl", "direction should have been changed from ltr to rtl");
SpecialPowers.removeAutoCompletePopupEventListener(window, "popupshown", popupShownListener);
SimpleTest.finish();
return;
default:
ok(false, "Unexpected invocation of test #" + testNum);
SpecialPowers.removeAutoCompletePopupEventListener(window, "popupshown", popupShownListener);
SimpleTest.finish();
return;
add_task(function* test_untrusted_events_ignored() {
// The autocomplete popup should not open from untrusted events.
for (let triggerFn of [
() => input.focus(),
() => input.click(),
() => doClickWithMouseEventUnprivileged(),
() => doKeyUnprivileged("down"),
() => doKeyUnprivileged("page_down"),
() => doKeyUnprivileged("return"),
() => doKeyUnprivileged("v"),
() => doKeyUnprivileged(" "),
() => doKeyUnprivileged("back_space"),
]) {
// We must wait for the entire timeout for each individual test, because the
// next event in the list might prevent the popup from opening.
yield expectPopupDoesNotOpen(triggerFn);
}
SimpleTest.executeSoon(runTest);
}
// A privileged key press will actually open the popup.
let popupShown = waitForNextPopup();
doKey("down");
yield popupShown;
function startTest() {
setupFormHistory(runTest);
}
// The selected autocomplete item should not change from untrusted events.
for (let triggerFn of [
() => doKeyUnprivileged("down"),
() => doKeyUnprivileged("page_down"),
]) {
triggerFn();
yield checkSelectedIndexAfterResponseTime(-1);
}
SimpleTest.waitForFocus(startTest);
// A privileged key press will actually change the selected index.
let indexChanged = new Promise(resolve => notifySelectedIndex(0, resolve));
doKey("down");
yield indexChanged;
SimpleTest.waitForExplicitFinish();
// The selected autocomplete item should not change and it should not be
// possible to use it from untrusted events.
for (let triggerFn of [
() => doKeyUnprivileged("down"),
() => doKeyUnprivileged("page_down"),
() => doKeyUnprivileged("right"),
() => doKeyUnprivileged(" "),
() => doKeyUnprivileged("back_space"),
() => doKeyUnprivileged("back_space"),
() => doKeyUnprivileged("return"),
]) {
triggerFn();
yield checkSelectedIndexAfterResponseTime(0);
is(input.value, "", "The selected item should not have been used.");
}
// Close the popup.
input.blur();
});
</script>
</pre>
</body>

View File

@ -3,7 +3,7 @@
<head>
<title>Test for Layout of Form History Autocomplete: Bug 787624</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
<script type="text/javascript" src="satchel_common.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
<style>
@ -36,13 +36,8 @@ Form History Layout test: form field autocomplete: Bug 787624
<p id="display"></p>
<!-- we presumably can't hide the content for this test. -->
<div id="content" style="direction: rtl;">
<!-- unused -->
<form id="unused" onsubmit="return false;">
<input type="text" name="field1" value="unused">
</form>
<!-- normal, basic form -->
<div id="content">
<!-- in this form, the input field is partially hidden and can scroll -->
<div class="container">
<div class="subcontainer">
<form id="form1" onsubmit="return false;">
@ -58,126 +53,35 @@ Form History Layout test: form field autocomplete: Bug 787624
/** Test for Form History autocomplete Layout: Bug 787624 **/
var autocompletePopup = NonE10SgetAutocompletePopup();
autocompletePopup.style.direction = "ltr";
var resolvePopupShownListener;
registerPopupShownListener(() => resolvePopupShownListener());
var input = $_(1, "field1");
var rect = input.getBoundingClientRect();
function waitForNextPopup() {
return new Promise(resolve => { resolvePopupShownListener = resolve; });
}
// Get the form history service
function setupFormHistory() {
updateFormHistory([
add_task(function* test_popup_not_move_input() {
var input = $_(1, "field1");
var rect = input.getBoundingClientRect();
yield new Promise(resolve => updateFormHistory([
{ op : "remove" },
{ op : "add", fieldname : "field1", value : "value1" },
{ op : "add", fieldname : "field1", value : "value2" },
], () => runTest(1));
}
], resolve));
function checkForm(expectedValue) {
var formID = input.parentNode.id;
if (input.value != expectedValue)
return false;
let popupShown = waitForNextPopup();
input.focus();
doKey("down");
yield popupShown;
is(input.value, expectedValue, "Checking " + formID + " input");
return true;
}
var newRect = input.getBoundingClientRect();
is(newRect.left, rect.left,
"autocomplete popup does not disturb the input position");
is(newRect.top, rect.top,
"autocomplete popup does not disturb the input position");
});
function checkPopupOpen(isOpen, expectedIndex) {
var actuallyOpen = autocompletePopup.popupOpen;
var actualIndex = autocompletePopup.selectedIndex;
if (actuallyOpen != isOpen)
return false;
is(actuallyOpen, isOpen, "popup should be " + (isOpen ? "open" : "closed"));
if (isOpen) {
if (actualIndex != expectedIndex)
return false;
is(actualIndex, expectedIndex, "checking selected index");
}
return true;
}
function runTest(testNum) {
ok(true, "Starting test #" + testNum);
var retry = false;
var formOK, popupOK;
switch(testNum) {
//
// Check initial state
//
case 1:
input.value = "";
formOK = checkForm("");
if (!formOK) {
retry = true;
break;
}
is(autocompletePopup.popupOpen, false, "popup should be initially closed");
break;
// try a focus()
case 2:
input.focus();
break;
case 3:
popupOK = checkPopupOpen(false);
formOK = checkForm("");
if (!formOK || !popupOK)
retry = true;
break;
// try a click()
case 4:
input.click();
break;
case 5:
popupOK = checkPopupOpen(false);
formOK = checkForm("");
if (!formOK || !popupOK)
retry = true;
break;
case 6:
// We're privileged for this test, so open the popup.
popupOK = checkPopupOpen(false);
formOK = checkForm("");
if (!formOK || !popupOK)
retry = true;
doKey("down");
break;
case 7:
popupOK = checkPopupOpen(true, -1);
formOK = checkForm("");
if (!formOK || !popupOK)
retry = true;
break;
case 8:
var newRect = input.getBoundingClientRect();
is(newRect.left, rect.left,
"autocomplete popup does not disturb the input position");
is(newRect.top, rect.top,
"autocomplete popup does not disturb the input position");
SimpleTest.finish();
return;
default:
ok(false, "Unexpected invocation of test #" + testNum);
SimpleTest.finish();
return;
}
if (!retry)
testNum++;
setTimeout(runTest, 0, testNum);
}
function startTest() {
setupFormHistory();
}
window.onload = startTest;
SimpleTest.waitForExplicitFinish();
</script>
</pre>
</body>

View File

@ -0,0 +1,61 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test for Popup Direction</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
<script type="text/javascript" src="satchel_common.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
Test for Popup Direction
<p id="display"></p>
<!-- we presumably can't hide the content for this test. -->
<div id="content">
<!-- normal, basic form -->
<form id="form1" onsubmit="return false;">
<input type="text" name="field1">
<button type="submit">Submit</button>
</form>
</div>
<pre id="test">
<script class="testbody" type="text/javascript">
var resolvePopupShownListener;
registerPopupShownListener(() => resolvePopupShownListener());
function waitForNextPopup() {
return new Promise(resolve => { resolvePopupShownListener = resolve; });
}
add_task(function* test_popup_direction() {
var input = $_(1, "field1");
yield new Promise(resolve => updateFormHistory([
{ op : "remove" },
{ op : "add", fieldname : "field1", value : "value1" },
{ op : "add", fieldname : "field1", value : "value2" },
], resolve));
for (let direction of ["ltr", "rtl"]) {
document.getElementById("content").style.direction = direction;
let popupShown = waitForNextPopup();
input.focus();
doKey("down");
yield popupShown;
let popupState = yield new Promise(resolve => getPopupState(resolve));
is(popupState.direction, direction, "Direction should match.");
// Close the popup.
input.blur();
}
});
</script>
</pre>
</body>
</html>

View File

@ -1240,19 +1240,6 @@ ReflectHistogramAndSamples(JSContext *cx, JS::Handle<JSObject*> obj, Histogram *
return REFLECT_FAILURE;
}
if (h->histogram_type() != Histogram::HISTOGRAM) {
// Export |sum_squares| as two separate 32-bit properties so that we
// can accurately reconstruct it on the analysis side.
uint64_t sum_squares = ss.sum_squares(locker);
// Cast to avoid implicit truncation warnings.
uint32_t lo = static_cast<uint32_t>(sum_squares);
uint32_t hi = static_cast<uint32_t>(sum_squares >> 32);
if (!(JS_DefineProperty(cx, obj, "sum_squares_lo", lo, JSPROP_ENUMERATE)
&& JS_DefineProperty(cx, obj, "sum_squares_hi", hi, JSPROP_ENUMERATE))) {
return REFLECT_FAILURE;
}
}
const size_t count = h->bucket_count();
JS::Rooted<JSObject*> rarray(cx, JS_NewArrayObject(cx, count));
if (!rarray) {

View File

@ -830,8 +830,6 @@ var Impl = {
* Returns an object:
* { range: [min, max], bucket_count: <number of buckets>,
* histogram_type: <histogram_type>, sum: <sum>,
* sum_squares_lo: <sum_squares_lo>,
* sum_squares_hi: <sum_squares_hi>,
* values: { bucket1: count1, bucket2: count2, ... } }
*/
packHistogram: function packHistogram(hgram) {
@ -845,11 +843,6 @@ var Impl = {
sum: hgram.sum
};
if (hgram.histogram_type != Telemetry.HISTOGRAM_EXPONENTIAL) {
retgram.sum_squares_lo = hgram.sum_squares_lo;
retgram.sum_squares_hi = hgram.sum_squares_hi;
}
let first = true;
let last = 0;

View File

@ -307,9 +307,7 @@ function checkPayload(payload, reason, successfulPings, savedPings) {
bucket_count: 3,
histogram_type: 3,
values: {0:1, 1:0},
sum: 0,
sum_squares_lo: 0,
sum_squares_hi: 0
sum: 0
};
let flag = payload.histograms[TELEMETRY_TEST_FLAG];
Assert.equal(uneval(flag), uneval(expected_flag));
@ -321,8 +319,6 @@ function checkPayload(payload, reason, successfulPings, savedPings) {
histogram_type: 4,
values: {0:1, 1:0},
sum: 1,
sum_squares_lo: 1,
sum_squares_hi: 0,
};
let count = payload.histograms[TELEMETRY_TEST_COUNT];
Assert.equal(uneval(count), uneval(expected_count));
@ -334,9 +330,7 @@ function checkPayload(payload, reason, successfulPings, savedPings) {
bucket_count: 3,
histogram_type: 2,
values: {0:2, 1:successfulPings, 2:0},
sum: successfulPings,
sum_squares_lo: successfulPings,
sum_squares_hi: 0
sum: successfulPings
};
let tc = payload.histograms[TELEMETRY_SUCCESS];
Assert.equal(uneval(tc), uneval(expected_tc));
@ -383,8 +377,6 @@ function checkPayload(payload, reason, successfulPings, savedPings) {
histogram_type: 4,
values: {0:2, 1:0},
sum: 2,
sum_squares_lo: 2,
sum_squares_hi: 0,
},
"b": {
range: [1, 2],
@ -392,8 +384,6 @@ function checkPayload(payload, reason, successfulPings, savedPings) {
histogram_type: 4,
values: {0:1, 1:0},
sum: 1,
sum_squares_lo: 1,
sum_squares_hi: 0,
},
};
Assert.deepEqual(expected_keyed_count, keyedHistograms[TELEMETRY_TEST_KEYED_COUNT]);

View File

@ -36,14 +36,6 @@ function test_histogram(histogram_type, name, min, max, bucket_count) {
var s = h.snapshot();
// verify properties
do_check_eq(sum, s.sum);
if (histogram_type == Telemetry.HISTOGRAM_EXPONENTIAL) {
do_check_false("sum_squares_lo" in s);
do_check_false("sum_squares_hi" in s);
} else {
// Doing the math to verify sum_squares was reflected correctly is
// tedious in JavaScript. Just make sure we have something.
do_check_neq(s.sum_squares_lo + s.sum_squares_hi, 0);
}
// there should be exactly one element per bucket
for (let i of s.counts) {
@ -70,10 +62,6 @@ function test_histogram(histogram_type, name, min, max, bucket_count) {
do_check_eq(i, 0);
}
do_check_eq(s.sum, 0);
if (histogram_type != Telemetry.HISTOGRAM_EXPONENTIAL) {
do_check_eq(s.sum_squares_lo, 0);
do_check_eq(s.sum_squares_hi, 0);
}
h.add(0);
h.add(1);
@ -191,10 +179,6 @@ function compareHistograms(h1, h2) {
do_check_eq(s1.min, s2.min);
do_check_eq(s1.max, s2.max);
do_check_eq(s1.sum, s2.sum);
if (s1.histogram_type != Telemetry.HISTOGRAM_EXPONENTIAL) {
do_check_eq(s1.sum_squares_lo, s2.sum_squares_lo);
do_check_eq(s1.sum_squares_hi, s2.sum_squares_hi);
}
do_check_eq(s1.counts.length, s2.counts.length);
for (let i = 0; i < s1.counts.length; i++)
@ -420,8 +404,6 @@ function test_keyed_boolean_histogram()
"max": 2,
"histogram_type": 2,
"sum": 1,
"sum_squares_lo": 1,
"sum_squares_hi": 0,
"ranges": [0, 1, 2],
"counts": [0, 1, 0]
};
@ -449,7 +431,6 @@ function test_keyed_boolean_histogram()
testKeys.push(key);
testSnapShot[key] = testHistograms[2];
testSnapShot[key].sum = 0;
testSnapShot[key].sum_squares_lo = 0;
testSnapShot[key].counts = [1, 0, 0];
Assert.deepEqual(h.keys().sort(), testKeys);
Assert.deepEqual(h.snapshot(), testSnapShot);
@ -471,8 +452,6 @@ function test_keyed_count_histogram()
"max": 2,
"histogram_type": 4,
"sum": 0,
"sum_squares_lo": 0,
"sum_squares_hi": 0,
"ranges": [0, 1, 2],
"counts": [1, 0, 0]
};
@ -490,7 +469,6 @@ function test_keyed_count_histogram()
}
testHistograms[i].counts[0] = value;
testHistograms[i].sum = value;
testHistograms[i].sum_squares_lo = value;
testSnapShot[key] = testHistograms[i];
testKeys.push(key);
@ -508,7 +486,6 @@ function test_keyed_count_histogram()
testKeys.push(key);
testHistograms[4].counts[0] = 1;
testHistograms[4].sum = 1;
testHistograms[4].sum_squares_lo = 1;
testSnapShot[key] = testHistograms[4];
Assert.deepEqual(h.keys().sort(), testKeys);
@ -536,8 +513,6 @@ function test_keyed_flag_histogram()
"max": 2,
"histogram_type": 3,
"sum": 1,
"sum_squares_lo": 1,
"sum_squares_hi": 0,
"ranges": [0, 1, 2],
"counts": [0, 1, 0]
};

View File

@ -69,9 +69,16 @@ td:last-child {
}
.submitting {
background-image: url(chrome://global/skin/icons/loading_16.png);
background-image: url(chrome://global/skin/icons/loading.png);
background-repeat: no-repeat;
background-position: right;
background-size: 16px;
}
@media (min-resolution: 1.1dppx) {
.submitting {
background-image: url(chrome://global/skin/icons/loading@2x.png);
}
}
</style>
<link rel="stylesheet" media="screen, projection" type="text/css"

View File

@ -156,5 +156,5 @@ findbar[noanim] {
}
.find-status-icon[status="pending"] {
list-style-image: url("chrome://global/skin/icons/loading_16.png");
list-style-image: url("chrome://global/skin/icons/loading.png");
}

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@ -46,7 +46,7 @@ toolkit.jar:
skin/classic/global/icons/blacklist_favicon.png (icons/blacklist_favicon.png)
skin/classic/global/icons/blacklist_large.png (icons/blacklist_large.png)
skin/classic/global/icons/close.svg (icons/close.svg)
skin/classic/global/icons/loading_16.png (icons/loading_16.png)
skin/classic/global/icons/loading.png (icons/loading.png)
skin/classic/global/icons/panelarrow-horizontal.svg (icons/panelarrow-horizontal.svg)
skin/classic/global/icons/panelarrow-vertical.svg (icons/panelarrow-vertical.svg)
skin/classic/global/icons/resizer.png (icons/resizer.png)

View File

@ -30,7 +30,7 @@ wizardpage {
}
.remoteLoadingThrobber[state="loading"] {
list-style-image: url("chrome://global/skin/icons/loading_16.png");
list-style-image: url("chrome://global/skin/icons/loading.png");
}
.remoteLoadingThrobber[state="error"] {

View File

@ -91,7 +91,7 @@ html|a {
.throbber {
padding-left: 16px; /* width of the background image */
background: url(chrome://global/skin/icons/loading_16.png) no-repeat;
background: url(chrome://global/skin/icons/loading.png) no-repeat;
margin-left: 5px;
}

View File

@ -205,7 +205,14 @@ label.findbar-find-fast:-moz-lwtheme,
.find-status-icon[status="pending"] {
display: block;
list-style-image: url("chrome://global/skin/icons/loading_16.png");
list-style-image: url("chrome://global/skin/icons/loading.png");
}
@media (min-resolution: 2dppx) {
.find-status-icon[status="pending"] {
width: 16px;
list-style-image: url("chrome://global/skin/icons/loading@2x.png");
}
}
.findbar-find-status,

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

View File

@ -103,7 +103,8 @@ toolkit.jar:
skin/classic/global/icons/information-32.png (icons/information-32.png)
skin/classic/global/icons/information-64.png (icons/information-64.png)
skin/classic/global/icons/information-large.png (icons/information-large.png)
skin/classic/global/icons/loading_16.png (icons/loading_16.png)
skin/classic/global/icons/loading.png (icons/loading.png)
skin/classic/global/icons/loading@2x.png (icons/loading@2x.png)
skin/classic/global/icons/menulist-dropmarker.png (icons/menulist-dropmarker.png)
skin/classic/global/icons/notfound.png (icons/notfound.png)
skin/classic/global/icons/panebutton-active.png (icons/panebutton-active.png)

View File

@ -7,7 +7,7 @@
}
.throbber {
list-style-image: url("chrome://global/skin/icons/loading_16.png");
list-style-image: url("chrome://global/skin/icons/loading.png");
width: 16px;
height: 16px;
margin-top: 5px;
@ -16,6 +16,12 @@
-moz-margin-end: 2px;
}
@media (min-resolution: 2dppx) {
.throbber {
list-style-image: url("chrome://global/skin/icons/loading@2x.png");
}
}
.alertBox {
background-color: InfoBackground;
color: InfoText;

View File

@ -56,7 +56,14 @@ wizardpage {
}
.remoteLoadingThrobber[state="loading"] {
list-style-image: url("chrome://global/skin/icons/loading_16.png");
list-style-image: url("chrome://global/skin/icons/loading.png");
}
@media (min-resolution: 2dppx) {
.remoteLoadingThrobber[state="loading"] {
width: 16px;
list-style-image: url("chrome://global/skin/icons/loading@2x.png");
}
}
.remoteLoadingThrobber[state="error"] {

View File

@ -130,11 +130,18 @@
}
.loading {
list-style-image: url("chrome://global/skin/icons/loading_16.png");
list-style-image: url("chrome://global/skin/icons/loading.png");
padding-left: 20px;
padding-right: 20px;
}
@media (min-resolution: 1.1dppx) {
.loading > image {
width: 16px;
list-style-image: url("chrome://global/skin/icons/loading@2x.png");
}
}
button.warning {
list-style-image: url("chrome://mozapps/skin/extensions/alerticon-warning.svg");
}
@ -693,26 +700,32 @@ button.warning {
#detail-screenshot-box {
-moz-margin-end: 2em;
background-image: linear-gradient(rgba(255,255,255,.5), transparent);
background-color: white;
box-shadow: 0 1px 2px #666;
border-radius: 2px;
}
#detail-screenshot {
max-width: 300px;
max-height: 300px;
background-color: white;
box-shadow: 0 1px 2px #666;
}
#detail-screenshot[loading] {
background-image: url("chrome://global/skin/icons/loading_16.png"),
linear-gradient(rgba(255, 255, 255, 0.5), transparent);
background-image: url("chrome://global/skin/icons/loading.png");
background-position: 50% 50%;
background-repeat: no-repeat;
border-radius: 2px;
}
@media (min-resolution: 1.1dppx) {
#detail-screenshot[loading] {
background-image: url("chrome://global/skin/icons/loading@2x.png");
background-size: 16px;
}
}
#detail-screenshot[loading="error"] {
background-image: url("chrome://global/skin/media/error.png"),
linear-gradient(rgba(255, 255, 255, 0.5), transparent);
background-image: url("chrome://global/skin/media/error.png");
}
#detail-desc-container {

View File

@ -91,10 +91,17 @@ html|a {
.throbber {
padding-left: 16px; /* width of the background image */
background: url(chrome://global/skin/icons/loading_16.png) no-repeat;
background: url(chrome://global/skin/icons/loading.png) no-repeat;
margin-left: 5px;
}
@media (min-resolution: 1.1dppx) {
.throbber {
background-image: url(chrome://global/skin/icons/loading@2x.png);
background-size: 16px;
}
}
.msgTapToPlay,
.msgClickToPlay {
text-decoration: underline;

View File

@ -149,5 +149,12 @@ findbar[noanim] {
}
.find-status-icon[status="pending"] {
list-style-image: url("chrome://global/skin/icons/loading_16.png");
list-style-image: url("chrome://global/skin/icons/loading.png");
}
@media (min-resolution: 1.1dppx) {
.find-status-icon[status="pending"] {
width: 16px;
list-style-image: url("chrome://global/skin/icons/loading@2x.png");
}
}

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -47,7 +47,8 @@ toolkit.jar:
skin/classic/global/icons/close-XPVista7@2x.png (icons/close-XPVista7@2x.png)
skin/classic/global/icons/close-inverted-XPVista7.png (icons/close-inverted-XPVista7.png)
skin/classic/global/icons/close-inverted-XPVista7@2x.png (icons/close-inverted-XPVista7@2x.png)
skin/classic/global/icons/loading_16.png (icons/loading_16.png)
skin/classic/global/icons/loading.png (icons/loading.png)
skin/classic/global/icons/loading@2x.png (icons/loading@2x.png)
skin/classic/global/icons/resizer.png (icons/resizer.png)
skin/classic/global/icons/sslWarning.png (icons/sslWarning.png)
* skin/classic/global/in-content/common.css (in-content/common.css)

View File

@ -7,7 +7,7 @@
}
.throbber {
list-style-image: url("chrome://global/skin/icons/loading_16.png");
list-style-image: url("chrome://global/skin/icons/loading.png");
width: 16px;
height: 16px;
margin-top: 5px;
@ -16,6 +16,12 @@
-moz-margin-end: 2px;
}
@media (min-resolution: 1.1dppx) {
.throbber {
list-style-image: url("chrome://global/skin/icons/loading@2x.png");
}
}
.alertBox {
background-color: InfoBackground;
color: InfoText;

View File

@ -30,7 +30,14 @@ wizardpage {
}
.remoteLoadingThrobber[state="loading"] {
list-style-image: url("chrome://global/skin/icons/loading_16.png");
list-style-image: url("chrome://global/skin/icons/loading.png");
}
@media (min-resolution: 1.1dppx) {
.remoteLoadingThrobber[state="loading"] {
width: 16px;
list-style-image: url("chrome://global/skin/icons/loading@2x.png");
}
}
.remoteLoadingThrobber[state="error"] {

Some files were not shown because too many files have changed in this diff Show More