I am writing a app, one of the features is that it delivers a notification at a set time. That time being 7am. Recently however I wanted to add a notification customize menu, Allowing you to change the time that notifications fire.
current code snippet: (Indents are correct in code, Formatting them correctly here is difficult)
val TTT = sharedPreferences.getInt("noti1", 7)
val calendar: Calendar = Calendar.getInstance()
calendar.timeInMillis = System.currentTimeMillis()
calendar.set(Calendar.HOUR_OF_DAY, TTT)
calendar.set(Calendar.MINUTE, 0)
if (calendar.timeInMillis < System.currentTimeMillis()) {
calendar.add(Calendar.DAY_OF_MONTH, 1)
}
val mIntent = Intent(this, MyReceiver::class.java)
val mPendingIntent = PendingIntent.getBroadcast(
this, 1, mIntent, PendingIntent.FLAG_IMMUTABLE
)
val mAlarmManager = this
.getSystemService(Context.ALARM_SERVICE) as AlarmManager
mAlarmManager.setRepeating(
AlarmManager.RTC_WAKEUP, calendar.timeInMillis, 1000*60*60*24, mPendingIntent,
)
The variable TTT, is a int and is the time that the notification should fire. It can be selected by the user in another activity and is then saved in shared prefs and got here to be used.
The receiver file is the code that runs the notification.
When the TTT value hasn't been set, or is just set to a integer, It works fine and repeats fine. However when the value is set by the user, It doesn't work at all. No notifications fire, or they occasionally work but still at the default time (7am).
Is there any way to make this code work so that the notification can fire daily, at whatever time the user sets?
All help is appreciated :)
Edit:
Here is how I went about processing the value set by the user.
val dropdown = dialog.findViewById<Spinner>(R.id.spinnyboi)
val items = arrayOf("5:00", "6:00", "7:00", "8:00", "9:00", "10:00", "11:00", "12:00", "13:00")
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, items)
dropdown?.adapter = adapter //applys list of items
val timeAA = sharedPreferences.getString("notifA", "7:00") //gets shared prefs for the spinner
//I gave up here, converts string value (got from dropdown) to needed int (time) based on the items position
if (timeAA == "5:00") {
val shit = 0
dropdown.setSelection(shit)
val noti1 = getSharedPreferences("settings", MODE_PRIVATE).edit()
noti1.putInt("noti1", 5)
noti1.apply()
}else if (timeAA == "6:00") {
val shit = 1
dropdown.setSelection(shit)
val noti1 = getSharedPreferences("settings", MODE_PRIVATE).edit()
noti1.putInt("noti1", 6)
noti1.apply()
(so on)
And here is another process used to save spinner state and save the shared pref.
//gets the position of selected and then saves it
dropdown.setOnItemSelectedListener(object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>,
view: View,
pos: Int,
id: Long
) {
val item = parent.getItemAtPosition(pos)
apply.setOnClickListener {
val timeA = getSharedPreferences("settings", MODE_PRIVATE).edit()
timeA.putString("notifA", item as String?)
timeA.commit()
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
})
From the code you provided, there are 2 points worth noticing:
The first thing is when the user has selected an item in the drop down list, and after that has clicked on apply, it will set the time String to notifA in SharedPreferences. However, will it also set the Int value noti1 you are using for the alarm? Because for this piece of code:
if (timeAA == "5:00") {
... // Better rename your variable
val noti1 = getSharedPreferences("settings", MODE_PRIVATE).edit()
noti1.putInt("noti1", 5)
noti1.apply()
} else if (timeAA == "6:00") {
... // Better rename your variable
val noti1 = getSharedPreferences("settings", MODE_PRIVATE).edit()
noti1.putInt("noti1", 6)
noti1.apply()
} ...
If you just put it in like onCreate(), it will just be triggered when the Activity is shown. There will not be any update after the user has chosen a new time String from drop down list and click on apply. So, you may have to consider setting noti1 inside the OnClickListener if the above code is not being called.
The second thing is that, assume your noti1 in SharedPreferences is correctly updated through drop down list, will it cancel the previous set alarm and set another alarm with the updated time? You may have to consider calling the function again after the user has selected from the drop down list to reset for an updated alarm.
Update an alarm
And if you would like to update an alarm with new time, when you are creating the PendingIntent, use flag FLAG_CANCEL_CURRENT:
Flag indicating that if the described PendingIntent already exists, the current one should be canceled before generating a new one.
So you should have your PendingIntent created like this:
// Change PendingIntent.FLAG_IMMUTABLE to PendingIntent.FLAG_CANCEL_CURRENT
val mPendingIntent = PendingIntent.getBroadcast(
this, 1, mIntent, PendingIntent.FLAG_CANCEL_CURRENT
)
And then you can call the above code again to update alarm with the new time.
And make sure the second parameter of the function PendingIntent.getBroadcast() is the same when recreating the alarm.
public static PendingIntent getBroadcast (Context context,
int requestCode,
Intent intent,
int flags)
A different requestCode will create another alarm event instead.
Cancel an alarm
Meanwhile you can also cancel the alarm first and create another alarm later if you want to.
And you should make sure you have the variable holding this PendingIntent:
val mPendingIntent = PendingIntent.getBroadcast(
this, 1, mIntent, PendingIntent.FLAG_IMMUTABLE
)
And you can call AlarmManager.cancel(PendingIntent) to cancel the alarm:
mAlarmManager.cancel(mPendingIntent)
Related
BootReceiver
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
val array = MyDatabase(context).getAllAlarm()
val receiver = Intent(context, BootReceiver::class.java)
receiver.action = "ALARM_SETTING"
val trigger = Calendar.getInstance()
for (i in array) {
trigger.time = SimpleDateFormat("yyyy/MM/dd hh:mm", Locale.getDefault()).parse(i.time)!!
receiver.putExtra("trigger", trigger.timeInMillis)
AlarmUtil.setAlarm(context, receiver, i.id, trigger.timeInMillis)
}
} else (intent.action == "ALARM_SETTING") {
val trigger = intent.getLongExtra("trigger", 0)
NotificationUtil.sendNotification(context, name, id, trigger)
}
}
AlarmUtil
class AlarmUtil {
companion object {
fun setAlarm(context: Context, intent: Intent, id: Int, calendar: Long) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val pendingIntent = PendingIntent.getBroadcast(context.applicationContext, id, intent, PendingIntent.FLAG_UPDATE_CURRENT)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, calendar, pendingIntent)
} else {
alarmManager.setExact(AlarmManager.RTC_WAKEUP, calendar, pendingIntent)
}
}
}
}
As you can see, when the mobile phone reboots, this code brings the alarm array stored in SQLiteDatabase and makes the alarm sound. If you look at setAlarm() in AlarmUtil, I wrote a code that makes it cry at the time specified in AlarmManager. The problem is that all the alarms stored in the array will be cleared at once regardless of the time specified in the alarm array (I implemented it as Notification). Isn't it normal to cry at the calendar time you put in the second argument of setExactAndAllowWhileIdle or setExact? Why do all the alarms cry at once as soon as the phone turns on again?
[EDIT]
The following picture is a situation where the phone is turned off and on after setting the alarm to ring at 02:22. The system time is 2020/11/02 02:21, but the trigger time I set to ring is 2020/11/02 02:22. In other words, the alarm goes off as soon as the time runs out. I want to take a log and show it, but the moment I turn off my phone, the app dies, so I couldn't see the log.
Hi you are passing older time as trigger time.
You should pass time in the future.
Check db time if it is less than now() than add a day
So I need to send a notification with different content every day at the same time. We don't really have the resources to make them push, so they are queued locally like this:
for (date in allDateStrings) { //Formatted as MM/dd/yyyy, up to 50 values
val sampleNotificationContent = "This notification is shown on $date"
//WHEN
val dateWithTimeFormatter = SimpleDateFormat("MM/dd/yyyy HH:mm")
val dateWithTime = dateWithTimeFormatter.parse("$date 07:00")
val timeInMillisUntilNotifShouldDeliver = dateWithTime.time
//REQUEST
val notificationIntent = Intent("android.media.action.DISPLAY_NOTIFICATION")
notificationIntent.addCategory("android.intent.category.DEFAULT")
notificationIntent.putExtra("body", sampleNotificationContent)
notificationIntent.putExtra("requestCode", date.replace("/", "").toInt())
//BROADCAST
val broadcast = PendingIntent.getBroadcast(context, requestCode, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT)
if (timeInMillisUntilNotifShouldDeliver > Calendar.getInstance().time.time) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.setExact(AlarmManager.RTC_WAKEUP, timeInMillisUntilNotifShouldDeliver, broadcast)
}
}
Up to 50 are queued at once, since we don't expect a large portion of users to actually open the app any more frequently than once every 50 days. The broadcasts are handled by a subclass of BroadcastReceiver and display successfully. Is there a better way to do this? I feel like I'm wasting a lot of battery with dozens of alarms being set at once. Do you think this would have a noticeable impact on performance or is it a non-issue?
I'm actually working on an app that should post a notification 5 days in the future.
Using AlarmManager, I send a PendingIntent to my Receiver class.
Everything works fine until I force close my app. In this case, the notification doesn't appear.
So my question:
What happens to this PendingIntent, which was fired and did not reach its target?
When my app is finally restarted, can I check for PendingIntents, that did not reach its target?
EDIT 1:
These are the essential parts of my Broadcast Receiver:
override fun onReceive(context: Context?, intent: Intent?) {
if (context != null && intent?.action != null) {
when (intent.action) {
INTENT_ACTION_BOOT_COMPLETED -> handleDeviceBoot()
INTENT_ACTION_REMINDER -> handleReminder(context, intent.getLongExtra(EXTRA_ITEM_ID, -1))
}
}
}
private suspend fun schedule(context: Context, itemId: Long, fireDate: LocalDateTime) = withContext(Dispatchers.IO) {
AlarmManagerCompat.setAndAllowWhileIdle(
getAlarmManager(context),
AlarmManager.RTC,
fireDate.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(),
makePendingIntent(context, itemId)
)
with(AppDatabase.get(context).reminderDao()) {
val oldReminder = getItemReminder(itemId)
if (oldReminder == null) {
insert(Reminder(itemId = itemId, fireDate = fireDate))
} else {
update(Reminder(id = oldReminder.id, itemId = itemId, fireDate = fireDate))
}
}
}
private suspend fun cancel(context: Context, itemId: Long) = withContext(Dispatchers.IO) {
val reminderDao = AppDatabase.get(context).reminderDao()
val reminder = reminderDao.getItemReminder(itemId)
reminder?.let {
getAlarmManager(context).cancel(makePendingIntent(context, itemId))
reminderDao.delete(it)
}
}
private fun getAlarmManager(context: Context) = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
private fun makePendingIntent(context: Context, itemId: Long): PendingIntent {
val alarmIntent = Intent(context, ReminderManager::class.java).apply {
action = INTENT_ACTION_REMINDER
putExtra(EXTRA_ITEM_ID, itemId)
}
return PendingIntent.getBroadcast(context, itemId.toInt(), alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT)
}
As defined in Official Android Documentation
A PendingIntent itself is simply a reference to a token maintained by the system describing the original data used to retrieve it. This means that, even if its owning application's process is killed, the PendingIntent itself will remain usable from other processes that have been given it. If the creating application later re-retrieves the same kind of PendingIntent (same operation, same Intent action, data, categories, and components, and same flags), it will receive a PendingIntent representing the same token if that is still valid, and can thus call cancel() to remove it.
Revisit your code to check if there is anything else that would be causing this issue.
When you "force close" an application, the application gets set to the "stopped state". In the "stopped state" your application will NOT be automatically started by Android until the user manually restarts the application. This means that if you "force close" your app, your app will not receive any broadcast Intents until it is manually restarted by the user.
I expect (although I have not tried it myself), that if you schedule an alarm to go off at time X and before time X you "force close" the app, when time X happens, the alarm manager will try to send the PendingIntent, however Android will refuse to actually execute the BroadcastReceiver because the app is in the "stopped state". In this case I expect the trigger is lost. Android will not retry or reschedule it.
Basically, when a user "force close"s an app, he is telling Android that he doesn't want that app to run anymore, including any background processes that the app might have, or want to start in the future.
The answer is short: Active PendingIntents are cancelled on an application force-stop.
I want to use AlarmService to trigger a notification at a certain time. Think of it as something similar as a calendar app that is showing a reminder as notification for an upcoming event.
The code to schedule the intent (via alarm service) looks like this:
fun scheduleNotification(event : CalendarEvent)
val startTime : Instant = event.startTime
val intent = buildPendingIntent(event)
val notificationTime = startTime.minusMillis(TimeUnit.MINUTES.toMillis(10)) // 10 Minutes earlier
if (Build.VERSION.SDK_INT < 23) {
alarmService().setExact(AlarmManager.RTC_WAKEUP,
notificationTime.toEpochMilli(), intent)
} else {
alarmService().setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP,
notificationTime.toEpochMilli(), intent)
}
}
fun buildPendingIntent(event : CalendarEvent){
val intent = Intent(context, NotificationReceiver::class.java)
intent.putExtra(EVENT_ID, event.id)
return PendingIntent.getBroadcast(context, 0, realIntent, 0)
}
class NotificationReceiver : WakefulBroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// build and display the notification
}
}
So 1 of 10 times the notification is tirggered and shown (by NotificationReceiver) correctly, also at the desired time. So I think the scheduling part is working properly.
Which leads me to another question: Whenever the user creates a new CalendarEvent the method scheduleNotification(newEvent) is called. It seems to me that AlarmService is internally updating the PendingIntents of existing and that this is the reason why 1 of 10 (usually the first scheduled PendingIntent) is triggered, but the others are not.
How many alarms can I schedule for an Android App? Do you spot any other issue with my code?
I have a collection widget with a ListView on Android and it works fine for versions above 19. Nevertheless, with KitKat (which is the minimum supported version in our app) it doesn't work.
I can see the widget -there's an ImageView and a FrameLayout with another ImageView and an animated-rotate for when the widget updates- but it has an empty ListView and the clicks on the aforementioned ImageView doesn't work.
Is there anything special to do so the widget runs on KitKat? Am I missing something?
I realised that the setRemoteAdapter works, and even the RemoteViewsService.RemoteViewsFactory gets called, but it's just not reflecting the changes in the screen, nor are the click events working on static views -not the ListView I'm using, which doesn't even show items.
This is the way I'm doing it:
private fun buildWidget(appWidgetIds: IntArray, appWidgetManager: AppWidgetManager, context: Context) {
val remoteViews = getRemoveViews(context)
// Set up the intent that starts the AppWidgetService, which will provide the views for this collection.
for (i in appWidgetIds.iterator()) {
val intent = Intent(context, AppWidgetService::class.java)
// Add the app widget ID to the intent extras.
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, i)
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
intent.data = Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME))
// Check for a valid session in order to either show the positions list or a view to ask the user to log in
if (userSession.hasValidSession()) {
// Set up the RemoteViews object to use a RemoteViews adapter. This adapter connects to a RemoteViewsService through the specified intent. This is how you populate the data.
remoteViews.setRemoteAdapter(R.id.app_widget_portfolio_list, intent)
// The empty view is displayed when the collection has no items. It should be in the same layout used to instantiate the RemoteViews object above.
remoteViews.setEmptyView(R.id.app_widget_portfolio_list, R.id.app_widget_empty_view)
setOpenAppOnClickPendingIntent(context, remoteViews, R.id.app_widget_empty_view)
// Bind a click listener template for the contents of the weather list. Note that we
// need to update the intent's data if we set an extra, since the extras will be
// ignored otherwise.
val onClickIntent = Intent(ACTION_LIST_ITEM_CLICK, null, context, AppWidget::class.java)
val onClickPendingIntent = PendingIntent.getBroadcast(context, number++, onClickIntent, PendingIntent.FLAG_UPDATE_CURRENT)
remoteViews.setPendingIntentTemplate(R.id.app_widget_portfolio_list, onClickPendingIntent)
val refreshPositionsIntent = Intent(ACTION_REFRESH, null, context, AppWidget::class.java)
refreshPositionsIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, i)
val refreshPositionsPendingIntent = PendingIntent.getBroadcast(context, 0, refreshPositionsIntent, 0)
remoteViews.setOnClickPendingIntent(R.id.app_widget_refresh_button, refreshPositionsPendingIntent)
setOpenAppOnClickPendingIntent(context, remoteViews, R.id.app_widget_logo)
appWidgetManager.partiallyUpdateAppWidget(i, remoteViews)
}
private fun setOpenAppOnClickPendingIntent(context: Context, remoteViews: RemoteViews, #IdRes viewId: Int) {
val openAppIntent = MainActivity.newNavigateIntent(context, AppUrl.getHomeUrl())
val loginPendingIntent = PendingIntent.getActivity(context, 0, openAppIntent, PendingIntent.FLAG_UPDATE_CURRENT)
remoteViews.setOnClickPendingIntent(viewId, loginPendingIntent)
}
I have tried so far to change the requestCode of the PendingIntent.getBroadcast() and use random numbers, but still, it doesn't work. I also tried appWidgetManager.updateAppWidget(i, remoteViews) instead of appWidgetManager.partiallyUpdateAppWidget(i, remoteViews) but still, no luck.
Thanks a lot in advance