Bug 1477041 - Modified MediaControlService and notification flow. r?sdaswani
Created a new parcelable which is sent to MediaControlService where
the notification is created based on the data of this object instead of
referencing a static notification. Toggling the service is now done through
startService with a specific action for shutting down instead of calling
stopService due to concurrency issues where the service can be stopped before
having a chance to call startForeground.
MozReview-Commit-ID: 6qNPintkVy
--- a/mobile/android/base/java/org/mozilla/gecko/IntentHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/IntentHelper.java
@@ -227,22 +227,22 @@ public final class IntentHelper implemen
if (mimeType != null && mimeType.length() > 0) {
shareIntent.setType(mimeType);
}
return shareIntent;
}
- public static Intent getTabSwitchIntent(final Tab tab) {
+ public static Intent getTabSwitchIntent(final int tabId) {
final Intent intent = new Intent(GeckoApp.ACTION_SWITCH_TAB);
intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(BrowserContract.SKIP_TAB_QUEUE_FLAG, true);
- intent.putExtra(INTENT_EXTRA_TAB_ID, tab.getId());
+ intent.putExtra(INTENT_EXTRA_TAB_ID, tabId);
intent.putExtra(INTENT_EXTRA_SESSION_UUID, GeckoApplication.getSessionUUID());
return intent;
}
public static Intent getPrivacySettingsIntent() {
final Intent intent = new Intent(GeckoApp.ACTION_LAUNCH_SETTINGS);
intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
GeckoPreferences.setResourceToOpen(intent, "preferences_privacy");
--- a/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaControlAgent.java
+++ b/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaControlAgent.java
@@ -34,37 +34,42 @@ import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.IntentHelper;
import org.mozilla.gecko.PrefsHelper;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.notifications.NotificationHelper;
import org.mozilla.gecko.util.GeckoBundle;
import org.mozilla.gecko.util.ThreadUtils;
+import java.io.ByteArrayOutputStream;
import static org.mozilla.gecko.BuildConfig.DEBUG;
public class GeckoMediaControlAgent {
private static final String LOGTAG = "GeckoMediaControlAgent";
@SuppressLint("StaticFieldLeak")
private static GeckoMediaControlAgent instance;
private Context mContext;
- public static final String ACTION_RESUME = "action_resume";
- public static final String ACTION_PAUSE = "action_pause";
- public static final String ACTION_STOP = "action_stop";
+ public static final String ACTION_RESUME = "action_resume";
+ public static final String ACTION_PAUSE = "action_pause";
+ public static final String ACTION_STOP = "action_stop";
+ /* package */ static final String ACTION_SHUTDOWN = "action_shutdown";
/* package */ static final String ACTION_RESUME_BY_AUDIO_FOCUS = "action_resume_audio_focus";
/* package */ static final String ACTION_PAUSE_BY_AUDIO_FOCUS = "action_pause_audio_focus";
/* package */ static final String ACTION_START_AUDIO_DUCK = "action_start_audio_duck";
/* package */ static final String ACTION_STOP_AUDIO_DUCK = "action_stop_audio_duck";
/* package */ static final String ACTION_TAB_STATE_PLAYING = "action_tab_state_playing";
/* package */ static final String ACTION_TAB_STATE_STOPPED = "action_tab_state_stopped";
/* package */ static final String ACTION_TAB_STATE_RESUMED = "action_tab_state_resumed";
/* package */ static final String ACTION_TAB_STATE_FAVICON = "action_tab_state_favicon";
+
+ /* package */ static final String EXTRA_NOTIFICATION_DATA = "notification_data";
+
private static final String MEDIA_CONTROL_PREF = "dom.audiochannel.mediaControl";
// This is maximum volume level difference when audio ducking. The number is arbitrary.
private static final int AUDIO_DUCK_MAX_STEPS = 3;
private enum AudioDucking { START, STOP }
private boolean mSupportsDucking = false;
private int mAudioDuckCurrentSteps = 0;
@@ -76,18 +81,16 @@ public class GeckoMediaControlAgent {
private final String[] mPrefs = { MEDIA_CONTROL_PREF };
private boolean mInitialized = false;
private boolean mIsMediaControlPrefOn = true;
private int minCoverSize;
private int coverSize;
- private Notification currentNotification;
-
/**
* Internal state of MediaControlService, to indicate it is playing media, or paused...etc.
*/
private static State sMediaState = State.STOPPED;
protected enum State {
PLAYING,
PAUSED,
@@ -268,17 +271,17 @@ public class GeckoMediaControlAgent {
private void onStateChanged() {
if (!mInitialized) {
return;
}
Log.d(LOGTAG, "onStateChanged, state = " + sMediaState);
if (isNeedToRemoveControlInterface(sMediaState)) {
- stopForegroundService();
+ toggleForegroundService(false);
NotificationManagerCompat.from(mContext).cancel(R.id.mediaControlNotification);
release();
return;
}
if (!mIsMediaControlPrefOn) {
return;
}
@@ -371,60 +374,59 @@ public class GeckoMediaControlAgent {
adjustDirection = AudioManager.ADJUST_RAISE;
}
for (int i = 0; i < mAudioDuckCurrentSteps; i++) {
mController.adjustVolume(adjustDirection, 0);
}
}
+ private void updateNotification(Tab tab) {
+ ThreadUtils.assertNotOnUiThread();
+
+ final boolean isPlaying = isMediaPlaying();
+ final int visibility = tab.isPrivate() ? Notification.VISIBILITY_PRIVATE : Notification.VISIBILITY_PUBLIC;
+
+ final MediaNotification mediaNotification = new MediaNotification(isPlaying, visibility, tab.getId(),
+ tab.getTitle(), tab.getURL(), generateCoverArt(tab.getFavicon()));
+
+ if (isPlaying) {
+ toggleForegroundService(true, mediaNotification);
+ } else {
+ toggleForegroundService(false);
+ NotificationManagerCompat.from(mContext).notify(R.id.mediaControlNotification, createNotification(mediaNotification));
+ }
+ }
+
@SuppressLint("NewApi")
- private void setCurrentNotification(Tab tab, boolean onGoing, int visibility) {
+ /* package */ Notification createNotification(MediaNotification mediaNotification) {
final Notification.MediaStyle style = new Notification.MediaStyle();
style.setShowActionsInCompactView(0);
final Notification.Builder notificationBuilder = new Notification.Builder(mContext)
.setSmallIcon(R.drawable.ic_status_logo)
- .setLargeIcon(generateCoverArt(tab))
- .setContentTitle(tab.getTitle())
- .setContentText(tab.getURL())
- .setContentIntent(createContentIntent(tab))
+ .setLargeIcon(BitmapFactory.decodeByteArray(mediaNotification.getBitmapBytes(),
+ 0, mediaNotification.getBitmapBytes().length))
+ .setContentTitle(mediaNotification.getTitle())
+ .setContentText(mediaNotification.getText())
+ .setContentIntent(createContentIntent(mediaNotification.getTabId()))
.setDeleteIntent(createDeleteIntent())
.setStyle(style)
.addAction(createNotificationAction())
- .setOngoing(onGoing)
+ .setOngoing(mediaNotification.isOnGoing())
.setShowWhen(false)
.setWhen(0)
- .setVisibility(visibility);
+ .setVisibility(mediaNotification.getVisibility());
if (!AppConstants.Versions.preO) {
notificationBuilder.setChannelId(NotificationHelper.getInstance(mContext)
.getNotificationChannel(NotificationHelper.Channel.DEFAULT).getId());
}
- currentNotification = notificationBuilder.build();
- }
-
- /* package */ Notification getCurrentNotification() {
- return currentNotification;
- }
-
- private void updateNotification(Tab tab) {
- ThreadUtils.assertNotOnUiThread();
-
- final boolean isPlaying = isMediaPlaying();
- final int visibility = tab.isPrivate() ? Notification.VISIBILITY_PRIVATE : Notification.VISIBILITY_PUBLIC;
- setCurrentNotification(tab, isPlaying, visibility);
-
- if (isPlaying) {
- startForegroundService();
- } else {
- stopForegroundService();
- NotificationManagerCompat.from(mContext).notify(R.id.mediaControlNotification, getCurrentNotification());
- }
+ return notificationBuilder.build();
}
private Notification.Action createNotificationAction() {
final Intent intent = createIntentUponState(sMediaState);
boolean isPlayAction = intent.getAction().equals(ACTION_RESUME);
int icon = isPlayAction ? R.drawable.ic_media_play : R.drawable.ic_media_pause;
String title = mContext.getString(isPlayAction ? R.string.media_play : R.string.media_pause);
@@ -443,75 +445,84 @@ public class GeckoMediaControlAgent {
@VisibleForTesting
Intent createIntentUponState(State state) {
String action = state.equals(State.PLAYING) ? ACTION_PAUSE : ACTION_RESUME;
final Intent intent = new Intent(mContext, MediaControlService.class);
intent.setAction(action);
return intent;
}
- private PendingIntent createContentIntent(Tab tab) {
- Intent intent = IntentHelper.getTabSwitchIntent(tab);
+ private PendingIntent createContentIntent(int tabId) {
+ Intent intent = IntentHelper.getTabSwitchIntent(tabId);
return PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
private PendingIntent createDeleteIntent() {
Intent intent = new Intent(mContext, MediaControlService.class);
intent.setAction(ACTION_STOP);
return PendingIntent.getService(mContext, 1, intent, 0);
}
- private Bitmap generateCoverArt(Tab tab) {
- final Bitmap favicon = tab.getFavicon();
+ private byte[] generateCoverArt(Bitmap tabFavicon) {
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
// 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() < minCoverSize || favicon.getHeight() < minCoverSize) {
+ if (tabFavicon == null || tabFavicon.getWidth() < minCoverSize || tabFavicon.getHeight() < minCoverSize) {
// Use the launcher icon as fallback
- return BitmapFactory.decodeResource(mContext.getResources(), R.drawable.notification_media);
+ BitmapFactory.decodeResource(mContext.getResources(), R.drawable.notification_media).compress(Bitmap.CompressFormat.PNG, 100, stream);
+ return stream.toByteArray();
}
// 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);
+ int width = Math.max(tabFavicon.getWidth(), coverSize / 2);
+ int height = Math.max(tabFavicon.getHeight(), coverSize / 2);
final Bitmap coverArt = Bitmap.createBitmap(coverSize, coverSize, Bitmap.Config.ARGB_8888);
final 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);
final Paint paint = new Paint();
paint.setAntiAlias(true);
- canvas.drawBitmap(favicon,
- new Rect(0, 0, favicon.getWidth(), favicon.getHeight()),
+ canvas.drawBitmap(tabFavicon,
+ new Rect(0, 0, tabFavicon.getWidth(), tabFavicon.getHeight()),
new Rect(left, top, right, bottom),
paint);
- return coverArt;
+ coverArt.compress(Bitmap.CompressFormat.PNG, 100, stream);
+ return stream.toByteArray();
+ }
+
+ private void toggleForegroundService(boolean startService) {
+ toggleForegroundService(startService, null);
}
@SuppressLint("NewApi")
- private void startForegroundService() {
+ private void toggleForegroundService(boolean startService, MediaNotification mediaNotification) {
Intent intent = new Intent(mContext, MediaControlService.class);
+ if (!startService) {
+ intent.setAction(GeckoMediaControlAgent.ACTION_SHUTDOWN);
+ }
+
+ if (mediaNotification != null) {
+ intent.putExtra(GeckoMediaControlAgent.EXTRA_NOTIFICATION_DATA, mediaNotification);
+ }
if (AppConstants.Versions.preO) {
mContext.startService(intent);
} else {
mContext.startForegroundService(intent);
}
}
- private void stopForegroundService() {
- mContext.stopService(new Intent(mContext, MediaControlService.class));
- }
-
private void release() {
if (!mInitialized) {
return;
}
mInitialized = false;
Log.d(LOGTAG, "release");
if (!sMediaState.equals(State.STOPPED)) {
--- a/mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java
@@ -1,27 +1,63 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * 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.media;
+import android.annotation.SuppressLint;
+import android.app.Notification;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
+import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.R;
+import org.mozilla.gecko.notifications.NotificationHelper;
public class MediaControlService extends Service {
private static final String LOGTAG = "MediaControlService";
+ private Notification currentNotification;
+
+ @SuppressLint("NewApi")
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ // Initialize our current notification as a blank notification for cases when the service is started directly with the shutdown
+ // action before being started with a valid notification.
+ if (AppConstants.Versions.preO) {
+ currentNotification = new Notification.Builder(this).build();
+ } else {
+ currentNotification = new Notification.Builder(this, NotificationHelper.getInstance(this)
+ .getNotificationChannel(NotificationHelper.Channel.DEFAULT).getId()).build();
+ }
+ }
+
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(LOGTAG, "onStartCommand");
- startForeground(R.id.mediaControlNotification, GeckoMediaControlAgent.getInstance().getCurrentNotification());
+
+ if (intent.hasExtra(GeckoMediaControlAgent.EXTRA_NOTIFICATION_DATA)) {
+ currentNotification = GeckoMediaControlAgent.getInstance().createNotification(
+ (MediaNotification) intent.getParcelableExtra(GeckoMediaControlAgent.EXTRA_NOTIFICATION_DATA));
+ }
+
+ startForeground(R.id.mediaControlNotification, currentNotification);
- if (intent != null && intent.getAction() != null) {
- GeckoMediaControlAgent.getInstance().handleAction(intent.getAction());
+ if (intent.getAction() != null) {
+ final String action = intent.getAction();
+ if (action.equals(GeckoMediaControlAgent.ACTION_SHUTDOWN)) {
+ stopForeground(true);
+ stopSelfResult(startId);
+ } else {
+ GeckoMediaControlAgent.getInstance().handleAction(action);
+ }
}
return START_NOT_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return null;
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/MediaNotification.java
@@ -0,0 +1,88 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * 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.media;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public final class MediaNotification implements Parcelable {
+ private final boolean onGoing;
+ private final int visibility;
+ private final int tabId;
+ private final String title;
+ private final String text;
+ private final byte[] bitmapBytes;
+
+ /* package */ MediaNotification(boolean onGoing, int visibility, int tabId,
+ String title, String content, byte[] bitmapByteArray) {
+ this.onGoing = onGoing;
+ this.visibility = visibility;
+ this.tabId = tabId;
+ this.title = title;
+ this.text = content;
+ this.bitmapBytes = bitmapByteArray;
+ }
+
+ /* package */ boolean isOnGoing() {
+ return onGoing;
+ }
+
+ /* package */ int getVisibility() {
+ return visibility;
+ }
+
+ /* package */ int getTabId() {
+ return tabId;
+ }
+
+ /* package */ String getTitle() {
+ return title;
+ }
+
+ /* package */ String getText() {
+ return text;
+ }
+
+ /* package */ byte[] getBitmapBytes() {
+ return bitmapBytes;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeByte((byte) (onGoing ? 1 : 0));
+ dest.writeInt(visibility);
+ dest.writeInt(tabId);
+ dest.writeString(title);
+ dest.writeString(text);
+ dest.writeInt(bitmapBytes.length);
+ dest.writeByteArray(bitmapBytes);
+ }
+
+ public static final Parcelable.Creator<MediaNotification> CREATOR = new Parcelable.Creator<MediaNotification>() {
+ @Override
+ public MediaNotification createFromParcel(final Parcel source) {
+ final boolean onGoing = source.readByte() != 0;
+ final int visibility = source.readInt();
+ final int tabId = source.readInt();
+ final String title = source.readString();
+ final String text = source.readString();
+ final int arrayLength = source.readInt();
+ final byte[] bitmapBytes = new byte[arrayLength];
+ source.readByteArray(bitmapBytes);
+
+ return new MediaNotification(onGoing, visibility, tabId, title, text, bitmapBytes);
+ }
+
+ @Override
+ public MediaNotification[] newArray(int size) {
+ return new MediaNotification[size];
+ }
+ };
+}