I'm facing a strange issue in my app's navigation.
I'm having a push Notification managing it with a FirebaseMessagingService class. This works great, I'm receiving the messages all the time, unfortunately my issue appears when I receive a notification.
My Service code looks like this:
class Service : FirebaseMessagingService() {
.....
override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)
if (data != null) {
notificationManagerCompat.from(this).notify(
NOTIFICATION_ID,
createNotification(
title = remoteMessage.notification?.title.orEmpty(),
message = remoteMessage.notification?.body.orEmpty(),
contentIntent = TaskStackBuilder.create(this)
.addNextIntentWithParentStack(
Intent(this, MainActivity::class.java)
)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
)
)
}
}
private fun createNotification(
title: String,
message: String?,
contentIntent: PendingIntent
): Notification {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
getSystemService<NotificationManager>()?.createNotificationChannel(
NotificationChannel(
CHANNEL_ID,
CHANNEL_ID,
NotificationManager.IMPORTANCE_DEFAULT
).apply {
importance = NotificationManager.IMPORTANCE_DEFAULT
}
)
}
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentIntent(contentIntent)
.setContentTitle(title)
.setContentText(message)
.setAutoCancel(true)
.build()
}
}
Where "MainActivity.kt" is a class where I'm trying to "centralize" all my navigation logic (this is a Work in Progress, in a future all the logic will be in the VM).
class MainActivity : AppCompatActivity() {
....
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
val pathSegments = intent.getStringExtra(LINK)?.toUri()?.pathSegments
val pathAction = pathSegments?.first()
val intentToNavigate: Intent? = when {
pathAction == "details" ->
Intent(this#MainActivity, OrdersActivity::class.java).apply {
putExtra(ORDER_ID,pathSegments[1].toInt())
}
pathAction == "rate" ->
Intent(this#MainActivity, PendingActivity::class.java).apply {
putExtra(ORDER_ID,pathSegments[1].toInt())
putExtra(SUPPLIER_ORDER_ID, pathSegments[2].toInt())
}
else -> null
}
viewModel.isOkToMove.observe(
this
) { isComplete ->
lifecycleScope.launch(Dispatchers.Main) {
when {
isComplete && intentToNavigate != null -> {
startActivity(intentToNavigate)
finish()
}
else -> {
startActivity(Intent(this#MainActivity,
HomeActivity::class.java))
finish()
}
}
}
}
}
companion object { private const val LINK = "link" }
}
So, each time I receive a notification and, let's say I'm navigating the app and make a click, I go to the correct Activity but when I press back, I go out of the app (missing all the steps that I had in the Stack).
As I understand, the "TaskStackBuilder" creates a new mini Task maintaining previous stack. But not in my case, what did I miss?
Related
I am using Firebase Cloud Messaging to implement notifications in android app.
I'm new to android. Please help me.
Problem
When I receive a notification in the foreground state, there is unexpected movement.
Expectation: Nothing is done at the timing when the notification is received. I want to process the notification when I tap it.
Actual: BroadcastReceiver's onReceive is called at the timing when the notification is received. It doesn't respond when I tap it ("test onCreate" is not displayed in logcat).
Note: This app is entirely operated by a fragment on MainActivity.
Files
PushNotificationListenerService.kt
class PushNotificationListenerService: FirebaseMessagingService() {
override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)
val channelId = message.notification?.channelId.toString()
val title: String = message.notification?.title.toString()
val text: String = message.notification?.body.toString()
sendNotification(
title,
text,
channelId
)
var broadcast = Intent()
broadcast.action = "FCM_RECEIVE_FOREGROUND"
sendBroadcast(broadcast)
}
private fun sendNotification(
title: String,
text: String,
receivedChannelId: String,
) {
val intent = Intent(this, HomeFragment::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
val pendingIntent =
PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT)
.apply {
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
val notificationBuilder = NotificationCompat.Builder(this, receivedChannelId)
.setContentTitle(title)
.setContentText(text)
.setSmallIcon(R.drawable.ic_menu_logo)
.setContentIntent(pendingIntent)
.setPriority(PRIORITY_MAX)
.setCategory(CATEGORY_CALL)
.setAutoCancel(true)
.setSound(null)
.setVibrate(null)
val notificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val uuid = UUID.randomUUID().hashCode()
notificationManager.notify(uuid, notificationBuilder.build())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationBuilder.setChannelId(receivedChannelId)
}
}
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var navController: NavController
private var mReceiver: BroadcastReceiver? = null
private val mIntentFilter = IntentFilter("FCM_RECEIVE_FOREGROUND")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Timber.d("test onCreate")
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.lifecycleOwner = this
mReceiver = (object: BroadcastReceiver() {
override fun onReceive(p0: Context?, p1: Intent?) {
Timber.d("test onReceive called")
if (p1 == null) { return }
}
})
setupChannels()
}
override fun onResume() {
super.onResume()
registerReceiver(mReceiver, mIntentFilter)
}
override fun onPause() {
if (mReceiver != null) {
unregisterReceiver(mReceiver)
mReceiver = null
}
super.onPause()
}
private fun setupChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val attribute = AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.build()
var uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val channel = NotificationChannel(
"channelId",
"channelName",
NotificationManager.IMPORTANCE_HIGH
).apply {
vibrationPattern = createVibrate()
lightColor = Color.BLUE
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
setSound(uri, attribute)
}
manager.createNotificationChannel(channel)
}
}
}
payload on notification from Firebase Cloud Messaging
const fcmTokenMessage: admin.messaging.TokenMessage = {
android: {
data: {},
notification: {
body: "message",
title: "title",
defaultSound: true,
channelId: "channelId",
sound: 'default',
priority: 'high',
notificationCount: fcmPushData.notificationCount
}
},
token: fcmPushData.fcmPushToken
};
Versions
build.gradle
buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:4.2.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.32"
classpath "org.jetbrains.kotlin:kotlin-serialization:1.4.32"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5"
classpath 'com.google.gms:google-services:4.3.8'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
That's all. I apologize for the inconvenience, and thank you for your cooperation.
Resolved.
The problem was that Fragment was specified as the Intent, and when Activity was specified, it worked.
I've spent some time now trying to get a notification with the battery temperature in it.
At the moment I have managed to make the notification appear. Only when I want to run the code below I get an error with the showNotification() part.
This error indicates:
No value passed for parameter 'context' No value passed for parameter
'intent'
But how do I know what parameters to add to showNotification()?
Anyone who can help me with this?
class MyWorker(context: Context, workerParameters: WorkerParameters) :
Worker(context, workerParameters) {
companion object {
const val CHANNEL_ID = "channel_id"
const val NOTIFICATION_ID = 1
}
override fun doWork(): Result {
Log.d("success",
"doWork: Success function called")
showNotification(HERE DO I GET THE PARAMETER MESSAGE)
return Result.success()
}
private fun showNotification(context: Context, intent: Intent) {
if (intent?.action == "android.intent.action.BATTERY_CHANGED") {
val temp = intent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0) / 10.0
Toast.makeText(context, "${temp}" + "°C", Toast.LENGTH_SHORT).show()
}
val intent = Intent(applicationContext, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent = PendingIntent.getActivity(
applicationContext,
0, intent, 0
)
val notification = NotificationCompat.Builder(
applicationContext,
CHANNEL_ID
)
.setSmallIcon(R.drawable.ic_launcher_background)
.setContentTitle("New Task")
.setContentText("Subscribe on the channel")
.setPriority(NotificationCompat.PRIORITY_MAX)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channelName = "Channel Name"
val channelDescription = "Channel Description"
val channelImportance = NotificationManager.IMPORTANCE_HIGH
val channel = NotificationChannel(CHANNEL_ID, channelName, channelImportance).apply {
description = channelDescription
}
val notificationManager = applicationContext.getSystemService(
Context.NOTIFICATION_SERVICE
) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
with(NotificationManagerCompat.from(applicationContext)) {
notify(NOTIFICATION_ID, notification.build())
}
}
}
There is the MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// simpleWork()
myWorkManager()
}
private fun myWorkManager() {
val constraints = Constraints.Builder()
.setRequiresCharging(false)
.setRequiredNetworkType(NetworkType.NOT_REQUIRED)
.setRequiresCharging(false)
.setRequiresBatteryNotLow(true)
.build()
val myRequest = PeriodicWorkRequest.Builder(
MyWorker::class.java,
15,
TimeUnit.MINUTES
).setConstraints(constraints)
.build()
WorkManager.getInstance(this)
.enqueueUniquePeriodicWork(
"my_id",
ExistingPeriodicWorkPolicy.KEEP,
myRequest
)
}
private fun simpleWork() {
val mRequest: WorkRequest = OneTimeWorkRequestBuilder<MyWorker>()
.build()
WorkManager.getInstance(this)
.enqueue(mRequest)
}
}
You can pass arguments when you create the Request for your WorkManager like this and pass the arguments.
val builder = Data.Builder()
val intent = Intent() // for example, you get your intent here
val data = workDataOf("mydata" to intent)
builder.putAll(data)
val request = OneTimeWorkRequestBuilder<YourWorker>()
.setInputData(builder.build())
.build()
and then receive arguments via
override fun doWork(): Result {
var intent: Intent? = null
intent = inputData.keyValueMap["mydata"] as Intent
if(intent != null) {
showNotification(context, intent)
}
return Result.success()
}
Do tell if that worked for you, you already have the context inside your Worker.
There a function in my intent service that works like a countdown. It is called counter.
What should be added to IntentService or directly into counter to stop this loop after some action in MainActivity?
class IntentServiceExample : IntentService("Loop_test") {
private val CHANNEL_ID = "ForegroundService Kotlin"
companion object {
val PARAM_OUT_MSG = "None"
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel(CHANNEL_ID, "Foreground Service Channel",
NotificationManager.IMPORTANCE_DEFAULT)
val manager = getSystemService(NotificationManager::class.java)
manager!!.createNotificationChannel(serviceChannel)
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
createNotificationChannel()
val notificationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
this,
0, notificationIntent, 0
)
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Foreground Service Kotlin Example")
.setContentText("kylsha")
.setSmallIcon(R.drawable.ic_notification)
.setContentIntent(pendingIntent)
.build()
startForeground(1, notification)
return super.onStartCommand(intent, flags, startId)
}
override fun onHandleIntent(p0: Intent?) {
Toast.makeText(this,"Service started",Toast.LENGTH_LONG).show()
val broadcastIntent = Intent()
broadcastIntent.action = "com.example.intenttest.action.RESPONSE"
broadcastIntent.addCategory(Intent.CATEGORY_DEFAULT)
sendBroadcast(broadcastIntent)
counter(broadcastIntent)
}
fun counter(bc: Intent){
for (i in 1..100){
bc.putExtra(PARAM_OUT_MSG, i.toString())
Thread.sleep(1000)
d("number", i.toString())
sendBroadcast(bc)
}
}
override fun onDestroy() {
super.onDestroy()
stopSelf()
}
}
create a variable in the class.
Create a setter to set the variable true.
In you rcounter routine, check for the variable being set.
private val cancelCounter = false
public fun setToCancel() {
cancelCounter = true
}
/*Stuff*/
fun counter(bc: Intent){
for (i in 1..100){
if (cancelCounter) {
cancelCounter = false
break
}
bc.putExtra(PARAM_OUT_MSG, i.toString())
Thread.sleep(1000)
d("number", i.toString())
sendBroadcast(bc)
}
}
You may not have direct access to the object from main - if not then you should create this class with a singleton pattern ;)
I don't code in kotlin enough to now the "right way" to do it, but some links to the right way:
https://blog.mindorks.com/how-to-create-a-singleton-class-in-kotlin
https://medium.com/swlh/singleton-class-in-kotlin-c3398e7fd76b
Both of these links have some information about why they make the decisions they make in the structure pattern, and some of how the code behind for the implementations works too ;)
For someone who also learn Kotlin I will post my solution as well. It looks pretty simple, however, there probably could be more solutions.
I created a simple Kotlin object like:
object Trigger {
var triggerStop = 0
fun getTrigger(): Int{
return triggerStop
}
}
As you can see variable triggerStop can be changed and called with function getTrigger()
So I added this object into MainActivity to buttons' setOnClickListeners:
class MainActivity : AppCompatActivity() {
lateinit var i:Intent
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
i = Intent(this, IntentServiceExample::class.java)
buttonStart.setOnClickListener{
Trigger.triggerStop = 0 // this variable will be checked in IntentService
startService(i)
}
buttonEnd.setOnClickListener{
Trigger.triggerStop = 1 // this variable will be checked in IntentService
stopService(i)
}
}
}
Then I put this object into my IntentService. In a loop that I want to be stopped by user interaction I put a check like in #Watachiaieto's answer.
fun counter(bc: Intent){
for (i in 1..100){
val stopIt = Trigger.getTrigger() // get trigger value
if (stopIt == 1) {
break
}
bc.putExtra(PARAM_OUT_MSG, i.toString())
Thread.sleep(1000)
sendBroadcast(bc)
}
}
I've been browsing many topics about resuming an activity from a foreground service without finding any concrete answer to my problem.
I'm trying to put a foreground service in my app, and I want the app to be resumed when clicking on the service notification instead of relaunching it. I've tried using the getLaunchIntentForPackage() method from PackageManager, which is the closest to what I want to do.
However, the activity's onCreate is still being called when resuming the app by clicking on the notification.
So here is my question, how to resume an app from a notification's content intent?
I'm starting my ForegroundService in the activity's onStop so it gets called when the app is killed or sent to background.
override fun onStop() {
super.onStop()
Log.v(TAG, "onStop")
ForegroundService.startService(this, "Hellooooooo, here is the background")
}
ForegroundService
class ForegroundService: Service() {
companion object {
private const val CHANNEL_ID = "ForegroundServiceChannel"
fun startService(context: Context, message: String) {
val startIntent = Intent(context, ForegroundService::class.java)
startIntent.putExtra("inputExtra", message)
ContextCompat.startForegroundService(context, startIntent)
}
fun stopService(context: Context) {
val stopIntent = Intent(context, ForegroundService::class.java)
context.stopService(stopIntent)
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val input = intent!!.getStringExtra("inputExtra")
val launchIntent = packageManager.getLaunchIntentForPackage(APP_PACKAGE)
val contentIntent = PendingIntent.getActivity(applicationContext, 0,
launchIntent, 0)
val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Foreground Service")
.setContentText(input)
.setContentIntent(contentIntent)
.setSmallIcon(R.drawable.ic_call_to_action)
.setOngoing(true)
.build()
startForeground(1, notification)
createNotificationChannel()
return START_NOT_STICKY
}
override fun onBind(p0: Intent?): IBinder? {
return null
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel(
CHANNEL_ID,
"Foreground Service Channel",
NotificationManager.IMPORTANCE_DEFAULT
)
val manager = getSystemService(
NotificationManager::class.java
)
manager?.createNotificationChannel(serviceChannel)
}
}
}
Try set in the manifest for activity android:launchMode="singleInstance".
Do not forget set some your action to your activity intent:
activityIntent.setAction(ACTION_STARTED_FROM_NOTIFICATION);
Override onNewIntent in the activity.
in onCreate and in onNewIntent do check
if(ACTION_STARTED_FROM_NOTIFICATION.equalsIgnoreCase(intent.getAction()))
{ do what you need }
I'm trying to make a simple notification service sent from server via postman work, I think I set everything in the right way within the activities.
FirebaseNotificationActivity :
class FirebasePushNotificationActivity : BaseActivity<FirebasePushNotificationContract.FirebasePushNotificationView, FirebasePushNotificationContract.FirebasePushNotificationPresenter>(),
FirebasePushNotificationContract.FirebasePushNotificationView {
private val TAG = "MyFirebaseToken"
override val layoutResId: Int
get() = R.layout.activity_firebase_push_notification
override fun createPresenter(): FirebasePushNotificationContract.FirebasePushNotificationPresenter {
return FirebasePushNotificationContract.FirebasePushNotificationPresenter()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_firebase_push_notification)
initView()
}
private fun initView() {
//This method will use for fetching Token
Thread(Runnable {
try {
Log.i(TAG, FirebaseInstanceId.getInstance().getToken(getString(R.string.SENDER_ID), "FCM"))
} catch (e: IOException) {
e.printStackTrace()
}
}).start()
}
}
MyFirebaseMessagingService
class MyFirebaseMessagingService: FirebaseMessagingService() {
private val TAG = "MyFirebaseToken"
private lateinit var notificationManager: NotificationManager
private val ADMIN_CHANNEL_ID = "Android4Dev"
override fun onNewToken(token: String?) {
super.onNewToken(token)
Log.i(TAG, token)
}
override fun onMessageReceived(remoteMessage: RemoteMessage?) {
super.onMessageReceived(remoteMessage)
remoteMessage?.let { message ->
Log.i(TAG, message.getData().get("message"))
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
//Setting up Notification channels for android O and above
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
setupNotificationChannels()
}
val notificationId = Random().nextInt(60000)
val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val notificationBuilder = NotificationCompat.Builder(this, ADMIN_CHANNEL_ID)
.setSmallIcon(R.mipmap.ic_launcher) //a resource for your custom small icon
.setContentTitle(message.data["title"]) //the "title" value you sent in your notification
.setContentText(message.data["message"]) //ditto
.setAutoCancel(true) //dismisses the notification on click
.setSound(defaultSoundUri)
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(notificationId /* ID of notification */, notificationBuilder.build())
}
}
#RequiresApi(api = Build.VERSION_CODES.O)
private fun setupNotificationChannels() {
val adminChannelName = getString(R.string.notifications_admin_channel_name)
val adminChannelDescription = getString(R.string.notifications_admin_channel_description)
val adminChannel: NotificationChannel
adminChannel = NotificationChannel(ADMIN_CHANNEL_ID, adminChannelName, NotificationManager.IMPORTANCE_LOW)
adminChannel.description = adminChannelDescription
adminChannel.enableLights(true)
adminChannel.lightColor = Color.RED
adminChannel.enableVibration(true)
notificationManager.createNotificationChannel(adminChannel)
}
}
The problem is that following the guide I have not well understood what to insert in the field "to :" within the definition of the notification in Postman, or rather, I understand that it is necessary to insert the token of the device but I do not know how to get it.
{
"to":
"Add your device token",
"data": {
"title": "Android4Dev",
"message": "Learn Firebase PushNotification From Android4Dev Blog"
}
}
Problem solved, I added in my main activity this :
FirebaseInstanceId.getInstance().instanceId.addOnSuccessListener(this#SplashActivity,
OnSuccessListener<InstanceIdResult> { instanceIdResult ->
val newToken = instanceIdResult.token
Log.e("newToken", newToken)
})