Bug 953381 - Basic media casting control bar r=wesj
authorMark Finkle <mfinkle@mozilla.com>
Mon, 20 Jan 2014 17:26:30 -0500
changeset 164381 febe9b4bf753f9eaf3d2aba54c21e7c909ee92e8
parent 164380 2c992bf97aa115031253cd16dd835707b84db067
child 164382 148382c24e1cd062d8a037f441cf9e9cc6dcfa4b
push id26043
push usercbook@mozilla.com
push dateTue, 21 Jan 2014 12:12:44 +0000
treeherdermozilla-central@15ed780a0674 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerswesj
bugs953381
milestone29.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 953381 - Basic media casting control bar r=wesj
CLOBBER
mobile/android/base/BrowserApp.java
mobile/android/base/MediaCastingBar.java
mobile/android/base/locales/en-US/android_strings.dtd
mobile/android/base/moz.build
mobile/android/base/resources/drawable-hdpi/media_bar_pause.png
mobile/android/base/resources/drawable-hdpi/media_bar_play.png
mobile/android/base/resources/drawable-hdpi/media_bar_stop.png
mobile/android/base/resources/drawable-mdpi/media_bar_pause.png
mobile/android/base/resources/drawable-mdpi/media_bar_play.png
mobile/android/base/resources/drawable-mdpi/media_bar_stop.png
mobile/android/base/resources/drawable-xhdpi/media_bar_pause.png
mobile/android/base/resources/drawable-xhdpi/media_bar_play.png
mobile/android/base/resources/drawable-xhdpi/media_bar_stop.png
mobile/android/base/resources/layout/gecko_app.xml
mobile/android/base/resources/layout/media_casting.xml
mobile/android/base/strings.xml.in
mobile/android/chrome/content/CastingApps.js
--- a/CLOBBER
+++ b/CLOBBER
@@ -17,9 +17,9 @@
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
 # Are you updating CLOBBER because you think it's needed for your WebIDL
 # changes to stick? As of bug 928195, this shouldn't be necessary! Please
 # don't change CLOBBER for WebIDL changes any more.
 
-Bug 917896 requires a clobber due to bug 961339.
+Bug 953381 requires a clobber due to bug 961339.
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -151,16 +151,17 @@ abstract public class BrowserApp extends
         @Override
         public float getInterpolation(float t) {
             t -= 1.0f;
             return t * t * t * t * t + 1.0f;
         }
     };
 
     private FindInPageBar mFindInPageBar;
+    private MediaCastingBar mMediaCastingBar;
 
     private boolean mAccessibilityEnabled = false;
 
     // We'll ask for feedback after the user launches the app this many times.
     private static final int FEEDBACK_LAUNCH_COUNT = 15;
 
     // Whether the dynamic toolbar pref is enabled.
     private boolean mDynamicToolbarEnabled = false;
@@ -525,16 +526,17 @@ abstract public class BrowserApp extends
         mBrowserToolbar.setOnKeyListener(this);
 
         if (mTabsPanel != null) {
             mTabsPanel.setTabsLayoutChangeListener(this);
             updateSideBarState();
         }
 
         mFindInPageBar = (FindInPageBar) findViewById(R.id.find_in_page);
+        mMediaCastingBar = (MediaCastingBar) findViewById(R.id.media_casting);
 
         registerEventListener("CharEncoding:Data");
         registerEventListener("CharEncoding:State");
         registerEventListener("Feedback:LastUrl");
         registerEventListener("Feedback:OpenPlayStore");
         registerEventListener("Feedback:MaybeLater");
         registerEventListener("Telemetry:Gather");
         registerEventListener("Settings:Show");
@@ -825,16 +827,21 @@ abstract public class BrowserApp extends
         if (mBrowserToolbar != null)
             mBrowserToolbar.onDestroy();
 
         if (mFindInPageBar != null) {
             mFindInPageBar.onDestroy();
             mFindInPageBar = null;
         }
 
+        if (mMediaCastingBar != null) {
+            mMediaCastingBar.onDestroy();
+            mMediaCastingBar = null;
+        }
+
         if (mSharedPreferencesHelper != null) {
             mSharedPreferencesHelper.uninit();
             mSharedPreferencesHelper = null;
         }
 
         if (mOrderedBroadcastHelper != null) {
             mOrderedBroadcastHelper.uninit();
             mOrderedBroadcastHelper = null;
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/MediaCastingBar.java
@@ -0,0 +1,118 @@
+/* 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.GeckoEventListener;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import org.json.JSONObject;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+public class MediaCastingBar extends RelativeLayout implements View.OnClickListener, GeckoEventListener  {
+    private static final String LOGTAG = "MediaCastingBar";
+
+    private TextView mCastingTo;
+    private ImageButton mMediaPlay;
+    private ImageButton mMediaPause;
+    private ImageButton mMediaStop;
+
+    private boolean mInflated = false;
+
+    public MediaCastingBar(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        GeckoAppShell.getEventDispatcher().registerEventListener("Casting:Started", this);
+        GeckoAppShell.getEventDispatcher().registerEventListener("Casting:Stopped", this);
+    }
+
+    public void inflateContent() {
+        LayoutInflater inflater = LayoutInflater.from(getContext());
+        View content = inflater.inflate(R.layout.media_casting, this);
+
+        mMediaPlay = (ImageButton) content.findViewById(R.id.media_play);
+        mMediaPlay.setOnClickListener(this);
+        mMediaPause = (ImageButton) content.findViewById(R.id.media_pause);
+        mMediaPause.setOnClickListener(this);
+        mMediaStop = (ImageButton) content.findViewById(R.id.media_stop);
+        mMediaStop.setOnClickListener(this);
+
+        mCastingTo = (TextView) content.findViewById(R.id.media_casting_to);
+
+        // Capture clicks on the rest of the view to prevent them from
+        // leaking into other views positioned below.
+        content.setOnClickListener(this);
+
+        mInflated = true;
+    }
+
+    public void show() {
+        if (!mInflated)
+            inflateContent();
+
+        setVisibility(VISIBLE);
+    }
+
+    public void hide() {
+        setVisibility(GONE);
+    }
+
+    public void onDestroy() {
+        GeckoAppShell.getEventDispatcher().unregisterEventListener("Casting:Started", this);
+        GeckoAppShell.getEventDispatcher().unregisterEventListener("Casting:Stopped", this);
+    }
+
+    // View.OnClickListener implementation
+    @Override
+    public void onClick(View v) {
+        final int viewId = v.getId();
+
+        if (viewId == R.id.media_play) {
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Casting:Play", ""));
+            mMediaPlay.setVisibility(GONE);
+            mMediaPause.setVisibility(VISIBLE);
+        } else if (viewId == R.id.media_pause) {
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Casting:Pause", ""));
+            mMediaPause.setVisibility(GONE);
+            mMediaPlay.setVisibility(VISIBLE);
+        } else if (viewId == R.id.media_stop) {
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Casting:Stop", ""));
+        }
+    }
+
+    // GeckoEventListener implementation
+    @Override
+    public void handleMessage(final String event, final JSONObject message) {
+        final String device = message.optString("device");
+
+        ThreadUtils.postToUiThread(new Runnable() {
+            @Override
+            public void run() {
+                if (event.equals("Casting:Started")) {
+                    show();
+                    if (!TextUtils.isEmpty(device)) {
+                        mCastingTo.setText(device);
+                    } else {
+                        // Should not happen
+                        mCastingTo.setText("");
+                        Log.d(LOGTAG, "Device name is empty.");
+                    }
+                    mMediaPlay.setVisibility(GONE);
+                    mMediaPause.setVisibility(VISIBLE);
+                } else if (event.equals("Casting:Stopped")) {
+                    hide();
+                }
+            }
+        });
+    }
+}
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -214,16 +214,23 @@ size. -->
 
 <!-- Localization note (find_text, find_prev, find_next, find_close) : These strings are used
      as alternate text for accessibility. They are not visible in the UI. -->
 <!ENTITY find_text "Find in Page">
 <!ENTITY find_prev "Previous">
 <!ENTITY find_next "Next">
 <!ENTITY find_close "Close">
 
+<!-- Localization note (media_casting_to, media_play, media_pause, media_stop) : These strings are used
+     as alternate text for accessibility. They are not visible in the UI. -->
+<!ENTITY media_casting_to "Casting to Device">
+<!ENTITY media_play "Play">
+<!ENTITY media_pause "Pause">
+<!ENTITY media_stop "Stop">
+
 <!ENTITY contextmenu_open_new_tab "Open in New Tab">
 <!ENTITY contextmenu_open_private_tab "Open in Private Tab">
 <!ENTITY contextmenu_open_in_reader "Open in Reader">
 <!ENTITY contextmenu_remove "Remove">
 <!ENTITY contextmenu_add_to_launcher "Add to Home Screen">
 <!ENTITY contextmenu_share "Share">
 <!ENTITY contextmenu_pasteandgo "Paste &amp; Go">
 <!ENTITY contextmenu_paste "Paste">
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -243,16 +243,17 @@ gbjar.sources += [
     'home/TopSitesPanel.java',
     'home/TopSitesThumbnailView.java',
     'home/TwoLinePageRow.java',
     'InputMethods.java',
     'JavaAddonManager.java',
     'LightweightTheme.java',
     'LightweightThemeDrawable.java',
     'LocaleManager.java',
+    'MediaCastingBar.java',
     'MemoryMonitor.java',
     'menu/GeckoMenu.java',
     'menu/GeckoMenuInflater.java',
     'menu/GeckoMenuItem.java',
     'menu/GeckoSubMenu.java',
     'menu/MenuItemActionBar.java',
     'menu/MenuItemActionView.java',
     'menu/MenuItemDefault.java',
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..5f635fc10aba60c7360e69170c69507a1f4df38c
GIT binary patch
literal 362
zc%17D@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCmSQK*5Dp-y;YjHK@;M7UB8wRq
zL=J;6qiBJz6HrjH#5JNMI6tkVJh3R1p}f3YFEcN@I61K(RWH9NefB#WDWIa~o-U3d
z8I5meI&vK{5OGmYTB-g~>XEHY%id1w{0F8k{-H<GVobOXs!iA>!Sq>y#pus7`&)UF
z9E5Z?2bzD-7Yj&cG(F3m6MjX8QHJX$$Ad}F^2->`WuE)UP_X`0Tguyti9IUI&nTQs
zT$tw(xlV6cdidAP&v;8R`DWTMy%4|ld&*S1+v_IiUixF7z-S6K4G*#TS&3DW-OO`$
z&*y!pHfLZ6K9#ggkD=na*=Dx~ED7-^+!?O7ewBWv{O!`b^S9EP^miSNaOh>dVxs?n
ZQ7LXG$Haem)<7>ac)I$ztaD0e0ssbzf=~be
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ac69ea59a43370a3d777570c7e8d5a882a32cf13
GIT binary patch
literal 670
zc$@*A0%84$P)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm00004b3#c}2nYxW
zd<bNS00009a7bBm000!j000!j0TpmfLI3~&8FWQhbW?9;ba!ELWdL_~cP?peYja~^
zaAhuUa%Y?FJQ@H10vkz0K~!jg?b*+Z%~2S~@z<Rzl!daf@-O(YP$b#0kj9TlvB3g6
zTUjVuODPMnku3`~Op&r-WBgpBP!>WIW$s*yGZ)<%&79|ZzV~}4^R8QWbDrn(>3Qxs
z&pEf%?RG6>Mw%8-l|=(hvuL1cA_ku00+trm5~3B@Mtch;un(__D#|2Mfu4c2c#6?-
za`LJ28)#z%ZsI(a6j78(q=h<WH~tX&@wTd{9ICWXKO6B3JK}QkDQTdMl^DahVC-xn
zE%ej;`u#o*;zMqN97<N8pRLlFu`Q5e-qdNJja9gZ(`W^9%}Jz%eqdQ3*KHie$H3Zu
zGRRqIyR=JfDjBrS7Md;BYCOQHV2l|=TIf5L7s!1BNARUekU>_UpHV!)hAKgtG%$iS
zc!c9sg5*;28KW&7ikEOr8V<8)+5msfheMKyEQZ0r-!Wh0E^K6m@1ryQIf*N|iz5a~
zR-l9T(vCC{3D9&4jbojlc$zWLnT~N5JMpPXutB%bB)&=aggX`D{AQ2_y3+k%58ec0
z%wQ1HXwUFvY*tV_^;cjLQ#gUEfn0M^r-3QF!)|G4tA#pSXbkJsD4z1$Xm5)=gI)L>
z5tT#97V1cslLNRLR+LF816@4EUJLALvG@HVwivK@s#TyPz0w`S^<s)Lsbb)@G)2BF
zrY1n-RpOTPGI11Niz~_`VqmEFFaM<+Di#ei&4M@Z3!9jND^Z?P5&!@I07*qoM6N<$
Ef_wTT0ssI2
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..b1bcd06079dc098921ce1501daf086a839a050ac
GIT binary patch
literal 763
zc$@+I0tEeuP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm00004b3#c}2nYxW
zd<bNS00009a7bBm000!j000!j0TpmfLI3~&8FWQhbW?9;ba!ELWdL_~cP?peYja~^
zaAhuUa%Y?FJQ@H10(eP8K~!jg?U}J^8$lFB@1{$wN}xs}LLU<KLKuSy#^hI27z{29
z217_bC4b;Pq;%mp4*rEmJ}G7gEz3%NZ)T-+ur5qFZ|)iGT<yFq0R7K;*R_FM9RRx4
z0ibKWS%AKzsidMM9|K9fl!++-LrJ%izDxS5e2AH(J4rt!P295q&lmv@z>>AT0EM$o
z9CP3=uqtc*0mhyhp2a=@ziZ~BZW7ENm9<utI!oZOT&q=paap*@eAKnW{Bc?9xuMRo
z+^A7o029JbyUl#mm7(4VVb`0?S0NsNVJ<)qcru!gy4+Illfiry;vVSd01Q|kGLJ4A
zYMnaFS2fK65HL%GIIq+=D~)^2f8&LOvmUxRn-ASKEr8F7=1uWz1rY1l^BN&x&W**q
z7eIvgOL>kx^2D(lK-gk1RhAge1`xhDv}#-YwhkadET$aJCN;(G&H+SvJXTeW+r0yb
z5R<7|<gVYwjck}P_bz+&%pEw-*!}qEo#lAQF@KR^z7BD^Z-C1d%vT{+glOIiz*URp
z>kucq2DsjV`8ve$E&vL`3zpYV3dEA|;@BEML3p+G8hIxCM=TN{=4}8JgiA$=`D#3R
z<90>3M9p#mrU~X(G{>Shb|b_f2cS>5SX(ajmN@jp?~>3^=;Z*2(AubX<2ISM#GoaH
zW0!97Qax)5jo93ki-*n=f7{|XcIQkjM(ctiwEODax!BV*$6jSL#d7S*wcc8cW6Zis
z7Y~^!?ixlto?|!8)KW|Z5aGFe!WeS~h#1rN7+Q(-Q9#mn;+~}Ul73~s&xh{|A0_>e
t^yr?P2=Hq3X1|fW8XW+-)&Zbv{R1#$Nns?;kY)e?002ovPDHLkV1gB0Q;7fo
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..81712cabed27c3aba37a0789a41f0196af80459d
GIT binary patch
literal 252
zc%17D@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzmSQK*5Dp-y;YjHK@;M7UB8!3Q
zuY)k7lg8`{prB-lYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&Kt+|FE{-7)
zt#7Xzay1z6v_9PbYRS5}ReksVn_N10)N#&~T^3Uoy;vafkkv@VW>)^%PQ##0s}@~n
z{lTrk@Y?gY%42<_hrA7}uhxhkP?aoy^Jz{k^M_1Mg$4#D4lr@S=ENC?`ER2VJf3mB
mXwW^~Ti!2SdzM9m_XgvIUy_Znsh%MJF?hQAxvX<aXaWEb+gQT@
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..77f87b387b96f89fb54374c8452fcdc121a05e20
GIT binary patch
literal 463
zc$@*x0WkiFP)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800004b3#c}2nYxW
zd<bNS00009a7bBm000fw000fw0YWI7cmMzZ8FWQhbW?9;ba!ELWdL_~cP?peYja~^
zaAhuUa%Y?FJQ@H10Zd6mK~z|U?bo|5L{S*U@!z09MB*8gXq2K95g|f})=P-m18C@V
zB7#EV0VdHYTneH=Bs7XMhD;d6G0hZapUXHQR=xLL|MRVHUrwoBuPag=g}VO(1Ok+?
z)GmNhKoh>Bie0SW?WX~1==y@V#T0Jx0!Y#W-5A6v=86DljPJuXHpG?5q$CkE>jhVs
z#(i3VEZeXLLpaAo5dhGMe(YgA&3aiPXjTPhn8jl#K%OUIFGg{R@ge|-D>Z;4tfACu
zLY@&ctAb<9e>bSzm(fM50U=+O^u<y~gk|jK0gyyhJYW*nArtb9AQCs<aHx4GKu81;
zR<R&HqJxz+f*PJNgOfB1WNE@kd=JdvDUCVldmzFV#<G}`Jc1AL0keq1EXGLy8WFtW
zPQ1l_{7LqL1B~QpPFMxX*uiF=#tnsBMs3xv_%r(30H3RcRM*L2<_rJ;002ovPDHLk
FV1mZ@xbXl0
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..64e7c9a3c200c669ee311ddc1452453e46c9020c
GIT binary patch
literal 497
zc$@+80S^9&P)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800004b3#c}2nYxW
zd<bNS00009a7bBm000fw000fw0YWI7cmMzZ8FWQhbW?9;ba!ELWdL_~cP?peYja~^
zaAhuUa%Y?FJQ@H10d7e|K~z|U?Uzk%!Y~kphpu~!h^VLsh(M6!{G1_2Xh4K@zAlV~
zq0Wq5Dn*s@B=78bU+m|ftN@u)Z?n2D0RIJ`5|K_s+Kjq|h#W<vF?#}9+W`Yk15g|N
z1y}=LzzAG{gVtXwbhs4=H2`~MOc|JaU2h+f1<*vyLx@<x51@ZC564tF0LO_!UKd~+
z^xr7vUR@4A!Oy9TK~%sea5DN1K$`>b93~CooM2v0`B;fAv8Ps(6<QO(3YVq}O8RpE
ze8{YIel`z7`Z9pv!gfwHk8ZhG%ef2QCGF@QMdX@XHIa0C`?3^`xaHdX)e`J7%tMGC
z7vPrw_S`R{TzQD{w*a+aKH@^D&CZBB%+|R8ZG!m;w==3lh$07|GYa-RhRh1-h`ZpD
z0V?7%poMEUF$Ub#-sAxIg5jIMPo);BSZLK(k+lo_tH=yf&}tr^aq4}Lat<`O^^1mR
nqP4gjnMd*pmS3|4U|xL#Wo(iwv{AY?00000NkvXXu0mjf#~;iT
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..bc3930fcc219c390aab75ba2d659264d19254c05
GIT binary patch
literal 389
zc%17D@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=EX7WqAsj$Z!;#Vf<Z~8yL>4nJ
zNUsNB#yF{oGC)De64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhq!<_&nLJ$_
zLn`LHon^?^<RHQ-AJ_HxMedgGOjUfgQ#Njil$I7&T_cdjYuqV1d;jE49g&iyQ`XGn
z)pbZ_h-x^-u!W(QF@sTx`2v$kG0P2gP38LSDt`~G`L{Lg&7Qv@Kl9qcAKqeouCiPI
zx}WE^sh@Y9`kG(C4OGmwfHk9o!MXj~66Uw!f7qcSHZ0TcZ7}6=;JzTyAhzJ6=7ukA
zYZJlt9GEB9q>I4>+V0=D)I5h5WIr3jw=V)YoM7gR#A8d7-rAlleDKTk<l>}z3zOcu
sKl>5#ZT&|vpfxZH3>kDCHveMqmr>R5lze6;0Sqz*Pgg&ebxsLQ095geRsaA1
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..2ce52f803941cc582a296542acddb3c94c9ccc7b
GIT binary patch
literal 853
zc$@)M1FHOqP)<h;3K|Lk000e1NJLTq002M$002M;1^@s6s%dfF00004b3#c}2nYxW
zd<bNS00009a7bBm000}W000}W0bUxB8~^|S8FWQhbW?9;ba!ELWdL_~cP?peYja~^
zaAhuUa%Y?FJQ@H10@6uDK~#90?b<(Rl~)+Y@lUMP(n`g}$*Dr2LWdL-aVUt0OP91*
zgfvJjROlp5E{=i@4&tJlLzXzGv|U;RrBs4cY(T+91W{5d4o0FvqPY$y7cWtZ$@`r5
zyf5DGk_+Mfp6C2{e&@b_&OoQr@jkPnbOWX78&JA|()0}|-9Tyj29$1~G!X{;fxqy1
zfL84c1Y8Jp1!nLg&Q=wylZXlE8n6cAIEhsOy0s870rMDG_}p3e{5N*vZdLKzL{0#&
zjM$8S@J)bzR-z5)WgULOQG5_kCkqh~c<${B>_*~G?7_cPi$#-j0(cdPuQ7=o0rf)3
zZ9p&UrE7-6L3Kljh`=m9T;g|r#}9Z|wRlzPoPb_-Nxu@?0_s<ylmV~-mvJzlhh-BH
zfoJ%rqQ-fQV!CQGOQk8B(Gad<u<pVoBe0myFoFF+dhbM&41m=*g)w{*sQXr$5`o1G
zOE*-Tau;ga1oZNSWWsPJeKgwu_*ja=X(=AzG&KT?`AOO!H%1FLeFAzJlzu9QB1PL|
z0DOvbIBx0pA8#b$N^n|gtZpXoJ?>W#i7?<1)>ai+CO7eA6@j)zU@;R_1=?!B3mg(~
z*}SrPFSQ%+pY(k3a)el04Cp>wy&`RVQ&GZgiNF9(V7tJ@(~b#vf_*Z*b8NZ+xA5J<
z-z+pa0^`^s<Kk(i0dqJY?esIj+ON^H2uw-4{FOk>|D{P2a7nu3t8MW#!+;KsVJ99`
zZ)a&lL}0$ccOPRfE(G+kEK2SHH}EZP2lTLPN=0A{TNPeB^&2pUpD}``0ktbrX9Vs^
z2dAzE)C?o{1a$v*5npN8;;APBN2K!r#ScTgiJTFbmQI2E7EmJ_xeS<;?&9tQ=w~HI
z1kOlDUmCx7Vhxy;9#W1<r$$;qL<D}7Oc=qn0KHm?crnsy+VXZd8+`*xH&B|s0i_!#
fP2Ygh4V303iw~YqV&68P00000NkvXXu0mjfz*lU%
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f00e43782fcc7b279bcc8dd7ffedbf495c55045b
GIT binary patch
literal 1261
zc$@+41QPp+P)<h;3K|Lk000e1NJLTq002M$002M;1^@s6s%dfF00004b3#c}2nYxW
zd<bNS00009a7bBm000}W000}W0bUxB8~^|S8FWQhbW?9;ba!ELWdL_~cP?peYja~^
zaAhuUa%Y?FJQ@H11Zqh{K~#90?V8JrTtyVde?1ddGGnIa#S9Z>Cn6F=#I^CU%t8<)
zD#1WdaFdOHM79nDg9*{lF(E`EAxqzjVVI0DCcbsZN|XpjP~64&T%0cK?&-&^s?$?Q
z`h$XL=H7G8_uZ;n=bkz>N!-mCD;+>-j07khKxvEwC>=m)j07khKxvEwC>=m)cnL58
zG=NLMvXjkTI0766W?ZaE${mxER`nllO4{RKx7%20@pE0$bgs?ZfbNsDrtR32wB$&<
zCh1ty_Ew9ZYm(}v1lTQUy$kt(pCm0hkw4yre85#nQ#&odl%$P5<O6<?w2(>uL?7}2
z8?pzoBLdWAs#nE#lI9cqj7d6GfqcNaY)KAPfN9yfi^VsR<|4_TjvybfCVMFlMSvOE
zn>Q6-Nt&(TXIxUFc~1=afK^Eo75qdCa5N#9P7K<*#wDF?UYkNbpwY)i+lWboehsNZ
zM%u9#_z<Xf`QI+!EO4Odqn6=kvhtNxm1>TVh~zvSY5Uf-OVR}=+czaGt0JdZ0Z{jZ
zBn4sXx<}H*Oj`k)ip)Z5QuK_j0dE4Yl0Q|i0Jnhs88!kofqlTwF*l_M07m*P;4t|j
zc;H8j{4cRLrhW|ycm;SHBcJnP%srU+9(bGt@{m3Q0>&^A4pL?&d<#5*Eu$pl)&sVH
zL%<pGXF@E##-@ETdGINSYymF==h!(BZeUOTw_HQHOQ03wSR(VBm%$$R5_k&u-NTq?
zP4FM!Mc_S0o1ORqcoHY_;9nHIgQc?XJKF{L9DDM&J&g{dDtrfc9=P26?XkwL08ayd
zIM`~#u*%~10`twkbGF7H-YwwotcZGG7y?WI9{`Uz*b3t92hL+(jIQt&ppJd+KI&{2
z;2CVIbrr!=fN9_(;1Nfgop=^_7u#}O;3dE;@G<bPlf7OLmS;3@62U`&IgI>X2U~*>
zGR*_PJ2=FjYhWJu1b8SnkO%}3Ucx@avN+`2ya0R(JeXl4HAe^{90X31vDKyuu!xa=
zAk}dbIXOZQ;Sg4wNwSq9z!LBoaDR%!4qV2vyi^1c4r67ycoAX>h|PlIrc(j)qO2gg
zTh-1P`ymu)k14Xq8*}lptmNzw=ml9RJlLL6+<9X^+bbH90GFHsJg;{0Vbqm&?%2)8
zkuLVLPlo=CUSBOM4O^e*fETbY#!lSEK6h_q_zCZTh3b)2p8*qUZ-t3{Dh=7$-ylD;
zvV+uAT$df>4&`KMQr%l(dLfWMEj#kA>Ss=Nq#lcFitHPT6#!WiLci9SPV%QB$%ip7
zYp$q@t4in>N)Z4J-ns($6S6j!R6h%{Hk(1%P-34@YGA1M))mMf&m<qlqO9Sn7uJ>7
zACxNq+P-zQCL|u@!&p-H1Z+(SeL<cAAbaVww$4N)A`xSc+7q!=MfL-A8(s$^Gr;Ru
z7s(qQ276!`Siy3V*MPrr4SJpo{Vy@<7B3w@X^aFY9YAS}1SlOqX^aFY9YAT^^#c40
X@w81DKldN}00000NkvXXu0mjfR9Gk5
--- a/mobile/android/base/resources/layout/gecko_app.xml
+++ b/mobile/android/base/resources/layout/gecko_app.xml
@@ -43,16 +43,23 @@
 
         <org.mozilla.gecko.FindInPageBar android:id="@+id/find_in_page"
                                          android:layout_width="fill_parent"
                                          android:layout_height="wrap_content"
                                          android:layout_alignParentBottom="true"
                                          style="@style/FindBar"
                                          android:visibility="gone"/>
 
+        <org.mozilla.gecko.MediaCastingBar android:id="@+id/media_casting"
+                                           android:layout_width="fill_parent"
+                                           android:layout_height="wrap_content"
+                                           android:layout_alignParentBottom="true"
+                                           style="@style/FindBar"
+                                           android:visibility="gone"/>
+
         <RelativeLayout android:id="@+id/camera_layout"
                         android:layout_height="wrap_content"
                         android:layout_width="wrap_content"
                         android:layout_alignParentRight="true"
                         android:layout_alignParentBottom="true">
         </RelativeLayout>
 
         <FrameLayout android:id="@+id/search_container"
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/media_casting.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <RelativeLayout android:id="@+id/media_controls"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_centerInParent="true">
+
+        <ImageButton android:id="@+id/media_play"
+                     style="@style/FindBar.ImageButton"
+                     android:contentDescription="@string/media_play"
+                     android:src="@drawable/media_bar_play"
+                     android:visibility="gone"/>
+
+        <ImageButton android:id="@+id/media_pause"
+                     style="@style/FindBar.ImageButton"
+                     android:contentDescription="@string/media_pause"
+                     android:src="@drawable/media_bar_pause"/>
+
+    </RelativeLayout>
+
+    <TextView android:id="@+id/media_casting_to"
+              android:layout_width="wrap_content"
+              android:layout_height="wrap_content"
+              android:layout_marginLeft="5dip"
+              android:layout_marginRight="5dip"
+              android:layout_alignParentLeft="true"
+              android:layout_toLeftOf="@id/media_controls"
+              android:layout_centerVertical="true"
+              android:singleLine="true"
+              android:ellipsize="end"
+              android:textColor="#FFFFFFFF"
+              android:contentDescription="@string/media_casting_to"/>
+
+    <ImageButton android:id="@+id/media_stop"
+                 style="@style/FindBar.ImageButton"
+                 android:contentDescription="@string/media_stop"
+                 android:layout_alignParentRight="true"
+                 android:src="@drawable/media_bar_stop"/>
+
+</merge>
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -78,16 +78,21 @@
   <string name="desktop_mode">&desktop_mode;</string>
   <string name="tools">&tools;</string>
 
   <string name="find_text">&find_text;</string>
   <string name="find_prev">&find_prev;</string>
   <string name="find_next">&find_next;</string>
   <string name="find_close">&find_close;</string>
 
+  <string name="media_casting_to">&media_casting_to;</string>
+  <string name="media_play">&media_play;</string>
+  <string name="media_pause">&media_pause;</string>
+  <string name="media_stop">&media_stop;</string>
+
   <string name="settings">&settings;</string>
   <string name="settings_title">&settings_title;</string>
   <string name="pref_category_advanced">&pref_category_advanced;</string>
   <string name="pref_category_customize">&pref_category_customize;</string>
   <string name="pref_category_search">&pref_category_search2;</string>
   <string name="pref_category_display">&pref_category_display;</string>
   <string name="pref_category_privacy_short">&pref_category_privacy_short;</string>
   <string name="pref_category_vendor">&pref_category_vendor;</string>
--- a/mobile/android/chrome/content/CastingApps.js
+++ b/mobile/android/chrome/content/CastingApps.js
@@ -14,26 +14,54 @@ var CastingApps = {
     // Search for devices continuously every 120 seconds
     SimpleServiceDiscovery.search(120 * 1000);
 
     this._castMenuId = NativeWindow.contextmenus.add(
       Strings.browser.GetStringFromName("contextmenu.castToScreen"),
       this.filterCast,
       this.openExternal.bind(this)
     );
+
+    Services.obs.addObserver(this, "Casting:Play", false);
+    Services.obs.addObserver(this, "Casting:Pause", false);
+    Services.obs.addObserver(this, "Casting:Stop", false);
   },
 
   uninit: function ca_uninit() {
+    Services.obs.removeObserver(this, "Casting:Play");
+    Services.obs.removeObserver(this, "Casting:Pause");
+    Services.obs.removeObserver(this, "Casting:Stop");
+
     NativeWindow.contextmenus.remove(this._castMenuId);
   },
 
   isEnabled: function isEnabled() {
     return Services.prefs.getBoolPref("browser.casting.enabled");
   },
 
+  observe: function (aSubject, aTopic, aData) {
+    switch (aTopic) {
+      case "Casting:Play":
+        if (this.session && this.session.remoteMedia.status == "paused") {
+          this.session.remoteMedia.play();
+        }
+        break;
+      case "Casting:Pause":
+        if (this.session && this.session.remoteMedia.status == "started") {
+          this.session.remoteMedia.pause();
+        }
+        break;
+      case "Casting:Stop":
+        if (this.session) {
+          this.closeExternal();
+        }
+        break;
+    }
+  },
+
   makeURI: function makeURI(aURL, aOriginCharset, aBaseURI) {
     return Services.io.newURI(aURL, aOriginCharset, aBaseURI);
   },
 
   getVideo: function(aElement, aX, aY) {
     // Fast path: Is the given element a video element
     let video = this._getVideo(aElement);
     if (video) {
@@ -187,19 +215,21 @@ var CastingApps = {
 
   // RemoteMedia callback API methods
   onRemoteMediaStart: function(aRemoteMedia) {
     if (!this.session) {
       return;
     }
 
     aRemoteMedia.load(this.session.data);
+    sendMessageToJava({ type: "Casting:Started", device: this.session.service.friendlyName });
   },
 
   onRemoteMediaStop: function(aRemoteMedia) {
+    sendMessageToJava({ type: "Casting:Stopped" });
   },
 
   onRemoteMediaStatus: function(aRemoteMedia) {
     if (!this.session) {
       return;
     }
 
     let status = aRemoteMedia.status;