Bug 1264901 - Implement media-control front-end. r=ahunt
MozReview-Commit-ID: H6Py4Wd35db
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -919,10 +919,9 @@ pref("identity.fxaccounts.remote.oauth.u
// Token server used by Firefox Account-authenticated Sync.
pref("identity.sync.tokenserver.uri", "https://token.services.mozilla.com/1.0/sync/1.5");
// Enable Presentation API
pref("dom.presentation.enabled", true);
pref("dom.presentation.discovery.enabled", true);
pref("dom.audiochannel.audioCompeting", true);
-// TODO : turn this pref default on in bug1264901
-pref("dom.audiochannel.mediaControl", false);
+pref("dom.audiochannel.mediaControl", true);
--- a/mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java
@@ -1,32 +1,38 @@
package org.mozilla.gecko.media;
-import org.mozilla.gecko.BrowserApp;
-import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoEvent;
-import org.mozilla.gecko.PrefsHelper;
-
import android.app.Notification;
-import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
-import android.content.Context;
import android.content.Intent;
-import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
import android.media.session.MediaController;
import android.media.session.MediaSession;
-import android.media.session.MediaSessionManager;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
+import android.support.v4.app.NotificationManagerCompat;
import android.util.Log;
-public class MediaControlService extends Service {
+import org.mozilla.gecko.BrowserApp;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.PrefsHelper;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.lang.ref.WeakReference;
+
+public class MediaControlService extends Service implements Tabs.OnTabsChangedListener {
private static final String LOGTAG = "MediaControlService";
public static final String ACTION_START = "action_start";
public static final String ACTION_PLAY = "action_play";
public static final String ACTION_PAUSE = "action_pause";
public static final String ACTION_STOP = "action_stop";
public static final String ACTION_REMOVE_CONTROL = "action_remove_control";
@@ -39,26 +45,38 @@ public class MediaControlService extends
private MediaController mController;
private PrefsHelper.PrefHandler mPrefsObserver;
private final String[] mPrefs = { MEDIA_CONTROL_PREF };
private boolean mIsInitMediaSession = false;
private boolean mIsMediaControlPrefOn = true;
+ private static WeakReference<Tab> mTabReference = null;
+
+ private int coverSize;
+
@Override
public void onCreate() {
+ mTabReference = new WeakReference<>(null);
+
getGeckoPreference();
initMediaSession();
+
+ coverSize = (int) getResources().getDimension(R.dimen.notification_media_cover);
+
+ Tabs.registerOnTabsChangedListener(this);
}
@Override
public void onDestroy() {
notifyControlInterfaceChanged(ACTION_REMOVE_CONTROL);
PrefsHelper.removeObserver(mPrefsObserver);
+
+ Tabs.unregisterOnTabsChangedListener(this);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
handleIntent(intent);
return super.onStartCommand(intent, flags, startId);
}
@@ -73,16 +91,32 @@ public class MediaControlService extends
return super.onUnbind(intent);
}
@Override
public void onTaskRemoved(Intent rootIntent) {
stopSelf();
}
+ @Override
+ public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
+ if (!mIsInitMediaSession) {
+ return;
+ }
+
+ if (tab == mTabReference.get()) {
+ return;
+ }
+
+ if (msg == Tabs.TabEvents.AUDIO_PLAYING_CHANGE && tab.isAudioPlaying()) {
+ mTabReference = new WeakReference<Tab>(tab);
+ notifyControlInterfaceChanged(ACTION_PAUSE);
+ }
+ }
+
private boolean isAndroidVersionLollopopOrHigher() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
}
private void handleIntent(Intent intent) {
if (intent == null || intent.getAction() == null ||
!mIsInitMediaSession) {
return;
@@ -196,88 +230,117 @@ public class MediaControlService extends
GeckoAppShell.notifyObservers(topic, data);
}
private boolean isNeedToRemoveControlInterface(String action) {
return (action.equals(ACTION_STOP) ||
action.equals(ACTION_REMOVE_CONTROL));
}
- private void notifyControlInterfaceChanged(String action) {
+ private void notifyControlInterfaceChanged(final String action) {
Log.d(LOGTAG, "notifyControlInterfaceChanged, action = " + action);
- NotificationManager notificationManager = (NotificationManager)
- getSystemService(Context.NOTIFICATION_SERVICE);
if (isNeedToRemoveControlInterface(action)) {
- notificationManager.cancel(MEDIA_CONTROL_ID);
+ NotificationManagerCompat.from(this).cancel(MEDIA_CONTROL_ID);
return;
}
if (!mIsMediaControlPrefOn) {
return;
}
- notificationManager.notify(MEDIA_CONTROL_ID, getNotification(action));
+ if (mTabReference.get() == null) {
+ return;
+ }
+
+ ThreadUtils.postToBackgroundThread(new Runnable() {
+ @Override
+ public void run() {
+ updateNotification(action);
+ }
+ });
}
- private Notification getNotification(String action) {
- // TODO : use website name, content and favicon in bug1264901.
- return new Notification.Builder(this)
- .setSmallIcon(android.R.drawable.ic_media_play)
- .setContentTitle("Media Title")
- .setContentText("Media Artist")
- .setDeleteIntent(getDeletePendingIntent())
- .setContentIntent(getClickPendingIntent())
- .setStyle(getMediaStyle())
- .addAction(getAction(action))
+ private void updateNotification(String action) {
+ ThreadUtils.assertNotOnUiThread();
+
+ Notification.MediaStyle style = new Notification.MediaStyle();
+ style.setShowActionsInCompactView(0);
+
+ final Tab tab = mTabReference.get();
+
+ Notification notification = new Notification.Builder(this)
+ .setSmallIcon(R.drawable.flat_icon)
+ .setLargeIcon(generateCoverArt(tab))
+ .setContentTitle(tab.getTitle())
+ .setContentText(tab.getURL())
+ .setContentIntent(createContentIntent())
+ .setDeleteIntent(createDeleteIntent())
+ .setStyle(style)
+ .addAction(createNotificationAction(action))
.setOngoing(action.equals(ACTION_PAUSE))
+ .setShowWhen(false)
+ .setWhen(0)
.build();
+
+ NotificationManagerCompat.from(this)
+ .notify(MEDIA_CONTROL_ID, notification);
}
- private Notification.Action getAction(String action) {
- int icon = getActionIcon(action);
- String title = getActionTitle(action);
+ private Notification.Action createNotificationAction(String action) {
+ boolean isPlayAction = action.equals(ACTION_PLAY);
+
+ int icon = isPlayAction ? R.drawable.ic_media_play : R.drawable.ic_media_pause;
+ String title = getString(isPlayAction ? R.string.media_pause : R.string.media_pause);
Intent intent = new Intent(getApplicationContext(), MediaControlService.class);
intent.setAction(action);
PendingIntent pendingIntent = PendingIntent.getService(getApplicationContext(), 1, intent, 0);
+
+ //noinspection deprecation - The new constructor is only for API > 23
return new Notification.Action.Builder(icon, title, pendingIntent).build();
}
- private int getActionIcon(String action) {
- switch (action) {
- case ACTION_PLAY :
- return android.R.drawable.ic_media_play;
- case ACTION_PAUSE :
- return android.R.drawable.ic_media_pause;
- default:
- return 0;
- }
- }
-
- private String getActionTitle(String action) {
- switch (action) {
- case ACTION_PLAY :
- return "Play";
- case ACTION_PAUSE :
- return "Pause";
- default:
- return null;
- }
- }
-
- private PendingIntent getDeletePendingIntent() {
- Intent intent = new Intent(getApplicationContext(), MediaControlService.class);
- intent.setAction(ACTION_REMOVE_CONTROL);
- return PendingIntent.getService(getApplicationContext(), 1, intent, 0);
- }
-
- private PendingIntent getClickPendingIntent() {
+ private PendingIntent createContentIntent() {
Intent intent = new Intent(getApplicationContext(), BrowserApp.class);
return PendingIntent.getActivity(getApplicationContext(), 0, intent, 0);
}
- private Notification.MediaStyle getMediaStyle() {
- Notification.MediaStyle style = new Notification.MediaStyle();
- style.setShowActionsInCompactView(0);
- return style;
+ private PendingIntent createDeleteIntent() {
+ Intent intent = new Intent(getApplicationContext(), MediaControlService.class);
+ intent.setAction(ACTION_REMOVE_CONTROL);
+ return PendingIntent.getService(getApplicationContext(), 1, intent, 0);
+ }
+
+ private Bitmap generateCoverArt(Tab tab) {
+ Bitmap favicon = tab.getFavicon();
+
+ // If we do not have a favicon or if it's smaller than 72 pixels then just use the default icon.
+ if (favicon == null || favicon.getWidth() < 72 || favicon.getHeight() < 72) {
+ // Use the launcher icon as fallback
+ return BitmapFactory.decodeResource(getResources(), R.drawable.notification_media);
+ }
+
+ // Favicon should at least have half of the size of the cover
+ int width = Math.max(favicon.getWidth(), coverSize / 2);
+ int height = Math.max(favicon.getHeight(), coverSize / 2);
+
+ Bitmap coverArt = Bitmap.createBitmap(coverSize, coverSize, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(coverArt);
+ canvas.drawColor(0xFF777777);
+
+ int left = Math.max(0, (coverArt.getWidth() / 2) - (width / 2));
+ int right = Math.min(coverSize, left + width);
+ int top = Math.max(0, (coverArt.getHeight() / 2) - (height / 2));
+ int bottom = Math.min(coverSize, top + height);
+
+ Paint paint = new Paint();
+ paint.setAntiAlias(true);
+
+ canvas.drawBitmap(favicon,
+ new Rect(0, 0, favicon.getWidth(), favicon.getHeight()),
+ new Rect(left, top, right, bottom),
+ paint);
+
+ return coverArt;
+
}
}
\ No newline at end of file
new file mode 100644
index 0000000000000000000000000000000000000000..1701f34b01c3f6fdab2a6a1b58d7ab0224d91295
GIT binary patch
literal 103
zc%17D@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K;o~MgrNCjiE1LNWZkw5tk4mFQj
xc%7Ikj=apju)=XMTd$N!7LYMT{2x%BfkFHwlX~y3uO&d844$rjF6*2UngGUE9ZCQI
new file mode 100644
index 0000000000000000000000000000000000000000..f77ad6b57b7168dfe5a49a0ba455eac2e7e58a0a
GIT binary patch
literal 194
zc%17D@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8r>Bc!NCo5DDF+!D19?~<K3l`6
z`os1`lh%qRy)e<P8$w~s)n!eL?hMQ)L?)<IzF#UNqo@$sD%O)B(aNx_WR4R<@+?0^
zjhB|K3~F2EI2j~6+|^*2FnNyC8{b5cU3W$1q-Jw14@eWy>bWY$)0_N!3$OFHmGvGj
qUc1~fj34)k982CEn(^=_`$4IwCxutdiZ*~;$l&Sf=d#Wzp$Pz=#6ji&
new file mode 100644
index 0000000000000000000000000000000000000000..c85e32c01989d27652abdc114c48b300d2cc2c4f
GIT binary patch
literal 621
zc%17D@N?(olHy`uVBq!ia0vp^2SAtuNHCOdH@?llz+~d-;uuoF`1V$FacrPS>&JiB
z)wkv*2i)G4TO4rv+1b47E2VAsuX(xM{k2M^XPxoyd8_X#&lG1M3pC_ktv`MDd-?Mp
zOw0E5bZ9Kw#S~W87xUbfac|#|{;E_?i6Z5?u!kH0zyE95H{7}}x!{{Q+ls$)8MErs
zA>8*|29Gk%=Wz%8E@zeasB>;xNpQoh{oD=z{&y_CD-4pq@Y-|5H#2#I&!1LjM8`5+
znxquMJ&X6oHK{;Bg|#Q<*BI~E*)Vf$pr*oF?yH^8nDks(8w8_+*Jb}oGF-Q^Vfs_%
zE&pZCBwkUk{aO>u7-*;Pe*W*`JImIKIqzj)p8wRwd*^lUqq%VnTTGv;*G~DaX=lK^
zgm>YsRTh@V7p66ogm$Z2+J(hDW0Q!>Fbyr-eyPsfVb`D6_v03noaNi#vHG-E^3-`Z
z7}YXL<1;5*%C>f>UwcpcYtXMh`x!G7`5v8)dYiqww@B%6(4hxOeWzbG`rpmZaI?v+
z_RcH|{=em|@80XlQy=Eaul|s5`rAb%{_?-_>%)IFCGRM^y!hDq87daHlm1T1|8X&H
zPuf%0?VndTUcdS}!^I%)@PWy%Pe?k~=km31ziVE9`HXx2bE7}JWJO{^R6OGw6_@3U
SH*D$#@jYGrT-G@yGywq2?D(nx
new file mode 100644
index 0000000000000000000000000000000000000000..f49aed757118a941b567629ec217cde1aaf257e8
GIT binary patch
literal 90
zc%17D@N?(olHy`uVBq!ia0vp^1|ZA`BpB)|k7xlYrjj7PU<QV=$!9HqJYi24$B+uf
k<OM<x{vY_Cm=S!1k)ch%{N(L`^Zg)Ap00i_>zopr0A<G){{R30
new file mode 100644
index 0000000000000000000000000000000000000000..9cc777c2c44a05f4bce98104d54d7116b8508785
GIT binary patch
literal 214
zc%17D@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0Dxt=bLAr*{ouX!^v7K*qPzJJOx
zp@ApJ!;w4af>Y<q1&w=`e&6^<NalPs>yNWDKG&B_sF!CH32<PvKBmXuKCQ%;VL|FE
zrh>GyCm4;Y)AShlx0&!wur%1-;PYr|!;#gDo#8A(>)BM|1U>ffO{mx}QL$m+&rAI^
zX{l+qj10GTvBuVK{=Mq0m1EuGi8B>M6&$S`nzpR#`(L+PZ$SgoyoJlPLKVb%T!9W{
N@O1TaS?83{1OVvqPUHXp
new file mode 100644
index 0000000000000000000000000000000000000000..6d9a57966dea46ca479c346c3f124e10192bb73d
GIT binary patch
literal 859
zc%17D@N?(olHy`uVBq!ia0y~yU}OMc4j{=;DfxXD0|T>?r;B4q1>@U0&6{N%1&%)a
zbG`e<wzEk&Hoy1Y*p|DxS-5)DqP@?{yKJky<L}0u^I3n!hJkW0L4s(dZ6<T73pOV=
zXgkFH*SYuP7lVHTqx+-ST{?`%;`_f>hDb5#R0_!(U*uEB@A5ak&g<~@zoGXThAaQ}
zF=W+8Gi=?j-*D@`xWn7`+zY;KXASs0n`y=0$qZH3g&J<zb1r!I=fEw-fZytjS6pMH
zVze2u{;M?XGJhQY>pBZio_B#Cd!Lvo!-8-A%n<_bFNJD0+>6$$TYpiQanoCQ{!sRW
zS6c(>m>u?6Px;-f&1~?>^N(G_nj7+`{;gtoviwmwQ_9ne0cqhZ33-3y8m2ZIr+%=N
zwFwt&cy^yTKs>xH<<y}7v2f;u>-h{9zW#qz-N<ZJwM~klx{kf!UH!VRp{CDT`TVyq
zuHas38@z4Rn{A94oKKI2Ton$n)5?iT_{z;2qZc|q%<?dsLbUiPt*NI&O@A_^95*cr
zy&4euyqBTP{@MkTsnhR?o#&QsSo3>xeeLvB8{HTto0d3U)e7zIW!Q8&zkS}zc>7@c
z8!uQk1Scnd=~=tzB<F&Mm$UyXh|cw6IBl}cA)htm4MT(21C<l06QmU%M?W;Q;hAa2
z#~)mJ?{BMz^7<ncny-FF)y%${{jr}ZT>D32i+^&B%-qMnE9Y<u`AiI47FRk?Woc%S
z_?%@Y4Fe~w+{Afu_Fi>w<Jftu!a6g5RwyOhl;Kv7w0g}jp+G6&r;W76hE}%OD>n4<
zNoz{xFTMJ9*UYG9v*6u^2db?1M$hfL{Zf^M3Mr6iP{F!idGmR{wRMj`JWp3Ymvv4F
FO#r&o8)N_g
new file mode 100644
index 0000000000000000000000000000000000000000..7192ad487eacb4f8f530ebe2878760e2528fbc5f
GIT binary patch
literal 92
zc%17D@N?(olHy`uVBq!ia0vp^9w5vJBp7O^^}Pa8OeH~n!3+##lh0ZJd7_>!jv*C{
m$r5W0{Noqkjk>0hJ%NeAb!~d=tj~okAZ?zmelF{r5}E*xz!xz9
new file mode 100644
index 0000000000000000000000000000000000000000..e0f6a850d17d4b29baca6be83c6f27207841c225
GIT binary patch
literal 279
zc%17D@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhaw?s>X6hEy=Vy|Hm8V<3Y|qQef>
z6%A4$3%PbJ;M#SeMXp3aoQZD+%Y*|gpYO8O^;_<KKh40JkyRwXfswVOnu#mJ=`uG9
z_pd2)9T<Xd#VJG>E_ukpG~?n&ri1egTZK6i{H}AeXt22{96i9fM53Zmgjsz;(gW5Y
zo;eO(jp81LADA@R{D6|&OC&6SlKMbN<|_H-hHpI@pBC2{{buA{Zm?~RoUHAs${kVr
z>-I=i#3h;*h)-a9$8hOD?f<FgQV;)be16D_xtuZc0iOb|jrjBXmQd#b3HGz;_Z<(E
Smr4MA%HZkh=d#Wzp$Pzz6Jk&R
new file mode 100644
index 0000000000000000000000000000000000000000..9046219e40a73a1857528dd8d31ba1c55edba3db
GIT binary patch
literal 1379
zc$~GAeK6Yx7{{NVA7WCp96^_`IlHT!HFvDu>ZPp-U8__@M@o81T)e+TNr&Hb*L170
zo8D-n3DwOInO42VkqvQICCtf+rbK#_h^xlDM9B2N-R-Wom)jrTe?H%PzV|$TJm0Uv
zLMdjZ_ND+}77`qI768HC$Yc+|7lNgD03(->z+>m~5VaiDI8z8>#YPNYXDx`=M5g#`
zNZLB77gLq}3>k0nq<kT2<&b}5-C>d5nmOf+><2J_0D!$P1RMbjVK4iy@a^uuh5v9Q
z&F-hC4^q{iYQTL_`27~cX9dWgk2^3}ucF~ZJDo)ucLDVBcIi=J4D6dK=e*v0Za8{L
z7u5*vcKV4#I4vn+K>&9(4oM>KB9CnKFjZOvse}Glo6;d*eAS{_y@?ym)uBl~yNEmS
z&$j+7bZ59?B#GV&Ey-Rm1GcSx!Gt{WMDBrl&hWf%RT$E%dfPXSKtYMV1ufA?*J)@q
zXSIlmW$wx)AHo`WS86~W5hg^yN9s*~A91m7{wW9KdS(9F^f2zVC818OCFY;9Fvz=f
zWj(B_$C=xbywUv*u2M_Rreo)uT}3_o49p@c59=t5%k}jh1B(Pa*0GR5y&|llV~b6x
zFjv3KPA*1Bj}@@O|2kgl*LW5G0by390Lj)6EoXT8_QYU0zB!YOWhM&wgbRA<(yNcH
z@Xba0F|F>RoR(8iF~PCIe}f`>>wgzYVW|~5Q9;WX10;32<%K>0?>Y(HJ7N3Yo!`vF
ztz&VpG88}6LT)H~*yY8>2Xx;ZT#G#Ck@$Pi8Uf2&3C@+<Of*E(2)@RSB+sgeJ3o@@
z2WmC<2BANDlEr8mA=<xm3x`z}btbS?VAER?i;~mN_<DCYQ{w~!u4BnCcU!<~98h(g
zrXOLOiPuWmZSD=eCx{uSV{AQxGtbOSaP_m<F{X!8^?n0fMSJZG^mnm0kGH$++fbQ6
z`b3|Uspdt;9Pq`>)<ksM^>oT)6e-ftLrQCCO}^$6j~DCM9gVViqDYnwiA#!_Xzq#(
zfLshuzvMg_3QO<bm9O8dEPJ0vT&^{N8@NrfIaXF%@E$6T`nKm5ZkBPVu+m6;#%jlt
zfbr|LuNq^vcjphE#9iCEVTswc?v#@-+bu3r>CY_Fg^O0hjhrafo&sf#moPC4`Jw$j
z%6<2<`B6X4daLT8WkyOuEooAwouyI;Maq|RHCcJ>N)=_YHOIwRYGm$2@T8p|=hN&l
z+s<F^uR8lVRcyNVNSH6|8zqF!o3s})P0*}M-*fik1fH;kRpW-OA4L9^<`4diK&nUa
zFc_z3l}8^$!c>}JI+7$h@XWM&F{s%a9z8QLkHuFXyX-pbKb;>1Nx!-(t$o^>3K9z6
k{^Yud|56qIL*?w4UQk)P5A^N^ziH2qPeKEQ$LYm?0El(~fdBvi
--- a/mobile/android/base/resources/values/dimens.xml
+++ b/mobile/android/base/resources/values/dimens.xml
@@ -212,9 +212,11 @@
<dimen name="action_bar_divider_height">2dp</dimen>
<!-- http://blog.danlew.net/2015/01/06/handling-android-resources-with-non-standard-formats/ -->
<item name="match_parent" type="dimen">-1</item>
<item name="wrap_content" type="dimen">-2</item>
<item name="tab_strip_content_start" type="dimen">12dp</item>
<item name="firstrun_tab_strip_content_start" type="dimen">15dp</item>
+
+ <item name="notification_media_cover" type="dimen">128dp</item>
</resources>