-
Notifications
You must be signed in to change notification settings - Fork 337
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Define MediaNotification.Provider so that apps can customize notification UX. Move MediaNotificationManager's functionality around notifications on DefaultMediaNotificationProvider PiperOrigin-RevId: 428024699
- Loading branch information
1 parent
0367faf
commit 71f21cc
Showing
6 changed files
with
730 additions
and
273 deletions.
There are no files selected for viewing
149 changes: 149 additions & 0 deletions
149
libraries/session/src/main/java/androidx/media3/session/DefaultActionFactory.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
/* | ||
* Copyright 2022 The Android Open Source Project | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package androidx.media3.session; | ||
|
||
import android.app.PendingIntent; | ||
import android.content.ComponentName; | ||
import android.content.Context; | ||
import android.content.Intent; | ||
import android.os.Bundle; | ||
import android.support.v4.media.session.PlaybackStateCompat; | ||
import android.view.KeyEvent; | ||
import androidx.annotation.Nullable; | ||
import androidx.annotation.RequiresApi; | ||
import androidx.core.app.NotificationCompat; | ||
import androidx.core.graphics.drawable.IconCompat; | ||
import androidx.media3.common.util.UnstableApi; | ||
import androidx.media3.common.util.Util; | ||
|
||
/** The default {@link MediaNotification.ActionFactory}. */ | ||
@UnstableApi | ||
/* package */ final class DefaultActionFactory implements MediaNotification.ActionFactory { | ||
|
||
private static final String ACTION_CUSTOM = "androidx.media3.session.CUSTOM_NOTIFICATION_ACTION"; | ||
private static final String EXTRAS_KEY_ACTION_CUSTOM = | ||
"androidx.media3.session.EXTRAS_KEY_CUSTOM_NOTIFICATION_ACTION"; | ||
public static final String EXTRAS_KEY_ACTION_CUSTOM_EXTRAS = | ||
"androidx.media3.session.EXTRAS_KEY_CUSTOM_NOTIFICATION_ACTION_EXTRAS"; | ||
|
||
private final Context context; | ||
|
||
public DefaultActionFactory(Context context) { | ||
this.context = context.getApplicationContext(); | ||
} | ||
|
||
@Override | ||
public NotificationCompat.Action createMediaAction( | ||
IconCompat icon, CharSequence title, @Command long command) { | ||
return new NotificationCompat.Action(icon, title, createMediaActionPendingIntent(command)); | ||
} | ||
|
||
@Override | ||
public NotificationCompat.Action createCustomAction( | ||
IconCompat icon, CharSequence title, String customAction, Bundle extras) { | ||
return new NotificationCompat.Action( | ||
icon, title, createCustomActionPendingIntent(customAction, extras)); | ||
} | ||
|
||
@Override | ||
public PendingIntent createMediaActionPendingIntent(@Command long command) { | ||
int keyCode = PlaybackStateCompat.toKeyCode(command); | ||
Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); | ||
intent.setComponent(new ComponentName(context, context.getClass())); | ||
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); | ||
if (Util.SDK_INT >= 26 && command != COMMAND_PAUSE && command != COMMAND_STOP) { | ||
return Api26.createPendingIntent(context, /* requestCode= */ keyCode, intent); | ||
} else { | ||
return PendingIntent.getService( | ||
context, | ||
/* requestCode= */ keyCode, | ||
intent, | ||
Util.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0); | ||
} | ||
} | ||
|
||
private PendingIntent createCustomActionPendingIntent(String action, Bundle extras) { | ||
Intent intent = new Intent(ACTION_CUSTOM); | ||
intent.setComponent(new ComponentName(context, context.getClass())); | ||
intent.putExtra(EXTRAS_KEY_ACTION_CUSTOM, action); | ||
intent.putExtra(EXTRAS_KEY_ACTION_CUSTOM_EXTRAS, extras); | ||
if (Util.SDK_INT >= 26) { | ||
return Api26.createPendingIntent( | ||
context, /* requestCode= */ KeyEvent.KEYCODE_UNKNOWN, intent); | ||
} else { | ||
return PendingIntent.getService( | ||
context, | ||
/* requestCode= */ KeyEvent.KEYCODE_UNKNOWN, | ||
intent, | ||
Util.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0); | ||
} | ||
} | ||
|
||
/** Returns whether {@code intent} was part of a {@link #createMediaAction media action}. */ | ||
public boolean isMediaAction(Intent intent) { | ||
return Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction()); | ||
} | ||
|
||
/** Returns whether {@code intent} was part of a {@link #createCustomAction custom action }. */ | ||
public boolean isCustomAction(Intent intent) { | ||
return ACTION_CUSTOM.equals(intent.getAction()); | ||
} | ||
|
||
/** | ||
* Returns the {@link KeyEvent} that was included in the media action, or {@code null} if no | ||
* {@link KeyEvent} is found in the {@code intent}. | ||
*/ | ||
@Nullable | ||
public KeyEvent getKeyEvent(Intent intent) { | ||
@Nullable Bundle extras = intent.getExtras(); | ||
if (extras != null && extras.containsKey(Intent.EXTRA_KEY_EVENT)) { | ||
return extras.getParcelable(Intent.EXTRA_KEY_EVENT); | ||
} | ||
return null; | ||
} | ||
|
||
/** | ||
* Returns the custom action that was included in the {@link #createCustomAction custom action}, | ||
* or {@code null} if no custom action is found in the {@code intent}. | ||
*/ | ||
@Nullable | ||
public String getCustomAction(Intent intent) { | ||
@Nullable Bundle extras = intent.getExtras(); | ||
@Nullable Object customAction = extras != null ? extras.get(EXTRAS_KEY_ACTION_CUSTOM) : null; | ||
return customAction instanceof String ? (String) customAction : null; | ||
} | ||
|
||
/** | ||
* Returns extras that were included in the {@link #createCustomAction custom action}, or {@link | ||
* Bundle#EMPTY} is no extras are found. | ||
*/ | ||
public Bundle getCustomActionExtras(Intent intent) { | ||
@Nullable Bundle extras = intent.getExtras(); | ||
@Nullable | ||
Object customExtras = extras != null ? extras.get(EXTRAS_KEY_ACTION_CUSTOM_EXTRAS) : null; | ||
return customExtras instanceof Bundle ? (Bundle) customExtras : Bundle.EMPTY; | ||
} | ||
|
||
@RequiresApi(26) | ||
private static final class Api26 { | ||
private Api26() {} | ||
|
||
public static PendingIntent createPendingIntent(Context context, int keyCode, Intent intent) { | ||
return PendingIntent.getForegroundService( | ||
context, /* requestCode= */ keyCode, intent, PendingIntent.FLAG_IMMUTABLE); | ||
} | ||
} | ||
} |
173 changes: 173 additions & 0 deletions
173
...aries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
/* | ||
* Copyright 2022 The Android Open Source Project | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License | ||
*/ | ||
package androidx.media3.session; | ||
|
||
import static androidx.media3.common.util.Assertions.checkStateNotNull; | ||
|
||
import android.app.Notification; | ||
import android.app.NotificationChannel; | ||
import android.app.NotificationManager; | ||
import android.content.Context; | ||
import android.graphics.Bitmap; | ||
import android.graphics.BitmapFactory; | ||
import android.os.Bundle; | ||
import androidx.core.app.NotificationCompat; | ||
import androidx.core.graphics.drawable.IconCompat; | ||
import androidx.media3.common.MediaMetadata; | ||
import androidx.media3.common.util.UnstableApi; | ||
import androidx.media3.common.util.Util; | ||
|
||
/** | ||
* The default {@link MediaNotification.Provider}. | ||
* | ||
* <h2>Actions</h2> | ||
* | ||
* The following actions are included in the provided notifications: | ||
* | ||
* <ul> | ||
* <li>{@link MediaNotification.ActionFactory#COMMAND_PLAY} to start playback. Included when | ||
* {@link MediaController#getPlayWhenReady()} returns {@code false}. | ||
* <li>{@link MediaNotification.ActionFactory#COMMAND_PAUSE}, to pause playback. Included when | ||
* ({@link MediaController#getPlayWhenReady()} returns {@code true}. | ||
* <li>{@link MediaNotification.ActionFactory#COMMAND_SKIP_TO_PREVIOUS} to skip to the previous | ||
* item. | ||
* <li>{@link MediaNotification.ActionFactory#COMMAND_SKIP_TO_NEXT} to skip to the next item. | ||
* </ul> | ||
* | ||
* <h2>Drawables</h2> | ||
* | ||
* The drawables used can be overridden by drawables with the same names defined the application. | ||
* The drawables are: | ||
* | ||
* <ul> | ||
* <li><b>{@code media3_notification_play}</b> - The play icon. | ||
* <li><b>{@code media3_notification_pause}</b> - The pause icon. | ||
* <li><b>{@code media3_notification_seek_to_previous}</b> - The previous icon. | ||
* <li><b>{@code media3_notification_seek_to_next}</b> - The next icon. | ||
* </ul> | ||
*/ | ||
@UnstableApi | ||
/* package */ final class DefaultMediaNotificationProvider implements MediaNotification.Provider { | ||
|
||
private static final int NOTIFICATION_ID = 1001; | ||
private static final String NOTIFICATION_CHANNEL_ID = "default_channel_id"; | ||
private static final String NOTIFICATION_CHANNEL_NAME = "Now playing"; | ||
|
||
private final Context context; | ||
private final NotificationManager notificationManager; | ||
|
||
/** Creates an instance. */ | ||
public DefaultMediaNotificationProvider(Context context) { | ||
this.context = context.getApplicationContext(); | ||
notificationManager = | ||
checkStateNotNull( | ||
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)); | ||
} | ||
|
||
@Override | ||
public MediaNotification createNotification( | ||
MediaController mediaController, | ||
MediaNotification.ActionFactory actionFactory, | ||
Callback onNotificationChangedCallback) { | ||
ensureNotificationChannel(); | ||
|
||
NotificationCompat.Builder builder = | ||
new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID); | ||
// TODO(b/193193926): Filter actions depending on the player's available commands. | ||
// Skip to previous action. | ||
builder.addAction( | ||
actionFactory.createMediaAction( | ||
IconCompat.createWithResource(context, R.drawable.media3_notification_seek_to_previous), | ||
context.getString(R.string.media3_controls_seek_to_previous_description), | ||
MediaNotification.ActionFactory.COMMAND_SKIP_TO_PREVIOUS)); | ||
if (mediaController.getPlayWhenReady()) { | ||
// Pause action. | ||
builder.addAction( | ||
actionFactory.createMediaAction( | ||
IconCompat.createWithResource(context, R.drawable.media3_notification_pause), | ||
context.getString(R.string.media3_controls_pause_description), | ||
MediaNotification.ActionFactory.COMMAND_PAUSE)); | ||
} else { | ||
// Play action. | ||
builder.addAction( | ||
actionFactory.createMediaAction( | ||
IconCompat.createWithResource(context, R.drawable.media3_notification_play), | ||
context.getString(R.string.media3_controls_play_description), | ||
MediaNotification.ActionFactory.COMMAND_PLAY)); | ||
} | ||
// Skip to next action. | ||
builder.addAction( | ||
actionFactory.createMediaAction( | ||
IconCompat.createWithResource(context, R.drawable.media3_notification_seek_to_next), | ||
context.getString(R.string.media3_controls_seek_to_next_description), | ||
MediaNotification.ActionFactory.COMMAND_SKIP_TO_NEXT)); | ||
|
||
// Set metadata info in the notification. | ||
MediaMetadata metadata = mediaController.getMediaMetadata(); | ||
builder.setContentTitle(metadata.title).setContentText(metadata.artist); | ||
if (metadata.artworkData != null) { | ||
Bitmap artworkBitmap = | ||
BitmapFactory.decodeByteArray(metadata.artworkData, 0, metadata.artworkData.length); | ||
builder.setLargeIcon(artworkBitmap); | ||
} | ||
|
||
androidx.media.app.NotificationCompat.MediaStyle mediaStyle = | ||
new androidx.media.app.NotificationCompat.MediaStyle() | ||
.setCancelButtonIntent( | ||
actionFactory.createMediaActionPendingIntent( | ||
MediaNotification.ActionFactory.COMMAND_STOP)) | ||
.setShowActionsInCompactView(1 /* Show play/pause button only in compact view */); | ||
|
||
Notification notification = | ||
builder | ||
.setContentIntent(mediaController.getSessionActivity()) | ||
.setDeleteIntent( | ||
actionFactory.createMediaActionPendingIntent( | ||
MediaNotification.ActionFactory.COMMAND_STOP)) | ||
.setOnlyAlertOnce(true) | ||
.setSmallIcon(getSmallIconResId(context)) | ||
.setStyle(mediaStyle) | ||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) | ||
.setOngoing(false) | ||
.build(); | ||
return new MediaNotification(NOTIFICATION_ID, notification); | ||
} | ||
|
||
@Override | ||
public void handleCustomAction(MediaController mediaController, String action, Bundle extras) { | ||
// We don't handle custom commands. | ||
} | ||
|
||
private void ensureNotificationChannel() { | ||
if (Util.SDK_INT < 26 | ||
|| notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) != null) { | ||
return; | ||
} | ||
NotificationChannel channel = | ||
new NotificationChannel( | ||
NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW); | ||
notificationManager.createNotificationChannel(channel); | ||
} | ||
|
||
private static int getSmallIconResId(Context context) { | ||
int appIcon = context.getApplicationInfo().icon; | ||
if (appIcon != 0) { | ||
return appIcon; | ||
} else { | ||
return Util.SDK_INT >= 21 ? R.drawable.media_session_service_notification_ic_music_note : 0; | ||
} | ||
} | ||
} |
Oops, something went wrong.