Media session has "no title" on AOD (Always on display) - android

In my app I displayed a notification with a foreground service, which is in charge of playing music. The notification is handled by
com.google.android.exoplayer2.ui.PlayerNotificationManager
android.support.v4.media.session.MediaSessionCompat
com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
mediaSession = MediaSessionCompat(this, "Player", null, null)
mediaSession.isActive = true
mediaSessionConnector = MediaSessionConnector(mediaSession)
mediaSessionConnector.setPlayer(exoPlayer)
playerNotificationManager = PlayerNotificationManager.createWithNotificationChannel(
this,
"notification_channel_player",
R.string.notification_channel_name_player,
0,
PLAYER_NOTIFICATION_ID,
object : PlayerNotificationManager.MediaDescriptionAdapter {
override fun createCurrentContentIntent(player: Player?): PendingIntent? {
// intent
}
override fun getCurrentLargeIcon(player: Player?, callback: PlayerNotificationManager.BitmapCallback?): Bitmap? {
// large icon
}
override fun getCurrentContentText(player: Player?): String? {
// artist
}
override fun getCurrentContentTitle(player: Player?): String {
// title
}
},
object : NotificationListener {
override fun onNotificationPosted(notificationId: Int, notification: Notification?, ongoing: Boolean) {
startForeground(notificationId, notification)
}
})
playerNotificationManager.setSmallIcon(R.drawable.ic_notification)
// has previous and next
playerNotificationManager.setUseNavigationActions(true)
playerNotificationManager.setUseNavigationActionsInCompactView(true)
// no fast-forward and rewind
playerNotificationManager.setFastForwardIncrementMs(0)
playerNotificationManager.setRewindIncrementMs(0)
// no stop
playerNotificationManager.setUseStopAction(false)
playerNotificationManager.setMediaSessionToken(mediaSession.sessionToken)
playerNotificationManager.setPlayer(exoPlayer)
When the screen is on, there is no problem displaying the content title and text. But when I lock screen and in AOD mode, on my Pixel 3 I see a "No title" displayed. But if I use Apple Music, it displays the title and artists very well.
My app :
Apple music:
My question is, how can I configure this title and text based on my current implementation? Thanks.

I just answer my own question because I have found and solved the problem.
I have only set the notification's media description adapter, but in fact, the media session needs to set metadata too.
Since we are using mediaSessionConnector, it can be setup by passing a QueueNavigator to the mediaSessionConnector, so we can use the player instance and window index to build current media's metadata. e.x:
val timelineQueueNavigator = object : TimelineQueueNavigator(mediaSession) {
override fun getMediaDescription(player: Player?, windowIndex: Int): MediaDescriptionCompat {
player?.let { safePlayer ->
return MediaDescriptionCompat.Builder().apply {
setTitle("......")
setSubtitle("......")
}.build()
}
return MediaDescriptionCompat.Builder().build()
}
}
mediaSessionConnector.setQueueNavigator(timelineQueueNavigator)
Another point is, by default the mediaSessionConnector use MediaSessionConnector.DefaultMediaMetadataProvider. It doesn't setup METADATA_KEY_ARTIST which will be used in AOD mode as the artist. So I have created my own MediaMetadataProvider, adding METADATA_KEY_ARTIST.
if (description.subtitle != null) {
val subTitle = description.subtitle.toString()
builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, subTitle)
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, subTitle)
}

Here I used METADATA_KEY_TITLE and METADATA_KEY_ARTIST for title and the description:
MediaMetaData data = PlayerManager.getInstance().getCurrentMediaMetaData();
Bitmap bitmap = ((BitmapDrawable) AppController.getInstance().getResources().getDrawable(R.drawable.app_logo)).getBitmap();
Bundle extras = new Bundle();
extras.putString(MediaMetadataCompat.METADATA_KEY_TITLE,data.getMediaTitle());
extras.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, data.getMediaAlbum());
return new MediaDescriptionCompat.Builder()
.setIconBitmap(bitmap)
.setExtras(extras)
.build();

You may have to build the notification like this:
Notification.Builder(context, channel).setContentTitle("Title").setContentText("Description").build()
Please, add your code here. It will be easier to help.
EDIT:
You are not returning the title at the adapter:
override fun getCurrentContentTitle(player: Player?): String = "Add the title here"

Related

How to show downloading progress Exoplayer

I am trying to download a video for offline in exoplayer, I want to show downloading progress inside an activity.
How can I bind to the DownloadService in exoplayer. so that I can update the current downloading progress in an activity? I try to override onBind method but there is no onBind method.
DownloadService
class MediaDownloadService : DownloadService(
C.DOWNLOAD_NOTIFICATION_ID, 1000,
C.CHANNEL_ID, R.string.channel_name, R.string.channel_description
) {
private lateinit var downloadManager: DownloadManager
override fun onCreate() {
downloadManager = DownloadUtil.getDownloadManager(this)
downloadManager.addListener(object : DownloadManager.Listener {
override fun onDownloadChanged(downloadManager: DownloadManager, download: Download) {
if (download.bytesDownloaded == download.contentLength) {
toast("Download Completed!")
}
debug(download.failureReason)
}
})
super.onCreate()
}
override fun getDownloadManager(): DownloadManager {
return downloadManager
}
override fun getForegroundNotification(downloads: MutableList<Download>): Notification {
val intent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
val notificationHelper = DownloadNotificationHelper(this, C.CHANNEL_ID)
return notificationHelper.buildProgressNotification(
R.drawable.ic_notification,
pendingIntent,
"simple message",
downloads
)
}
override fun getScheduler(): Scheduler? {
return null
}
inner class DownloadBinder: Binder() {
val service: MediaDownloadService
get() = this#MediaDownloadService
}
}
To show the current download progress in an activity
You need to override and listen to the DownloadTracker.Listener which will enable you to know the different state of your download.
When the state is at Download.STATE_DOWNLOADING start a coroutine/flow to send the current value of the download to your activity.
I have used this flow (which send every 1 second the value of the percentDownload of the download you'd like)
val downloadManager: DownloadManager // is set before in my object
suspend fun getCurrentProgressDownload(uri: Uri?): Flow<Float?> {
var percent: Float? = downloadManager.currentDownloads.find { it.request.uri == uri }?.percentDownloaded
return callbackFlow {
while(percent != null) {
percent = downloadManager.currentDownloads.find { it.request.uri == uri }?.percentDownloaded
offer(percent)
withContext(Dispatchers.IO) {
delay(1000)
}
}
}
}
Display it the way you like
I have created a repository where you can see download progress in the activity, This is merely an example that could use some optimisation.
https://github.com/yoobi/exoplayer-kotlin/tree/master/downloadvideo/src/main/java/io/github/yoobi/downloadvideo
if some thing are not clear don't hesitate to ask

ExoPlayer problems trying to download current track

I am using Exoplayer to create my own music player. I am also adding the option to download the track but I have a problem when I am trying to download the track that I am playing. I add a notification to the download to check the progress of the download and it appears but it even doesn't start. What I think is that it might have some kind of problem with the buffering cache and the download since they are stored in the same folder.
To download the tracks I do the following:
override fun addDownloadTrack(track: Track) {
getIfTrackIsCached.run({ isCached ->
if (!isCached) {
val data = Util.toByteArray(track.title.byteInputStream())
val downloadRequest =
DownloadRequest(track.id, DownloadRequest.TYPE_PROGRESSIVE, Uri.parse(track.href), Collections.emptyList(), track.id, data)
DownloadService.sendAddDownload(context, ExoPlayerDownloadService::class.java, downloadRequest, false)
}
}, ::onError, GetIfTrackIsCached.Params(track.id))
}
This is the DownloadService:
class ExoPlayerDownloadService : DownloadService(
FOREGROUND_NOTIFICATION_ID,
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
Constants.CHANNEL_DOWNLOAD_ID,
R.string.exo_download_notification_channel_name) {
private val manager: DownloadManager by inject()
private val channelIsCached: ChannelPublisher<CachedMedia> by inject(PUBLISHER_IS_CACHED)
private val notificationHelper: DownloadNotificationHelper by inject()
private var nextNotificationId: Int = FOREGROUND_NOTIFICATION_ID
override fun onCreate() {
super.onCreate()
if (!manager.isInitialized) {
manager.maxParallelDownloads = MAX_PARALLEL_DOWNLOADS
}
}
override fun getDownloadManager(): DownloadManager = manager
override fun getForegroundNotification(downloads: MutableList<Download>?): Notification {
var text = ""
var index = 1
downloads?.forEach { text += "${if (downloads.size > 1) "${index++} - " else ""}${Util.fromUtf8Bytes(it.request.data)}\n" }
return notificationHelper.buildProgressNotification(R.drawable.ic_stat_downloading, null, text, downloads)
}
override fun getScheduler(): Scheduler? = null
override fun onDownloadChanged(download: Download?) {
val notification = when (download?.state) {
Download.STATE_COMPLETED -> {
channelIsCached.publish(CachedMedia(download.request.id, true))
notificationHelper.buildDownloadCompletedNotification(R.drawable.ic_stat_download_complete, null, Util.fromUtf8Bytes(download.request.data))
}
Download.STATE_FAILED ->
notificationHelper.buildDownloadFailedNotification(R.drawable.ic_stat_download_failed, null, Util.fromUtf8Bytes(download.request.data))
else -> null
}
notification?.let { NotificationUtil.setNotification(this#ExoPlayerDownloadService, ++nextNotificationId, it) }
}
companion object {
private const val MAX_PARALLEL_DOWNLOADS = 3
private const val FOREGROUND_NOTIFICATION_ID = 2000
}
}
And to create the cache I use this:
SimpleCache(File(androidContext().cacheDir, CACHE_MEDIA_FOLDER), NoOpCacheEvictor(), get<DatabaseProvider>())
How can I avoid conflicts between buffering cache and downloaded files?
I had this issue also, and found the solution!
The downloading documentation states
The CacheDataSource.Factory should be configured as read-only to avoid downloading that content as well during playback.
To do this you must call setCacheWriteDataSinkFactory(null) on your CacheDataSource.Factory object.
This will prevent the stream from writing to the cache, allowing the downloader to write as expected.

Enable Audio method issue in android agora rtc sdk

I am using interactive video broadcasting in my app.
I am attaching class in which I am using live streaming.
I am getting the audio issue when I go back from the live streaming screen to the previous screen. I still listen to the audio of the host.
previously I was using leave channel method and destroying rtc client object, but after implementing this when I go back from streaming class then it closes all users screen who are using this app because of leave channel method. after that, I removed this option from my on destroy method.
Now I am using disable audio method which disables the audio but when I open live streaming class it doesn't enable audio. Enable audio method is not working I also used the mute audio local stream method and rtc handler on user mute audio method.
I am getting error--
"LiveStreamingActivity has leaked IntentReceiver io.agora.rtc.internal.AudioRoutingController$HeadsetBroadcastReceiver#101a7a7
that was originally registered here. Are you missing a call to
unregisterReceiver()? android.app.IntentReceiverLeaked: Activity
com.allin.activities.home.homeActivities.LiveStreamingActivity has
leaked IntentReceiver
io.agora.rtc.internal.AudioRoutingController$HeadsetBroadcastReceiver#101a7a7
that was originally registered here. Are you missing a call to
unregisterReceiver()?"
Receiver is registering in SDK and exception is coming inside the SDK that is jar file I can't edit.
Please help this in resolving my issue as I have to live the app on
play store.
//firstly I have tried this but it automatically stops other
devices streaming.
override fun onDestroy() {
/* if (mRtcEngine != null) {
leaveChannel()
RtcEngine.destroy(mRtcEngine)
mRtcEngine = null
}*/
//second I have tried disabling the audio so that user will
not hear
the host voice
if (mRtcEngine != null) //
{
mRtcEngine!!.disableAudio()
}
super.onDestroy()
}
// then I when I came back from the previous screen to live streaming activity everything is initializing again but the audio is not able to audible.
override fun onResume() {
super.onResume()
Log.e("resume", "resume")
if (mRtcEngine != null) {
mRtcEngine!!.enableAudio()
// mRtcEngine!!.resumeAudio()
}
}
code I am using
//agora rtc engine and handler initialization-----------------
private var mRtcEngine: RtcEngine? = null
private var mRtcEventHandler = object : IRtcEngineEventHandler() {
#SuppressLint("LongLogTag")
override fun onFirstRemoteVideoDecoded(uid: Int, width: Int,
height: Int, elapsed: Int) {
}
override fun onUserOffline(uid: Int, reason: Int) {
runOnUiThread {
val a = reason //if login =0 user is offline
try {
if (mUid == uid) {
if (surfaceView?.parent != null)
(surfaceView?.parent as ViewGroup).removeAllViews()
if (mRtcEngine != null) {
leaveChannel()
RtcEngine.destroy(mRtcEngine)
mRtcEngine = null
}
setResult(IntentConstants.REQUEST_CODE_LIVE_STREAMING)
finish()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
override fun onUserMuteVideo(uid: Int, muted: Boolean) {
runOnUiThread {
// onRemoteUserVideoMuted(uid, muted);
Log.e("video","muted")
}
}
override fun onAudioQuality(uid: Int, quality: Int, delay:
Short, lost: Short) {
super.onAudioQuality(uid, quality, delay, lost)
Log.e("", "")
}
override fun onUserJoined(uid: Int, elapsed: Int) {
// super.onUserJoined(uid, elapsed)
mUid = uid
runOnUiThread {
try {
setupRemoteVideo(mUid!!)
} catch (e: Exception) {
e.printStackTrace()
}
}
Log.e("differnt_uid----", mUid.toString())
}
}
private fun initAgoraEngineAndJoinChannel() {
if(mRtcEngine==null)
{
initializeAgoraEngine()
setupVideoProfile()
}
}
//initializing rtc engine class
#Throws(Exception::class)
private fun initializeAgoraEngine() {
try {
var s = RtcEngine.getSdkVersion()
mRtcEngine = RtcEngine.create(baseContext, AgoraConstants.APPLICATION_ID, mRtcEventHandler)
} catch (e: Exception) {
// Log.e(LOG_TAG, Log.getStackTraceString(e));
throw RuntimeException("NEED TO check rtc sdk init fatal error\n" + Log.getStackTraceString(e))
}
}
#Throws(Exception::class)
private fun setupVideoProfile() {
//mRtcEngine?.muteAllRemoteAudioStreams(true)
// mLogger.log("channelName account = " + channelName + ",uid = " + 0);
mRtcEngine?.enableVideo()
//mRtcEngine.clearVideoCompositingLayout();
mRtcEngine?.enableLocalVideo(false)
mRtcEngine?.setEnableSpeakerphone(false)
mRtcEngine?.muteLocalAudioStream(true)
joinChannel()
mRtcEngine?.setVideoProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING, true)
mRtcEngine?.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING)
mRtcEngine?.setClientRole(Constants.CLIENT_ROLE_AUDIENCE,"")
val speaker = mRtcEngine?.isSpeakerphoneEnabled
val camerafocus = mRtcEngine?.isCameraAutoFocusFaceModeSupported
Log.e("", "")
}
#Throws(Exception::class)
private fun setupRemoteVideo(uid: Int) {
val container = findViewById<FrameLayout>(R.id.fl_video_container)
if (container.childCount >= 1) {
return
}
surfaceView = RtcEngine.CreateRendererView(baseContext)
container.addView(surfaceView)
mRtcEngine?.setupRemoteVideo(VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_HIDDEN, uid))
mRtcEngine?.setRemoteVideoStreamType(uid, 1)
mRtcEngine?.setCameraAutoFocusFaceModeEnabled(false)
mRtcEngine?.muteRemoteAudioStream(uid, false)
mRtcEngine?.adjustPlaybackSignalVolume(0)
// mRtcEngine.setVideoProfile(Constants.VIDEO_PROFILE_180P, false); // Earlier than 2.3.0
surfaceView?.tag = uid // for mark purpose
val audioManager: AudioManager =
this#LiveStreamingActivity.getSystemService(Context.AUDIO_SERVICE) as AudioManager
//audioManager.mode = AudioManager.MODE_IN_CALL
val isConnected: Boolean = audioManager.isWiredHeadsetOn
if (isConnected) {
/* audioManager.isSpeakerphoneOn = false
audioManager.isWiredHeadsetOn = true*/
mRtcEngine?.setEnableSpeakerphone(false)
mRtcEngine?.setDefaultAudioRoutetoSpeakerphone(false)
mRtcEngine?.setSpeakerphoneVolume(0)
mRtcEngine?.enableInEarMonitoring(true)
// Sets the in-ear monitoring volume to 50% of original volume.
mRtcEngine?.setInEarMonitoringVolume(200)
mRtcEngine?.adjustPlaybackSignalVolume(200)
} else {
/* audioManager.isSpeakerphoneOn = true
audioManager.isWiredHeadsetOn = false*/
mRtcEngine?.setEnableSpeakerphone(true)
mRtcEngine?.setDefaultAudioRoutetoSpeakerphone(true)
mRtcEngine?.setSpeakerphoneVolume(50)
mRtcEngine?.adjustPlaybackSignalVolume(50)
mRtcEngine?.enableInEarMonitoring(false)
// Sets the in-ear monitoring volume to 50% of original volume.
mRtcEngine?.setInEarMonitoringVolume(0)
}
Log.e("", "")
}
#Throws(Exception::class)
private fun joinChannel() {
mRtcEngine?.joinChannel(
null,
AgoraConstants.CHANNEL_NAME,
"Extra Optional Data",
0
) // if you do not specify the uid, we will generate the uid for you
}
#Throws(Exception::class)
private fun leaveChannel() {
mRtcEngine!!.leaveChannel()
}
I think first you want to put setupRemoteVideo in onFirstRemoteVideoDecoded callback instead of the onUserJoined callback. Also, in the onDestroy callback, you should call RtcEngine.destroy() instead of RtcEngine.destroy(mRtcEngine).

How to cast web url from android app via miracast?

I try to cast youtube video from my android app to chromecast or smart tv through miracast.
But I can cast only source url for video like https://media.w3.org/2010/05/sintel/trailer.mp4
How can I cast web page with youtube or vimeo url video?
I know that YouTube app can cast video to some smart tv without chromecast and it looks like YouTube TV page. For example here https://www.youtube.com/watch?v=x5ImUYDjocY
I try to use Presentation API to set WebView in it:
#TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
class CastPresentation constructor(outerContext: Context?, display: Display?) : Presentation(outerContext, display) {
override fun onCreate(savedInstanceState: Bundle?) {
val wv = WebView(context)
wv.settings.javaScriptEnabled = true
wv.webChromeClient = WebChromeClient()
wv.loadUrl("https://www.youtube.com/watch?v=DxGLn_Cu5l0")
setContentView(wv)
super.onCreate(savedInstanceState)
}
}
But it doesn't affect. I don't undertand how to use it.
This is how I use it:
#TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
class CastDelegate constructor(private val activity: AppCompatActivity) {
private var mediaRouter: MediaRouter? = null
private var mediaRouteSelector: MediaRouteSelector? = null
// Variables to hold the currently selected route and its playback client
private var route: MediaRouter.RouteInfo? = null
private var remotePlaybackClient: RemotePlaybackClient? = null
private var presentation: Presentation? = null
// Define the Callback object and its methods, save the object in a class variable
private val mediaRouterCallback = object : MediaRouter.Callback() {
override fun onRouteSelected(router: MediaRouter, route: MediaRouter.RouteInfo) {
Timber.d("CastDelegate --> onRouteSelected: route=$route")
if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
// Stop local playback (if necessary)
// ...
// Save the new route
this#CastDelegate.route = route
// Attach a new playback client
remotePlaybackClient = RemotePlaybackClient(activity, this#CastDelegate.route)
// Start remote playback (if necessary)
// ...
updatePresentation()
val uri = Uri.parse("https://media.w3.org/2010/05/sintel/trailer.mp4")
remotePlaybackClient?.play(uri, null, null, 0, null, object: RemotePlaybackClient.ItemActionCallback() {
override fun onResult(data: Bundle?, sessionId: String?, sessionStatus: MediaSessionStatus?, itemId: String?, itemStatus: MediaItemStatus?) {
super.onResult(data, sessionId, sessionStatus, itemId, itemStatus)
}
})
}
}
override fun onRouteUnselected(router: MediaRouter, route: MediaRouter.RouteInfo, reason: Int) {
Timber.d("CastDelegate --> onRouteUnselected: route=$route")
if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
// Changed route: tear down previous client
this#CastDelegate.route?.also {
remotePlaybackClient?.release()
remotePlaybackClient = null
}
// Save the new route
this#CastDelegate.route = route
updatePresentation()
when (reason) {
MediaRouter.UNSELECT_REASON_ROUTE_CHANGED -> {
// Resume local playback (if necessary)
// ...
}
}
}
}
override fun onRoutePresentationDisplayChanged(router: MediaRouter?, route: MediaRouter.RouteInfo?) {
updatePresentation()
}
}
fun onCreate() {
// Get the media router service.
mediaRouter = MediaRouter.getInstance(activity)
// Create a route selector for the type of routes your app supports.
mediaRouteSelector = MediaRouteSelector.Builder()
// These are the framework-supported intents
.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
.build()
// val selectedRoute = mediaRouter?.selectedRoute ?: return
// val presentationDisplay = selectedRoute.presentationDisplay ?: return
// presentation = CastPresentation(activity, presentationDisplay)
// presentation?.show()
}
fun onStart() {
mediaRouteSelector?.also { selector ->
mediaRouter?.addCallback(selector, mediaRouterCallback,
MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY)
}
updatePresentation()
}
fun onStop() {
mediaRouter?.removeCallback(mediaRouterCallback)
presentation?.dismiss()
presentation = null
}
fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
// Attach the MediaRouteSelector to the menu item
val mediaRouteMenuItem = menu?.findItem(R.id.media_route_menu_item)
val mediaRouteActionProvider = MenuItemCompat.getActionProvider(mediaRouteMenuItem) as MediaRouteActionProvider
// Attach the MediaRouteSelector that you built in onCreate()
mediaRouteSelector?.also(mediaRouteActionProvider::setRouteSelector)
}
private fun updatePresentation() {
// Get the current route and its presentation display.
val selectedRoute = mediaRouter?.selectedRoute
val presentationDisplay = selectedRoute?.presentationDisplay
// Dismiss the current presentation if the display has changed.
if (presentation?.display != presentationDisplay) {
Timber.d("CastDelegate --> Dismissing presentation because the current route no longer " + "has a presentation display.")
presentation?.dismiss()
presentation = null
}
// Show a new presentation if needed.
if (presentation == null && presentationDisplay != null) {
Timber.d("CastDelegate --> Showing presentation on display: $presentationDisplay")
presentation = CastPresentation(activity, presentationDisplay)
try {
presentation?.show()
} catch (ex: WindowManager.InvalidDisplayException) {
Timber.d("CastDelegate --> Couldn't show presentation! Display was removed in the meantime.", ex)
presentation = null
}
}
}
}
As a result now playing video https://media.w3.org/2010/05/sintel/trailer.mp4 from
remotePlaybackClient?.play(...)

MessagingStyle Not Displaying Text

New to Android development and I’m trying out the latest addHistoricMessage, and I’m missing something because it’s not displaying anything. On rare occasion the addMessage text is displayed, but the addHistoricMessage is never displayed. addMessage works consistently when using NotificationCompat, but NotificationCompat doesn’t appear to have addHistoricMessage.
Any thoughts appreciated - using androidx.appcompat:appcompat:1.0.2 and compileSdkVersion and targetSdkVersion are both 28.
An example of what I’m seeing is:
Test button that calls notification:
fun test(view: View) {
val job = GlobalScope.launch {
val repository = DataRepository.getInstance(Db.getDb(this#MainActivity))
AlarmReceiver().notifyTest(
this#MainActivity,
repository.upcomingDetail(9),
arrayListOf("Hi!", "Miss you!", "Hello!")
)
}
}
Notification methods and related (less important code removed):
fun notifyTest(context: Context, upcoming: UpcomingDetail, top3Sent: List<String>?) {
//...
#TargetApi(Build.VERSION_CODES.P)
when (Build.VERSION.SDK_INT) {
in 1..27 -> {
with(NotificationManagerCompat.from(context)) {
notify(upcoming.id.toInt(), legacyNotificationBuilder(
context,
upcoming,
noteIntent,
contentPending,
disablePending,
deletePending,
postponePending,
top3Sent
).build())
}
}
else -> context.getSystemService(NotificationManager::class.java)
.notify(upcoming.id.toInt(), notificationBuilder(
context,
upcoming,
noteIntent,
contentPending,
disablePending,
deletePending,
postponePending,
top3Sent
).build())
}
}
#RequiresApi(Build.VERSION_CODES.P)
private fun notificationBuilder(
context: Context,
upcoming: UpcomingDetail,
noteIntent: Intent,
contentPending: PendingIntent,
deletePending: PendingIntent,
disablePending: PendingIntent,
postponePending: PendingIntent,
top3Sent: List<String>?
): Notification.Builder {
val recipient: android.app.Person = android.app.Person.Builder().setName("Darren").setImportant(true).build()
val you: android.app.Person? = null
val messageStyle = Notification.MessagingStyle(recipient)
val message1 = Notification.MessagingStyle.Message("Hello!", Instant.now().minusSeconds(10 * 60).toEpochMilli(), recipient)
messageStyle.addHistoricMessage(message1)
messageStyle.addMessage(Notification.MessagingStyle.Message("Hi", Instant.now().toEpochMilli(), recipient))
val remoteInput: android.app.RemoteInput = android.app.RemoteInput.Builder(upcoming.id.toString()).run {
top3Sent?.let { setChoices(top3Sent.toTypedArray()) }
build()
}
//...
val inputAction = Notification.Action.Builder(0, context.getString(R.string.button_edit), inputPending).run {
addRemoteInput(remoteInput)
build()
}
return Notification.Builder(context, "Input").apply {
setSmallIcon(R.drawable.ic_stat)
style = messageStyle
setAutoCancel(true)
setCategory(Notification.CATEGORY_REMINDER)
setColor(ContextCompat.getColor(context, R.color.secondaryDarkColor))
setContentIntent(contentPending)
setDeleteIntent(deletePending)
setGroup("notifications")
setOnlyAlertOnce(true)
setVisibility(Notification.VISIBILITY_PRIVATE)
addAction(inputAction)
}
}
This is the behavior of historic message
Historic message is not normally shown at the notification. It is a special message that is only shown when user is replying through a RemoteInput. See the image above to see the behaviour. It should only be used when the message is not the main subject of the notification but may give context to a conversation.
Reference: Android MessagingStyle Notification As Clear As Possible

Categories

Resources