I'm making a note app that has an option for pinning notes in the notifications.
I'm using foreground service but the problem is when I want to pin more than one note, the notification for the second one replaces the first one.
I'm using each note's unique ID as notificationId. Here's my code :
class MyService : Service() {
lateinit var note: Note
override fun onBind(p0: Intent?): IBinder? {
return null
}
override fun onCreate() {
super.onCreate()
createNotificationChannel()
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
note = intent.getParcelableExtra(NOTIFICATION_MESSAGE_EXTRA)!!
showNotification()
return START_STICKY
}
private fun showNotification() {
val notificationIntent = Intent(this, MyService::class.java)
val pendingIntent = PendingIntent.getActivity(
this, 0, notificationIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("Title")
.setContentText(note.noteText)
.setContentIntent(pendingIntent)
.setGroup(CHANNEL_GROUP_KEY)
.build()
startForeground(note.id, notification)
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
)
val notificationManager =
getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(channel)
}
}
}
You can done this by using
startForeground(notifyId.toInt(), notificationBuilder) in onCreate method of service and then in onStartCommand use
notificationManager.notify(notifyId.toInt(), notificationBuilder);
Basically you need to only use startForeground once and then you need to use notification manager for showing notification. That way you are able to show all the notifications and all are using foreground service
Related
I'm creating a countdown timer and I want to notify the user all the time that the countdown timer runs. Therefore I have created a service which is started by a ViewModel. I use Hilt for dependency injection as I want to inject the service into the ViewModel. Additionally the UI library is jetpack compose. Following is my approach.
This is my service.
#AndroidEntryPoint
class TimerService: Service(){
override fun onBind(p0: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Notifications.createNotification(applicationContext)
return super.onStartCommand(intent, flags, startId)
}
}
This is how notifications are created.
object Notifications {
private var notificationId = UUID.randomUUID().hashCode()
fun createNotification(context: Context){
val notification = NotificationCompat.Builder(context, "ChannelId")
.setSmallIcon(R.drawable.ic_launcher_background)
.setContentTitle("Timer started")
.setContentText("Timer started and running...")
.build()
NotificationManagerCompat.from(context).notify(notificationId, notification)
}
fun createNotificationChannel(context: Context){
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
val name = "timerNotifyChannel"
val description = "Timer Notification Channel"
val importance = NotificationManager.IMPORTANCE_HIGH
// The notification channel
val channel = NotificationChannel("ChannelId", name, importance).apply {
description
}
val notificationManager : NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
}
And a manager for starting and stopping the service.
class TimerServiceManager #Inject constructor(
#ApplicationContext private val applicationContext: Context,
){
private val serviceIntent = Intent(applicationContext, TimerService::class.java)
fun startTimerService(){
ContextCompat.startForegroundService(applicationContext, serviceIntent)
}
fun stopTimerService(){
applicationContext.stopService(serviceIntent)
}
}
The Application.
#HiltAndroidApp
class TimerApp: Application(){}
The TimerViewModel, which starts the service whenever the service is started.
#HiltViewModel
class TimerViewModel #Inject constructor(
private val timerServiceManager: TimerServiceManager,
): ViewModel() {
//...
fun startcountDown(){
//...
countDownTimer = object : CountDownTimer(...){...}
countDownTimer?.start()
timerServiceManage.startTimerService()
}
private fun cancelTimer(){
countDownTimer?.cancel()
_isRunning.postValue(false)
timerServiceManager.stopTimerService()
}
}
And the MainActivity
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
// private val timerViewModel by viewModels<TimerViewModel>()
private val timeViewModel by viewModels<TimeViewModel>()
// val timeViewModel: TimeViewModel = ViewModelProvider(checkNotNull(LocalViewModelStoreOwner.current))[TimeViewModel::class.java]
override fun onCreate(savedInstanceState: Bundle?) {
//...
val timerViewModel = hiltViewModel<TimerViewModel>()
callUI(timerViewModel = timerViewModel, timeViewModel, localConfig = LocalConfiguration.current)
}
}
The problem is when I debug I can see that it calls the ContextCompact.startForegrounService(...) withing theTimerServiceManager.startTimerService() function. But it doesn't start the service. I say it doesn't start because when I put a break point in the onCreated(...) method in the TimerService it's never reached. Why is this failing? What's wrong?
After weeks of trial-and-errors I could show notifications from my app. I was being dumb at the first place for not allowing the application to access notifications in the emulator. So if you're also having this issue make sure your application has access to the notifications.
However I have also made some changes. I don't know whether the app working because of these changes or not.
I removed the Notificationobject and then add that functionality to the TimerService.
#AndroidEntryPoint
class TimerService: Service() {
private lateinit var notificationManager: NotificationManagerCompat
private lateinit var notification: Notification
override fun onCreate() {
// the notification channel creates when the Service is created
super.onCreate()
notificationManager = NotificationManagerCompat.from(this)
createNotificationChannel()
}
#RequiresApi(Build.VERSION_CODES.N)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// when the service starts, a notification will be created
notification = NotificationCompat.Builder(this, TIMER_SERVICE_NOTIFICATION_CHANNEL_ID)
.setContentTitle("Timer service")
.setContentText("Timer running")
.setSmallIcon(R.drawable.ic_launcher_background)
.setOngoing(true) // an ongoing notification means can't dismiss by the user.
.setOnlyAlertOnce(true)
.build()
startForeground(TIMER_SERVICE_NOTIFICATION_ID, notification)
return START_STICKY
}
private fun createNotificationChannel(){
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
val serviceChannel = NotificationChannel(
TIMER_SERVICE_NOTIFICATION_CHANNEL_ID,
getString(R.string.app_name),
NotificationManager.IMPORTANCE_HIGH
)
notificationManager.createNotificationChannel(serviceChannel)
}
}
#RequiresApi(Build.VERSION_CODES.N)
override fun onDestroy() {
super.onDestroy()
stopForeground(STOP_FOREGROUND_REMOVE)
}
override fun onBind(p0: Intent?): IBinder? = null // We don't need a binder
companion object {
public const val TIMER_SERVICE_NOTIFICATION_CHANNEL_ID = "TimerServiceChannel"
public const val TIMER_SERVICE_NOTIFICATION_ID = 69
}
}
The setTimeOutAfter method for notification seems not t be working. My application meets the requirement for minSdk but I do not know why the heads-up-notification dismises from the screen before time elapses.
#Singleton
class NotificationHelper #Inject constructor(
#ApplicationContext private var context: Context,
) {
companion object {
private const val NOTIFICATION_CHANNEL_ID = "NOTIFICATION_CHANNEL"
private const val NOTIFICATION_CHANNEL_NAME = "NOTIFICATION_CHANNEL_NAME"
private const val BROADCAST_REQUEST_CODE = 0
private const val NOTIFICATION_TIME_OUT = 30000L
}
fun getNotificationManager() =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
fun createNotification(
title: String,
subtitle: String = "",
): Notification {
createNotificationChannel()
return createNotificationBase(title, subtitle)
.build()
}
fun <T> createNotification(
title: String,
actionTitle: String,
buttonAction: String,
broadcastReceiver: Class<T>,
subtitle: String = ""
): Notification {
createNotificationChannel()
val intent = Intent(context, broadcastReceiver)
.apply {
action = buttonAction
}
val pendingIntent = pendingIntentWithBroadcast(intent)
return createNotificationBase(title, subtitle)
.setContentIntent(pendingIntent)
.addAction(R.drawable.ic_amazon_blue_logo, actionTitle, pendingIntent)
.build()
}
fun sendNotification(notification: Notification, notificationId: Int) {
getNotificationManager().notify(notificationId, notification)
}
private fun pendingIntentWithBroadcast(intent: Intent): PendingIntent =
PendingIntent.getBroadcast(
context,
BROADCAST_REQUEST_CODE,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
private fun createNotificationChannel() {
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
NOTIFICATION_CHANNEL_NAME,
NotificationManager.IMPORTANCE_HIGH
)
.apply {
lockscreenVisibility = Notification.VISIBILITY_PRIVATE
}
getNotificationManager().createNotificationChannel(channel)
}
private fun createNotificationBase(
title: String,
subtitle: String,
): NotificationCompat.Builder {
return NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_alexa_notification)
.setContentTitle(title)
.setContentText(subtitle)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setTimeoutAfter(NOTIFICATION_TIME_OUT)
.setOnlyAlertOnce(true)
}
fun cancelNotification(notificationId:Int) = getNotificationManager().cancel(notificationId)
}
How can I make this work?
do you want to keep heads-up-always-visible Notification present on screen for TimeOutAfter duration? thats not possible, duration of head-up layout visible on screen is configured in system and you won't override this
setTimeOutAfter value will make Notification auto-dismiss after this period, but only in API26+ (when introduced). There is no guarantee that AppCompat/AndroidX version will handle this method in older system versions, common approach is that compat library just won't call setTimeOutAfter under the hood on APIs below 26 protecting your app from NoSuchMethodException crash. still this isn't same as bringing this feature to older OS versions. but you can do by yourself with e.g.
new Handler(Looper.getMainLooper()).postDelayed (() -> {
getNotificationManager().cancel(notificationId);
}, NOTIFICATION_TIME_OUT);
placed just after getNotificationManager().notify(... call
I was able to achieve the desired result with adding setOngoing(true) to the notification builder.
I want to start an activity even if the app is killed. Below API 29 there is no problem but above 29 even though service is triggered in background startActivity isn't working.
Here is my service class
class MyService : Service() {
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (Build.VERSION.SDK_INT >= 26) {
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(channel)
}
val notificationBuilder = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("setContentTitle")
.setContentText("setContentText")
.build()
val mIntent = Intent(applicationContext, MyActivity::class.java)
mIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(mIntent)
startForeground(NOTIFICATION_ID, notificationBuilder)
return START_NOT_STICKY
}
companion object {
const val CHANNEL_ID = "channel_id"
const val NOTIFICATION_ID = 5
const val CHANNEL_NAME = "channel_name"
}
}
in Manifest I added FOREGROUND_SERVICE permission
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<service android:name=".service.MyService" />
<receiver android:name=".receiver.MyReceiver" />
and here is my receiver class, above API 29 I want to start a service
class MyReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
intent?.let {
if (Build.VERSION.SDK_INT >= 29) {
context?.startForegroundService(Intent(context, MyService::class.java))
} else {
val intentActivity = Intent(context, MyActivity::class.java)
intentActivity.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context?.startActivity(intentActivity)
}
}
}
}
You can use PendingIntent here.
PendingIntent is an intent that will perform at a later time or in other words PendingIntent specifies an action to take in future. The main differences between a pendingIntent and regular intent is pendingIntent will perform at a later time where Normal/Regular intent starts immediately.
It comes with different use cases like this.
PendingIntent.getActivity() — This will start an Activity like calling context.startActivity()
PendingIntent.getBroadcast() — This will perform a Broadcast like calling context.sendBroadcast()
PendingIntent.getService() — This will start a Services like calling context.startService()
PendingIntent.getForegroundService() — This will start a foregroundService like calling context.startForegroundService()
There are tons of examples you'll find on the internet about it. Just google it & it'll get you work done. :)
Is the following the correct way to create a service for Android version O and newer, as well as handle older versions? For O and newer I'd like to create a foreground service. For older versions, I assume a regular service needs to be created? Is the following code correct to handle this or is there a better way?
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Intent(this, ForegroundService::class.java).also { intent ->
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
}
}
}
ForegroundService.kt
class ForegroundService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel("channel_service", "Foreground Service Channel", NotificationManager.IMPORTANCE_DEFAULT)
channel.lightColor = Color.BLUE
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
service.createNotificationChannel(channel)
val notification: Notification = Notification.Builder(this, "channel_service")
.setContentTitle("Title")
.setContentText("Text")
.setSmallIcon(R.drawable.fancy_small_icon)
.build()
startForeground(1, notification)
}
/**
* Do work
*/
stopSelf()
return START_STICKY
}
override fun onBind(intent: Intent): IBinder? {
return null
}
}
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 }