mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
merge fx-team to mozilla-central a=merge
--HG-- extra : amend_source : d62f5ddf7f62ae7c9aad4d22253f53a255ea06fa
This commit is contained in:
commit
8386a5e5a4
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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]
|
||||
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
@ -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(() => {
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
*
|
||||
|
279
mobile/android/base/ChromeCast.java
Normal file
279
mobile/android/base/ChromeCast.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
@ -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
|
||||
|
||||
|
@ -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;
|
||||
|
231
mobile/android/base/MediaPlayerManager.java
Normal file
231
mobile/android/base/MediaPlayerManager.java
Normal 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() { }
|
||||
|
||||
}
|
95
mobile/android/base/docs/localeswitching.rst
Normal file
95
mobile/android/base/docs/localeswitching.rst
Normal 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.
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
106
mobile/android/modules/MediaPlayerApp.jsm
Normal file
106
mobile/android/modules/MediaPlayerApp.jsm
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ EXTRA_JS_MODULES += [
|
||||
'HomeProvider.jsm',
|
||||
'JNI.jsm',
|
||||
'LightweightThemeConsumer.jsm',
|
||||
'MediaPlayerApp.jsm',
|
||||
'Messaging.jsm',
|
||||
'Notifications.jsm',
|
||||
'OrderedBroadcast.jsm',
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user