Chromium Code Reviews| 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 private static final int WEARABLE_NOTIFICATION_BACKGROUND_WIDTH = 400; |
| 50 private static final int WEARABLE_NOTIFICATION_BACKGROUND_HEIGHT = 400; | |
|
mlamouri (slow - plz ping)
2016/04/07 19:47:34
Where did you find these numbers?
Zhiqiang Zhang (Slow)
2016/04/11 09:26:28
http://developer.android.com/training/wearables/no
| |
| 48 | 51 |
| 49 // We're always used on the UI thread but the LOCK is required by lint when creating the | 52 // We're always used on the UI thread but the LOCK is required by lint when creating the |
| 50 // singleton. | 53 // singleton. |
| 51 private static final Object LOCK = new Object(); | 54 private static final Object LOCK = new Object(); |
| 52 | 55 |
| 53 // Maps the notification ids to their corresponding notification managers. | 56 // Maps the notification ids to their corresponding notification managers. |
| 54 private static SparseArray<MediaNotificationManager> sManagers; | 57 private static SparseArray<MediaNotificationManager> sManagers; |
| 55 | 58 |
| 56 /** | 59 /** |
| 57 * Service used to transform intent requests triggered from the notification into | 60 * Service used to transform intent requests triggered from the notification into |
| (...skipping 415 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 473 } | 476 } |
| 474 mContext.stopService(createIntent(mContext)); | 477 mContext.stopService(createIntent(mContext)); |
| 475 mMediaNotificationInfo = null; | 478 mMediaNotificationInfo = null; |
| 476 } | 479 } |
| 477 | 480 |
| 478 private void hideNotification(int tabId) { | 481 private void hideNotification(int tabId) { |
| 479 if (mMediaNotificationInfo == null || tabId != mMediaNotificationInfo.ta bId) return; | 482 if (mMediaNotificationInfo == null || tabId != mMediaNotificationInfo.ta bId) return; |
| 480 clearNotification(); | 483 clearNotification(); |
| 481 } | 484 } |
| 482 | 485 |
| 483 private RemoteViews createContentView() { | |
| 484 RemoteViews contentView = | |
| 485 new RemoteViews(mContext.getPackageName(), R.layout.playback_not ification_bar); | |
| 486 | |
| 487 // By default, play/pause button is the only one. | |
| 488 int playPauseButtonId = R.id.button1; | |
| 489 // On Android pre-L, dismissing the notification when the service is no longer in foreground | |
| 490 // doesn't work. Instead, a STOP button is shown. | |
| 491 if (mMediaNotificationInfo.supportsSwipeAway() | |
| 492 && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP | |
| 493 || mMediaNotificationInfo.supportsStop()) { | |
| 494 contentView.setOnClickPendingIntent(R.id.button1, | |
| 495 createPendingIntent(ListenerService.ACTION_STOP)); | |
| 496 contentView.setContentDescription(R.id.button1, mStopDescription); | |
| 497 | |
| 498 // If the play/pause needs to be shown, it moves over to the second button from the end. | |
| 499 playPauseButtonId = R.id.button2; | |
| 500 } | |
| 501 | |
| 502 contentView.setTextViewText(R.id.title, mMediaNotificationInfo.metadata. getTitle()); | |
| 503 contentView.setTextViewText(R.id.status, mMediaNotificationInfo.origin); | |
| 504 if (mNotificationIcon != null) { | |
| 505 contentView.setImageViewBitmap(R.id.icon, mNotificationIcon); | |
| 506 } else { | |
| 507 contentView.setImageViewResource(R.id.icon, mMediaNotificationInfo.i con); | |
| 508 } | |
| 509 | |
| 510 if (mMediaNotificationInfo.supportsPlayPause()) { | |
| 511 if (mMediaNotificationInfo.isPaused) { | |
| 512 contentView.setImageViewResource(playPauseButtonId, R.drawable.i c_vidcontrol_play); | |
| 513 contentView.setContentDescription(playPauseButtonId, mPlayDescri ption); | |
| 514 contentView.setOnClickPendingIntent(playPauseButtonId, | |
| 515 createPendingIntent(ListenerService.ACTION_PLAY)); | |
| 516 } else { | |
| 517 // If we're here, the notification supports play/pause button an d is playing. | |
| 518 contentView.setImageViewResource(playPauseButtonId, R.drawable.i c_vidcontrol_pause); | |
| 519 contentView.setContentDescription(playPauseButtonId, mPauseDescr iption); | |
| 520 contentView.setOnClickPendingIntent(playPauseButtonId, | |
| 521 createPendingIntent(ListenerService.ACTION_PAUSE)); | |
| 522 } | |
| 523 | |
| 524 contentView.setViewVisibility(playPauseButtonId, View.VISIBLE); | |
| 525 } else { | |
| 526 contentView.setViewVisibility(playPauseButtonId, View.GONE); | |
| 527 } | |
| 528 | |
| 529 return contentView; | |
| 530 } | |
| 531 | |
| 532 private MediaMetadataCompat createMetadata() { | 486 private MediaMetadataCompat createMetadata() { |
| 533 MediaMetadataCompat.Builder metadataBuilder = new MediaMetadataCompat.Bu ilder(); | 487 MediaMetadataCompat.Builder metadataBuilder = new MediaMetadataCompat.Bu ilder(); |
| 534 | 488 |
| 535 // Choose the image to use as the icon. | 489 // Choose the image to use as the icon. |
| 536 Bitmap mediaSessionImage = mMediaNotificationInfo.image == null ? mDefau ltMediaSessionImage | 490 Bitmap mediaSessionImage = mMediaNotificationInfo.image == null ? mDefau ltMediaSessionImage |
| 537 : mMediaNotificationInfo.image; | 491 : mMediaNotificationInfo.image; |
| 538 | 492 |
| 539 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | 493 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { |
| 540 metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_T ITLE, | 494 metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_T ITLE, |
| 541 mMediaNotificationInfo.metadata.getTitle()); | 495 mMediaNotificationInfo.metadata.getTitle()); |
| 542 metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_S UBTITLE, | 496 metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_S UBTITLE, |
| 543 mMediaNotificationInfo.origin); | 497 mMediaNotificationInfo.origin); |
| 544 metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_I CON, | 498 if (mMediaNotificationInfo.largeIcon == null) { |
| 545 mediaSessionImage); | 499 metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPL AY_ICON, |
| 500 mediaSessionImage); | |
| 501 } else { | |
| 502 metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPL AY_ICON, | |
| 503 scaleBitmapForWearable(mMediaNotificationInfo.largeIcon) ); | |
| 504 } | |
| 546 // 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 |
| 547 // better than the default image. | 506 // better than the default image. |
|
mlamouri (slow - plz ping)
2016/04/07 19:47:34
Should we try to use METADATA_KEY_ART with the lar
Zhiqiang Zhang (Slow)
2016/04/11 09:26:28
Done. Also I merged MediaNotificationInfo.image an
| |
| 548 if (mMediaNotificationInfo.image != null) { | 507 if (mMediaNotificationInfo.image != null) { |
| 549 metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, | 508 metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, |
| 550 mMediaNotificationInfo.image); | 509 mMediaNotificationInfo.image); |
| 551 } | 510 } |
| 552 } else { | 511 } else { |
| 553 metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, | 512 metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, |
| 554 mMediaNotificationInfo.metadata.getTitle()); | 513 mMediaNotificationInfo.metadata.getTitle()); |
| 555 metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, | 514 metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, |
| 556 mMediaNotificationInfo.origin); | 515 mMediaNotificationInfo.origin); |
| 557 metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, medi aSessionImage); | 516 if (mMediaNotificationInfo.largeIcon == null) { |
| 517 metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, mediaSessionImage); | |
| 518 } else { | |
| 519 metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, | |
| 520 mMediaNotificationInfo.largeIcon); | |
| 521 } | |
| 558 } | 522 } |
| 559 | 523 |
| 560 if (!TextUtils.isEmpty(mMediaNotificationInfo.metadata.getArtist())) { | 524 if (!TextUtils.isEmpty(mMediaNotificationInfo.metadata.getArtist())) { |
| 561 metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, | 525 metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, |
| 562 mMediaNotificationInfo.metadata.getArtist()); | 526 mMediaNotificationInfo.metadata.getArtist()); |
| 563 } | 527 } |
| 564 if (!TextUtils.isEmpty(mMediaNotificationInfo.metadata.getAlbum())) { | 528 if (!TextUtils.isEmpty(mMediaNotificationInfo.metadata.getAlbum())) { |
| 565 metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, | 529 metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, |
| 566 mMediaNotificationInfo.metadata.getAlbum()); | 530 mMediaNotificationInfo.metadata.getAlbum()); |
| 567 } | 531 } |
| 568 | 532 |
| 569 return metadataBuilder.build(); | 533 return metadataBuilder.build(); |
| 570 } | 534 } |
| 571 | 535 |
| 572 private void updateNotification() { | 536 private void updateNotification() { |
| 573 if (mService == null) return; | 537 if (mService == null) return; |
| 574 | 538 |
| 575 if (mMediaNotificationInfo == null) return; | 539 if (mMediaNotificationInfo == null) return; |
| 576 | 540 |
| 577 // Android doesn't badge the icons for RemoteViews automatically when | 541 updateMediaSession(); |
| 578 // running the app under the Work profile. | 542 |
| 579 if (mNotificationIcon == null) { | 543 mNotificationBuilder = new NotificationCompat.Builder(mContext); |
| 580 Drawable notificationIconDrawable = ApiCompatibilityUtils.getUserBad gedIcon( | 544 if (ChromeFeatureList.isEnabled(ChromeFeatureList.USE_NEW_MEDIA_NOTIFICA TION)) { |
| 581 mContext, mMediaNotificationInfo.icon); | 545 setMediaStyleLayoutForNotificationBuilder(mNotificationBuilder); |
| 582 mNotificationIcon = drawableToBitmap(notificationIconDrawable); | 546 } else { |
| 547 setCustomLayoutForNotificationBuilder(mNotificationBuilder); | |
| 583 } | 548 } |
| 584 | 549 // TODO: smallIcon set to Chrome's icon? |
| 585 if (mNotificationBuilder == null) { | 550 mNotificationBuilder.setSmallIcon(mMediaNotificationInfo.icon); |
| 586 mNotificationBuilder = new NotificationCompat.Builder(mContext) | 551 mNotificationBuilder.setAutoCancel(false); |
| 587 .setSmallIcon(mMediaNotificationInfo.icon) | 552 mNotificationBuilder.setLocalOnly(true); |
| 588 .setAutoCancel(false) | 553 mNotificationBuilder.setDeleteIntent(createPendingIntent(ListenerService .ACTION_STOP)); |
| 589 .setLocalOnly(true) | |
| 590 .setDeleteIntent(createPendingIntent(ListenerService.ACTION_STOP )); | |
| 591 } | |
| 592 | 554 |
| 593 if (mMediaNotificationInfo.supportsSwipeAway()) { | 555 if (mMediaNotificationInfo.supportsSwipeAway()) { |
| 594 mNotificationBuilder.setOngoing(!mMediaNotificationInfo.isPaused); | 556 mNotificationBuilder.setOngoing(!mMediaNotificationInfo.isPaused); |
| 595 } | 557 } |
| 596 | 558 |
| 597 // The intent will currently only be null when using a custom tab. | 559 // The intent will currently only be null when using a custom tab. |
| 598 // TODO(avayvod) work out what we should do in this case. See https://cr bug.com/585395. | 560 // TODO(avayvod) work out what we should do in this case. See https://cr bug.com/585395. |
| 599 if (mMediaNotificationInfo.contentIntent != null) { | 561 if (mMediaNotificationInfo.contentIntent != null) { |
| 600 mNotificationBuilder.setContentIntent(PendingIntent.getActivity(mCon text, | 562 mNotificationBuilder.setContentIntent(PendingIntent.getActivity(mCon text, |
| 601 mMediaNotificationInfo.tabId, | 563 mMediaNotificationInfo.tabId, |
| 602 mMediaNotificationInfo.contentIntent, 0)); | 564 mMediaNotificationInfo.contentIntent, 0)); |
| 603 } | 565 } |
| 604 | 566 |
| 605 mNotificationBuilder.setContent(createContentView()); | |
| 606 mNotificationBuilder.setVisibility( | 567 mNotificationBuilder.setVisibility( |
| 607 mMediaNotificationInfo.isPrivate ? NotificationCompat.VISIBILITY _PRIVATE | 568 mMediaNotificationInfo.isPrivate ? NotificationCompat.VISIBILITY _PRIVATE |
| 608 : NotificationCompat.VISIBILITY _PUBLIC); | 569 : NotificationCompat.VISIBILITY _PUBLIC); |
| 609 | 570 |
| 610 | |
| 611 if (mMediaNotificationInfo.supportsPlayPause()) { | |
| 612 | |
| 613 if (mMediaSession == null) mMediaSession = createMediaSession(); | |
| 614 try { | |
| 615 // Tell the MediaRouter about the session, so that Chrome can co ntrol the volume | |
| 616 // on the remote cast device (if any). | |
| 617 // Pre-MR1 versions of JB do not have the complete MediaRouter A PIs, | |
| 618 // so getting the MediaRouter instance will throw an exception. | |
| 619 MediaRouter.getInstance(mContext).setMediaSessionCompat(mMediaSe ssion); | |
| 620 } catch (NoSuchMethodError e) { | |
| 621 // Do nothing. Chrome can't be casting without a MediaRouter, so there is nothing | |
| 622 // to do here. | |
| 623 } | |
| 624 mMediaSession.setMetadata(createMetadata()); | |
| 625 | |
| 626 PlaybackStateCompat.Builder playbackStateBuilder = new PlaybackState Compat.Builder() | |
| 627 .setActions(PlaybackStateCompat.ACTION_PLAY | PlaybackStateC ompat.ACTION_PAUSE); | |
| 628 if (mMediaNotificationInfo.isPaused) { | |
| 629 playbackStateBuilder.setState(PlaybackStateCompat.STATE_PAUSED, | |
| 630 PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0f); | |
| 631 } else { | |
| 632 playbackStateBuilder.setState(PlaybackStateCompat.STATE_PLAYING, | |
| 633 PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0f); | |
| 634 } | |
| 635 mMediaSession.setPlaybackState(playbackStateBuilder.build()); | |
| 636 } | |
| 637 | |
| 638 Notification notification = mNotificationBuilder.build(); | 571 Notification notification = mNotificationBuilder.build(); |
| 639 | 572 |
| 640 // We keep the service as a foreground service while the media is playin g. When it is not, | 573 // We keep the service as a foreground service while the media is playin g. When it is not, |
| 641 // the service isn't stopped but is no longer in foreground, thus at a l ower priority. | 574 // the service isn't stopped but is no longer in foreground, thus at a l ower priority. |
| 642 // While the service is in foreground, the associated notification can't be swipped away. | 575 // While the service is in foreground, the associated notification can't be swipped away. |
| 643 // Moving it back to background allows the user to remove the notificati on. | 576 // Moving it back to background allows the user to remove the notificati on. |
| 644 if (mMediaNotificationInfo.supportsSwipeAway() && mMediaNotificationInfo .isPaused) { | 577 if (mMediaNotificationInfo.supportsSwipeAway() && mMediaNotificationInfo .isPaused) { |
| 645 mService.stopForeground(false /* removeNotification */); | 578 mService.stopForeground(false /* removeNotification */); |
| 646 | 579 |
| 647 NotificationManagerCompat manager = NotificationManagerCompat.from(m Context); | 580 NotificationManagerCompat manager = NotificationManagerCompat.from(m Context); |
| 648 manager.notify(mMediaNotificationInfo.id, notification); | 581 manager.notify(mMediaNotificationInfo.id, notification); |
| 649 } else { | 582 } else { |
| 650 mService.startForeground(mMediaNotificationInfo.id, notification); | 583 mService.startForeground(mMediaNotificationInfo.id, notification); |
| 651 } | 584 } |
| 652 } | 585 } |
| 653 | 586 |
| 587 private void updateMediaSession() { | |
| 588 if (!mMediaNotificationInfo.supportsPlayPause()) return; | |
| 589 | |
| 590 if (mMediaSession == null) mMediaSession = createMediaSession(); | |
| 591 | |
| 592 try { | |
| 593 // Tell the MediaRouter about the session, so that Chrome can contro l the volume | |
| 594 // on the remote cast device (if any). | |
| 595 // Pre-MR1 versions of JB do not have the complete MediaRouter APIs, | |
| 596 // so getting the MediaRouter instance will throw an exception. | |
| 597 MediaRouter.getInstance(mContext).setMediaSessionCompat(mMediaSessio n); | |
| 598 } catch (NoSuchMethodError e) { | |
| 599 // Do nothing. Chrome can't be casting without a MediaRouter, so the re is nothing | |
| 600 // to do here. | |
| 601 } | |
| 602 | |
| 603 mMediaSession.setMetadata(createMetadata()); | |
| 604 | |
| 605 PlaybackStateCompat.Builder playbackStateBuilder = new PlaybackStateComp at.Builder() | |
| 606 .setActions(PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompa t.ACTION_PAUSE); | |
| 607 if (mMediaNotificationInfo.isPaused) { | |
| 608 playbackStateBuilder.setState(PlaybackStateCompat.STATE_PAUSED, | |
| 609 PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0f); | |
| 610 } else { | |
| 611 // If notification only supports stop, still pretend | |
| 612 playbackStateBuilder.setState(PlaybackStateCompat.STATE_PLAYING, | |
| 613 PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0f); | |
| 614 } | |
| 615 mMediaSession.setPlaybackState(playbackStateBuilder.build()); | |
| 616 } | |
| 617 | |
| 654 private MediaSessionCompat createMediaSession() { | 618 private MediaSessionCompat createMediaSession() { |
| 655 MediaSessionCompat mediaSession = new MediaSessionCompat( | 619 MediaSessionCompat mediaSession = new MediaSessionCompat( |
| 656 mContext, | 620 mContext, |
| 657 mContext.getString(R.string.app_name), | 621 mContext.getString(R.string.app_name), |
| 658 new ComponentName(mContext.getPackageName(), | 622 new ComponentName(mContext.getPackageName(), |
| 659 getButtonReceiverClassName()), | 623 getButtonReceiverClassName()), |
| 660 null); | 624 null); |
| 661 mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | 625 mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
| 662 | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); | 626 | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); |
| 663 mediaSession.setCallback(mMediaSessionCallback); | 627 mediaSession.setCallback(mMediaSessionCallback); |
| 664 | 628 |
| 665 // TODO(mlamouri): the following code is to work around a bug that hopef ully | 629 // TODO(mlamouri): the following code is to work around a bug that hopef ully |
| 666 // MediaSessionCompat will handle directly. see b/24051980. | 630 // MediaSessionCompat will handle directly. see b/24051980. |
| 667 try { | 631 try { |
| 668 mediaSession.setActive(true); | 632 mediaSession.setActive(true); |
| 669 } catch (NullPointerException e) { | 633 } catch (NullPointerException e) { |
| 670 // Some versions of KitKat do not support AudioManager.registerMedia ButtonIntent | 634 // Some versions of KitKat do not support AudioManager.registerMedia ButtonIntent |
| 671 // with a PendingIntent. They will throw a NullPointerException, in which case | 635 // with a PendingIntent. They will throw a NullPointerException, in which case |
| 672 // they should be able to activate a MediaSessionCompat with only tr ansport | 636 // they should be able to activate a MediaSessionCompat with only tr ansport |
| 673 // controls. | 637 // controls. |
| 674 mediaSession.setActive(false); | 638 mediaSession.setActive(false); |
| 675 mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONT ROLS); | 639 mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONT ROLS); |
| 676 mediaSession.setActive(true); | 640 mediaSession.setActive(true); |
| 677 } | 641 } |
| 678 return mediaSession; | 642 return mediaSession; |
| 679 } | 643 } |
| 680 | 644 |
| 645 private void setMediaStyleLayoutForNotificationBuilder(NotificationCompat.Bu ilder builder) { | |
| 646 NotificationCompat.MediaStyle style = new NotificationCompat.MediaStyle( ); | |
| 647 if (mMediaSession != null) { | |
|
mlamouri (slow - plz ping)
2016/04/07 19:47:34
Is such a thing even possible?
Zhiqiang Zhang (Slow)
2016/04/11 09:26:28
Done. Merged all logic when mNotificationInfo.supp
| |
| 648 style.setMediaSession(mMediaSession.getSessionToken()); | |
| 649 } | |
| 650 | |
| 651 // TODO: with custom notifications, can do: | |
| 652 // - setContentTitle for track title, | |
| 653 // - setContetText() for track artist, | |
| 654 // - setSubText() for track album | |
| 655 // or use subText() for origin and setContentText for artist + album. | |
|
mlamouri (slow - plz ping)
2016/04/07 19:47:34
After we ship the new style, we should see how to
Zhiqiang Zhang (Slow)
2016/04/11 09:26:28
Done.
| |
| 656 builder.setContentTitle(mMediaNotificationInfo.metadata.getTitle()); | |
| 657 builder.setContentText(mMediaNotificationInfo.origin); | |
| 658 // TODO: have our own "Media" icon so we can have large resolution in ca se of we have no | |
| 659 // large icon? | |
| 660 if (mMediaNotificationInfo.largeIcon != null) { | |
| 661 builder.setLargeIcon(mMediaNotificationInfo.largeIcon); | |
| 662 } else { | |
| 663 builder.setLargeIcon(mDefaultMediaSessionImage); | |
| 664 } | |
| 665 builder.setShowWhen(false); | |
| 666 | |
| 667 if (mMediaNotificationInfo.supportsPlayPause()) { | |
|
mlamouri (slow - plz ping)
2016/04/07 19:47:34
Add a TODO: if it doesn't supports play/pause, we
Zhiqiang Zhang (Slow)
2016/04/11 09:26:28
The logic is already that. Added comments.
| |
| 668 if (mMediaNotificationInfo.isPaused) { | |
| 669 builder.addAction(R.drawable.ic_vidcontrol_play, mPlayDescriptio n, | |
| 670 createPendingIntent(ListenerService.ACTION_PLAY)); | |
| 671 } else { | |
| 672 // If we're here, the notification supports play/pause button an d is playing. | |
| 673 builder.addAction(R.drawable.ic_vidcontrol_pause, mPauseDescript ion, | |
| 674 createPendingIntent(ListenerService.ACTION_PAUSE)); | |
| 675 } | |
| 676 style.setShowActionsInCompactView(0); | |
| 677 } | |
| 678 | |
| 679 if (mMediaNotificationInfo.supportsStop()) { | |
| 680 builder.addAction(R.drawable.ic_vidcontrol_stop, mStopDescription, | |
| 681 createPendingIntent(ListenerService.ACTION_STOP)); | |
| 682 } | |
| 683 if (mMediaSession != null) { | |
| 684 builder.setStyle(style); | |
| 685 } | |
| 686 } | |
| 687 | |
| 688 private void setCustomLayoutForNotificationBuilder(NotificationCompat.Builde r builder) { | |
| 689 builder.setContent(createContentView()); | |
| 690 } | |
| 691 | |
| 692 private RemoteViews createContentView() { | |
| 693 RemoteViews contentView = | |
| 694 new RemoteViews(mContext.getPackageName(), R.layout.playback_no tification_bar); | |
| 695 | |
| 696 // By default, play/pause button is the only one. | |
| 697 int playPauseButtonId = R.id.button1; | |
| 698 // On Android pre-L, dismissing the notification when the service is no longer in foreground | |
| 699 // doesn't work. Instead, a STOP button is shown. | |
| 700 if (mMediaNotificationInfo.supportsSwipeAway() | |
| 701 && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP | |
| 702 || mMediaNotificationInfo.supportsStop()) { | |
| 703 contentView.setOnClickPendingIntent(R.id.button1, | |
| 704 createPendingIntent(ListenerService.ACTION_STOP)); | |
| 705 contentView.setContentDescription(R.id.button1, mStopDescription); | |
| 706 | |
| 707 // If the play/pause needs to be shown, it moves over to the second button from the end. | |
| 708 playPauseButtonId = R.id.button2; | |
| 709 } | |
| 710 | |
| 711 contentView.setTextViewText(R.id.title, mMediaNotificationInfo.metadata. getTitle()); | |
| 712 contentView.setTextViewText(R.id.status, mMediaNotificationInfo.origin); | |
| 713 | |
| 714 // Android doesn't badge the icons for RemoteViews automatically when | |
| 715 // running the app under the Work profile. | |
| 716 if (mNotificationIcon == null) { | |
| 717 Drawable notificationIconDrawable = ApiCompatibilityUtils.getUserBad gedIcon( | |
| 718 mContext, mMediaNotificationInfo.icon); | |
| 719 mNotificationIcon = drawableToBitmap(notificationIconDrawable); | |
| 720 } | |
| 721 | |
| 722 if (mNotificationIcon != null) { | |
| 723 contentView.setImageViewBitmap(R.id.icon, mNotificationIcon); | |
| 724 } else { | |
| 725 contentView.setImageViewResource(R.id.icon, mMediaNotificationInfo.i con); | |
| 726 } | |
| 727 | |
| 728 if (mMediaNotificationInfo.supportsPlayPause()) { | |
| 729 if (mMediaNotificationInfo.isPaused) { | |
| 730 contentView.setImageViewResource(playPauseButtonId, R.drawable.i c_vidcontrol_play); | |
| 731 contentView.setContentDescription(playPauseButtonId, mPlayDescri ption); | |
| 732 contentView.setOnClickPendingIntent(playPauseButtonId, | |
| 733 createPendingIntent(ListenerService.ACTION_PLAY)); | |
| 734 } else { | |
| 735 // If we're here, the notification supports play/pause button an d is playing. | |
| 736 contentView.setImageViewResource(playPauseButtonId, R.drawable.i c_vidcontrol_pause); | |
| 737 contentView.setContentDescription(playPauseButtonId, mPauseDescr iption); | |
| 738 contentView.setOnClickPendingIntent(playPauseButtonId, | |
| 739 createPendingIntent(ListenerService.ACTION_PAUSE)); | |
| 740 } | |
| 741 | |
| 742 contentView.setViewVisibility(playPauseButtonId, View.VISIBLE); | |
| 743 } else { | |
| 744 contentView.setViewVisibility(playPauseButtonId, View.GONE); | |
| 745 } | |
| 746 | |
| 747 return contentView; | |
| 748 } | |
| 749 | |
| 750 /** | |
| 751 * Scales the Bitmap to have better looking on Wearable devices. | |
| 752 * The returned Bitmap size will be exactly 400*400. | |
| 753 */ | |
| 754 private Bitmap scaleBitmapForWearable(Bitmap original) { | |
| 755 Bitmap result = Bitmap.createScaledBitmap( | |
| 756 original, | |
| 757 WEARABLE_NOTIFICATION_BACKGROUND_WIDTH, | |
| 758 WEARABLE_NOTIFICATION_BACKGROUND_HEIGHT, | |
| 759 true); | |
| 760 return result; | |
| 761 } | |
| 762 | |
| 681 private Bitmap drawableToBitmap(Drawable drawable) { | 763 private Bitmap drawableToBitmap(Drawable drawable) { |
| 682 if (!(drawable instanceof BitmapDrawable)) return null; | 764 if (!(drawable instanceof BitmapDrawable)) return null; |
| 683 | 765 |
| 684 BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; | 766 BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; |
| 685 return bitmapDrawable.getBitmap(); | 767 return bitmapDrawable.getBitmap(); |
| 686 } | 768 } |
| 687 } | 769 } |
| OLD | NEW |