Bug 1477041 - Modified MediaControlService and notification flow. r=jchen

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

--HG--
extra : rebase_source : b161d07e8fb0831deb7b4549985e6b5c276366c0
This commit is contained in:
Vlad Baicu 2018-07-30 16:21:09 +03:00
Родитель 5a154d0f19
Коммит 2abaca92ad
4 изменённых файлов: 189 добавлений и 54 удалений

Просмотреть файл

@ -232,12 +232,12 @@ public final class IntentHelper implements BundleEventListener {
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;
}

Просмотреть файл

@ -39,6 +39,7 @@ 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;
@ -49,9 +50,10 @@ public class GeckoMediaControlAgent {
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";
@ -60,6 +62,9 @@ public class GeckoMediaControlAgent {
/* 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.
@ -81,8 +86,6 @@ public class GeckoMediaControlAgent {
private int minCoverSize;
private int coverSize;
private Notification currentNotification;
/**
* Internal state of MediaControlService, to indicate it is playing media, or paused...etc.
*/
@ -273,7 +276,7 @@ public class GeckoMediaControlAgent {
Log.d(LOGTAG, "onStateChanged, state = " + sMediaState);
if (isNeedToRemoveControlInterface(sMediaState)) {
stopForegroundService();
toggleForegroundService(false);
NotificationManagerCompat.from(mContext).cancel(R.id.mediaControlNotification);
release();
return;
@ -376,50 +379,49 @@ public class GeckoMediaControlAgent {
}
}
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() {
@ -448,8 +450,8 @@ public class GeckoMediaControlAgent {
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);
}
@ -459,18 +461,19 @@ public class GeckoMediaControlAgent {
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);
@ -484,17 +487,29 @@ public class GeckoMediaControlAgent {
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);
@ -503,10 +518,6 @@ public class GeckoMediaControlAgent {
}
}
private void stopForegroundService() {
mContext.stopService(new Intent(mContext, MediaControlService.class));
}
private void release() {
if (!mInitialized) {
return;

Просмотреть файл

@ -1,22 +1,58 @@
/* -*- 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 != null && intent.getAction() != null) {
GeckoMediaControlAgent.getInstance().handleAction(intent.getAction());
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.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;

Просмотреть файл

@ -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];
}
};
}