Muhammad Umer
Muhammad Umer

Reputation: 78

Android - AlarmManager not firing BroadcastReceiver to show Local Notifications

I am writing a prayer application which requires the application to show Local Notifications on PrayerTimes. Prayer times and different for each day, thus I am using the following bit of code to show a Location Notification from BroadcastReceiver and right after that schedule next notification.

The problem is, the application is required to open at least once a day for the notifications to keep firing on their specific timings.

Is there a way to schedule BroadcastReceiver using Alarm Manager to fire Local Notifications without opening the app?

fun MakkahPrayer.setNotificationForPrayer(prayer: Prayer, date: Date) {
    val app = App.instance!!.applicationContext
    val preferences = PreferenceManager.getInstance(app)

    if(!preferences.isPrayerAlarmSet(prayer.name)) {
        val calendar = Calendar.getInstance()
        calendar.add(Calendar.DAY_OF_YEAR, 0)

        val dayOfYear = calendar[Calendar.DAY_OF_YEAR]

        NotificationUtils.instance.setNotification(date.time, prayer.name, dayOfYear.toString())
        preferences.setPrayerIsAlarmOn(prayer.name, true)
    }
}

NotificationUtils.kt

class NotificationUtils {
    companion object {
        val instance = NotificationUtils()
    }

    fun setNotification(timeInMilliSeconds: Long, name: String, day: String) {

        val cal = Calendar.getInstance()
        cal.time = Date()
        val millis = cal.timeInMillis

        if (timeInMilliSeconds > 0 && timeInMilliSeconds > millis) {

            val key = name + day

            val alarmManager =
                App.instance?.getSystemService(Activity.ALARM_SERVICE) as AlarmManager
            val alarmIntent = Intent(App.instance?.applicationContext, AlarmReceiver::class.java)

            alarmIntent.putExtra("prayer", name)
            alarmIntent.putExtra("timestamp", timeInMilliSeconds)
            alarmIntent.putExtra("notificationID", key)

            val calendar = Calendar.getInstance()
            calendar.timeInMillis = timeInMilliSeconds

            val pendingIntent = PendingIntent.getBroadcast(
                App.instance,
                timeInMilliSeconds.toInt(),
                alarmIntent,
                PendingIntent.FLAG_UPDATE_CURRENT
            )

            alarmManager.setExact(AlarmManager.RTC_WAKEUP, timeInMilliSeconds, pendingIntent)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, timeInMilliSeconds, pendingIntent)
            } else {
                alarmManager.setExact(AlarmManager.RTC_WAKEUP, timeInMilliSeconds, pendingIntent)
            }
        }
    }
}

AlarmReceiver.kt

class AlarmReceiver : BroadcastReceiver() {
    companion object {
        private lateinit var mNotification: Notification

        const val CHANNEL_ID = "CHANNEL_ID"
        const val CHANNEL_NAME = "Prayer Notification"
    }

    override fun onReceive(context: Context, intent: Intent) {
        val manager = createChannel(context)
        showNotification(context, intent, manager)
        setNextPrayerAlarm(intent)
    }

    private fun setNextPrayerAlarm(intent: Intent) {
        if (intent.extras != null) {
            val prayerName = intent.extras!!.getString("prayer", "Prayer")
            val prayer = Prayer.valueOf(prayerName)
            MakkahPrayer.instance.removePrayerNotification(prayer)
        }

        val (nextPrayer, date) = MakkahPrayer.instance.nextPrayerWithTime()
        MakkahPrayer.instance.setNotificationForPrayer(nextPrayer, date)
    }

    private fun showNotification(
        context: Context,
        intent: Intent,
        notificationManager: NotificationManager
    ) {
        var timestamp: Long = 0
        var prayerName = "Prayer"

        var mNotificationId = ""

        if (intent.extras != null) {
            timestamp = intent.extras!!.getLong("timestamp")
            prayerName = intent.extras!!.getString("prayer", "Prayer")
            mNotificationId = intent.extras!!.getString("notificationID", "")
        }

        if (timestamp > 0) {
            val notifyIntent = Intent(context, MainActivity::class.java)

            val title = capitalize(prayerName)
            val message = "It is $title time"

            notifyIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK

            val calendar = Calendar.getInstance()
            calendar.timeInMillis = timestamp

            val pendingIntent = PendingIntent.getActivity(
                context,
                0,
                notifyIntent,
                PendingIntent.FLAG_UPDATE_CURRENT
            )
            val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)

            mNotification = NotificationCompat.Builder(context, NotificationService.CHANNEL_ID)
                .setContentIntent(pendingIntent)
                .setSmallIcon(R.drawable.ic_alarm_black_24dp)
                .setLargeIcon(
                    BitmapFactory.decodeResource(
                        context.resources,
                        R.mipmap.ic_launcher
                    )
                )
                .setSound(uri)
                .setAutoCancel(true)
                .setContentTitle(title)
                .setStyle(
                    NotificationCompat.BigTextStyle()
                        .bigText(message)
                )
                .setColor(ContextCompat.getColor(context, R.color.colorSecondary))
                .setContentText(message).build()

            notificationManager.notify(timestamp.toInt(), mNotification)
        }
    }

    @SuppressLint("NewApi")
    private fun createChannel(context: Context): NotificationManager {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val soundUri =
                Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + App.instance?.applicationContext?.packageName + "/" + R.raw.azan)

            val audioAttributes = AudioAttributes.Builder()
                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                .setUsage(AudioAttributes.USAGE_NOTIFICATION)
                .build()

            val notificationManager =
                context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

            val importance = NotificationManager.IMPORTANCE_HIGH
            val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, importance)
            channel.enableVibration(true)
            channel.setShowBadge(true)
            channel.canShowBadge()
            channel.enableLights(true)
            channel.lightColor = context.getColor(R.color.colorSecondary)
            channel.description =
                context.getString(R.string.notification_channel_description)
            channel.setSound(soundUri, audioAttributes)
            channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
            notificationManager.createNotificationChannel(channel)

            return notificationManager
        } else {
            return context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        }
    }
}

Edit: After using the following methods, as described by people below, it still is not working, i.e app must be open at least one time in 24 hours, for it to produce local notifications. I am looking for a solution, where the app should not have to be open for leats say 4,5 days and the app should deliver local notifications. For now, it works for only 24 hours, when the next day comes, notifications stop firing, requiring the app to be open for at least once a day.

Upvotes: 4

Views: 2257

Answers (3)

You can create a PrayerWorker using Androidx Work Manager to schedule a background API/setting of notifications (all without using opening app, and instead being trigered when notification is received.

Documentation can be found here

Your setNextPrayerAlarm function will instead have the logic moved to the PrayerWorker and look something like this :

private fun setNextPrayerAlarm(intent: Intent) {
    if (intent.extras != null) {
        val oneTimeWorkRequestBuilder = OneTimeWorkRequest.Builder(PrayerWorker::class.java)
        oneTimeWorkRequestBuilder.setInputData(`put your input data here`)
        WorkManager.getInstance(context).enqueueUniqueWork("setPrayerWorker",ExistingWorkPolicy.REPLACE, oneTimeWorkRequestBuilder.build())
    }            
}

and the PrayerWorker may look something like this

class PrayerWorker(context: Context, workerParameters: WorkerParameters): Worker(context, workerParameters) {
    override fun doWork(): Result {
        //Insert logic to determine alarms to set 
        return Result.success() //for success case
    }
}

EDIT 1 :

Hi, i should have been clearer in the method, sorry. There's two ways you can make this a repeating alarm.

Method 1: Modify the OneTimeWorkRequest to a PeriodicWorkRequest(refer to documentation here). Using this method, you can specify how you want the worker that sets to repeat (e.g. every 2 hours, every 24 hours). The min interval is 15 mins.

Method 2: Modify PrayerWorker to also schedule the next worker. This will utilise the fact that you can add a delay to the triggering of the worker(refer to documentation), which in this case will be 24 hours. Below is the example

    class PrayerWorker(context: Context, workerParameters: WorkerParameters): Worker(context, workerParameters) {
            override fun doWork(): Result {
                //Insert logic to determine alarms to set 
                val oneTimeWorkRequestBuilder = OneTimeWorkRequest.Builder(PrayerWorker::class.java)
                oneTimeWorkRequestBuilder.setInputData(`put your input data here`)
oneTimeWorkRequestBuilder.setInitialDelay(`initialDelay`, `timeUnit`)
              WorkManager.getInstance(context).enqueueUniqueWork("setPrayerWorker",ExistingWorkPolicy.REPLACE, oneTimeWorkRequestBuilder.build())
                return Result.success() //for success case
            }
    }

Upvotes: 1

AouledIssa
AouledIssa

Reputation: 2824

I don't know which android Sdk level your app is targeting, but Google has changed it's APIs starting from O. Declaring implicit Broadcast receiver from manifest will not work.

As part of the Android 8.0 (API level 26) Background Execution Limits, apps that target the API level 26 or higher can no longer register broadcast receivers for implicit broadcasts in their manifest. However, several broadcasts are currently exempted from these limitations. Apps can continue to register listeners for the following broadcasts, no matter what API level the apps target.

more on that here: https://developer.android.com/guide/components/broadcast-exceptions

Upvotes: 0

Suresh Maidaragi
Suresh Maidaragi

Reputation: 2308

Try following steps

1. In NotificationUtils.kt add an intent Flag FLAG_RECEIVER_FOREGROUND

as like below which will do the trick for you

        val alarmIntent = Intent(App.instance?.applicationContext, AlarmReceiver::class.java)
        alarmIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
        alarmIntent.putExtra("prayer", name)
        ....
        ...

2. Also make sure you have registered AlarmReceiver in Manifest

like below

<receiver android:name="com.myapp.receiver.AlarmReceiver">
    </receiver>

Upvotes: 0

Related Questions