зеркало из https://github.com/mozilla/gecko-dev.git
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 : 3aa19de4994e0f1d8f14d1dd8b8ef8e32566b6f5
This commit is contained in:
Родитель
a87e1a24c8
Коммит
23132f91dd
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -40,6 +40,8 @@ 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 {
|
||||
|
@ -49,9 +51,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 +63,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 +87,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 +277,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 +380,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 +451,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 +462,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 +488,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 +519,6 @@ public class GeckoMediaControlAgent {
|
|||
}
|
||||
}
|
||||
|
||||
private void stopForegroundService() {
|
||||
mContext.stopService(new Intent(mContext, MediaControlService.class));
|
||||
}
|
||||
|
||||
private void release() {
|
||||
if (!mInitialized) {
|
||||
return;
|
||||
|
|
|
@ -1,22 +1,57 @@
|
|||
/* -*- 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.GeckoApplication;
|
||||
import org.mozilla.gecko.R;
|
||||
|
||||
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, GeckoApplication.getDefaultNotificationChannel().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];
|
||||
}
|
||||
};
|
||||
}
|
Загрузка…
Ссылка в новой задаче