Geofencing api not working in android with pending intent - android

I'm trying to implement simple geofence app but its not getting triggered at all.
here is the code of what I've done so far -
class GeoFencingHelper(context: Context, private val task: Task, private val pendingIntent: PendingIntent){
private val geofencingClient = LocationServices.getGeofencingClient(context)
init {
Timber.e(task.id.toString())
}
#SuppressLint("MissingPermission")
fun initiateGeoFencing() {
val geoReq = createGeoFenceList()
geofencingClient.addGeofences(geoReq, pendingIntent).apply {
addOnSuccessListener {
Timber.e("added geofence!")
}
addOnFailureListener {
Timber.e("geofence failed!")
}
}
}
private fun createGeoFenceList(): GeofencingRequest {
val geofenceList = arrayListOf<Geofence>()
geofenceList.add(Geofence.Builder().apply {
setRequestId(task.id.toString())
setCircularRegion(
task.location.latitude,
task.location.longitude,
task.range.toFloat()
)
// Set the expiration duration of the geofence. This geofence gets automatically
// removed after this period of time.
setExpirationDuration(Geofence.NEVER_EXPIRE)
setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT)
// Create the geofence.
}.build())
return getGeofencingRequest(geofenceList)
}
private fun getGeofencingRequest(
fenceList: List<Geofence>,
): GeofencingRequest {
return GeofencingRequest.Builder().apply {
setInitialTrigger(
if(task.reminderCondition == ReminderCondition.ON_ENTRY)
GeofencingRequest.INITIAL_TRIGGER_ENTER
else
GeofencingRequest.INITIAL_TRIGGER_EXIT)
addGeofences(fenceList)
}.build()}
I'm adding geofencing here
fun createGeoFence(context: Context, task: Task){
val geofencePendingIntent: PendingIntent by lazy {
task.range = 150.0
val intent = Intent(context, ReminderNotificationBroadcastReceiver::class.java)
intent.putExtra(TASK_ID, task.id)
if(Build.VERSION.SDK_INT > 30){
PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}else{
PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
}
val geoFencingHelper = GeoFencingHelper(context, task, geofencePendingIntent)
geoFencingHelper.initiateGeoFencing()}
here is the broadcast receiver to show notification to user when geofence event gets triggered.
class ReminderNotificationBroadcastReceiver #Inject constructor(private val dao: TaskDao): BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val builder = NotificationCompat.Builder(context, "2222")
.setSmallIcon(R.drawable.ic_launcher_background)
.setContentTitle("Reminder \"\".")
.setContentText("Reminder \".")
.setOngoing(true)
.setColor(Color.BLUE)
with(NotificationManagerCompat.from(context)) {
notify(2222, builder.build())
}}}
things I've done so far -
disabled battery optimization.
background location permission is allowed all the time.
Issue -
I used mock location apps like lockito and gps emulator to test the app.
but the geofence event is not getting triggered, I also tried to build and test sample project provided by android codelab, but I'm facing same issue in that app as well.

There are similar questions on SO and on the net but in short, Google codelab is not up to date for this. The PendingIntent used for Geofencing needs to be mutable for it to work. So in your case:
fun createGeoFence(context: Context, task: Task){
val geofencePendingIntent: PendingIntent by lazy {
....
if(Build.VERSION.SDK_INT > 30){
PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE)
} else{
PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
}
or just like this without the if as PendingIntent is mutable by default up until Build.VERSION_CODES.R:
fun createGeoFence(context: Context, task: Task){
val geofencePendingIntent: PendingIntent by lazy {
....
PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE)
}

Related

How to cancel a scheduled local notification in Kotlin?

I have an app where the user creates its own local notifications. User declares name, date and time the nofication should popup and specifies the repeating frequency.
Then the notifications are listed in a recyclerview in another fragment.
The user is able to delete notification by swiping its recyclerview item to the left.
But when I create a notification, delete it then it still pops up at the specified time.
I am storing the notificationID in SharedPreferences as a date when its created (so that I can store it in my DB). I am passing it as a string with putExtra to my BroadcastReceiver class, I am getting the notificationID as a String in my BroadcastReceiver class with getStringExtra. Then passing the same notificationID.toInt() to my pendingIntent.getActivity. Then in my Fragment with recyclerView I am passing the same notificationID for cancelling and it still doesn't cancel.
Perhaps I'm using wrong flags?
Thanks a lot for any help.
Here's my BroadcastReceiver class:
const val titleExtra = "titleExtra"
const val descriptionExtra = "descriptionExtra"
val notificationID = "notificationID"
class Notification: BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val intentToRepeat = Intent(context, MainActivity::class.java)
val id = intent.getStringExtra(notificationID).toString()
val pendingIntent = PendingIntent.getActivity(context, id.toInt(), intentToRepeat, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
val notification = NotificationCompat.Builder(context, channelID)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentTitle(intent.getStringExtra(titleExtra))
.setContentText(intent.getStringExtra(descriptionExtra))
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build()
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (intent.action == "cancel") {
manager.cancel(id.toInt())
}
else {
manager.notify(id.toInt(), notification)
}
}
}
My AndroidManifest:
<receiver android:name=".powiadomienia.Powiadomienie" android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="cancel"/>
<action android:name="create" />
</intent-filter>
</receiver>
In my recyclerview with notifications listed:
val currentNotification: SetNotification = listAdapter.getNotificationByPosition(viewHolder.bindingAdapterPosition)
if(direction == ItemTouchHelper.LEFT) {
// CANCEL
//val manager = requireContext().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
//manager.cancel(currentPowiadomienie.notificationId!!)
val alarmManager = requireActivity().getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = Intent(requireContext(), Notification::class.java)
intent.action = "cancel"
val pendingIntent = PendingIntent.getService(requireContext(), currentNotification.notificationId!!.toInt(), intent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE)
pendingIntent?.let { _pendingIntent ->
alarmManager.cancel(_pendingIntent)
}
Neither manager.cancel() nor alarmManager.cancel() works.
The notification creates but how to cancel it?!
I think you need to call notifydatasetchanged() method after the alarmManager.cancel() like this:
val currentNotification: SetNotification =
listAdapter.getNotificationByPosition(viewHolder.bindingAdapterPosition)
if(direction == ItemTouchHelper.LEFT) {
val alarmManager =
requireActivity().getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = Intent(requireContext(), Notification::class.java)
intent.action = "cancel"
val pendingIntent = PendingIntent.getService(requireContext(),
currentNotification.notificationId!!.toInt(), intent,
PendingIntent.FLAG_CANCEL_CURRENT or
PendingIntent.FLAG_IMMUTABLE)
pendingIntent?.let { _pendingIntent ->
alarmManager.cancel(_pendingIntent)
notifyDataSetChanged()
}
I've solved my issue for not canceling coming notifications:
I think I was passing the wrong context. Check if you're passing the right one
To cancel a notification:
private fun removeAlarm(id: Int){
val alarmManager = activity?.getSystemService(Context.ALARM_SERVICE) as AlarmManager
// Notification = BroadcastReceiver class
val intent = Intent(requireContext(), Notification::class.java)
val pendingIntent = PendingIntent.getBroadcast(requireContext(), id, intent, PendingIntent.FLAG_IMMUTABLE)
alarmManager.cancel(pendingIntent)
}

Canceling running coroutineWorker doesn't fire failure in a certain pattern

I'm trying stopping running coroutineWorker from notification button. I tried 3 methods and 2 of them calls "Result.failure()" & working fine. However another one doesn't.
Below CoroutineWorker shows foregroundInfo and Starts ringtone.
class RingWork(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
companion object {
val ALARM_CHANNEL_ID = "alarm_channel6"
}
lateinit var ringtoneSound: Ringtone
val context = applicationContext
#RequiresApi(Build.VERSION_CODES.Q)
override suspend fun doWork(): Result {
return try {
val alarmId = inputData.getInt("alarmId", 0)
val notificationMgr =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
//CHANNEL
val alarmChannel = NotificationChannel(
ALARM_CHANNEL_ID, "alarm" ,NotificationManager.IMPORTANCE_HIGH
)
alarmChannel.setSound(null, null)
alarmChannel.enableVibration(false)
alarmChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
notificationMgr.createNotificationChannel(alarmChannel)
val fullScreenIntent = Intent(context, LockscreenActivity::class.java).putExtra("alarmId", alarmId)
//This calls "failure" properly
val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, fullScreenIntent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
//This calls "failure" properly
val stop1PendingIntent =
WorkManager.getInstance(context).createCancelPendingIntent(getId())
val s2Intent = Intent(context, StopAlarmReceiver::class.java).putExtra("alarmId", alarmId)
//This is not.
val stop2PendingIntent = PendingIntent.getBroadcast(context, 1, s2Intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
val builder = NotificationCompat.Builder(context, ALARM_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_stat_name)
.setContentTitle("title")
.setFullScreenIntent(fullScreenPendingIntent, true)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_ALARM)
.setAutoCancel(true)
.setSound(null)
.setVibrate(null)
.addAction(R.drawable.ic_stat_name, "Stop1", stop1PendingIntent)
.addAction(R.drawable.ic_stat_name, "Stop2", stop2PendingIntent)
setForeground(
ForegroundInfo(1999999, builder.build(), FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK)
)
ringtoneSound =
RingtoneManager.getRingtone(context, RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM))
ringtoneSound.play()
delay(30000L)
ringtoneSound.stop()
Result.success()
} catch (e: Exception) {
Result.failure()
} finally {
cleanup()
}
}
fun cleanup(){
ringtoneSound.stop()
}
}
In LockScreenActivity, there is a button to stop ringtone.
binding.stoppingbutton.setOnClickListener {
val workMgr = WorkManager.getInstance(applicationContext)
workMgr.cancelUniqueWork("RingWork-$alarmId")
finish()
}
This calls "result.failure" and "finally" then ringtone will stop, notification will disapear. working fine.
However, if I press "Stop2" button on the notification.
class StopAlarmReceiver: BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val alarmId = intent.getIntExtra("alarmId", 0)
val workMgr = WorkManager.getInstance(context)
workMgr.cancelUniqueWork("RingWork-$alarmId")
}
}
It cancels worker, but it won't call "result.failure" and "finally", so ringtone won't stop. Notification also won't disappear.
fullScreenPendingIntent and stop2PendingIntent are doing the same thing, but why it won't behave same?
You can edit your PendingIntent like this and it will trigger onReceive:
val stop2PendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
PendingIntent.getBroadcast(context, downloadFile.productId, cancelIntent, PendingIntent.FLAG_IMMUTABLE)
else
PendingIntent.getBroadcast(context, downloadFile.productId, cancelIntent, 0)

How to solve this Targeting S+ (version 31 and above) requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE

Have been trying to solve this but couldn't . this is udacity lesson where i am trying to create notifications using pending intent but there is problem with Pending Intent being Mutable i tried with this way
ViewModel
private val REQUEST_CODE = 0
private val TRIGGER_TIME = "TRIGGER_AT"
private val minute: Long = 60_000L
private val second: Long = 1_000L
private val timerLengthOptions: IntArray
private val notifyPendingIntent: PendingIntent
private val alarmManager = app.getSystemService(Context.ALARM_SERVICE) as AlarmManager
private var prefs =
app.getSharedPreferences("com.shivaConsulting.androidProjects.kotlinnotificatons", Context.MODE_PRIVATE)
private val notifyIntent = Intent(app, AlarmReciever::class.java)
private val _timeSelection = MutableLiveData<Int>()
val timeSelection: LiveData<Int>
get() = _timeSelection
private val _elapsedTime = MutableLiveData<Long>()
val elapsedTime: LiveData<Long>
get() = _elapsedTime
private var _alarmOn = MutableLiveData<Boolean>()
val isAlarmOn: LiveData<Boolean>
get() = _alarmOn
private lateinit var timer: CountDownTimer
init {
_alarmOn.value = PendingIntent.getBroadcast(
getApplication(),
REQUEST_CODE,
notifyIntent,
PendingIntent.FLAG_NO_CREATE
) != null
notifyPendingIntent = PendingIntent.getBroadcast(
getApplication(),
REQUEST_CODE,
notifyIntent,
PendingIntent.FLAG_UPDATE_CURRENT
)
timerLengthOptions = app.resources.getIntArray(R.array.minutes_array)
//If alarm is not null, resume the timer back for this alarm
if (_alarmOn.value!!) {
createTimer()
}
}
/**
* Turns on or off the alarm
*
* #param isChecked, alarm status to be set.
*/
fun setAlarm(isChecked: Boolean) {
when (isChecked) {
true -> timeSelection.value?.let { startTimer(it) }
false -> cancelNotification()
}
}
/**
* Sets the desired interval for the alarm
*
* #param timerLengthSelection, interval timerLengthSelection value.
*/
fun setTimeSelected(timerLengthSelection: Int) {
_timeSelection.value = timerLengthSelection
}
/**
* Creates a new alarm, notification and timer
*/
private fun startTimer(timerLengthSelection: Int) {
_alarmOn.value?.let {
if (!it) {
_alarmOn.value = true
val selectedInterval = when (timerLengthSelection) {
0 -> second * 10 //For testing only
else ->timerLengthOptions[timerLengthSelection] * minute
}
val triggerTime = SystemClock.elapsedRealtime() + selectedInterval
// TODO: Step 1.5 get an instance of NotificationManager and call sendNotification
// TODO: Step 1.15 call cancel notification
AlarmManagerCompat.setExactAndAllowWhileIdle(
alarmManager,
AlarmManager.ELAPSED_REALTIME_WAKEUP,
triggerTime,
notifyPendingIntent
)
viewModelScope.launch {
saveTime(triggerTime)
}
}
}
createTimer()
}
/**
* Creates a new timer
*/
private fun createTimer() {
viewModelScope.launch {
val triggerTime = loadTime()
timer = object : CountDownTimer(triggerTime, second) {
override fun onTick(millisUntilFinished: Long) {
_elapsedTime.value = triggerTime - SystemClock.elapsedRealtime()
if (_elapsedTime.value!! <= 0) {
resetTimer()
}
}
override fun onFinish() {
resetTimer()
}
}
timer.start()
}
}
/**
* Cancels the alarm, notification and resets the timer
*/
private fun cancelNotification() {
resetTimer()
alarmManager.cancel(notifyPendingIntent)
}
/**
* Resets the timer on screen and sets alarm value false
*/
private fun resetTimer() {
timer.cancel()
_elapsedTime.value = 0
_alarmOn.value = false
}
private suspend fun saveTime(triggerTime: Long) =
withContext(Dispatchers.IO) {
prefs.edit().putLong(TRIGGER_TIME, triggerTime).apply()
}
private suspend fun loadTime(): Long =
withContext(Dispatchers.IO) {
prefs.getLong(TRIGGER_TIME, 0)
}'''
Fragment
class BlankFragment : Fragment() {
companion object {
fun newInstance() = BlankFragment()
}
private val viewModel : BlankViewModel by lazy {
ViewModelProvider(this).get(BlankViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentBlankBinding.inflate(inflater)
binding.lifecycleOwner = this
binding.blankViewModel = viewModel
return binding.root
} '''
as i was looking into other similar questions which was solved by the gradle dependencies i have already added that
implementation 'androidx.work:work-runtime-ktx:2.7.1'
but showing this error
E/AndroidRuntime: Caused by: java.lang.IllegalArgumentException: com.shivaconsulting.kotlinnotifications: Targeting S+ (version 31 and above) requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntent.
Strongly consider using FLAG_IMMUTABLE, only use FLAG_MUTABLE if some functionality depends on the PendingIntent being mutable, e.g. if it needs to be used with inline replies or bubbles.
at android.app.PendingIntent.checkFlags(PendingIntent.java:378)
at android.app.PendingIntent.getBroadcastAsUser(PendingIntent.java:648)
at android.app.PendingIntent.getBroadcast(PendingIntent.java:635)
at com.shivaconsulting.kotlinnotifications.ui.BlankViewModel.<init>(BlankViewModel.kt:52)
... 44 more
Udacity Starter Code
Ref : https://developer.android.com/reference/android/app/PendingIntent#FLAG_MUTABLE
Flag indicating that the created PendingIntent should be mutable. This flag cannot be combined with FLAG_IMMUTABLE.
Up until Build.VERSION_CODES.R, PendingIntents are assumed to be mutable by default, unless FLAG_IMMUTABLE is set. Starting with Build.VERSION_CODES.S, it will be required to explicitly specify the mutability of PendingIntents on creation with either (#link #FLAG_IMMUTABLE} or FLAG_MUTABLE. It is strongly recommended to use FLAG_IMMUTABLE when creating a PendingIntent. FLAG_MUTABLE should only be used when some functionality relies on modifying the underlying intent, e.g. any PendingIntent that needs to be used with inline reply or bubbles.
notifyPendingIntent = PendingIntent.getBroadcast(
getApplication(),
REQUEST_CODE,
notifyIntent,
PendingIntent.FLAG_UPDATE_CURRENT
)
The code should be.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
notifyPendingIntent = PendingIntent.getBroadcast(
getApplication(),
REQUEST_CODE,
notifyIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
} else {
notifyPendingIntent = PendingIntent.getBroadcast(
getApplication(),
REQUEST_CODE,
notifyIntent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
The same way you have to in
_alarmOn.value = PendingIntent.getBroadcast(
getApplication(),
REQUEST_CODE,
notifyIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_MUTABLE else PendingIntent.FLAG_NO_CREATE
) != null
Edit 2
Also if you are not using PendingIntent and still having crash from system while using other components like MediaSessionCompat etc.
implementation 'androidx.work:work-runtime-ktx:RUNTIME-VERSION'
RUNTIME VERSION : https://developer.android.com/jetpack/androidx/releases/work
[Solution]
Adding below dependency. This is work for me
implementation 'androidx.work:work-runtime:2.7.0-alpha05'
this was better way
private val flags = when{
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ->
PendingIntent.FLAG_MUTABLE
else -> {PendingIntent.FLAG_MUTABLE}
}
In addition to #Ashvinsolanki answer's , you may need to increase the version of
implementation "androidx.work:work-runtime-ktx:2.5.0" to the latest update
[Solution] as mentioned above #Thilina
I updated from 2.7.0-alpha04 to 2.7.0-alpha05 and it worked for me
With API 31 and 33
implementation 'androidx.work:work-runtime:2.7.0-alpha05'
If you want the fastest solution, you should modify app/build.gradle inside
targetSdkVersion,
The maximum value is 30

How to set an alarm on Android Q?

Background
Android Q seems to have plenty of new restrictions, but alarms shouldn't be one of them:
https://developer.android.com/guide/components/activities/background-starts
The problem
It seems that old code that I made for setting an alarm, which worked fine on P, can't work well anymore:
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var manager: AlarmManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
manager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
button.setOnClickListener {
Log.d("AppLog", "alarm set")
Toast.makeText(this, "alarm set", Toast.LENGTH_SHORT).show()
val timeToTrigger = System.currentTimeMillis() + 10 * 1000
setAlarm(this, timeToTrigger, 1)
}
}
companion object {
fun setAlarm(context: Context, timeToTrigger: Long, requestId: Int) {
val manager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val pendingIntent = PendingIntent.getBroadcast(context, requestId, Intent(context, AlarmReceiver::class.java), PendingIntent.FLAG_UPDATE_CURRENT)
when {
VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP -> manager.setAlarmClock(AlarmClockInfo(timeToTrigger, pendingIntent), pendingIntent)
VERSION.SDK_INT >= VERSION_CODES.KITKAT -> manager.setExact(AlarmManager.RTC_WAKEUP, timeToTrigger, pendingIntent)
else -> manager.set(AlarmManager.RTC_WAKEUP, timeToTrigger, pendingIntent)
}
}
}
}
The receiver does get the Intent, but when it tries to open the Activity, sometimes nothing occurs:
AlarmReceiver.kt
class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Log.d("AppLog", "AlarmReceiver onReceive")
context.startActivity(Intent(context, Main2Activity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
}
}
Seeing this as a bug, I reported here (including sample code)
What I've tried
I tried to find what's new on Q, to see what could cause it, and I couldn't find it.
I also tried (if you look at the code) to directly open the Activity instead of via a BroadcastReceiver.
And, I tried to set the BroadcastReceiver to run on a different process.
All of those didn't help.
What I have found is that while some alarm clock apps fail to work properly (such as Timely), some apps work just fine (such as "Alarm Clock Xtreme").
The questions
On Android Q, is there an official way to let alarms work correctly? To open an Activity that will be shown to the user, exactly as an alarm clock app should?
What's wrong in the code I've made? How come it works on P but not always on Q?
EDIT: OK after being adviced to have a notification shown while I start the Activity, and also use FullScreenIntent, I got something to work, but it's only working when the screen is turned off. When the screen is turned on, it only shows the notification, which is a bad thing because the whole point is to have an alarm being shown to the user, and some users (like me) don't want to have heads-up-notification for alarms, popping out in the middle of something and not pausing anything. I hope someone can help with this, as this used to be a very easy thing to do, and now it got way too complex...
Here's the current code (available here) :
NotificationId
object NotificationId {
const val ALARM_TRIGGERED = 1
#JvmStatic
private var hasInitialized = false
#UiThread
#JvmStatic
fun initNotificationsChannels(context: Context) {
if (hasInitialized || Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
return
hasInitialized = true
val channelsToUpdateOrAdd = HashMap<String, NotificationChannel>()
val channel = NotificationChannel(context.getString(R.string.channel_id__alarm_triggered), context.getString(R.string.channel_name__alarm_triggered), NotificationManager.IMPORTANCE_HIGH)
channel.description = context.getString(R.string.channel_description__alarm_triggered)
channel.enableLights(true)
channel.setSound(null, null)
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
channel.enableVibration(false)
channel.setShowBadge(false)
channelsToUpdateOrAdd[channel.id] = channel
//
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val existingChannels = notificationManager.notificationChannels
if (existingChannels != null)
for (existingChannel in existingChannels) {
// The importance of an existing channel will only be changed if the new importance is lower than the current value and the user has not altered any settings on this channel.
// The group an existing channel will only be changed if the channel does not already belong to a group. All other fields are ignored for channels that already exist.
val channelToUpdateOrAdd = channelsToUpdateOrAdd[existingChannel.id]
if (channelToUpdateOrAdd == null) //|| channelToUpdateOrAdd.importance > existingChannel.importance || (existingChannel.group != null && channelToUpdateOrAdd.group != existingChannel.group))
notificationManager.deleteNotificationChannel(existingChannel.id)
}
for (notificationChannel in channelsToUpdateOrAdd.values) {
notificationManager.createNotificationChannel(notificationChannel)
}
}
}
MyService.kt
class MyService : Service() {
override fun onBind(p0: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d("AppLog", "MyService onStartCommand")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationId.initNotificationsChannels(this)
val builder = NotificationCompat.Builder(this, getString(R.string.channel_id__alarm_triggered)).setSmallIcon(android.R.drawable.sym_def_app_icon) //
.setPriority(NotificationCompat.PRIORITY_HIGH).setCategory(NotificationCompat.CATEGORY_ALARM)
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
builder.setShowWhen(false)
builder.setContentText("Alarm is triggered!")
builder.setContentTitle("Alarm!!!")
val fullScreenIntent = Intent(this, Main2Activity::class.java)
val fullScreenPendingIntent = PendingIntent.getActivity(this, 0,
fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT)
builder.setFullScreenIntent(fullScreenPendingIntent, true)
startForeground(NotificationId.ALARM_TRIGGERED, builder.build())
startActivity(Intent(this, Main2Activity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
Handler().postDelayed({
stopForeground(true)
stopSelf()
}, 2000L)
} else {
startActivity(Intent(this, Main2Activity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
}
return super.onStartCommand(intent, flags, startId)
}
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var manager: AlarmManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
manager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
button.setOnClickListener {
Log.d("AppLog", "alarm set")
Toast.makeText(this, "alarm set", Toast.LENGTH_SHORT).show()
val timeToTrigger = System.currentTimeMillis() + 10 * 1000
setAlarm(this, timeToTrigger, 1)
}
}
companion object {
fun setAlarm(context: Context, timeToTrigger: Long, requestId: Int) {
val manager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val pendingIntent = PendingIntent.getBroadcast(context, requestId, Intent(context, AlarmReceiver::class.java), PendingIntent.FLAG_UPDATE_CURRENT)
// val pendingIntent = PendingIntent.getBroadcast(context, requestId, Intent(context, Main2Activity::class.java), PendingIntent.FLAG_UPDATE_CURRENT)
// val pendingIntent = PendingIntent.getService(context, requestId, Intent(context, MyService::class.java), PendingIntent.FLAG_UPDATE_CURRENT)
when {
VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP -> manager.setAlarmClock(AlarmClockInfo(timeToTrigger, pendingIntent), pendingIntent)
VERSION.SDK_INT >= VERSION_CODES.KITKAT -> manager.setExact(AlarmManager.RTC_WAKEUP, timeToTrigger, pendingIntent)
else -> manager.set(AlarmManager.RTC_WAKEUP, timeToTrigger, pendingIntent)
}
}
}
}
AlarmReceiver.kt
class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Log.d("AppLog", "AlarmReceiver onReceive")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(Intent(context, MyService::class.java))
} else context.startService(Intent(context, MyService::class.java))
}
}
What's wrong in the code I've made? How come it works on P but not always on Q?
You are attempting to start an activity from the background. That is banned on Android 10+ for the most part.
According to the docs, alarms shouldn't be harmed.
From the material that you quoted, with emphasis added: "The app receives a notification PendingIntent from the system". You are not using notifications. And, therefore, this exception does not apply.
On Android Q, is there an official way to let alarms work correctly? To open an Activity that will be shown to the user, exactly as an alarm clock app should?
Use a notification with a full-screen Intent, as is covered in the documentation. If the screen is locked, your activity will be displayed when the notification is raised. If the screen is unlocked, a high-priority ("heads up") notification will be displayed instead. In other words:
If the device is not being used, you get what you want
If the device is probably being used, the user find out about the event without your taking over the screen, so you do not interfere with whatever the user is doing (e.g., relying on a navigation app while driving)

How to Repeat a function every Hour in Android Studio using Kotlin?

I want to repeat a function( or any action ) every one Hour ( for example ) even if the app is not running.
I've created a demo project so you can take a look at it :
https://github.com/joancolmenerodev/BroadcastReceiverAndAlarmManagerInKotlin
You first have to create a BroadcastReceiver, and then using AlarmManager you can decide the interval of time you want to be called.
Create a BroadcastReceiver you can do it as follows :
val broadCastReceiver = object : BroadcastReceiver() {
override fun onReceive(contxt: Context?, intent: Intent?) {
toast("This toast will be shown every X minutes")
}
}
And then you have this method to start the job :
val mIntent = Intent(context, broadCastReceiver)
val mPendingIntent = PendingIntent.getBroadcast(context, REQUEST_CODE, mIntent, 0)
val mAlarmManager = context
.getSystemService(Context.ALARM_SERVICE) as AlarmManager
mAlarmManager.setRepeating(
AlarmManager.ELAPSED_REALTIME_WAKEUP, System.currentTimeMillis(),
CHANGETOYOURDESIREDSECONDS, mPendingIntent
)
And then you'll be able to see the Toast even if the app is closed.
Edit
You can register your BroadcastReceiver using context.registerReceiver(receiver, IntentFilter("something"))
and then adding to the mIntent and action for "something".
If you don't like this way, you can create a new class named MyReceiver that extends BradcastReceiver as follows :
class MyReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Toast.makeText(context,"This toast will be shown every X minutes", Toast.LENGTH_SHORT).show()
}
}
And then start the alarm doing this :
val mIntent = Intent(this, MyReceiver::class.java)
val mPendingIntent = PendingIntent.getBroadcast(this, 0, mIntent, PendingIntent.FLAG_UPDATE_CURRENT)
val mAlarmManager = this
.getSystemService(Context.ALARM_SERVICE) as AlarmManager
mAlarmManager.setRepeating(
AlarmManager.RTC_WAKEUP, System.currentTimeMillis(),
WHATEVERYOUWANT, mPendingIntent
)
Note: By default is set to 60000
Value will be forced up to 60000 as of Android 5.1; don't rely on this to be exact
If you are using AndroidX(JetPack) library then concider to use Workmanager
Simple example:
public class MyWorker extends Worker {
static final String TAG = "workmng";
#NonNull
#Override
public WorkerResult doWork() {
Log.d(TAG, "doWork: start");
//Do your job here
Log.d(TAG, "doWork: end");
return WorkerResult.SUCCESS;
}
}
And start like this, to do your job each hour :
PeriodicWorkRequest myWorkRequest = new PeriodicWorkRequest.Builder(MyWorker.class, 60, TimeUnit.MINUTES)
.build();
Add in app gradle file:
dependencies {
def work_version = 2.0.0
// (Java only)
implementation "androidx.work:work-runtime:$work_version"
}

Categories

Resources