merge fx-team to mozilla-central a=merge

--HG--
extra : amend_source : d62f5ddf7f62ae7c9aad4d22253f53a255ea06fa
This commit is contained in:
Carsten "Tomcat" Book 2014-07-01 14:21:32 +02:00
commit 8386a5e5a4
31 changed files with 1084 additions and 55 deletions

View File

@ -1436,6 +1436,7 @@ pref("devtools.editor.expandtab", true);
pref("devtools.editor.keymap", "default");
pref("devtools.editor.autoclosebrackets", true);
pref("devtools.editor.detectindentation", true);
pref("devtools.editor.autocomplete", true);
// Enable the Font Inspector
pref("devtools.fontinspector.enabled", true);

View File

@ -169,7 +169,6 @@ AutocompletePopup.prototype = {
if (this.isOpen) {
this.hidePopup();
}
this.clearItems();
if (this.onSelect) {
this._list.removeEventListener("select", this.onSelect, false);
@ -187,6 +186,8 @@ AutocompletePopup.prototype = {
gDevTools.off("pref-changed", this._handleThemeChange);
}
this._list.remove();
this._panel.remove();
this._document = null;
this._list = null;
this._panel = null;

View File

@ -16,8 +16,11 @@ const privates = new WeakMap();
/**
* Prepares an editor instance for autocompletion.
*/
function setupAutoCompletion(ctx, options) {
function initializeAutoCompletion(ctx, options = {}) {
let { cm, ed, Editor } = ctx;
if (privates.has(ed)) {
return;
}
let win = ed.container.contentWindow.wrappedJSObject;
let { CodeMirror, document } = win;
@ -59,11 +62,10 @@ function setupAutoCompletion(ctx, options) {
return tip;
}
});
cm.on("cursorActivity", (cm) => {
cm.tern.updateArgHints(cm);
});
let keyMap = {};
let updateArgHintsCallback = cm.tern.updateArgHints.bind(cm.tern, cm);
cm.on("cursorActivity", updateArgHintsCallback);
keyMap[autocompleteKey] = (cm) => {
cm.tern.getHint(cm, (data) => {
@ -79,9 +81,22 @@ function setupAutoCompletion(ctx, options) {
ed.emit("show-information");
});
};
cm.addKeyMap(keyMap);
let destroyTern = function() {
ed.off("destroy", destroyTern);
cm.off("cursorActivity", updateArgHintsCallback);
cm.removeKeyMap(keyMap);
win.tern = cm.tern = null;
privates.delete(ed);
};
ed.on("destroy", destroyTern);
privates.set(ed, {
destroy: destroyTern
});
// TODO: Integrate tern autocompletion with this autocomplete API.
return;
} else if (ed.config.mode == Editor.modes.css) {
@ -126,27 +141,48 @@ function setupAutoCompletion(ctx, options) {
return CodeMirror.Pass;
}
};
keyMap[autocompleteKey] = cm => autoComplete(ctx);
let autoCompleteCallback = autoComplete.bind(null, ctx);
let keypressCallback = onEditorKeypress.bind(null, ctx);
keyMap[autocompleteKey] = autoCompleteCallback;
cm.addKeyMap(keyMap);
cm.on("keydown", (cm, e) => onEditorKeypress(ctx, e));
ed.on("change", () => autoComplete(ctx));
ed.on("destroy", () => {
cm.off("keydown", (cm, e) => onEditorKeypress(ctx, e));
ed.off("change", () => autoComplete(ctx));
cm.on("keydown", keypressCallback);
ed.on("change", autoCompleteCallback);
ed.on("destroy", destroy);
function destroy() {
ed.off("destroy", destroy);
cm.off("keydown", keypressCallback);
ed.off("change", autoCompleteCallback);
cm.removeKeyMap(keyMap);
popup.destroy();
popup = null;
completer = null;
});
keyMap = popup = completer = null;
privates.delete(ed);
}
privates.set(ed, {
popup: popup,
completer: completer,
keyMap: keyMap,
destroy: destroy,
insertingSuggestion: false,
suggestionInsertedOnce: false
});
}
/**
* Destroy autocompletion on an editor instance.
*/
function destroyAutoCompletion(ctx) {
let { ed } = ctx;
if (!privates.has(ed)) {
return;
}
let {destroy} = privates.get(ed);
destroy();
}
/**
* Provides suggestions to autocomplete the current token/word being typed.
*/
@ -226,7 +262,7 @@ function cycleSuggestions(ed, reverse) {
* onkeydown handler for the editor instance to prevent autocompleting on some
* keypresses.
*/
function onEditorKeypress({ ed, Editor }, event) {
function onEditorKeypress({ ed, Editor }, cm, event) {
let private = privates.get(ed);
// Do not try to autocomplete with multiple selections.
@ -283,7 +319,10 @@ function onEditorKeypress({ ed, Editor }, event) {
* Returns the private popup. This method is used by tests to test the feature.
*/
function getPopup({ ed }) {
return privates.get(ed).popup;
if (privates.has(ed))
return privates.get(ed).popup;
return null;
}
/**
@ -300,6 +339,7 @@ function getInfoAt({ ed }, caret) {
// Export functions
module.exports.setupAutoCompletion = setupAutoCompletion;
module.exports.initializeAutoCompletion = initializeAutoCompletion;
module.exports.destroyAutoCompletion = destroyAutoCompletion;
module.exports.getAutocompletionPopup = getPopup;
module.exports.getInfoAt = getInfoAt;

View File

@ -12,6 +12,7 @@ const TAB_SIZE = "devtools.editor.tabsize";
const EXPAND_TAB = "devtools.editor.expandtab";
const KEYMAP = "devtools.editor.keymap";
const AUTO_CLOSE = "devtools.editor.autoclosebrackets";
const AUTOCOMPLETE = "devtools.editor.autocomplete";
const DETECT_INDENT = "devtools.editor.detectindentation";
const DETECT_INDENT_MAX_LINES = 500;
const L10N_BUNDLE = "chrome://browser/locale/devtools/sourceeditor.properties";
@ -98,9 +99,7 @@ const CM_MAPPING = [
"clearHistory",
"openDialog",
"refresh",
"getScrollInfo",
"getOption",
"setOption"
"getScrollInfo"
];
const { cssProperties, cssValues, cssColors } = getCSSKeywords();
@ -360,10 +359,17 @@ Editor.prototype = {
/**
* Changes the value of a currently used highlighting mode.
* See Editor.modes for the list of all suppoert modes.
* See Editor.modes for the list of all supported modes.
*/
setMode: function (value) {
this.setOption("mode", value);
// If autocomplete was set up and the mode is changing, then
// turn it off and back on again so the proper mode can be used.
if (this.config.autocomplete) {
this.setOption("autocomplete", false);
this.setOption("autocomplete", true);
}
},
/**
@ -865,16 +871,54 @@ Editor.prototype = {
cm.refresh();
},
/**
* Sets an option for the editor. For most options it just defers to
* CodeMirror.setOption, but certain ones are maintained within the editor
* instance.
*/
setOption: function(o, v) {
let cm = editors.get(this);
if (o === "autocomplete") {
this.config.autocomplete = v;
this.setupAutoCompletion();
} else {
cm.setOption(o, v);
}
},
/**
* Gets an option for the editor. For most options it just defers to
* CodeMirror.getOption, but certain ones are maintained within the editor
* instance.
*/
getOption: function(o) {
let cm = editors.get(this);
if (o === "autocomplete") {
return this.config.autocomplete;
} else {
return cm.getOption(o);
}
},
/**
* Sets up autocompletion for the editor. Lazily imports the required
* dependencies because they vary by editor mode.
*
* Autocompletion is special, because we don't want to automatically use
* it just because it is preffed on (it still needs to be requested by the
* editor), but we do want to always disable it if it is preffed off.
*/
setupAutoCompletion: function (options = {}) {
if (this.config.autocomplete) {
// The autocomplete module will overwrite this.initializeAutoCompletion
// with a mode specific autocompletion handler.
if (!this.initializeAutoCompletion) {
this.extend(require("./autocomplete"));
// The autocomplete module will overwrite this.setupAutoCompletion with
// a mode specific autocompletion handler.
this.setupAutoCompletion(options);
}
if (this.config.autocomplete && Services.prefs.getBoolPref(AUTOCOMPLETE)) {
this.initializeAutoCompletion(options);
} else {
this.destroyAutoCompletion();
}
},

View File

@ -20,6 +20,8 @@ support-files =
vimemacs.html
head.js
[browser_editor_autocomplete_basic.js]
[browser_editor_autocomplete_js.js]
[browser_editor_basic.js]
[browser_editor_cursor.js]
[browser_editor_goto_line.js]

View File

@ -0,0 +1,62 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const AUTOCOMPLETION_PREF = "devtools.editor.autocomplete";
// Test to make sure that different autocompletion modes can be created,
// switched, and destroyed. This doesn't test the actual autocompletion
// popups, only their integration with the editor.
function test() {
waitForExplicitFinish();
setup((ed, win) => {
let edWin = ed.container.contentWindow.wrappedJSObject;
testJS(ed, edWin);
testCSS(ed, edWin);
testPref(ed, edWin);
teardown(ed, win);
});
}
function testJS(ed, win) {
ok (!ed.getOption("autocomplete"), "Autocompletion is not set");
ok (!win.tern, "Tern is not defined on the window");
ed.setMode(Editor.modes.js);
ed.setOption("autocomplete", true);
ok (ed.getOption("autocomplete"), "Autocompletion is set");
ok (win.tern, "Tern is defined on the window");
}
function testCSS(ed, win) {
ok (ed.getOption("autocomplete"), "Autocompletion is set");
ok (win.tern, "Tern is currently defined on the window");
ed.setMode(Editor.modes.css);
ed.setOption("autocomplete", true);
ok (ed.getOption("autocomplete"), "Autocompletion is still set");
ok (!win.tern, "Tern is no longer defined on the window");
}
function testPref(ed, win) {
ed.setMode(Editor.modes.js);
ed.setOption("autocomplete", true);
ok (ed.getOption("autocomplete"), "Autocompletion is set");
ok (win.tern, "Tern is defined on the window");
info ("Preffing autocompletion off");
Services.prefs.setBoolPref(AUTOCOMPLETION_PREF, false);
ed.setupAutoCompletion();
ok (ed.getOption("autocomplete"), "Autocompletion is still set");
ok (!win.tern, "Tern is no longer defined on the window");
Services.prefs.clearUserPref(AUTOCOMPLETION_PREF);
}

View File

@ -0,0 +1,44 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test to make sure that JS autocompletion is opening popups.
function test() {
waitForExplicitFinish();
setup((ed, win) => {
let edWin = ed.container.contentWindow.wrappedJSObject;
testJS(ed, edWin).then(() => {
teardown(ed, win);
});
});
}
function testJS(ed, win) {
ok (!ed.getOption("autocomplete"), "Autocompletion is not set");
ok (!win.tern, "Tern is not defined on the window");
ed.setMode(Editor.modes.js);
ed.setOption("autocomplete", true);
ok (ed.getOption("autocomplete"), "Autocompletion is set");
ok (win.tern, "Tern is defined on the window");
ed.focus();
ed.setText("document.");
ed.setCursor({line: 0, ch: 9});
let waitForSuggestion = promise.defer();
ed.on("before-suggest", () => {
info("before-suggest has been triggered");
EventUtils.synthesizeKey("VK_ESCAPE", { }, win);
waitForSuggestion.resolve();
});
let autocompleteKey = Editor.keyFor("autocompletion", { noaccel: true }).toUpperCase();
EventUtils.synthesizeKey("VK_" + autocompleteKey, { ctrlKey: true }, win);
return waitForSuggestion.promise;
}

View File

@ -7,6 +7,7 @@
const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const { require } = devtools;
const Editor = require("devtools/sourceeditor/editor");
const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
gDevTools.testing = true;
SimpleTest.registerCleanupFunction(() => {

View File

@ -190,7 +190,9 @@ function testAutocompletionDisabled() {
function testEditorAddedDisabled(panel) {
info("Editor added, getting the source editor and starting tests");
panel.UI.editors[0].getSourceEditor().then(editor => {
ok(!editor.sourceEditor.getAutocompletionPopup,
is(editor.sourceEditor.getOption("autocomplete"), false,
"Autocompletion option does not exist");
ok(!editor.sourceEditor.getAutocompletionPopup(),
"Autocompletion popup does not exist");
cleanup();
});

View File

@ -3626,7 +3626,6 @@ notification[value="translation"] {
}
button.translate-infobar-element {
background: linear-gradient(rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.1)) repeat scroll 0% 0% padding-box transparent;
color: #333333;
border: 1px solid;
border-color: rgba(23, 51, 78, 0.15) rgba(23, 51, 78, 0.17) rgba(23, 51, 78, 0.2);
@ -3650,7 +3649,6 @@ label.translate-infobar-element {
}
button.translate-infobar-element:hover {
background: #f0f0f0;
box-shadow: 0 1px 0 hsla(0,0%,100%,.1) inset, 0 0 0 1px hsla(0,0%,100%,.05) inset, 0 1px 0 hsla(210,54%,20%,.01), 0 0 4px hsla(206,100%,20%,.1);
}
@ -3671,6 +3669,14 @@ button.translate-infobar-element[anonid="translate"]:hover {
background-image: linear-gradient(#66bdff, #0d9eff);
}
button.translate-infobar-element[anonid="notNow"] {
background: linear-gradient(rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.1)) repeat scroll 0% 0% padding-box transparent;
}
button.translate-infobar-element[anonid="notNow"]:hover {
background: #f0f0f0;
}
button.translate-infobar-element.options-menu-button {
-moz-padding-start: 0.5em !important;
-moz-padding-end: 0.3em !important;

View File

@ -92,7 +92,7 @@
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true"/>
#ifdef GOOGLE_PLAY_SERVICES
#ifdef MOZ_NATIVE_DEVICES
<!-- This resources comes from Google Play Services. Required for casting support. -->
<meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />
#endif

View File

@ -163,6 +163,13 @@ public class AppConstants {
false;
#endif
public static final boolean MOZ_MEDIA_PLAYER =
#ifdef MOZ_NATIVE_DEVICES
true;
#else
false;
#endif
// Official corresponds, roughly, to whether this build is performed on
// Mozilla's continuous integration infrastructure. You should disable
// developer-only functionality when this flag is set.

View File

@ -7,6 +7,8 @@ package org.mozilla.gecko;
import java.io.File;
import java.io.FileNotFoundException;
import java.lang.Class;
import java.lang.reflect.Method;
import java.net.URLEncoder;
import java.util.EnumSet;
import java.util.List;
@ -547,6 +549,20 @@ public class BrowserApp extends GeckoApp
"Updater:Launch");
Distribution.init(this);
// Shipping Native casting is optional and dependent on whether you've downloaded the support
// and google play libraries
if (AppConstants.MOZ_MEDIA_PLAYER) {
try {
Class<?> mediaManagerClass = Class.forName("org.mozilla.gecko.MediaPlayerManager");
Method init = mediaManagerClass.getMethod("init", Context.class);
init.invoke(null, this);
} catch(Exception ex) {
// Ignore failures
Log.i(LOGTAG, "No native casting support", ex);
}
}
JavaAddonManager.getInstance().init(getApplicationContext());
mSharedPreferencesHelper = new SharedPreferencesHelper(getApplicationContext());
mOrderedBroadcastHelper = new OrderedBroadcastHelper(getApplicationContext());
@ -582,8 +598,32 @@ public class BrowserApp extends GeckoApp
// Set the maximum bits-per-pixel the favicon system cares about.
IconDirectoryEntry.setMaxBPP(GeckoAppShell.getScreenDepth());
Class<?> mediaManagerClass = getMediaPlayerManager();
if (mediaManagerClass != null) {
try {
Method init = mediaManagerClass.getMethod("init", Context.class);
init.invoke(null, this);
} catch(Exception ex) {
Log.i(LOGTAG, "Error initializing media manager", ex);
}
}
}
private Class<?> getMediaPlayerManager() {
if (AppConstants.MOZ_MEDIA_PLAYER) {
try {
return Class.forName("org.mozilla.gecko.MediaPlayerManager");
} catch(Exception ex) {
// Ignore failures
Log.i(LOGTAG, "No native casting support", ex);
}
}
return null;
}
@Override
public void onBackPressed() {
if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
@ -925,6 +965,16 @@ public class BrowserApp extends GeckoApp
}
}
Class<?> mediaManagerClass = getMediaPlayerManager();
if (mediaManagerClass != null) {
try {
Method destroy = mediaManagerClass.getMethod("onDestroy", (Class[]) null);
destroy.invoke(null);
} catch(Exception ex) {
Log.i(LOGTAG, "Error destroying media manager", ex);
}
}
super.onDestroy();
}

View File

@ -205,7 +205,7 @@ public class BrowserLocaleManager implements LocaleManager {
*
* If we're currently mirroring the system locale, this method returns the
* supplied configuration's locale, unless the current activity locale is
* correct. , If we're not currently mirroring, this methodupdates the
* correct. If we're not currently mirroring, this method updates the
* configuration object to match the user's currently selected locale, and
* returns that, unless the current activity locale is correct.
*

View File

@ -0,0 +1,279 @@
/* 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;
import java.io.IOException;
import org.mozilla.gecko.util.EventCallback;
import org.json.JSONObject;
import org.json.JSONException;
import com.google.android.gms.cast.Cast;
import com.google.android.gms.cast.Cast.ApplicationConnectionResult;
import com.google.android.gms.cast.CastDevice;
import com.google.android.gms.cast.CastMediaControlIntent;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaMetadata;
import com.google.android.gms.cast.MediaStatus;
import com.google.android.gms.cast.RemoteMediaPlayer;
import com.google.android.gms.cast.RemoteMediaPlayer.MediaChannelResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.common.api.Status;
import android.content.Context;
import android.os.Bundle;
import android.support.v7.media.MediaRouter.RouteInfo;
import android.util.Log;
/* Implementation of GeckoMediaPlayer for talking to ChromeCast devices */
class ChromeCast implements GeckoMediaPlayer {
private static final boolean SHOW_DEBUG = false;
private final Context context;
private final RouteInfo route;
private GoogleApiClient apiClient;
private RemoteMediaPlayer remoteMediaPlayer;
// Callback to start playback of a url on a remote device
private class VideoPlayCallback implements ResultCallback<ApplicationConnectionResult>,
RemoteMediaPlayer.OnStatusUpdatedListener,
RemoteMediaPlayer.OnMetadataUpdatedListener {
private final String url;
private final String type;
private final String title;
private final EventCallback callback;
public VideoPlayCallback(String url, String type, String title, EventCallback callback) {
this.url = url;
this.type = type;
this.title = title;
this.callback = callback;
}
@Override
public void onStatusUpdated() {
MediaStatus mediaStatus = remoteMediaPlayer.getMediaStatus();
boolean isPlaying = mediaStatus.getPlayerState() == MediaStatus.PLAYER_STATE_PLAYING;
// TODO: Do we want to shutdown when there are errors?
if (mediaStatus.getPlayerState() == MediaStatus.PLAYER_STATE_IDLE &&
mediaStatus.getIdleReason() == MediaStatus.IDLE_REASON_FINISHED) {
stop(null);
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Casting:Stop", null));
}
}
@Override
public void onMetadataUpdated() {
MediaInfo mediaInfo = remoteMediaPlayer.getMediaInfo();
MediaMetadata metadata = mediaInfo.getMetadata();
debug("metadata updated " + metadata);
}
@Override
public void onResult(ApplicationConnectionResult result) {
Status status = result.getStatus();
debug("ApplicationConnectionResultCallback.onResult: statusCode" + status.getStatusCode());
if (status.isSuccess()) {
remoteMediaPlayer = new RemoteMediaPlayer();
remoteMediaPlayer.setOnStatusUpdatedListener(this);
remoteMediaPlayer.setOnMetadataUpdatedListener(this);
try {
Cast.CastApi.setMessageReceivedCallbacks(apiClient, remoteMediaPlayer.getNamespace(), remoteMediaPlayer);
} catch (IOException e) {
debug("Exception while creating media channel", e);
}
startPlayback();
} else {
callback.sendError(null);
}
}
private void startPlayback() {
MediaMetadata mediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
mediaMetadata.putString(MediaMetadata.KEY_TITLE, title);
MediaInfo mediaInfo = new MediaInfo.Builder(url)
.setContentType(type)
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.setMetadata(mediaMetadata)
.build();
try {
remoteMediaPlayer.load(apiClient, mediaInfo, true).setResultCallback(new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() {
@Override
public void onResult(MediaChannelResult result) {
if (result.getStatus().isSuccess()) {
callback.sendSuccess(null);
debug("Media loaded successfully");
return;
}
debug("Media load failed " + result.getStatus());
callback.sendError(null);
}
});
return;
} catch (IllegalStateException e) {
debug("Problem occurred with media during loading", e);
} catch (Exception e) {
debug("Problem opening media during loading", e);
}
callback.sendError(null);
}
}
public ChromeCast(Context context, RouteInfo route) {
this.context = context;
this.route = route;
}
// This dumps everything we can find about the device into JSON. This will hopefully make it
// easier to filter out duplicate devices from different sources in js.
public JSONObject toJSON() {
final JSONObject obj = new JSONObject();
try {
final CastDevice device = CastDevice.getFromBundle(route.getExtras());
obj.put("uuid", route.getId());
obj.put("version", device.getDeviceVersion());
obj.put("friendlyName", device.getFriendlyName());
obj.put("location", device.getIpAddress().toString());
obj.put("modelName", device.getModelName());
// For now we just assume all of these are Google devices
obj.put("manufacturer", "Google Inc.");
} catch(JSONException ex) {
debug("Error building route", ex);
}
return obj;
}
public void load(final String title, final String url, final String type, final EventCallback callback) {
final CastDevice device = CastDevice.getFromBundle(route.getExtras());
Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions.builder(device, new Cast.Listener() {
@Override
public void onApplicationStatusChanged() { }
@Override
public void onVolumeChanged() { }
@Override
public void onApplicationDisconnected(int errorCode) { }
});
apiClient = new GoogleApiClient.Builder(context)
.addApi(Cast.API, apiOptionsBuilder.build())
.addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
@Override
public void onConnected(Bundle connectionHint) {
if (!apiClient.isConnected()) {
return;
}
// Launch the media player app and launch this url once its loaded
try {
Cast.CastApi.launchApplication(apiClient, CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID, true)
.setResultCallback(new VideoPlayCallback(url, type, title, callback));
} catch (Exception e) {
debug("Failed to launch application", e);
}
}
@Override
public void onConnectionSuspended(int cause) {
debug("suspended");
}
}).build();
apiClient.connect();
}
public void start(final EventCallback callback) {
// Nothing to be done here
callback.sendSuccess(null);
}
public void stop(final EventCallback callback) {
// Nothing to be done here
callback.sendSuccess(null);
}
public void play(final EventCallback callback) {
remoteMediaPlayer.play(apiClient).setResultCallback(new ResultCallback<MediaChannelResult>() {
@Override
public void onResult(MediaChannelResult result) {
Status status = result.getStatus();
if (!status.isSuccess()) {
debug("Unable to toggle pause: " + status.getStatusCode());
callback.sendError(null);
} else {
callback.sendSuccess(null);
}
}
});
}
public void pause(final EventCallback callback) {
remoteMediaPlayer.pause(apiClient).setResultCallback(new ResultCallback<MediaChannelResult>() {
@Override
public void onResult(MediaChannelResult result) {
Status status = result.getStatus();
if (!status.isSuccess()) {
debug("Unable to toggle pause: " + status.getStatusCode());
callback.sendError(null);
} else {
callback.sendSuccess(null);
}
}
});
}
public void end(final EventCallback callback) {
Cast.CastApi.stopApplication(apiClient).setResultCallback(new ResultCallback<Status>() {
@Override
public void onResult(Status result) {
if (result.isSuccess()) {
try {
Cast.CastApi.removeMessageReceivedCallbacks(apiClient, remoteMediaPlayer.getNamespace());
remoteMediaPlayer = null;
apiClient.disconnect();
apiClient = null;
if (callback != null) {
callback.sendSuccess(null);
}
return;
} catch(Exception ex) {
debug("Error ending", ex);
}
}
if (callback != null) {
callback.sendError(null);
}
}
});
}
private static final String LOGTAG = "GeckoChromeCast";
private void debug(String msg, Exception e) {
if (SHOW_DEBUG) {
Log.e(LOGTAG, msg, e);
}
}
private void debug(String msg) {
if (SHOW_DEBUG) {
Log.d(LOGTAG, msg);
}
}
}

View File

@ -175,7 +175,7 @@ public abstract class GeckoApp
protected RelativeLayout mGeckoLayout;
private View mCameraView;
private OrientationEventListener mCameraOrientationEventListener;
public List<GeckoAppShell.AppStateListener> mAppStateListeners;
public List<GeckoAppShell.AppStateListener> mAppStateListeners = new LinkedList<GeckoAppShell.AppStateListener>();
protected MenuPanel mMenuPanel;
protected Menu mMenu;
protected GeckoProfile mProfile;

View File

@ -280,7 +280,7 @@ all_resources = \
# $(1): zip file to add to (or create).
# $(2): directory to zip contents of.
define zip_directory_with_relative_paths
cd $(2) && zip -q $(1) -r * -x $(not_android_res_files)
cd $(2) && zip -q $(1) -r * -x $(subst *,\\*,$(not_android_res_files))
endef

View File

@ -20,7 +20,7 @@ import android.widget.RelativeLayout;
import android.widget.TextView;
public class MediaCastingBar extends RelativeLayout implements View.OnClickListener, GeckoEventListener {
private static final String LOGTAG = "MediaCastingBar";
private static final String LOGTAG = "GeckoMediaCastingBar";
private TextView mCastingTo;
private ImageButton mMediaPlay;

View File

@ -0,0 +1,231 @@
/* 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;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.NativeEventListener;
import org.mozilla.gecko.util.NativeJSObject;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONException;
import android.content.Context;
import android.support.v7.media.MediaControlIntent;
import android.support.v7.media.MediaRouteSelector;
import android.support.v7.media.MediaRouter;
import android.support.v7.media.MediaRouter.RouteInfo;
import android.util.Log;
import java.util.HashMap;
/* Wraper for different MediaRouter types supproted by Android. i.e. Chromecast, Miracast, etc. */
interface GeckoMediaPlayer {
public JSONObject toJSON();
public void load(String title, String url, String type, EventCallback callback);
public void play(EventCallback callback);
public void pause(EventCallback callback);
public void stop(EventCallback callback);
public void start(EventCallback callback);
public void end(EventCallback callback);
}
/* Manages a list of GeckoMediaPlayers methods (i.e. Chromecast/Miracast). Routes messages
* from Gecko to the correct caster based on the id of the display
*/
class MediaPlayerManager implements NativeEventListener,
GeckoAppShell.AppStateListener {
private static final String LOGTAG = "GeckoMediaPlayerManager";
private static final boolean SHOW_DEBUG = false;
// Simplified debugging interfaces
private static void debug(String msg, Exception e) {
if (SHOW_DEBUG) {
Log.e(LOGTAG, msg, e);
}
}
private static void debug(String msg) {
if (SHOW_DEBUG) {
Log.d(LOGTAG, msg);
}
}
private final Context context;
private final MediaRouter mediaRouter;
private final HashMap<String, GeckoMediaPlayer> displays = new HashMap<String, GeckoMediaPlayer>();
private static MediaPlayerManager instance;
public static void init(Context context) {
if (instance != null) {
debug("MediaPlayerManager initialized twice");
}
instance = new MediaPlayerManager(context);
}
private MediaPlayerManager(Context context) {
this.context = context;
if (context instanceof GeckoApp) {
GeckoApp app = (GeckoApp) context;
app.addAppStateListener(this);
}
mediaRouter = MediaRouter.getInstance(context);
EventDispatcher.getInstance().registerGeckoThreadListener(this, "MediaPlayer:Load",
"MediaPlayer:Start",
"MediaPlayer:Stop",
"MediaPlayer:Play",
"MediaPlayer:Pause",
"MediaPlayer:Get",
"MediaPlayer:End");
}
public static void onDestroy() {
if (instance == null) {
return;
}
EventDispatcher.getInstance().unregisterGeckoThreadListener(instance, "MediaPlayer:Load",
"MediaPlayer:Start",
"MediaPlayer:Stop",
"MediaPlayer:Play",
"MediaPlayer:Pause",
"MediaPlayer:Get",
"MediaPlayer:End");
if (instance.context instanceof GeckoApp) {
GeckoApp app = (GeckoApp) instance.context;
app.removeAppStateListener(instance);
}
}
// GeckoEventListener implementation
@Override
public void handleMessage(String event, final NativeJSObject message, final EventCallback callback) {
debug(event);
if ("MediaPlayer:Get".equals(event)) {
final JSONObject result = new JSONObject();
final JSONArray disps = new JSONArray();
for (GeckoMediaPlayer disp : displays.values()) {
try {
disps.put(disp.toJSON());
} catch(Exception ex) {
// This may happen if the device isn't a real Chromecast,
// for example Firefly casting devices.
Log.e(LOGTAG, "Couldn't create JSON for display", ex);
}
}
try {
result.put("displays", disps);
} catch(JSONException ex) {
Log.i(LOGTAG, "Error sending displays", ex);
}
callback.sendSuccess(result);
return;
}
final GeckoMediaPlayer display = displays.get(message.getString("id"));
if (display == null) {
Log.e(LOGTAG, "Couldn't find a display for this id");
callback.sendError(null);
return;
}
if ("MediaPlayer:Play".equals(event)) {
display.play(callback);
} else if ("MediaPlayer:Start".equals(event)) {
display.start(callback);
} else if ("MediaPlayer:Stop".equals(event)) {
display.stop(callback);
} else if ("MediaPlayer:Pause".equals(event)) {
display.pause(callback);
} else if ("MediaPlayer:End".equals(event)) {
display.end(callback);
} else if ("MediaPlayer:Load".equals(event)) {
final String url = message.optString("source", "");
final String type = message.optString("type", "video/mp4");
final String title = message.optString("title", "");
display.load(title, url, type, callback);
}
}
private final MediaRouter.Callback callback = new MediaRouter.Callback() {
@Override
public void onRouteRemoved(MediaRouter router, RouteInfo route) {
debug("onRouteRemoved: route=" + route);
displays.remove(route.getId());
}
@SuppressWarnings("unused")
public void onRouteSelected(MediaRouter router, int type, MediaRouter.RouteInfo route) {
}
// These methods aren't used by the support version Media Router
@SuppressWarnings("unused")
public void onRouteUnselected(MediaRouter router, int type, RouteInfo route) {
}
@Override
public void onRoutePresentationDisplayChanged(MediaRouter router, RouteInfo route) {
}
@Override
public void onRouteVolumeChanged(MediaRouter router, RouteInfo route) {
}
@Override
public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo route) {
debug("onRouteAdded: route=" + route);
GeckoMediaPlayer display = getMediaPlayerForRoute(route);
if (display != null) {
displays.put(route.getId(), display);
}
}
@Override
public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) {
debug("onRouteChanged: route=" + route);
GeckoMediaPlayer display = displays.get(route.getId());
if (display != null) {
displays.put(route.getId(), display);
}
}
};
private GeckoMediaPlayer getMediaPlayerForRoute(MediaRouter.RouteInfo route) {
try {
if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
return new ChromeCast(context, route);
}
} catch(Exception ex) {
debug("Error handling presentation", ex);
}
return null;
}
/* Implementing GeckoAppShell.AppStateListener */
@Override
public void onPause() {
mediaRouter.removeCallback(callback);
}
@Override
public void onResume() {
MediaRouteSelector selectorBuilder = new MediaRouteSelector.Builder()
.addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO)
.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
.build();
mediaRouter.addCallback(selectorBuilder, callback, MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
}
@Override
public void onOrientationChanged() { }
}

View File

@ -0,0 +1,95 @@
==================================
Runtime locale switching in Fennec
==================================
`Bug 917480 <https://bugzilla.mozilla.org/show_bug.cgi?id=917480>`_ built on `Bug 936756 <https://bugzilla.mozilla.org/show_bug.cgi?id=936756>`_ to allow users to switch between supported locales at runtime, within Fennec, without altering the system locale.
This document aims to describe the overall architecture of the solution, along with guidelines for Fennec developers.
Overview
========
There are two places that locales are relevant to an Android application: the Java ``Locale`` object and the Android configuration itself.
Locale switching involves manipulating these values (to affect future UI), persisting them for future activities, and selectively redisplaying existing UI elements to give the appearance of responsive switching.
The user's choice of locale is stored in a per-app pref, ``"locale"``. If missing, the system default locale is used. If set, it should be a locale code like ``"es"`` or ``"en-US"``.
``BrowserLocaleManager`` takes care of updating the active locale when asked to do so. It also manages persistence and retrieval of the locale preference.
The question, then, is when to do so.
Locale events
=============
One might imagine that we need only set the locale when our Application is instantiated, and when a new locale is set. Alas, that's not the case: whenever there's a configuration change (*e.g.*, screen rotation), when a new activity is started, and at other apparently random times, Android will supply our activities with a configuration that's been reset to the sytem locale.
For this reason, each starting activity must ask ``BrowserLocaleManager`` to fix its locale.
Ideally, we also need to perform some amount of work when our configuration changes, when our activity is resumed, and perhaps when a result is returned from another activity, if that activity can change the app locale (as is the case for any activity that calls out to ``GeckoPreferences`` -- see ``BrowserApp#onActivityResult``).
``GeckoApp`` itself does some additional work, because it has particular performance constraints, and also is the typical root of the preferences activity.
Here's an example of the work that a typical activity should do::
// This is cribbed from o.m.g.sync.setup.activities.LocaleAware.
public static void initializeLocale(Context context) {
final LocaleManager localeManager = BrowserLocaleManager.getInstance();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
localeManager.getAndApplyPersistedLocale(context);
} else {
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
StrictMode.allowThreadDiskWrites();
try {
localeManager.getAndApplyPersistedLocale(context);
} finally {
StrictMode.setThreadPolicy(savedPolicy);
}
}
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
final LocaleManager localeManager = BrowserLocaleManager.getInstance();
final Locale changed = localeManager.onSystemConfigurationChanged(this, getResources(), newConfig, mLastLocale);
if (changed != null) {
// Redisplay to match the locale.
onLocaleChanged(BrowserLocaleManager.getLanguageTag(changed));
}
}
@Override
public void onCreate(Bundle icicle) {
// Note that we don't do this in onResume. We should,
// but it's an edge case that we feel free to ignore.
// We also don't have a hook in this example for when
// the user picks a new locale.
initializeLocale(this);
super.onCreate(icicle);
}
``GeckoApplication`` itself handles correcting locales when the configuration changes; your activity shouldn't need to do this itself. See ``GeckoApplication``'s and ``GeckoApp``'s ``onConfigurationChanged`` methods.
System locale changes
=====================
Fennec can be in one of two states.
If the user has not explicitly chosen a Fennec-specific locale, we say
we are "mirroring" the system locale.
When we are not mirroring, system locale changes do not impact Fennec
and are essentially ignored; the user's locale selection is the only
thing we care about, and we actively correct incoming configuration
changes to reflect the user's chosen locale.
By contrast, when we are mirroring, system locale changes cause Fennec
to reflect the new system locale, as if the user picked the new locale.
When the system locale changes when we're mirroring, your activity will receive an ``onConfigurationChanged`` call. Simply pass this on to ``BrowserLocaleManager``, and then handle the response appropriately.
Further reference
=================
``GeckoPreferences``, ``GeckoApp``, and ``BrowserApp`` are excellent resources for figuring out what you should do.

View File

@ -303,20 +303,26 @@ public class RecentTabsPanel extends HomeFragment
RecentTabs.TYPE });
if (closedTabs != null && closedTabs.length > 0) {
addRow(c, null, context.getString(R.string.home_closed_tabs_title), RecentTabs.TYPE_HEADER);
// How many closed tabs are actually displayed.
int visibleClosedTabs = 0;
final int length = closedTabs.length;
for (int i = 0; i < length; i++) {
final String url = closedTabs[i].url;
// Don't show recent tabs for about:home
// Don't show recent tabs for about:home.
if (!AboutPages.isAboutHome(url)) {
// If this is the first closed tab we're adding, add a header for the section.
if (visibleClosedTabs == 0) {
addRow(c, null, context.getString(R.string.home_closed_tabs_title), RecentTabs.TYPE_HEADER);
}
addRow(c, url, closedTabs[i].title, RecentTabs.TYPE_CLOSED);
visibleClosedTabs++;
}
}
// Add an "Open all" button if more than 2 tabs were added to the list.
if (length > 1) {
if (visibleClosedTabs > 1) {
addRow(c, null, null, RecentTabs.TYPE_OPEN_ALL_CLOSED);
}
}

View File

@ -374,7 +374,7 @@ size. -->
<!ENTITY home_move_up_to_filter "Up to &formatS;">
<!ENTITY private_browsing_title "Private Browsing">
<!ENTITY private_tabs_panel_description "Your private tabs will show up here. While we don\'t keep any of your browsing history or cookies, bookmarks and files that you download will still be saved on your device.">
<!ENTITY private_tabs_panel_empty_desc "Your private tabs will show up here. While we don\'t keep any of your browsing history or cookies, bookmarks and files that you download will still be saved on your device.">
<!ENTITY private_tabs_panel_learn_more "Want to learn more?">
<!ENTITY pin_site_dialog_hint "Enter a search keyword">

View File

@ -19,7 +19,6 @@ resjar.generated_sources += [
if CONFIG['MOZ_NATIVE_DEVICES']:
resjar.generated_sources += ['com/google/android/gms/R.java']
DEFINES["GOOGLE_PLAY_SERVICES"] = 1
resjar.generated_sources += ['android/support/v7/appcompat/R.java']
resjar.generated_sources += ['android/support/v7/mediarouter/R.java']
@ -477,6 +476,8 @@ if CONFIG['MOZ_NATIVE_DEVICES']:
gbjar.extra_jars += [CONFIG['ANDROID_APPCOMPAT_LIB']]
gbjar.extra_jars += [CONFIG['ANDROID_MEDIAROUTER_LIB']]
gbjar.extra_jars += [CONFIG['GOOGLE_PLAY_SERVICES_LIB']]
gbjar.sources += ['ChromeCast.java']
gbjar.sources += ['MediaPlayerManager.java']
gbjar.javac_flags += ['-Xlint:all,-deprecation,-fallthrough']
@ -528,7 +529,7 @@ ANDROID_GENERATED_RESFILES += [
]
for var in ('MOZ_ANDROID_ANR_REPORTER', 'MOZ_LINKER_EXTRACT', 'MOZILLA_OFFICIAL', 'MOZ_DEBUG',
'MOZ_ANDROID_SEARCH_ACTIVITY'):
'MOZ_ANDROID_SEARCH_ACTIVITY', 'MOZ_NATIVE_DEVICES'):
if CONFIG[var]:
DEFINES[var] = 1

View File

@ -30,7 +30,7 @@
<TextView style="@style/TabsPanelItem.TextAppearance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/private_tabs_panel_description"/>
android:text="@string/private_tabs_panel_empty_desc"/>
</LinearLayout>

View File

@ -333,7 +333,7 @@
<string name="home_default_empty">&home_default_empty;</string>
<string name="home_move_up_to_filter">&home_move_up_to_filter;</string>
<string name="private_browsing_title">&private_browsing_title;</string>
<string name="private_tabs_panel_description">&private_tabs_panel_description;</string>
<string name="private_tabs_panel_empty_desc">&private_tabs_panel_empty_desc;</string>
<string name="private_tabs_panel_learn_more">&private_tabs_panel_learn_more;</string>
<!-- https://support.mozilla.org/%LOCALE%/kb/mobile-private-browsing-browse-web-without-saving-syncing-info -->
<string name="private_tabs_panel_learn_more_link">https://support.mozilla.org/&formatS1;/kb/mobile-private-browsing-browse-web-without-saving-syncing-info</string>

View File

@ -25,6 +25,14 @@ var fireflyTarget = {
}
};
var mediaPlayerTarget = {
target: "media:router",
factory: function(aService) {
Cu.import("resource://gre/modules/MediaPlayerApp.jsm");
return new MediaPlayerApp(aService);
}
};
var CastingApps = {
_castMenuId: -1,
@ -36,6 +44,7 @@ var CastingApps = {
// Register targets
SimpleServiceDiscovery.registerTarget(rokuTarget);
SimpleServiceDiscovery.registerTarget(fireflyTarget);
SimpleServiceDiscovery.registerTarget(mediaPlayerTarget);
// Search for devices continuously every 120 seconds
SimpleServiceDiscovery.search(120 * 1000);

View File

@ -0,0 +1,106 @@
// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["MediaPlayerApp"];
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Messaging.jsm");
let log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.d.bind(null, "MediaPlayerApp");
// Helper function for sending commands to Java.
function send(type, data, callback) {
let msg = {
type: type
};
for (let i in data) {
msg[i] = data[i];
}
sendMessageToJava(msg, callback);
}
/* These apps represent players supported natively by the platform. This class will proxy commands
* to native controls */
function MediaPlayerApp(service) {
this.service = service;
this.location = service.location;
this.id = service.uuid;
}
MediaPlayerApp.prototype = {
start: function start(callback) {
send("MediaPlayer:Start", { id: this.id }, (result) => {
if (callback) callback(true);
});
},
stop: function stop(callback) {
send("MediaPlayer:Stop", { id: this.id }, (result) => {
if (callback) callback(true);
});
},
remoteMedia: function remoteMedia(callback, listener) {
if (callback) {
callback(new RemoteMedia(this.id, listener));
}
},
}
/* RemoteMedia provides a proxy to a native media player session.
*/
function RemoteMedia(id, listener) {
this._id = id;
this._listener = listener;
if ("onRemoteMediaStart" in this._listener) {
Services.tm.mainThread.dispatch((function() {
this._listener.onRemoteMediaStart(this);
}).bind(this), Ci.nsIThread.DISPATCH_NORMAL);
}
}
RemoteMedia.prototype = {
shutdown: function shutdown() {
this._send("MediaPlayer:End", {}, (result) => {
this._status = "shutdown";
if ("onRemoteMediaStop" in this._listener) {
this._listener.onRemoteMediaStop(this);
}
});
},
play: function play() {
this._send("MediaPlayer:Play", {}, (result) => {
this._status = "started";
});
},
pause: function pause() {
this._send("MediaPlayer:Pause", {}, (result) => {
this._status = "paused";
});
},
load: function load(aData) {
this._send("MediaPlayer:Load", aData, (result) => {
this._status = "started";
})
},
get status() {
return this._status;
},
_send: function(msg, data, callback) {
data.id = this._id;
send(msg, data, callback);
}
}

View File

@ -11,6 +11,7 @@ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Messaging.jsm");
// Define the "log" function as a binding of the Log.d function so it specifies
// the "debug" priority and a log tag.
@ -169,6 +170,29 @@ var SimpleServiceDiscovery = {
log("failed to convert to byte array: " + e);
}
}
// We also query Java directly here for any devices that Android might support natively (i.e. Chromecast or Miracast)
this.getAndroidDevices();
},
getAndroidDevices: function() {
sendMessageToJava({ type: "MediaPlayer:Get" }, (result) => {
for (let id in result.displays) {
let display = result.displays[id];
// Convert the native data into something matching what is created in _processService()
let service = {
location: display.location,
target: "media:router",
friendlyName: display.friendlyName,
uuid: display.uuid,
manufacturer: display.manufacturer,
modelName: display.modelName
};
this._addService(service);
}
})
},
_searchFixedTargets: function _searchFixedTargets() {
@ -313,22 +337,26 @@ var SimpleServiceDiscovery = {
aService.manufacturer = doc.querySelector("manufacturer").textContent;
aService.modelName = doc.querySelector("modelName").textContent;
// Filter out services that do not match the target filter
if (!this._filterService(aService)) {
return;
}
// Only add and notify if we don't already know about this service
if (!this._services.has(aService.uuid)) {
this._services.set(aService.uuid, aService);
Services.obs.notifyObservers(null, EVENT_SERVICE_FOUND, aService.uuid);
}
// Make sure we remember this service is not stale
this._services.get(aService.uuid).lastPing = this._searchTimestamp;
this._addService(aService);
}
}).bind(this), false);
xhr.send(null);
},
_addService: function(service) {
// Filter out services that do not match the target filter
if (!this._filterService(service)) {
return;
}
// Only add and notify if we don't already know about this service
if (!this._services.has(service.uuid)) {
this._services.set(service.uuid, service);
Services.obs.notifyObservers(null, EVENT_SERVICE_FOUND, service.uuid);
}
// Make sure we remember this service is not stale
this._services.get(service.uuid).lastPing = this._searchTimestamp;
}
}

View File

@ -15,6 +15,7 @@ EXTRA_JS_MODULES += [
'HomeProvider.jsm',
'JNI.jsm',
'LightweightThemeConsumer.jsm',
'MediaPlayerApp.jsm',
'Messaging.jsm',
'Notifications.jsm',
'OrderedBroadcast.jsm',

View File

@ -106,6 +106,8 @@ exports.items = [
let width;
let height;
let div = document.createElementNS("http://www.w3.org/1999/xhtml", "div");
let currentX = window.scrollX;
let currentY = window.scrollY;
if (!fullpage) {
if (!node) {
@ -122,6 +124,7 @@ exports.items = [
height = rect.height;
}
} else {
window.scrollTo(0,0);
width = window.innerWidth + window.scrollMaxX;
height = window.innerHeight + window.scrollMaxY;
}
@ -132,6 +135,10 @@ exports.items = [
ctx.drawWindow(window, left, top, width, height, "#fff");
let data = canvas.toDataURL("image/png", "");
if(fullpage) {
window.scrollTo(currentX, currentY);
}
let loadContext = document.defaultView
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)

View File

@ -318,6 +318,7 @@ let CallWatcherActor = exports.CallWatcherActor = protocol.ActorClass({
return;
}
this._initialized = false;
this._finalized = true;
this._contentObserver.stopListening();
off(this._contentObserver, "global-created", this._onGlobalCreated);
@ -534,6 +535,11 @@ let CallWatcherActor = exports.CallWatcherActor = protocol.ActorClass({
* Invoked whenever an instrumented function is called.
*/
_onContentFunctionCall: function(...details) {
// If the consuming tool has finalized call-watcher, ignore the
// still-instrumented calls.
if (this._finalized) {
return;
}
let functionCall = new FunctionCallActor(this.conn, details, this._holdWeak);
this._functionCalls.push(functionCall);
this.onCall(functionCall);