OLD | NEW |
1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 package org.chromium.chrome.browser.media.ui; | 5 package org.chromium.chrome.browser.media.ui; |
6 | 6 |
7 import android.app.Notification; | 7 import android.app.Notification; |
8 import android.app.PendingIntent; | 8 import android.app.PendingIntent; |
9 import android.app.Service; | 9 import android.app.Service; |
10 import android.content.BroadcastReceiver; | 10 import android.content.BroadcastReceiver; |
11 import android.content.ComponentName; | 11 import android.content.ComponentName; |
12 import android.content.Context; | 12 import android.content.Context; |
13 import android.content.Intent; | 13 import android.content.Intent; |
14 import android.content.IntentFilter; | 14 import android.content.IntentFilter; |
15 import android.graphics.Bitmap; | 15 import android.graphics.Bitmap; |
16 import android.graphics.drawable.BitmapDrawable; | 16 import android.graphics.drawable.BitmapDrawable; |
17 import android.graphics.drawable.Drawable; | 17 import android.graphics.drawable.Drawable; |
18 import android.media.AudioManager; | 18 import android.media.AudioManager; |
19 import android.os.Build; | 19 import android.os.Build; |
20 import android.os.IBinder; | 20 import android.os.IBinder; |
21 import android.support.v4.app.NotificationCompat; | |
22 import android.support.v4.app.NotificationManagerCompat; | 21 import android.support.v4.app.NotificationManagerCompat; |
23 import android.support.v4.media.MediaMetadataCompat; | 22 import android.support.v4.media.MediaMetadataCompat; |
24 import android.support.v4.media.session.MediaSessionCompat; | 23 import android.support.v4.media.session.MediaSessionCompat; |
25 import android.support.v4.media.session.PlaybackStateCompat; | 24 import android.support.v4.media.session.PlaybackStateCompat; |
| 25 import android.support.v7.app.NotificationCompat; |
26 import android.support.v7.media.MediaRouter; | 26 import android.support.v7.media.MediaRouter; |
27 import android.text.TextUtils; | 27 import android.text.TextUtils; |
28 import android.util.SparseArray; | 28 import android.util.SparseArray; |
29 import android.view.KeyEvent; | 29 import android.view.KeyEvent; |
30 import android.view.View; | 30 import android.view.View; |
31 import android.widget.RemoteViews; | 31 import android.widget.RemoteViews; |
32 | 32 |
33 import org.chromium.base.ApiCompatibilityUtils; | 33 import org.chromium.base.ApiCompatibilityUtils; |
34 import org.chromium.base.VisibleForTesting; | 34 import org.chromium.base.VisibleForTesting; |
35 import org.chromium.chrome.R; | 35 import org.chromium.chrome.R; |
| 36 import org.chromium.chrome.browser.ChromeFeatureList; |
36 | 37 |
37 import javax.annotation.Nullable; | 38 import javax.annotation.Nullable; |
38 | 39 |
39 /** | 40 /** |
40 * A class for notifications that provide information and optional media control
s for a given media. | 41 * A class for notifications that provide information and optional media control
s for a given media. |
41 * Internally implements a Service for transforming notification Intents into | 42 * Internally implements a Service for transforming notification Intents into |
42 * {@link MediaNotificationListener} calls for all registered listeners. | 43 * {@link MediaNotificationListener} calls for all registered listeners. |
43 * There's one service started for a distinct notification id. | 44 * There's one service started for a distinct notification id. |
44 */ | 45 */ |
45 public class MediaNotificationManager { | 46 public class MediaNotificationManager { |
| 47 private static final String TAG = "MediaNotification"; |
46 | 48 |
47 private static final String TAG = "MediaNotification"; | 49 // The background notification size on Android Wear. See: |
| 50 // http://developer.android.com/training/wearables/notifications/creating.ht
ml |
| 51 private static final int WEARABLE_NOTIFICATION_BACKGROUND_WIDTH = 400; |
| 52 private static final int WEARABLE_NOTIFICATION_BACKGROUND_HEIGHT = 400; |
48 | 53 |
49 // We're always used on the UI thread but the LOCK is required by lint when
creating the | 54 // We're always used on the UI thread but the LOCK is required by lint when
creating the |
50 // singleton. | 55 // singleton. |
51 private static final Object LOCK = new Object(); | 56 private static final Object LOCK = new Object(); |
52 | 57 |
53 // Maps the notification ids to their corresponding notification managers. | 58 // Maps the notification ids to their corresponding notification managers. |
54 private static SparseArray<MediaNotificationManager> sManagers; | 59 private static SparseArray<MediaNotificationManager> sManagers; |
55 | 60 |
56 /** | 61 /** |
57 * Service used to transform intent requests triggered from the notification
into | 62 * Service used to transform intent requests triggered from the notification
into |
(...skipping 417 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
475 } | 480 } |
476 mContext.stopService(createIntent(mContext)); | 481 mContext.stopService(createIntent(mContext)); |
477 mMediaNotificationInfo = null; | 482 mMediaNotificationInfo = null; |
478 } | 483 } |
479 | 484 |
480 private void hideNotification(int tabId) { | 485 private void hideNotification(int tabId) { |
481 if (mMediaNotificationInfo == null || tabId != mMediaNotificationInfo.ta
bId) return; | 486 if (mMediaNotificationInfo == null || tabId != mMediaNotificationInfo.ta
bId) return; |
482 clearNotification(); | 487 clearNotification(); |
483 } | 488 } |
484 | 489 |
485 private RemoteViews createContentView() { | |
486 RemoteViews contentView = | |
487 new RemoteViews(mContext.getPackageName(), R.layout.playback_not
ification_bar); | |
488 | |
489 // By default, play/pause button is the only one. | |
490 int playPauseButtonId = R.id.button1; | |
491 // On Android pre-L, dismissing the notification when the service is no
longer in foreground | |
492 // doesn't work. Instead, a STOP button is shown. | |
493 if (mMediaNotificationInfo.supportsSwipeAway() | |
494 && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP | |
495 || mMediaNotificationInfo.supportsStop()) { | |
496 contentView.setOnClickPendingIntent(R.id.button1, | |
497 createPendingIntent(ListenerService.ACTION_STOP)); | |
498 contentView.setContentDescription(R.id.button1, mStopDescription); | |
499 | |
500 // If the play/pause needs to be shown, it moves over to the second
button from the end. | |
501 playPauseButtonId = R.id.button2; | |
502 } | |
503 | |
504 contentView.setTextViewText(R.id.title, mMediaNotificationInfo.metadata.
getTitle()); | |
505 contentView.setTextViewText(R.id.status, mMediaNotificationInfo.origin); | |
506 if (mNotificationIcon != null) { | |
507 contentView.setImageViewBitmap(R.id.icon, mNotificationIcon); | |
508 } else { | |
509 contentView.setImageViewResource(R.id.icon, mMediaNotificationInfo.i
con); | |
510 } | |
511 | |
512 if (mMediaNotificationInfo.supportsPlayPause()) { | |
513 if (mMediaNotificationInfo.isPaused) { | |
514 contentView.setImageViewResource(playPauseButtonId, R.drawable.i
c_vidcontrol_play); | |
515 contentView.setContentDescription(playPauseButtonId, mPlayDescri
ption); | |
516 contentView.setOnClickPendingIntent(playPauseButtonId, | |
517 createPendingIntent(ListenerService.ACTION_PLAY)); | |
518 } else { | |
519 // If we're here, the notification supports play/pause button an
d is playing. | |
520 contentView.setImageViewResource(playPauseButtonId, R.drawable.i
c_vidcontrol_pause); | |
521 contentView.setContentDescription(playPauseButtonId, mPauseDescr
iption); | |
522 contentView.setOnClickPendingIntent(playPauseButtonId, | |
523 createPendingIntent(ListenerService.ACTION_PAUSE)); | |
524 } | |
525 | |
526 contentView.setViewVisibility(playPauseButtonId, View.VISIBLE); | |
527 } else { | |
528 contentView.setViewVisibility(playPauseButtonId, View.GONE); | |
529 } | |
530 | |
531 return contentView; | |
532 } | |
533 | |
534 private MediaMetadataCompat createMetadata() { | 490 private MediaMetadataCompat createMetadata() { |
535 MediaMetadataCompat.Builder metadataBuilder = new MediaMetadataCompat.Bu
ilder(); | 491 MediaMetadataCompat.Builder metadataBuilder = new MediaMetadataCompat.Bu
ilder(); |
536 | 492 |
537 // Choose the image to use as the icon. | 493 // Choose the image to use as the icon. |
538 Bitmap mediaSessionImage = mMediaNotificationInfo.image == null ? mDefau
ltMediaSessionImage | 494 Bitmap mediaSessionImage = mMediaNotificationInfo.largeIcon == null |
539 : mMediaNotificationInfo.image; | 495 ? mDefaultMediaSessionImage |
| 496 : mMediaNotificationInfo.largeIcon; |
540 | 497 |
541 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | 498 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { |
542 metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_T
ITLE, | 499 metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_T
ITLE, |
543 mMediaNotificationInfo.metadata.getTitle()); | 500 mMediaNotificationInfo.metadata.getTitle()); |
544 metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_S
UBTITLE, | 501 metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_S
UBTITLE, |
545 mMediaNotificationInfo.origin); | 502 mMediaNotificationInfo.origin); |
546 metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_I
CON, | 503 metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_I
CON, |
547 mediaSessionImage); | 504 scaleBitmapForWearable(mediaSessionImage)); |
548 // METADATA_KEY_ART is optional and should only be used if we can pr
ovide something | 505 // METADATA_KEY_ART is optional and should only be used if we can pr
ovide something |
549 // better than the default image. | 506 // better than the default image. |
550 if (mMediaNotificationInfo.image != null) { | 507 if (mMediaNotificationInfo.largeIcon != null) { |
551 metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, | 508 metadataBuilder.putBitmap( |
552 mMediaNotificationInfo.image); | 509 MediaMetadataCompat.METADATA_KEY_ART, mMediaNotification
Info.largeIcon); |
553 } | 510 } |
554 } else { | 511 } else { |
555 metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, | 512 metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, |
556 mMediaNotificationInfo.metadata.getTitle()); | 513 mMediaNotificationInfo.metadata.getTitle()); |
557 metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, | 514 metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, |
558 mMediaNotificationInfo.origin); | 515 mMediaNotificationInfo.origin); |
559 metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, medi
aSessionImage); | 516 metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, medi
aSessionImage); |
560 } | 517 } |
561 | 518 |
562 if (!TextUtils.isEmpty(mMediaNotificationInfo.metadata.getArtist())) { | 519 if (!TextUtils.isEmpty(mMediaNotificationInfo.metadata.getArtist())) { |
563 metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, | 520 metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, |
564 mMediaNotificationInfo.metadata.getArtist()); | 521 mMediaNotificationInfo.metadata.getArtist()); |
565 } | 522 } |
566 if (!TextUtils.isEmpty(mMediaNotificationInfo.metadata.getAlbum())) { | 523 if (!TextUtils.isEmpty(mMediaNotificationInfo.metadata.getAlbum())) { |
567 metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, | 524 metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, |
568 mMediaNotificationInfo.metadata.getAlbum()); | 525 mMediaNotificationInfo.metadata.getAlbum()); |
569 } | 526 } |
570 | 527 |
571 return metadataBuilder.build(); | 528 return metadataBuilder.build(); |
572 } | 529 } |
573 | 530 |
574 private void updateNotification() { | 531 private void updateNotification() { |
575 if (mService == null) return; | 532 if (mService == null) return; |
576 | 533 |
577 if (mMediaNotificationInfo == null) return; | 534 if (mMediaNotificationInfo == null) return; |
578 | 535 |
579 // Android doesn't badge the icons for RemoteViews automatically when | 536 updateMediaSession(); |
580 // running the app under the Work profile. | 537 |
581 if (mNotificationIcon == null) { | 538 mNotificationBuilder = new NotificationCompat.Builder(mContext); |
582 Drawable notificationIconDrawable = ApiCompatibilityUtils.getUserBad
gedIcon( | 539 if (ChromeFeatureList.isEnabled(ChromeFeatureList.MEDIA_STYLE_NOTIFICATI
ON)) { |
583 mContext, mMediaNotificationInfo.icon); | 540 setMediaStyleLayoutForNotificationBuilder(mNotificationBuilder); |
584 mNotificationIcon = drawableToBitmap(notificationIconDrawable); | 541 } else { |
| 542 setCustomLayoutForNotificationBuilder(mNotificationBuilder); |
585 } | 543 } |
586 | 544 mNotificationBuilder.setSmallIcon(mMediaNotificationInfo.icon); |
587 if (mNotificationBuilder == null) { | 545 mNotificationBuilder.setAutoCancel(false); |
588 mNotificationBuilder = new NotificationCompat.Builder(mContext) | 546 mNotificationBuilder.setLocalOnly(true); |
589 .setSmallIcon(mMediaNotificationInfo.icon) | |
590 .setAutoCancel(false) | |
591 .setLocalOnly(true) | |
592 .setDeleteIntent(createPendingIntent(ListenerService.ACTION_STOP
)); | |
593 } | |
594 | 547 |
595 if (mMediaNotificationInfo.supportsSwipeAway()) { | 548 if (mMediaNotificationInfo.supportsSwipeAway()) { |
596 mNotificationBuilder.setOngoing(!mMediaNotificationInfo.isPaused); | 549 mNotificationBuilder.setOngoing(!mMediaNotificationInfo.isPaused); |
597 } | 550 } |
598 | 551 |
599 // The intent will currently only be null when using a custom tab. | 552 // The intent will currently only be null when using a custom tab. |
600 // TODO(avayvod) work out what we should do in this case. See https://cr
bug.com/585395. | 553 // TODO(avayvod) work out what we should do in this case. See https://cr
bug.com/585395. |
601 if (mMediaNotificationInfo.contentIntent != null) { | 554 if (mMediaNotificationInfo.contentIntent != null) { |
602 mNotificationBuilder.setContentIntent(PendingIntent.getActivity(mCon
text, | 555 mNotificationBuilder.setContentIntent(PendingIntent.getActivity(mCon
text, |
603 mMediaNotificationInfo.tabId, | 556 mMediaNotificationInfo.tabId, |
604 mMediaNotificationInfo.contentIntent, 0)); | 557 mMediaNotificationInfo.contentIntent, 0)); |
605 } | 558 } |
606 | 559 |
607 mNotificationBuilder.setContent(createContentView()); | |
608 mNotificationBuilder.setVisibility( | 560 mNotificationBuilder.setVisibility( |
609 mMediaNotificationInfo.isPrivate ? NotificationCompat.VISIBILITY
_PRIVATE | 561 mMediaNotificationInfo.isPrivate ? NotificationCompat.VISIBILITY
_PRIVATE |
610 : NotificationCompat.VISIBILITY
_PUBLIC); | 562 : NotificationCompat.VISIBILITY
_PUBLIC); |
611 | 563 |
612 | |
613 if (mMediaNotificationInfo.supportsPlayPause()) { | |
614 | |
615 if (mMediaSession == null) mMediaSession = createMediaSession(); | |
616 try { | |
617 // Tell the MediaRouter about the session, so that Chrome can co
ntrol the volume | |
618 // on the remote cast device (if any). | |
619 // Pre-MR1 versions of JB do not have the complete MediaRouter A
PIs, | |
620 // so getting the MediaRouter instance will throw an exception. | |
621 MediaRouter.getInstance(mContext).setMediaSessionCompat(mMediaSe
ssion); | |
622 } catch (NoSuchMethodError e) { | |
623 // Do nothing. Chrome can't be casting without a MediaRouter, so
there is nothing | |
624 // to do here. | |
625 } | |
626 mMediaSession.setMetadata(createMetadata()); | |
627 | |
628 PlaybackStateCompat.Builder playbackStateBuilder = new PlaybackState
Compat.Builder() | |
629 .setActions(PlaybackStateCompat.ACTION_PLAY | PlaybackStateC
ompat.ACTION_PAUSE); | |
630 if (mMediaNotificationInfo.isPaused) { | |
631 playbackStateBuilder.setState(PlaybackStateCompat.STATE_PAUSED, | |
632 PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0f); | |
633 } else { | |
634 playbackStateBuilder.setState(PlaybackStateCompat.STATE_PLAYING, | |
635 PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0f); | |
636 } | |
637 mMediaSession.setPlaybackState(playbackStateBuilder.build()); | |
638 } | |
639 | |
640 Notification notification = mNotificationBuilder.build(); | 564 Notification notification = mNotificationBuilder.build(); |
641 | 565 |
642 // We keep the service as a foreground service while the media is playin
g. When it is not, | 566 // We keep the service as a foreground service while the media is playin
g. When it is not, |
643 // the service isn't stopped but is no longer in foreground, thus at a l
ower priority. | 567 // the service isn't stopped but is no longer in foreground, thus at a l
ower priority. |
644 // While the service is in foreground, the associated notification can't
be swipped away. | 568 // While the service is in foreground, the associated notification can't
be swipped away. |
645 // Moving it back to background allows the user to remove the notificati
on. | 569 // Moving it back to background allows the user to remove the notificati
on. |
646 if (mMediaNotificationInfo.supportsSwipeAway() && mMediaNotificationInfo
.isPaused) { | 570 if (mMediaNotificationInfo.supportsSwipeAway() && mMediaNotificationInfo
.isPaused) { |
647 mService.stopForeground(false /* removeNotification */); | 571 mService.stopForeground(false /* removeNotification */); |
648 | 572 |
649 NotificationManagerCompat manager = NotificationManagerCompat.from(m
Context); | 573 NotificationManagerCompat manager = NotificationManagerCompat.from(m
Context); |
650 manager.notify(mMediaNotificationInfo.id, notification); | 574 manager.notify(mMediaNotificationInfo.id, notification); |
651 } else { | 575 } else { |
652 mService.startForeground(mMediaNotificationInfo.id, notification); | 576 mService.startForeground(mMediaNotificationInfo.id, notification); |
653 } | 577 } |
654 } | 578 } |
655 | 579 |
| 580 private void updateMediaSession() { |
| 581 if (!mMediaNotificationInfo.supportsPlayPause()) return; |
| 582 |
| 583 if (mMediaSession == null) mMediaSession = createMediaSession(); |
| 584 |
| 585 try { |
| 586 // Tell the MediaRouter about the session, so that Chrome can contro
l the volume |
| 587 // on the remote cast device (if any). |
| 588 // Pre-MR1 versions of JB do not have the complete MediaRouter APIs, |
| 589 // so getting the MediaRouter instance will throw an exception. |
| 590 MediaRouter.getInstance(mContext).setMediaSessionCompat(mMediaSessio
n); |
| 591 } catch (NoSuchMethodError e) { |
| 592 // Do nothing. Chrome can't be casting without a MediaRouter, so the
re is nothing |
| 593 // to do here. |
| 594 } |
| 595 |
| 596 mMediaSession.setMetadata(createMetadata()); |
| 597 |
| 598 PlaybackStateCompat.Builder playbackStateBuilder = |
| 599 new PlaybackStateCompat.Builder().setActions( |
| 600 PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.AC
TION_PAUSE); |
| 601 if (mMediaNotificationInfo.isPaused) { |
| 602 playbackStateBuilder.setState(PlaybackStateCompat.STATE_PAUSED, |
| 603 PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0f); |
| 604 } else { |
| 605 // If notification only supports stop, still pretend |
| 606 playbackStateBuilder.setState(PlaybackStateCompat.STATE_PLAYING, |
| 607 PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0f); |
| 608 } |
| 609 mMediaSession.setPlaybackState(playbackStateBuilder.build()); |
| 610 } |
| 611 |
656 private MediaSessionCompat createMediaSession() { | 612 private MediaSessionCompat createMediaSession() { |
657 MediaSessionCompat mediaSession = new MediaSessionCompat( | 613 MediaSessionCompat mediaSession = new MediaSessionCompat( |
658 mContext, | 614 mContext, |
659 mContext.getString(R.string.app_name), | 615 mContext.getString(R.string.app_name), |
660 new ComponentName(mContext.getPackageName(), | 616 new ComponentName(mContext.getPackageName(), |
661 getButtonReceiverClassName()), | 617 getButtonReceiverClassName()), |
662 null); | 618 null); |
663 mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | 619 mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
664 | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); | 620 | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); |
665 mediaSession.setCallback(mMediaSessionCallback); | 621 mediaSession.setCallback(mMediaSessionCallback); |
666 | 622 |
667 // TODO(mlamouri): the following code is to work around a bug that hopef
ully | 623 // TODO(mlamouri): the following code is to work around a bug that hopef
ully |
668 // MediaSessionCompat will handle directly. see b/24051980. | 624 // MediaSessionCompat will handle directly. see b/24051980. |
669 try { | 625 try { |
670 mediaSession.setActive(true); | 626 mediaSession.setActive(true); |
671 } catch (NullPointerException e) { | 627 } catch (NullPointerException e) { |
672 // Some versions of KitKat do not support AudioManager.registerMedia
ButtonIntent | 628 // Some versions of KitKat do not support AudioManager.registerMedia
ButtonIntent |
673 // with a PendingIntent. They will throw a NullPointerException, in
which case | 629 // with a PendingIntent. They will throw a NullPointerException, in
which case |
674 // they should be able to activate a MediaSessionCompat with only tr
ansport | 630 // they should be able to activate a MediaSessionCompat with only tr
ansport |
675 // controls. | 631 // controls. |
676 mediaSession.setActive(false); | 632 mediaSession.setActive(false); |
677 mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONT
ROLS); | 633 mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONT
ROLS); |
678 mediaSession.setActive(true); | 634 mediaSession.setActive(true); |
679 } | 635 } |
680 return mediaSession; | 636 return mediaSession; |
681 } | 637 } |
682 | 638 |
| 639 private void setMediaStyleLayoutForNotificationBuilder(NotificationCompat.Bu
ilder builder) { |
| 640 // TODO(zqzhang): After we ship the new style, we should see how to pres
ent the |
| 641 // metadata.artist and metadata.album. See http://crbug.com/599937 |
| 642 builder.setContentTitle(mMediaNotificationInfo.metadata.getTitle()); |
| 643 builder.setContentText(mMediaNotificationInfo.origin); |
| 644 // TODO(zqzhang): Update the default icon when a new one in provided. |
| 645 // See http://crbug.com/600396. |
| 646 if (mMediaNotificationInfo.largeIcon != null) { |
| 647 builder.setLargeIcon(mMediaNotificationInfo.largeIcon); |
| 648 } else { |
| 649 builder.setLargeIcon(mDefaultMediaSessionImage); |
| 650 } |
| 651 // TODO(zqzhang): It's weird that setShowWhen() don't work on K. Calling
setWhen() to force |
| 652 // removing the time. |
| 653 builder.setShowWhen(false).setWhen(0); |
| 654 |
| 655 // Only apply MediaStyle when NotificationInfo supports play/pause. |
| 656 if (mMediaNotificationInfo.supportsPlayPause()) { |
| 657 NotificationCompat.MediaStyle style = new NotificationCompat.MediaSt
yle(); |
| 658 style.setMediaSession(mMediaSession.getSessionToken()); |
| 659 |
| 660 if (mMediaNotificationInfo.isPaused) { |
| 661 builder.addAction(R.drawable.ic_vidcontrol_play, mPlayDescriptio
n, |
| 662 createPendingIntent(ListenerService.ACTION_PLAY)); |
| 663 } else { |
| 664 // If we're here, the notification supports play/pause button an
d is playing. |
| 665 builder.addAction(R.drawable.ic_vidcontrol_pause, mPauseDescript
ion, |
| 666 createPendingIntent(ListenerService.ACTION_PAUSE)); |
| 667 } |
| 668 style.setShowActionsInCompactView(0); |
| 669 style.setCancelButtonIntent(createPendingIntent(ListenerService.ACTI
ON_STOP)); |
| 670 style.setShowCancelButton(true); |
| 671 builder.setStyle(style); |
| 672 } |
| 673 |
| 674 if (mMediaNotificationInfo.supportsStop()) { |
| 675 builder.addAction(R.drawable.ic_vidcontrol_stop, mStopDescription, |
| 676 createPendingIntent(ListenerService.ACTION_STOP)); |
| 677 } |
| 678 } |
| 679 |
| 680 private void setCustomLayoutForNotificationBuilder(NotificationCompat.Builde
r builder) { |
| 681 builder.setContent(createContentView()); |
| 682 } |
| 683 |
| 684 private RemoteViews createContentView() { |
| 685 RemoteViews contentView = |
| 686 new RemoteViews(mContext.getPackageName(), R.layout.playback_not
ification_bar); |
| 687 |
| 688 // By default, play/pause button is the only one. |
| 689 int playPauseButtonId = R.id.button1; |
| 690 // On Android pre-L, dismissing the notification when the service is no
longer in foreground |
| 691 // doesn't work. Instead, a STOP button is shown. |
| 692 if (mMediaNotificationInfo.supportsSwipeAway() |
| 693 && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP |
| 694 || mMediaNotificationInfo.supportsStop()) { |
| 695 contentView.setOnClickPendingIntent( |
| 696 R.id.button1, createPendingIntent(ListenerService.ACTION_STO
P)); |
| 697 contentView.setContentDescription(R.id.button1, mStopDescription); |
| 698 |
| 699 // If the play/pause needs to be shown, it moves over to the second
button from the end. |
| 700 playPauseButtonId = R.id.button2; |
| 701 } |
| 702 |
| 703 contentView.setTextViewText(R.id.title, mMediaNotificationInfo.metadata.
getTitle()); |
| 704 contentView.setTextViewText(R.id.status, mMediaNotificationInfo.origin); |
| 705 |
| 706 // Android doesn't badge the icons for RemoteViews automatically when |
| 707 // running the app under the Work profile. |
| 708 if (mNotificationIcon == null) { |
| 709 Drawable notificationIconDrawable = |
| 710 ApiCompatibilityUtils.getUserBadgedIcon(mContext, mMediaNoti
ficationInfo.icon); |
| 711 mNotificationIcon = drawableToBitmap(notificationIconDrawable); |
| 712 } |
| 713 |
| 714 if (mNotificationIcon != null) { |
| 715 contentView.setImageViewBitmap(R.id.icon, mNotificationIcon); |
| 716 } else { |
| 717 contentView.setImageViewResource(R.id.icon, mMediaNotificationInfo.i
con); |
| 718 } |
| 719 |
| 720 if (mMediaNotificationInfo.supportsPlayPause()) { |
| 721 if (mMediaNotificationInfo.isPaused) { |
| 722 contentView.setImageViewResource(playPauseButtonId, R.drawable.i
c_vidcontrol_play); |
| 723 contentView.setContentDescription(playPauseButtonId, mPlayDescri
ption); |
| 724 contentView.setOnClickPendingIntent( |
| 725 playPauseButtonId, createPendingIntent(ListenerService.A
CTION_PLAY)); |
| 726 } else { |
| 727 // If we're here, the notification supports play/pause button an
d is playing. |
| 728 contentView.setImageViewResource(playPauseButtonId, R.drawable.i
c_vidcontrol_pause); |
| 729 contentView.setContentDescription(playPauseButtonId, mPauseDescr
iption); |
| 730 contentView.setOnClickPendingIntent( |
| 731 playPauseButtonId, createPendingIntent(ListenerService.A
CTION_PAUSE)); |
| 732 } |
| 733 |
| 734 contentView.setViewVisibility(playPauseButtonId, View.VISIBLE); |
| 735 } else { |
| 736 contentView.setViewVisibility(playPauseButtonId, View.GONE); |
| 737 } |
| 738 |
| 739 return contentView; |
| 740 } |
| 741 |
| 742 /** |
| 743 * Scales the Bitmap to make the notification background on Wearable devices
look better. |
| 744 * The returned Bitmap size will be exactly 400x400. |
| 745 * According to http://developer.android.com/training/wearables/notification
s/creating.html, |
| 746 * the background image of Android Wear notifications should be of size 400x
400 or 640x400. |
| 747 * Otherwise, it will be scaled to fit the desired size. However for some re
ason (maybe battery |
| 748 * concern), the smoothing filter is not applied when scaling the image, so
we need to manually |
| 749 * scale the image with filter applied before sending to Android Wear. |
| 750 */ |
| 751 private Bitmap scaleBitmapForWearable(Bitmap original) { |
| 752 Bitmap result = Bitmap.createScaledBitmap(original, WEARABLE_NOTIFICATIO
N_BACKGROUND_WIDTH, |
| 753 WEARABLE_NOTIFICATION_BACKGROUND_HEIGHT, true); |
| 754 return result; |
| 755 } |
| 756 |
683 private Bitmap drawableToBitmap(Drawable drawable) { | 757 private Bitmap drawableToBitmap(Drawable drawable) { |
684 if (!(drawable instanceof BitmapDrawable)) return null; | 758 if (!(drawable instanceof BitmapDrawable)) return null; |
685 | 759 |
686 BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; | 760 BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; |
687 return bitmapDrawable.getBitmap(); | 761 return bitmapDrawable.getBitmap(); |
688 } | 762 } |
689 } | 763 } |
OLD | NEW |