Home | History | Annotate | Download | only in app
      1 /*
      2  * Copyright 2018 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package androidx.media.app;
     18 
     19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
     20 import static androidx.core.app.NotificationCompat.COLOR_DEFAULT;
     21 
     22 import android.app.Notification;
     23 import android.app.PendingIntent;
     24 import android.media.session.MediaSession;
     25 import android.os.Build;
     26 import android.os.Bundle;
     27 import android.os.IBinder;
     28 import android.os.Parcel;
     29 import android.support.v4.media.session.MediaSessionCompat;
     30 import android.view.View;
     31 import android.widget.RemoteViews;
     32 
     33 import androidx.annotation.RequiresApi;
     34 import androidx.annotation.RestrictTo;
     35 import androidx.core.app.BundleCompat;
     36 import androidx.core.app.NotificationBuilderWithBuilderAccessor;
     37 import androidx.media.R;
     38 
     39 /**
     40  * Class containing media specfic {@link androidx.core.app.NotificationCompat.Style styles}
     41  * that you can use with {@link androidx.core.app.NotificationCompat.Builder#setStyle}.
     42  */
     43 public class NotificationCompat {
     44 
     45     private NotificationCompat() {
     46     }
     47 
     48     /**
     49      * Notification style for media playback notifications.
     50      *
     51      * In the expanded form, up to 5
     52      * {@link androidx.core.app.NotificationCompat.Action actions} specified with
     53      * {@link androidx.core.app.NotificationCompat.Builder
     54      * #addAction(int, CharSequence, PendingIntent) addAction} will be shown as icon-only
     55      * pushbuttons, suitable for transport controls. The Bitmap given to
     56      * {@link androidx.core.app.NotificationCompat.Builder
     57      * #setLargeIcon(android.graphics.Bitmap) setLargeIcon()} will
     58      * be treated as album artwork.
     59      *
     60      * Unlike the other styles provided here, MediaStyle can also modify the standard-size
     61      * content view; by providing action indices to
     62      * {@link #setShowActionsInCompactView(int...)} you can promote up to 3 actions to be displayed
     63      * in the standard view alongside the usual content.
     64      *
     65      * Notifications created with MediaStyle will have their category set to
     66      * {@link androidx.core.app.NotificationCompat#CATEGORY_TRANSPORT CATEGORY_TRANSPORT}
     67      * unless you set a different category using
     68      * {@link androidx.core.app.NotificationCompat.Builder#setCategory(String)
     69      * setCategory()}.
     70      *
     71      * Finally, if you attach a {@link MediaSession.Token} using
     72      * {@link NotificationCompat.MediaStyle#setMediaSession}, the
     73      * System UI can identify this as a notification representing an active media session and
     74      * respond accordingly (by showing album artwork in the lockscreen, for example).
     75      *
     76      * To use this style with your Notification, feed it to
     77      * {@link androidx.core.app.NotificationCompat.Builder#setStyle} like so:
     78      * <pre class="prettyprint">
     79      * Notification noti = new NotificationCompat.Builder()
     80      *     .setSmallIcon(R.drawable.ic_stat_player)
     81      *     .setContentTitle(&quot;Track title&quot;)
     82      *     .setContentText(&quot;Artist - Album&quot;)
     83      *     .setLargeIcon(albumArtBitmap))
     84      *     .setStyle(<b>new NotificationCompat.MediaStyle()</b>
     85      *         .setMediaSession(mySession))
     86      *     .build();
     87      * </pre>
     88      *
     89      * @see Notification#bigContentView
     90      */
     91     public static class MediaStyle extends androidx.core.app.NotificationCompat.Style {
     92 
     93         /**
     94          * Extracts a {@link MediaSessionCompat.Token} from the extra values
     95          * in the {@link MediaStyle} {@link Notification notification}.
     96          *
     97          * @param notification The notification to extract a {@link MediaSessionCompat.Token} from.
     98          * @return The {@link MediaSessionCompat.Token} in the {@code notification} if it contains,
     99          *         null otherwise.
    100          */
    101         public static MediaSessionCompat.Token getMediaSession(Notification notification) {
    102             Bundle extras = androidx.core.app.NotificationCompat.getExtras(notification);
    103             if (extras != null) {
    104                 if (Build.VERSION.SDK_INT >= 21) {
    105                     Object tokenInner = extras.getParcelable(
    106                             androidx.core.app.NotificationCompat.EXTRA_MEDIA_SESSION);
    107                     if (tokenInner != null) {
    108                         return MediaSessionCompat.Token.fromToken(tokenInner);
    109                     }
    110                 } else {
    111                     IBinder tokenInner = BundleCompat.getBinder(extras,
    112                             androidx.core.app.NotificationCompat.EXTRA_MEDIA_SESSION);
    113                     if (tokenInner != null) {
    114                         Parcel p = Parcel.obtain();
    115                         p.writeStrongBinder(tokenInner);
    116                         p.setDataPosition(0);
    117                         MediaSessionCompat.Token token =
    118                                 MediaSessionCompat.Token.CREATOR.createFromParcel(p);
    119                         p.recycle();
    120                         return token;
    121                     }
    122                 }
    123             }
    124             return null;
    125         }
    126 
    127         private static final int MAX_MEDIA_BUTTONS_IN_COMPACT = 3;
    128         private static final int MAX_MEDIA_BUTTONS = 5;
    129 
    130         int[] mActionsToShowInCompact = null;
    131         MediaSessionCompat.Token mToken;
    132         boolean mShowCancelButton;
    133         PendingIntent mCancelButtonIntent;
    134 
    135         public MediaStyle() {
    136         }
    137 
    138         public MediaStyle(androidx.core.app.NotificationCompat.Builder builder) {
    139             setBuilder(builder);
    140         }
    141 
    142         /**
    143          * Requests up to 3 actions (by index in the order of addition) to be shown in the compact
    144          * notification view.
    145          *
    146          * @param actions the indices of the actions to show in the compact notification view
    147          */
    148         public MediaStyle setShowActionsInCompactView(int...actions) {
    149             mActionsToShowInCompact = actions;
    150             return this;
    151         }
    152 
    153         /**
    154          * Attaches a {@link MediaSessionCompat.Token} to this Notification
    155          * to provide additional playback information and control to the SystemUI.
    156          */
    157         public MediaStyle setMediaSession(MediaSessionCompat.Token token) {
    158             mToken = token;
    159             return this;
    160         }
    161 
    162         /**
    163          * Sets whether a cancel button at the top right should be shown in the notification on
    164          * platforms before Lollipop.
    165          *
    166          * <p>Prior to Lollipop, there was a bug in the framework which prevented the developer to
    167          * make a notification dismissable again after having used the same notification as the
    168          * ongoing notification for a foreground service. When the notification was posted by
    169          * {@link android.app.Service#startForeground}, but then the service exited foreground mode
    170          * via {@link android.app.Service#stopForeground}, without removing the notification, the
    171          * notification stayed ongoing, and thus not dismissable.
    172          *
    173          * <p>This is a common scenario for media notifications, as this is exactly the service
    174          * lifecycle that happens when playing/pausing media. Thus, a workaround is provided by the
    175          * support library: Instead of making the notification ongoing depending on the playback
    176          * state, the support library provides the ability to add an explicit cancel button to the
    177          * notification.
    178          *
    179          * <p>Note that the notification is enforced to be ongoing if a cancel button is shown to
    180          * provide a consistent user experience.
    181          *
    182          * <p>Also note that this method is a no-op when running on Lollipop and later.
    183          *
    184          * @param show whether to show a cancel button
    185          */
    186         public MediaStyle setShowCancelButton(boolean show) {
    187             if (Build.VERSION.SDK_INT < 21) {
    188                 mShowCancelButton = show;
    189             }
    190             return this;
    191         }
    192 
    193         /**
    194          * Sets the pending intent to be sent when the cancel button is pressed. See {@link
    195          * #setShowCancelButton}.
    196          *
    197          * @param pendingIntent the intent to be sent when the cancel button is pressed
    198          */
    199         public MediaStyle setCancelButtonIntent(PendingIntent pendingIntent) {
    200             mCancelButtonIntent = pendingIntent;
    201             return this;
    202         }
    203 
    204         /**
    205          * @hide
    206          */
    207         @RestrictTo(LIBRARY_GROUP)
    208         @Override
    209         public void apply(NotificationBuilderWithBuilderAccessor builder) {
    210             if (Build.VERSION.SDK_INT >= 21) {
    211                 builder.getBuilder().setStyle(
    212                         fillInMediaStyle(new Notification.MediaStyle()));
    213             } else if (mShowCancelButton) {
    214                 builder.getBuilder().setOngoing(true);
    215             }
    216         }
    217 
    218         @RequiresApi(21)
    219         Notification.MediaStyle fillInMediaStyle(Notification.MediaStyle style) {
    220             if (mActionsToShowInCompact != null) {
    221                 style.setShowActionsInCompactView(mActionsToShowInCompact);
    222             }
    223             if (mToken != null) {
    224                 style.setMediaSession((MediaSession.Token) mToken.getToken());
    225             }
    226             return style;
    227         }
    228 
    229         /**
    230          * @hide
    231          */
    232         @RestrictTo(LIBRARY_GROUP)
    233         @Override
    234         public RemoteViews makeContentView(NotificationBuilderWithBuilderAccessor builder) {
    235             if (Build.VERSION.SDK_INT >= 21) {
    236                 // No custom content view required
    237                 return null;
    238             }
    239             return generateContentView();
    240         }
    241 
    242         RemoteViews generateContentView() {
    243             RemoteViews view = applyStandardTemplate(false /* showSmallIcon */,
    244                     getContentViewLayoutResource(), true /* fitIn1U */);
    245 
    246             final int numActions = mBuilder.mActions.size();
    247             final int numActionsInCompact = mActionsToShowInCompact == null
    248                     ? 0
    249                     : Math.min(mActionsToShowInCompact.length, MAX_MEDIA_BUTTONS_IN_COMPACT);
    250             view.removeAllViews(R.id.media_actions);
    251             if (numActionsInCompact > 0) {
    252                 for (int i = 0; i < numActionsInCompact; i++) {
    253                     if (i >= numActions) {
    254                         throw new IllegalArgumentException(String.format(
    255                                 "setShowActionsInCompactView: action %d out of bounds (max %d)",
    256                                 i, numActions - 1));
    257                     }
    258 
    259                     final androidx.core.app.NotificationCompat.Action action =
    260                             mBuilder.mActions.get(mActionsToShowInCompact[i]);
    261                     final RemoteViews button = generateMediaActionButton(action);
    262                     view.addView(R.id.media_actions, button);
    263                 }
    264             }
    265             if (mShowCancelButton) {
    266                 view.setViewVisibility(R.id.end_padder, View.GONE);
    267                 view.setViewVisibility(R.id.cancel_action, View.VISIBLE);
    268                 view.setOnClickPendingIntent(R.id.cancel_action, mCancelButtonIntent);
    269                 view.setInt(R.id.cancel_action, "setAlpha", mBuilder.mContext
    270                         .getResources().getInteger(R.integer.cancel_button_image_alpha));
    271             } else {
    272                 view.setViewVisibility(R.id.end_padder, View.VISIBLE);
    273                 view.setViewVisibility(R.id.cancel_action, View.GONE);
    274             }
    275             return view;
    276         }
    277 
    278         private RemoteViews generateMediaActionButton(
    279                 androidx.core.app.NotificationCompat.Action action) {
    280             final boolean tombstone = (action.getActionIntent() == null);
    281             RemoteViews button = new RemoteViews(mBuilder.mContext.getPackageName(),
    282                     R.layout.notification_media_action);
    283             button.setImageViewResource(R.id.action0, action.getIcon());
    284             if (!tombstone) {
    285                 button.setOnClickPendingIntent(R.id.action0, action.getActionIntent());
    286             }
    287             if (Build.VERSION.SDK_INT >= 15) {
    288                 button.setContentDescription(R.id.action0, action.getTitle());
    289             }
    290             return button;
    291         }
    292 
    293         int getContentViewLayoutResource() {
    294             return R.layout.notification_template_media;
    295         }
    296 
    297         /**
    298          * @hide
    299          */
    300         @RestrictTo(LIBRARY_GROUP)
    301         @Override
    302         public RemoteViews makeBigContentView(NotificationBuilderWithBuilderAccessor builder) {
    303             if (Build.VERSION.SDK_INT >= 21) {
    304                 // No custom content view required
    305                 return null;
    306             }
    307             return generateBigContentView();
    308         }
    309 
    310         RemoteViews generateBigContentView() {
    311             final int actionCount = Math.min(mBuilder.mActions.size(), MAX_MEDIA_BUTTONS);
    312             RemoteViews big = applyStandardTemplate(false /* showSmallIcon */,
    313                     getBigContentViewLayoutResource(actionCount), false /* fitIn1U */);
    314 
    315             big.removeAllViews(R.id.media_actions);
    316             if (actionCount > 0) {
    317                 for (int i = 0; i < actionCount; i++) {
    318                     final RemoteViews button = generateMediaActionButton(mBuilder.mActions.get(i));
    319                     big.addView(R.id.media_actions, button);
    320                 }
    321             }
    322             if (mShowCancelButton) {
    323                 big.setViewVisibility(R.id.cancel_action, View.VISIBLE);
    324                 big.setInt(R.id.cancel_action, "setAlpha", mBuilder.mContext
    325                         .getResources().getInteger(R.integer.cancel_button_image_alpha));
    326                 big.setOnClickPendingIntent(R.id.cancel_action, mCancelButtonIntent);
    327             } else {
    328                 big.setViewVisibility(R.id.cancel_action, View.GONE);
    329             }
    330             return big;
    331         }
    332 
    333         int getBigContentViewLayoutResource(int actionCount) {
    334             return actionCount <= 3
    335                     ? R.layout.notification_template_big_media_narrow
    336                     : R.layout.notification_template_big_media;
    337         }
    338     }
    339 
    340     /**
    341      * Notification style for media custom views that are decorated by the system.
    342      *
    343      * <p>Instead of providing a media notification that is completely custom, a developer can set
    344      * this style and still obtain system decorations like the notification header with the expand
    345      * affordance and actions.
    346      *
    347      * <p>Use {@link androidx.core.app.NotificationCompat.Builder
    348      * #setCustomContentView(RemoteViews)},
    349      * {@link androidx.core.app.NotificationCompat.Builder
    350      * #setCustomBigContentView(RemoteViews)} and
    351      * {@link androidx.core.app.NotificationCompat.Builder
    352      * #setCustomHeadsUpContentView(RemoteViews)} to set the
    353      * corresponding custom views to display.
    354      *
    355      * <p>To use this style with your Notification, feed it to
    356      * {@link androidx.core.app.NotificationCompat.Builder
    357      * #setStyle(androidx.core.app.NotificationCompat.Style)} like so:
    358      * <pre class="prettyprint">
    359      * Notification noti = new NotificationCompat.Builder()
    360      *     .setSmallIcon(R.drawable.ic_stat_player)
    361      *     .setLargeIcon(albumArtBitmap))
    362      *     .setCustomContentView(contentView)
    363      *     .setStyle(<b>new NotificationCompat.DecoratedMediaCustomViewStyle()</b>
    364      *          .setMediaSession(mySession))
    365      *     .build();
    366      * </pre>
    367      *
    368      * <p>If you are using this style, consider using the corresponding styles like
    369      * {@link androidx.media.R.style#TextAppearance_Compat_Notification_Media} or
    370      * {@link
    371      * androidx.media.R.style#TextAppearance_Compat_Notification_Title_Media} in
    372      * your custom views in order to get the correct styling on each platform version.
    373      *
    374      * @see androidx.core.app.NotificationCompat.DecoratedCustomViewStyle
    375      * @see MediaStyle
    376      */
    377     public static class DecoratedMediaCustomViewStyle extends MediaStyle {
    378 
    379         public DecoratedMediaCustomViewStyle() {
    380         }
    381 
    382         /**
    383          * @hide
    384          */
    385         @RestrictTo(LIBRARY_GROUP)
    386         @Override
    387         public void apply(NotificationBuilderWithBuilderAccessor builder) {
    388             if (Build.VERSION.SDK_INT >= 24) {
    389                 builder.getBuilder().setStyle(
    390                         fillInMediaStyle(new Notification.DecoratedMediaCustomViewStyle()));
    391             } else {
    392                 super.apply(builder);
    393             }
    394         }
    395 
    396         /**
    397          * @hide
    398          */
    399         @RestrictTo(LIBRARY_GROUP)
    400         @Override
    401         public RemoteViews makeContentView(NotificationBuilderWithBuilderAccessor builder) {
    402             if (Build.VERSION.SDK_INT >= 24) {
    403                 // No custom content view required
    404                 return null;
    405             }
    406             boolean hasContentView = mBuilder.getContentView() != null;
    407             if (Build.VERSION.SDK_INT >= 21) {
    408                 // If we are on L/M the media notification will only be colored if the expanded
    409                 // version is of media style, so we have to create a custom view for the collapsed
    410                 // version as well in that case.
    411                 boolean createCustomContent = hasContentView
    412                         || mBuilder.getBigContentView() != null;
    413                 if (createCustomContent) {
    414                     RemoteViews contentView = generateContentView();
    415                     if (hasContentView) {
    416                         buildIntoRemoteViews(contentView, mBuilder.getContentView());
    417                     }
    418                     setBackgroundColor(contentView);
    419                     return contentView;
    420                 }
    421             } else {
    422                 RemoteViews contentView = generateContentView();
    423                 if (hasContentView) {
    424                     buildIntoRemoteViews(contentView, mBuilder.getContentView());
    425                     return contentView;
    426                 }
    427             }
    428             return null;
    429         }
    430 
    431         @Override
    432         int getContentViewLayoutResource() {
    433             return mBuilder.getContentView() != null
    434                     ? R.layout.notification_template_media_custom
    435                     : super.getContentViewLayoutResource();
    436         }
    437 
    438         /**
    439          * @hide
    440          */
    441         @RestrictTo(LIBRARY_GROUP)
    442         @Override
    443         public RemoteViews makeBigContentView(NotificationBuilderWithBuilderAccessor builder) {
    444             if (Build.VERSION.SDK_INT >= 24) {
    445                 // No custom big content view required
    446                 return null;
    447             }
    448             RemoteViews innerView = mBuilder.getBigContentView() != null
    449                     ? mBuilder.getBigContentView()
    450                     : mBuilder.getContentView();
    451             if (innerView == null) {
    452                 // No expandable notification
    453                 return null;
    454             }
    455             RemoteViews bigContentView = generateBigContentView();
    456             buildIntoRemoteViews(bigContentView, innerView);
    457             if (Build.VERSION.SDK_INT >= 21) {
    458                 setBackgroundColor(bigContentView);
    459             }
    460             return bigContentView;
    461         }
    462 
    463         @Override
    464         int getBigContentViewLayoutResource(int actionCount) {
    465             return actionCount <= 3
    466                     ? R.layout.notification_template_big_media_narrow_custom
    467                     : R.layout.notification_template_big_media_custom;
    468         }
    469 
    470         /**
    471          * @hide
    472          */
    473         @RestrictTo(LIBRARY_GROUP)
    474         @Override
    475         public RemoteViews makeHeadsUpContentView(NotificationBuilderWithBuilderAccessor builder) {
    476             if (Build.VERSION.SDK_INT >= 24) {
    477                 // No custom heads up content view required
    478                 return null;
    479             }
    480             RemoteViews innerView = mBuilder.getHeadsUpContentView() != null
    481                     ? mBuilder.getHeadsUpContentView()
    482                     : mBuilder.getContentView();
    483             if (innerView == null) {
    484                 // No expandable notification
    485                 return null;
    486             }
    487             RemoteViews headsUpContentView = generateBigContentView();
    488             buildIntoRemoteViews(headsUpContentView, innerView);
    489             if (Build.VERSION.SDK_INT >= 21) {
    490                 setBackgroundColor(headsUpContentView);
    491             }
    492             return headsUpContentView;
    493         }
    494 
    495         private void setBackgroundColor(RemoteViews views) {
    496             int color = mBuilder.getColor() != COLOR_DEFAULT
    497                     ? mBuilder.getColor()
    498                     : mBuilder.mContext.getResources().getColor(
    499                             R.color.notification_material_background_media_default_color);
    500             views.setInt(R.id.status_bar_latest_event_content, "setBackgroundColor", color);
    501         }
    502     }
    503 }
    504