Skip to content

Commit

Permalink
Create MediaNotification.Provider
Browse files Browse the repository at this point in the history
Define MediaNotification.Provider so that apps can customize
notification UX. Move MediaNotificationManager's functionality
around notifications on DefaultMediaNotificationProvider

PiperOrigin-RevId: 428024699
  • Loading branch information
christosts authored and icbaker committed Feb 21, 2022
1 parent 0367faf commit 71f21cc
Show file tree
Hide file tree
Showing 6 changed files with 730 additions and 273 deletions.
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);
}
}
}
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;
}
}
}
Loading

0 comments on commit 71f21cc

Please sign in to comment.