Bug 953381 - Basic media casting control bar r=wesj
authorMark Finkle <mfinkle@mozilla.com>
Mon, 20 Jan 2014 17:26:30 -0500
changeset 174577 49f1c6fa27f87a20c026e8f3f6ecff6c94003a02
parent 174576 d350b1b55075b49c4343961a2c5d41031a26db49
child 174578 4d5f60e6521e1bf4227264b7af7f90205b67374c
push idunknown
push userunknown
push dateunknown
reviewerswesj
bugs953381
milestone29.0a1
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;