From 7fbbb4c677c034a0dd805ce16733b76bbbb5d99f Mon Sep 17 00:00:00 2001 From: Michael Comella Date: Mon, 30 Jun 2014 12:49:20 -0700 Subject: [PATCH 01/11] Bug 1029989 - Update empty private tabs page entity name. r=wesj --- mobile/android/base/locales/en-US/android_strings.dtd | 2 +- mobile/android/base/resources/layout/private_tabs_panel.xml | 2 +- mobile/android/base/strings.xml.in | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mobile/android/base/locales/en-US/android_strings.dtd b/mobile/android/base/locales/en-US/android_strings.dtd index fd90cfe5407..c19d137b30f 100644 --- a/mobile/android/base/locales/en-US/android_strings.dtd +++ b/mobile/android/base/locales/en-US/android_strings.dtd @@ -374,7 +374,7 @@ size. --> - + diff --git a/mobile/android/base/resources/layout/private_tabs_panel.xml b/mobile/android/base/resources/layout/private_tabs_panel.xml index 4acb548dd75..82c01d1e8ad 100644 --- a/mobile/android/base/resources/layout/private_tabs_panel.xml +++ b/mobile/android/base/resources/layout/private_tabs_panel.xml @@ -30,7 +30,7 @@ + android:text="@string/private_tabs_panel_empty_desc"/> diff --git a/mobile/android/base/strings.xml.in b/mobile/android/base/strings.xml.in index 400f0e14698..95421b772d5 100644 --- a/mobile/android/base/strings.xml.in +++ b/mobile/android/base/strings.xml.in @@ -333,7 +333,7 @@ &home_default_empty; &home_move_up_to_filter; &private_browsing_title; - &private_tabs_panel_description; + &private_tabs_panel_empty_desc; &private_tabs_panel_learn_more; https://support.mozilla.org/&formatS1;/kb/mobile-private-browsing-browse-web-without-saving-syncing-info From fb6150dfa80f1517e866171a4d5586ea7a46178e Mon Sep 17 00:00:00 2001 From: Margaret Leibovic Date: Fri, 27 Jun 2014 16:49:53 -0700 Subject: [PATCH 02/11] Bug 1030736 - Don't include ignored about:home tabs in count to determine whether or not to show "Open all" button. r=liuche --- mobile/android/base/home/RecentTabsPanel.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/mobile/android/base/home/RecentTabsPanel.java b/mobile/android/base/home/RecentTabsPanel.java index 9d93f58d823..0b5441b605d 100644 --- a/mobile/android/base/home/RecentTabsPanel.java +++ b/mobile/android/base/home/RecentTabsPanel.java @@ -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); } } From 588e73de21e8eff98028f51cb415d073741eeae7 Mon Sep 17 00:00:00 2001 From: Richard Newman Date: Mon, 30 Jun 2014 15:22:48 -0700 Subject: [PATCH 03/11] Bug 917480 - Developer docs for Fennec locale switching. DONTBUILD, r=doc-only --- mobile/android/base/BrowserLocaleManager.java | 2 +- mobile/android/base/docs/localeswitching.rst | 95 +++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 mobile/android/base/docs/localeswitching.rst diff --git a/mobile/android/base/BrowserLocaleManager.java b/mobile/android/base/BrowserLocaleManager.java index eff725fde47..13b6de95628 100644 --- a/mobile/android/base/BrowserLocaleManager.java +++ b/mobile/android/base/BrowserLocaleManager.java @@ -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. * diff --git a/mobile/android/base/docs/localeswitching.rst b/mobile/android/base/docs/localeswitching.rst new file mode 100644 index 00000000000..3f1f60876e3 --- /dev/null +++ b/mobile/android/base/docs/localeswitching.rst @@ -0,0 +1,95 @@ +================================== +Runtime locale switching in Fennec +================================== + +`Bug 917480 `_ built on `Bug 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. From 8603228c15c06d92cd5989155ef2e44e9f2b1769 Mon Sep 17 00:00:00 2001 From: Steven MacLeod Date: Fri, 27 Jun 2014 15:10:08 -0400 Subject: [PATCH 04/11] Bug 1028942 - Fix 'Translate' button border on OSX. r=florian --HG-- extra : rebase_source : 722788a20a14adb593fbdbcbe269cda7f93cf61d --- browser/themes/osx/browser.css | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/browser/themes/osx/browser.css b/browser/themes/osx/browser.css index aeaa2b5d5dc..5fbf4ad6ec3 100644 --- a/browser/themes/osx/browser.css +++ b/browser/themes/osx/browser.css @@ -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; From a629a2c81ff1a04e615a51d6c56d8de7c6b36442 Mon Sep 17 00:00:00 2001 From: Wes Johnston Date: Mon, 23 Jun 2014 00:23:00 -0700 Subject: [PATCH 05/11] Bug 901803 - Add a native media player backend for casting videos. r=mfinkle --- mobile/android/base/AndroidManifest.xml.in | 2 +- mobile/android/base/AppConstants.java.in | 7 + mobile/android/base/BrowserApp.java | 50 +++++ mobile/android/base/GeckoApp.java | 2 +- mobile/android/base/MediaCastingBar.java | 2 +- mobile/android/base/MediaPlayerManager.java | 217 ++++++++++++++++++++ mobile/android/base/moz.build | 4 +- 7 files changed, 279 insertions(+), 5 deletions(-) create mode 100644 mobile/android/base/MediaPlayerManager.java diff --git a/mobile/android/base/AndroidManifest.xml.in b/mobile/android/base/AndroidManifest.xml.in index 8ca6e8341e5..da37375c0b9 100644 --- a/mobile/android/base/AndroidManifest.xml.in +++ b/mobile/android/base/AndroidManifest.xml.in @@ -92,7 +92,7 @@ -#ifdef GOOGLE_PLAY_SERVICES +#ifdef MOZ_NATIVE_DEVICES #endif diff --git a/mobile/android/base/AppConstants.java.in b/mobile/android/base/AppConstants.java.in index 1c7e6b7a4e7..8aca4537d6b 100644 --- a/mobile/android/base/AppConstants.java.in +++ b/mobile/android/base/AppConstants.java.in @@ -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. diff --git a/mobile/android/base/BrowserApp.java b/mobile/android/base/BrowserApp.java index bf94c5ad21c..b83e7280e1a 100644 --- a/mobile/android/base/BrowserApp.java +++ b/mobile/android/base/BrowserApp.java @@ -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(); } diff --git a/mobile/android/base/GeckoApp.java b/mobile/android/base/GeckoApp.java index c97a7ccf0c5..d2e8e7e32a8 100644 --- a/mobile/android/base/GeckoApp.java +++ b/mobile/android/base/GeckoApp.java @@ -175,7 +175,7 @@ public abstract class GeckoApp protected RelativeLayout mGeckoLayout; private View mCameraView; private OrientationEventListener mCameraOrientationEventListener; - public List mAppStateListeners; + public List mAppStateListeners = new LinkedList(); protected MenuPanel mMenuPanel; protected Menu mMenu; protected GeckoProfile mProfile; diff --git a/mobile/android/base/MediaCastingBar.java b/mobile/android/base/MediaCastingBar.java index b06a8cbeeb1..67ca3f8bbac 100644 --- a/mobile/android/base/MediaCastingBar.java +++ b/mobile/android/base/MediaCastingBar.java @@ -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; diff --git a/mobile/android/base/MediaPlayerManager.java b/mobile/android/base/MediaPlayerManager.java new file mode 100644 index 00000000000..4b435cb1230 --- /dev/null +++ b/mobile/android/base/MediaPlayerManager.java @@ -0,0 +1,217 @@ +/* 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 displays = new HashMap(); + 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()) { + disps.put(disp.toJSON()); + } + + 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) { + 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() { } + +} diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build index 808eed4ae16..1da25583c93 100644 --- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -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,7 @@ 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 += ['MediaPlayerManager.java'] gbjar.javac_flags += ['-Xlint:all,-deprecation,-fallthrough'] @@ -528,7 +528,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 From 640d8780d176f49a5b6e9f01d34621cf95007e9e Mon Sep 17 00:00:00 2001 From: Wes Johnston Date: Mon, 23 Jun 2014 00:23:00 -0700 Subject: [PATCH 06/11] Bug 901803 - Add a native Chromecast backend on Android. r=mfinkle --- mobile/android/base/ChromeCast.java | 279 ++++++++++++++++++++ mobile/android/base/MediaPlayerManager.java | 16 +- mobile/android/base/moz.build | 1 + 3 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 mobile/android/base/ChromeCast.java diff --git a/mobile/android/base/ChromeCast.java b/mobile/android/base/ChromeCast.java new file mode 100644 index 00000000000..9e0a1b6ce81 --- /dev/null +++ b/mobile/android/base/ChromeCast.java @@ -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, + 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() { + @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() { + @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() { + @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() { + @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); + } + } + +} diff --git a/mobile/android/base/MediaPlayerManager.java b/mobile/android/base/MediaPlayerManager.java index 4b435cb1230..878d0c1f76d 100644 --- a/mobile/android/base/MediaPlayerManager.java +++ b/mobile/android/base/MediaPlayerManager.java @@ -111,7 +111,13 @@ class MediaPlayerManager implements NativeEventListener, final JSONObject result = new JSONObject(); final JSONArray disps = new JSONArray(); for (GeckoMediaPlayer disp : displays.values()) { - disps.put(disp.toJSON()); + 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 { @@ -193,6 +199,14 @@ class MediaPlayerManager implements NativeEventListener, }; 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; } diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build index 1da25583c93..9e1ec684446 100644 --- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -476,6 +476,7 @@ 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'] From d7855765092703daf01e89970773b4ccf88c0ad9 Mon Sep 17 00:00:00 2001 From: Wes Johnston Date: Tue, 24 Jun 2014 16:52:00 -0700 Subject: [PATCH 07/11] Bug 901803 - Add a JS component for casting to native devices. r=mfinkle --- mobile/android/chrome/content/CastingApps.js | 9 ++ mobile/android/modules/MediaPlayerApp.jsm | 106 ++++++++++++++++++ .../modules/SimpleServiceDiscovery.jsm | 54 ++++++--- mobile/android/modules/moz.build | 1 + 4 files changed, 157 insertions(+), 13 deletions(-) create mode 100644 mobile/android/modules/MediaPlayerApp.jsm diff --git a/mobile/android/chrome/content/CastingApps.js b/mobile/android/chrome/content/CastingApps.js index 187982cade7..421bec4f3d4 100644 --- a/mobile/android/chrome/content/CastingApps.js +++ b/mobile/android/chrome/content/CastingApps.js @@ -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); diff --git a/mobile/android/modules/MediaPlayerApp.jsm b/mobile/android/modules/MediaPlayerApp.jsm new file mode 100644 index 00000000000..1b17c21cb38 --- /dev/null +++ b/mobile/android/modules/MediaPlayerApp.jsm @@ -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); + } +} diff --git a/mobile/android/modules/SimpleServiceDiscovery.jsm b/mobile/android/modules/SimpleServiceDiscovery.jsm index 75ab0ab714c..bdbc503a766 100644 --- a/mobile/android/modules/SimpleServiceDiscovery.jsm +++ b/mobile/android/modules/SimpleServiceDiscovery.jsm @@ -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; } } diff --git a/mobile/android/modules/moz.build b/mobile/android/modules/moz.build index f7225e7bb1a..f9b983bca2a 100644 --- a/mobile/android/modules/moz.build +++ b/mobile/android/modules/moz.build @@ -15,6 +15,7 @@ EXTRA_JS_MODULES += [ 'HomeProvider.jsm', 'JNI.jsm', 'LightweightThemeConsumer.jsm', + 'MediaPlayerApp.jsm', 'Messaging.jsm', 'Notifications.jsm', 'OrderedBroadcast.jsm', From a27a6bd01e3fbe54402380ddb31b6b3f7f4e2df6 Mon Sep 17 00:00:00 2001 From: Chris Kitching Date: Tue, 1 Jul 2014 06:53:16 +0100 Subject: [PATCH 08/11] Bug 1032615: Prevent wildcard expansion from breaking builds using zsh. r=nalexander --- mobile/android/base/Makefile.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/android/base/Makefile.in b/mobile/android/base/Makefile.in index d4802e865af..99c9dc54c4c 100644 --- a/mobile/android/base/Makefile.in +++ b/mobile/android/base/Makefile.in @@ -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 From fdd8936fd0fc6107835995adce6590b6045db25a Mon Sep 17 00:00:00 2001 From: Brian Grinstead Date: Mon, 30 Jun 2014 11:23:00 +0200 Subject: [PATCH 09/11] Bug 1029511 - Source Editor: Add ability to toggle autocomplete option. r=vporof --- browser/app/profile/firefox.js | 1 + browser/devtools/shared/autocomplete-popup.js | 3 +- browser/devtools/sourceeditor/autocomplete.js | 74 ++++++++++++++----- browser/devtools/sourceeditor/editor.js | 60 +++++++++++++-- .../devtools/sourceeditor/test/browser.ini | 2 + .../test/browser_editor_autocomplete_basic.js | 62 ++++++++++++++++ .../test/browser_editor_autocomplete_js.js | 44 +++++++++++ browser/devtools/sourceeditor/test/head.js | 1 + .../test/browser_styleeditor_autocomplete.js | 4 +- 9 files changed, 224 insertions(+), 27 deletions(-) create mode 100644 browser/devtools/sourceeditor/test/browser_editor_autocomplete_basic.js create mode 100644 browser/devtools/sourceeditor/test/browser_editor_autocomplete_js.js diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 7d877b66062..c037aa9933b 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -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); diff --git a/browser/devtools/shared/autocomplete-popup.js b/browser/devtools/shared/autocomplete-popup.js index cd296d766f9..1eb1a30aaec 100644 --- a/browser/devtools/shared/autocomplete-popup.js +++ b/browser/devtools/shared/autocomplete-popup.js @@ -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; diff --git a/browser/devtools/sourceeditor/autocomplete.js b/browser/devtools/sourceeditor/autocomplete.js index 919be8ea9a8..f9d330c3ee1 100644 --- a/browser/devtools/sourceeditor/autocomplete.js +++ b/browser/devtools/sourceeditor/autocomplete.js @@ -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; diff --git a/browser/devtools/sourceeditor/editor.js b/browser/devtools/sourceeditor/editor.js index 21bacd2e5a6..6bc200b9e5c 100644 --- a/browser/devtools/sourceeditor/editor.js +++ b/browser/devtools/sourceeditor/editor.js @@ -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(); } }, diff --git a/browser/devtools/sourceeditor/test/browser.ini b/browser/devtools/sourceeditor/test/browser.ini index 002c0257696..d5f6040555d 100644 --- a/browser/devtools/sourceeditor/test/browser.ini +++ b/browser/devtools/sourceeditor/test/browser.ini @@ -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] diff --git a/browser/devtools/sourceeditor/test/browser_editor_autocomplete_basic.js b/browser/devtools/sourceeditor/test/browser_editor_autocomplete_basic.js new file mode 100644 index 00000000000..5d211ea7bee --- /dev/null +++ b/browser/devtools/sourceeditor/test/browser_editor_autocomplete_basic.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); +} diff --git a/browser/devtools/sourceeditor/test/browser_editor_autocomplete_js.js b/browser/devtools/sourceeditor/test/browser_editor_autocomplete_js.js new file mode 100644 index 00000000000..d683b017e2b --- /dev/null +++ b/browser/devtools/sourceeditor/test/browser_editor_autocomplete_js.js @@ -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; +} diff --git a/browser/devtools/sourceeditor/test/head.js b/browser/devtools/sourceeditor/test/head.js index f0053c028a1..c4dc5890dcf 100644 --- a/browser/devtools/sourceeditor/test/head.js +++ b/browser/devtools/sourceeditor/test/head.js @@ -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(() => { diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_autocomplete.js b/browser/devtools/styleeditor/test/browser_styleeditor_autocomplete.js index 7fd8723de4a..39467f01dae 100644 --- a/browser/devtools/styleeditor/test/browser_styleeditor_autocomplete.js +++ b/browser/devtools/styleeditor/test/browser_styleeditor_autocomplete.js @@ -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(); }); From bf48aefb60a1dd6f8e85ae62675efb59f7576ce1 Mon Sep 17 00:00:00 2001 From: Jordan Santell Date: Mon, 30 Jun 2014 11:42:00 +0200 Subject: [PATCH 10/11] Bug 1029738 - Halt firing instrumented function calls after destruction in CallWatcherActor. r=vp --- toolkit/devtools/server/actors/call-watcher.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/toolkit/devtools/server/actors/call-watcher.js b/toolkit/devtools/server/actors/call-watcher.js index ffbac194714..98999c75aec 100644 --- a/toolkit/devtools/server/actors/call-watcher.js +++ b/toolkit/devtools/server/actors/call-watcher.js @@ -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); From 1672a2683c53c791518f1f0092d03c906b5939af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Blin?= Date: Mon, 30 Jun 2014 17:46:20 +0200 Subject: [PATCH 11/11] Bug 961832 - GCLI screenshot shows fixed position element in wrong position. r=pbrosset. --- toolkit/devtools/gcli/commands/screenshot.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/toolkit/devtools/gcli/commands/screenshot.js b/toolkit/devtools/gcli/commands/screenshot.js index 86854955cb3..290ec9477a5 100644 --- a/toolkit/devtools/gcli/commands/screenshot.js +++ b/toolkit/devtools/gcli/commands/screenshot.js @@ -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)