| 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 |