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
Related
I just need to be able to use findViewById(int). I've seen several examples. But I couldn't understand it because it was a little different from me.
I need to know the true value of the checkbox in the notification, and to do that I have to use findViewById(int).
Please help me how can i fix the code so i can reference the checkbox.
The error only occurs with findViewById(int).
Here's the code:
companion object {
const val NOTIFICATION_ID = 100
const val NOTIFICATION_CHANNEL_ID = "1000"
}
override fun onReceive(context: Context, intent: Intent) {
createNotificationChannel(context)
notifyNotification(context)
}
private fun createNotificationChannel(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationChannel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
"기상 알람",
NotificationManager.IMPORTANCE_HIGH
)
NotificationManagerCompat.from(context).createNotificationChannel(notificationChannel)
}
}
private fun notifyNotification(context: Context) {
with(NotificationManagerCompat.from(context)) {
val build = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
.setContentTitle("알람")
.setContentText("일어날 시간입니다.")
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setPriority(NotificationCompat.PRIORITY_HIGH)
notify(NOTIFICATION_ID, build.build())
val firebaseDatabase = FirebaseDatabase.getInstance()
val databaseReference = firebaseDatabase.reference
val cb1 = findViewById<CheckBox>(R.id.checkBox)
if (cb1.isChecked == true) {
databaseReference.child("c").setValue("C")
}
}
}
Can't use requireView
val cb = requireView().findViewById<CheckBox>(R.id.checkbox)
if (cb.ischecked == true)
You cannot access the layout's elements outside the activity, or without a reference to the activity which inflated the layout. You could define a constant value somewhere in your application:
object Constants{
var checked = false
}
and update it on the onChecked event of the checkbox. Then you can access the checked variable in your notification code:
if(Constants.checked){
databaseReference.child("c").setValue("C")
}
Edit, thanks to user tenfour for reminding me:
You can also access the elements of a view, when you have a reference to the root view of the layout.
view.findViewById<CheckBox>(R.id.checkbox).isChecked
I am coding a simple app that measures all available sensors of the android device (Wifi, BT, etc). One of them is the user activity (via ActivityRecognition API), but I can't make it works properly.
I code a class to do everything related to user activity. I want to get only 4 states and one attribute to store the current one:
var VEHICLE = "vehicle"
var WALKING = "walking"
var STILL = "still"
var UNKNOWN = "unknown"
private var current: String? = null
It also includes a BroadcastReceiver object to handle activity transitions:
private var recognitionHandler = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (ActivityRecognitionResult.hasResult(intent)) {
val result = ActivityRecognitionResult.extractResult(intent)
val activity = result.mostProbableActivity
current = when(activity.type) {
DetectedActivity.IN_VEHICLE,
DetectedActivity.ON_BICYCLE -> VEHICLE
DetectedActivity.WALKING,
DetectedActivity.RUNNING -> WALKING
DetectedActivity.STILL -> STILL
else -> UNKNOWN
}
}
}
}
The class also have two methods to define the intent and request:
private fun createIntent() : PendingIntent {
val intent = Intent(context, recognitionHandler.javaClass)
val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0)
context.registerReceiver(recognitionHandler, IntentFilter())
return pendingIntent
}
private fun createRequest() : ActivityTransitionRequest {
val types = listOf(
DetectedActivity.IN_VEHICLE,
DetectedActivity.WALKING,
DetectedActivity.RUNNING,
DetectedActivity.ON_BICYCLE,
DetectedActivity.STILL
)
val transitions = mutableListOf<ActivityTransition>()
types.forEach { activity ->
transitions.add(
ActivityTransition.Builder()
.setActivityType(activity)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_ENTER)
.build()
)
}
return ActivityTransitionRequest(transitions)
}
And also one to start listening:
override fun start(onResult: (res: String?) -> Unit) {
// ...
intent = createIntent()
val request = createRequest()
ActivityRecognition.getClient(context)
.requestActivityTransitionUpdates(request, intent)
.addOnSuccessListener {
Log.d("UserActivity Service info", "listening...")
}
.addOnFailureListener { e ->
Log.d("UserActivity Service error", e.toString())
}
// ...
}
The problem is that the current attribute is always null. I think I have some issues with intent or handler registration, but I have no idea where.
Does someone have any comments? :)
Thanks!
This is your problem. In this code from createIntent():
val intent = Intent(context, recognitionHandler.javaClass)
val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0)
context.registerReceiver(recognitionHandler, IntentFilter())
return pendingIntent
You return a PendingIntent that you use in the call to requestActivityTransitionUpdates(). However, that PendingIntent refers to a dynamically created inner class (your BroadcastReceiver) and Android cannot instantiate that class.
You also additionally call registerReceiver(), however you pass an empty IntentFilter in that call so the registered BroadcastReceiver is never called.
To fix the problem, you can either provide a correctIntentFilter that matches your PendingIntent OR you can refactor your BroadcastReceiver into a proper class (not a private inner class) and make sure that you've added the BroadcastReceiver to your manifest and make it publicly available (exported="true").
Here's an example of how to do this using a BroadcastReceiver:
https://steemit.com/utopian-io/#betheleyo/implementing-android-s-new-activity-recognition-transition-api
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"
Initially I setup a BroadcastReceiver to receive intents from the Nearby Messages API.
class BeaconMessageReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Nearby.getMessagesClient(context).handleIntent(intent, object : MessageListener() {
override fun onFound(message: Message) {
val id = IBeaconId.from(message)
Timber.i("Found iBeacon=$id")
sendNotification(context, "Found iBeacon=$id")
}
override fun onLost(message: Message) {
val id = IBeaconId.from(message)
Timber.i("Lost iBeacon=$id")
sendNotification(context, "Lost iBeacon=$id")
}
})
}
private fun sendNotification(context: Context, text: String) {
Timber.d("Send notification.")
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification = NotificationCompat.Builder(context, Notifications.CHANNEL_GENERAL)
.setContentTitle("Beacons")
.setContentText(text)
.setSmallIcon(R.drawable.ic_notification_white)
.build()
manager.notify(NotificationIdGenerator.nextID(), notification)
}
}
Then registered this receiver in my MainActivity after location permissions have been granted.
class MainActivity : AppCompatActivity() {
// ...
private fun onLocationPermissionsGranted() {
val filter = MessageFilter.Builder()
.includeIBeaconIds(UUID.fromString("B9407F30-F5F8-466E-AFF9-25556B57FEED"), null, null)
.build()
val options = SubscribeOptions.Builder().setStrategy(Strategy.BLE_ONLY).setFilter(filter).build()
Nearby.getMessagesClient(context).subscribe(getPendingIntent(), options)
}
private fun getPendingIntent(): PendingIntent = PendingIntent.getBroadcast(
this, 0, Intent(context, BeaconMessageReceiver::class.java), PendingIntent.FLAG_UPDATE_CURRENT)
}
This worked well while the app was open, but does not work when the app is closed. So I found this example, that demonstrates how to setup an IntentService to receive messages while the app is in the background.
The example does use the Nearby.Messages class, which was deprecated in favor of the MessagesClient. So I replaced the deprecated code with the MessagesClient implementation.
class MainActivity : AppCompatActivity() {
// ...
private fun onLocationPermissionsGranted() {
val filter = MessageFilter.Builder()
.includeIBeaconIds(UUID.fromString("B9407F30-F5F8-466E-AFF9-25556B57FEED"), null, null)
.build()
val options = SubscribeOptions.Builder().setStrategy(Strategy.BLE_ONLY).setFilter(filter).build()
Nearby.getMessagesClient(context).subscribe(getPendingIntent(), options)
.addOnSuccessListener {
Timber.i("Subscribed successfully.")
startService(Intent(this, BeaconMessageIntentService::class.java))
}.addOnFailureListener {
Timber.e(exception, "Subscription failed.")
}
}
private fun getPendingIntent(): PendingIntent = PendingIntent.getBroadcast(
this, 0, Intent(context, BeaconMessageIntentService::class.java), PendingIntent.FLAG_UPDATE_CURRENT)
}
And this is the IntentService (which is almost identical to my BroadcastReceiver).
class BeaconMessageIntentService : IntentService("BeaconMessageIntentService") {
override fun onHandleIntent(intent: Intent?) {
intent?.let {
Nearby.getMessagesClient(this)
.handleIntent(it, object : MessageListener() {
override fun onFound(message: Message) {
val id = IBeaconId.from(message)
Timber.i("Found iBeacon=$id")
sendNotification("Found iBeacon=$id")
}
override fun onLost(message: Message) {
val id = IBeaconId.from(message)
Timber.i("Lost iBeacon=$id")
sendNotification("Lost iBeacon=$id")
}
})
}
}
private fun sendNotification(text: String) {
Timber.d("Send notification.")
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification = NotificationCompat.Builder(this, Notifications.CHANNEL_GENERAL)
.setContentTitle("Beacons")
.setContentText(text)
.setSmallIcon(R.drawable.ic_notification_white)
.build()
manager.notify(NotificationIdGenerator.nextID(), notification)
}
}
onHandleIntent is called, and the Intent is not null; yet for some reason onFound() and onLost() are never called. Why would this be the case?
It's not really a solution but what I found is this (credit to this answer):
I've tried a few configurations including a BroadcastReceiver and adding a JobIntentService to run the code in the background, but every time I got this the onExpired callback which you can set to the SubscribeOptions:
options.setCallback(new SubscribeCallback() {
#Override
public void onExpired() {
super.onExpired();
Toast.makeText(context.get(), "No longer Subscribing!", Toast.LENGTH_SHORT).show();
}
}
When the subscribe occurred in the background it was delayed, but it was still called.
Notes:
1. When I've tested with Strategy.BLE_ONLY I did not get the onFound callback.
2. From Google's documentation:
Background subscriptions consumes less power than foreground
subscriptions, but have higher latency and lower reliability
When testing I found this "lower reliability" to be an understatement: onFound was rarely called and I never got the onLost.
I know this is a late reply, but I had the same problem and found out by debugging that it is an issue related to this error: "Attempting to perform a high-power operation from a non-Activity Context". This can be solved when calling Nearby.getMessagesClient(this) by passing in an activity context instead of this.
In my case I added a class extending Application which helps in returning this context (the below is in java but should be translatable to kotlin easily)
public class MyApplication extends Application {
private Activity currentActivity = null;
public Activity getCurrentActivity(){
return currentActivity;
}
public void setCurrentActivity(Activity mCurrentActivity){
this.currentActivity = mCurrentActivity;
}
}
And in my base activity, from which all activities extend, I set the current activity by calling ((MyApplication) this.getApplicationContext()).setCurrentActivity(this); in the constructor.
My service can then call getMessagesClient with the correct context like below:
final Activity context = ((MyApplication)getApplicationContext()).getCurrentActivity();
Nearby.getMessagesClient(context).[...]
Do not forget to register your Application class in the AndroidManifest:
<application
android:name="com.example.xxx.MyApplication"`
I show a notification with actions to the user, I handle these actions with a BroadcastReceiver, from there I update a realm database, but it doesn't get updated, even though I'm sure(through logs) the transaction gets executed.
NotificationBroadcastReceiver:
override fun onReceive(context: Context, intent: Intent) {
val notionId = intent.getStringExtra(NOTION_ID_EXTRA)
val actionType = intent.getIntExtra(ACTION_TYPE, ACTION_TYPE_PUTBACK)
when (actionType) {
ACTION_TYPE_PUTBACK -> {
Toast.makeText(context, R.string.notion_is_putback, Toast.LENGTH_SHORT).show()
}
ACTION_TYPE_ARCHIVE -> {
NotionsRealm.changeIdleState(notionId, true)
}
}
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.cancel(NotionsReminder.NOTION_NOTIFICATION_ID)
}
NotionsRealm:
fun changeIdleState(id: String, state: Boolean) {
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
val notion = it.where<Notion>().equalTo("id", id).findFirst()
notion?.isArchived = state
debug("${notion?.isArchived}") //prints true to the log, but the data doesn't change.
}
closeRealm(realm)
}
private fun closeRealm(realm: Realm) {
try {
realm.close()
} catch (e: Exception) {
error(e)
} finally {
debug("realm closed")
}
}
edit:
I just let the receiver start an empty activity(with no layout) to handle the database. the same thing happened. I think it's no longer a BroadcastReceiver issue. It's strange, other realm transactions run perfectly in other activities/fragments.
Turns out it's not a problem with realm, it was How I fired the broadcast, I did it this way:
fun notificationAction(context: Context, id: String, actionType: Int): PendingIntent {
return PendingIntent.getBroadcast(
context, actionType,
Intent(context, NotificationBroadcastReceiver::class.java).apply {
putExtra(NotificationBroadcastReceiver.NOTION_ID_EXTRA, id)
putExtra(NotificationBroadcastReceiver.ACTION_TYPE, actionType)
}, 0)
}
I found that the passed id was incorrect, after some searching I found out that I should include this flag in the broadcast: PendingIntent.FLAG_UPDATE_CURRENT so it's like this:
fun notificationAction(context: Context, id: String, actionType: Int): PendingIntent {
return PendingIntent.getBroadcast(
context, actionType,
Intent(context, NotificationBroadcastReceiver::class.java).apply {
putExtra(NotificationBroadcastReceiver.NOTION_ID_EXTRA, id)
putExtra(NotificationBroadcastReceiver.ACTION_TYPE, actionType)
}, PendingIntent.FLAG_UPDATE_CURRENT)
}
Now the passed id is the right one, I still don't understand why did this happen, or why was the id extra completely different(yet not random, I kept seeing the same wrong id every time) without this flag.