Turn on phone screen when AlarmManager fires and BroadCastReceiver onReceive is triggered - android

I am implementing a custom alarm with some logic behind it. When the alarm fires and the Broadcast receiver run the onReceive, the app starts a new activity for vibration and ringtone deactivation.
Because is an alarm, I want it to fires also when the phone is locked, and in sleep mode as well.
The alarm fires properly, even if the phone is in sleep mode. However, when I test the app on my smartphone (not just mine, but also my brother one), if the screen is OFF, it is not turned ON. On the contrary, the system works properly on the simulator with a pixel2 and android 9.
Below the receiver, and the manifest for the receiver and the app to disable the vibration and the tone ring.
class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val extras = intent?.getExtras()
val alarmId = intent?.getIntExtra("alarmId",-1)
if(alarmId != null){
val ta = MemoryManager.getAlarmWithId(alarmId.toInt())
if(ta != null && ta!!.isSet){
startAlarmActivity(context,alarmId)
}
}
}
}
<activity
android:name=".AlarmDeactivation"
android:configChanges="orientation|keyboardHidden|screenSize"
android:label="#string/title_activity_alarm_deactivation"
android:theme="#style/AppTheme.NoActionBar"
android:showOnLockScreen="true"
android:turnScreenOn="true">
</activity>
<receiver android:name=".AlarmReceiver">
</receiver>
And this how I set the alarm.
if (Build.VERSION.SDK_INT >= 23) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pendingIntent)
}
else if (Build.VERSION.SDK_INT >= 19) {
alarmManager.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent)
}
else{
alarmManager.set(AlarmManager.RTC_WAKEUP, time, pendingIntent)
}
How can I let the app to turn on the screen when the alarm fires?
Thanks to all.
EDIT
As suggested here, I tried to define a class where I implemented the powerManager WakeLock acquire and release, but without any changed.
class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val extras = intent?.getExtras()
val alarmId = intent?.getIntExtra("alarmId",-1)
AlertWakeLock.acquireWakeLock(context!!)
if(alarmId != null){
val ta = MemoryManager.getAlarmWithId(alarmId.toInt())
if(ta != null && ta!!.isSet){
startAlarmActivity(context,alarmId)
}
}
}
}
class AlertWakeLock {
companion object {
private var wakeLock: PowerManager.WakeLock? = null
fun acquireWakeLock(context: Context) {
if (wakeLock != null) {
return
}
val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
wakeLock = pm.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK or
PowerManager.ACQUIRE_CAUSES_WAKEUP or
PowerManager.ON_AFTER_RELEASE, "ToddlerTimer:WakeLock"
)
wakeLock!!.acquire()
}
fun releaseWakeLock() {
if (wakeLock != null) {
wakeLock!!.release()
wakeLock = null
}
}
}
}
class AlarmDeactivation : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
try {
super.onCreate(savedInstanceState)
setContentView(R.layout.alarm_deactivation)
val alarmId = intent.getIntExtra("ALARM_ID", -1)
val ta = MemoryManager.getAlarmWithId(alarmId.toInt())
val button: View = findViewById(R.id.saveButton)
val afm = AlarmFeedbackManager(this)
afm.play()
button.setOnClickListener{
MemoryManager.deSetAlarm(alarmId)
MemoryManager.saveAlarmIntoMemory()
afm.stop()
val mCtx = GlobalApplication.appContext
val intent = Intent(mCtx, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
mCtx?.startActivity(intent)
finish()
}
}
catch (e: Exception){
var i = e
}
}
override fun onDestroy() {
super.onDestroy()
AlertWakeLock.releaseWakeLock()
}
}

Related

WakeLock not turning on the screen

I am trying to create a basic alarm clock app. I set the alarm using the setExact method of the AlarmManager. And then I created a BroadcastReceiver that will display a toast and will start a new activity when the previously set alarm goes off. I also have a WakeLock in my BroadcastReceiver to try to turn on the screen. The BroadcastReceiver is working. It is able to receive the event and then created the activity. But the screen is not turning on. I've read and followed a lot of answers of posts like this in SO but I still cant make it work.
This is my code for setting the alarm:
alarmIntent = Intent(context, AlarmReceiver::class.java).let { intent ->
PendingIntent.getBroadcast(context, 1, intent, 0)
}
val alarmTime = Calendar.getInstance().apply {
timeInMillis = System.currentTimeMillis()
set(Calendar.HOUR_OF_DAY, alarmForm.time.get(Calendar.HOUR_OF_DAY))
set(Calendar.MINUTE, alarmForm.time.get(Calendar.MINUTE))
set(Calendar.SECOND, 0)
}
alarmManager.setExact(
AlarmManager.RTC_WAKEUP,
alarmTime.timeInMillis,
alarmIntent
)
This is my BroadcastReceiver:
class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val wakeLock: PowerManager.WakeLock =
(context.getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp::MyWakelockTag").apply {
acquire(10*60*1000L /*10 minutes*/)
}
}
Toast.makeText(context, "Wake Up!", Toast.LENGTH_LONG).show()
val alarmDisplayIntent = Intent(context, AlarmDisplayActivity::class.java)
alarmDisplayIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(alarmDisplayIntent)
wakeLock.release()
}
}
And this is the activity I display when alarm goes off:
class AlarmDisplayActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_alarm_display)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
override fun onResume() {
super.onResume()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setTurnScreenOn(true)
}
}
}
I am using a phone with android version 9. My minSdkVersion is 21.
What do you think I am doing wrong or am I missing something?

Starting an activity intent from a broadcast receiver shows the wrong activity

I am making a library that can be used to incorporate breaking and entering detection into any application
There is an arduino set to the alarm of the house which sends an SMS to a specific phone upon trigger
Within my sdk I register an sms receiver which upon receiving an sms with a specific text, should show a full screen activity (on top of the lockscreen too) that will alert the user
I created an application to test this behaviour
the application's package is : com.example.demo
the library's package is : com.example.sdk
the sms receiver looks like this:
class SMSReceiver : BroadcastReceiver() {
companion object {
const val TAG = "SMSReceiver"
}
private val logger by lazy { Injections.logger }
override fun onReceive(context: Context?, intent: Intent?) {
logger.log(TAG) { "Got sms" }
val ctx = context ?: return
val bundle = intent?.extras ?: return
val format = bundle.getString("format") ?: return
val pdus = (bundle["pdus"] as? Array<*>) ?: return
for (idx in pdus.indices) {
val pdu = pdus[idx] as? ByteArray ?: continue
val msg = SmsMessage.createFromPdu(pdu, format)
if (msg.messageBody.startsWith("theft event", true)) {
logger.log(TAG) { "Got theft event" }
abortBroadcast()
showTheftActivity(ctx, msg.messageBody)
break
}
}
}
private fun showTheftActivity(context: Context, messageBody: String) {
val intent = Intent(context, TheftActivity::class.java)
intent.addFlags(Intent.FLAG_FROM_BACKGROUND)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
// .addCategory(Intent.CATEGORY_LAUNCHER)
val location = messageBody.split(" ").getOrNull(2)
if (location != null) {
val coords = location.split(",")
if (coords.size == 2) {
val x = coords[0].toBigDecimalOrNull()
val y = coords[1].toBigDecimalOrNull()
if (x != null && y != null) {
intent.putExtra(TheftActivity.X, x.toString())
intent.putExtra(TheftActivity.Y, y.toString())
}
}
}
context.startActivity(intent)
}
}
the activity that should show on top of everything is this :
class TheftActivity : Activity() {
companion object {
const val X = "locationX"
const val Y = "locationY"
}
private val x: String? by lazy { intent.getStringExtra(X) }
private val y: String? by lazy { intent.getStringExtra(Y) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_theft)
val location = findViewById<Button>(R.id.locate)
if (x != null && y != null) {
location.setOnClickListener { Toast.makeText(applicationContext, "Going to $x , $y", Toast.LENGTH_SHORT).show() }
location.isEnabled = true
finish()
} else {
location.isEnabled = false
}
findViewById<Button>(R.id.cancel).setOnClickListener {
finish()
}
turnScreenOnAndKeyguardOff()
}
private fun turnScreenOnAndKeyguardOff() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(true)
setTurnScreenOn(true)
} else {
window.addFlags(
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
)
}
with(getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
requestDismissKeyguard(this#TheftActivity, null)
}
}
}
override fun onDestroy() {
super.onDestroy()
turnScreenOffAndKeyguardOn()
}
private fun turnScreenOffAndKeyguardOn() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(false)
setTurnScreenOn(false)
} else {
window.clearFlags(
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
)
}
}
}
and the sdk's android manifest contains this:
<application>
<activity
android:name=".ui.TheftActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:excludeFromRecents="true"
android:label="#string/title_activity_theft"
android:showOnLockScreen="true"
android:theme="#style/Theme.Sdk.Fullscreen" />
<receiver
android:name=".receivers.SMSReceiver"
android:enabled="true"
android:exported="true"
android:permission="android.permission.BROADCAST_SMS">
<intent-filter>
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
</application>
when testing this on an emulator I send the sms to trigger the theft event
if the activity for testing (the one in the com.example.demo package) is not closed then it is brought to the front , but if it is closed nothing happens (though I do see the log messages from the receiver)
how can I make my sms receiver open the TheftActivity instead of the main activity from the main package?
edit: if it helps, the theft activity seems to start and then get immediately destroyed
It looks like the system can't bring the activity to the foreground due to the restrictions implemented in Android Q
With Android Q, it is impossible to start an activity from the background automatically if your app does not include those exceptions listed in the link below.
https://developer.android.com/guide/components/activities/background-starts
For possible solutions :
https://stackoverflow.com/a/59421118/11982611

Android Kotlin Service issue

I have implemented a service that runs in the background of the application. The service calls the function every minute, which checks if the time has come to trigger the notification - reminder (retrieves the current date and time and compares it with the data saved in the database. If the date and time match, the notification is triggered. The function is called every 1 minute. Function this one must check every 1 minute the condition when the user uses the application, the application runs in the background, does not use the application and how the phone is asleep. By implementing reminders the previous day evening to the evening and the next day. Reminders appear for the same day for the evening and the first reminder for the next day. The next ones do not appear. On the other hand, the reminder set for 5 minutes works and for 30 no longer. Looking for information I found on the internet that the problem lies in:
time drift - When the screen is on and the phone is fully awake, the interval between two consecutive events will remain constant most of the time, but may jump from time to time (lengthen)
the phone goes into sleep mode
Any of you have an idea how to solve this, get around it?
ForegroundService
class ForegroundService : Service() {
companion object {
val CHANNEL_ID = "ForegroundServiceChannel"
val CHANNEL_ID_CHILD = "ForegroundServiceChannelCHILD"
private var isRunning = true
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val input = intent.getIntExtra("time",15)
createNotificationChannel()
val notificationIntent = Intent(this, Menu::class.java)
val pendingIntent = PendingIntent.getActivity(
this,
0, notificationIntent, 0
)
val wakeLock: PowerManager.WakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp::AR_Apteczka").apply {
acquire()
}
}
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Twoja Elektroniczna Apteczka")
.setContentText("Dbamy o Twoje zdrowie")
.setSmallIcon(com.example.agnieszka.ar_apteczka.R.drawable.pills)
.setOnlyAlertOnce(true)
//.setContentIntent(pendingIntent)
//.setSound(null)
.build()
startForeground(1, notification)
isRunning = true
val context = this
val intent = Intent(this, ShowAllTodaysMedicines::class.java)
val pendingIntentNotification = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
doAsync {
while(true)
{
var message= createReminderMessage(context)
uiThread {
if(true) {
if (message != "Nadszedł czas by zażyć: ") {
val notification =
NotificationCompat.Builder(context, CHANNEL_ID_CHILD)
.setContentTitle("Zażyj leki")
.setContentText(message)
.setSmallIcon(com.example.agnieszka.ar_apteczka.R.drawable.pills)
.setContentIntent(pendingIntentNotification)
.setAutoCancel(true)
.build()
with(NotificationManagerCompat.from(context)) {
notificationManager.notify(2, notification)
}
}
}
}
//SystemClock.sleep(60000)
SystemClock.sleep(60*1000-SystemClock.elapsedRealtime()%1000)
// SystemClock.sleep(50000) //10*1000-SystemClock.elapsedRealtime()%1000
}
}
return START_NOT_STICKY
}
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onDestroy() {
super.onDestroy()
isRunning = false
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel(
CHANNEL_ID,
"Foreground Service Channel",
//NotificationManager.IMPORTANCE_DEFAULT
NotificationManager.IMPORTANCE_DEFAULT
)
//serviceChannel.setSound(null, null) //
val serviceChannel2 = NotificationChannel(
CHANNEL_ID_CHILD,
"Foreground Service ChannelChild ",
NotificationManager.IMPORTANCE_DEFAULT
//NotificationManager.IMPORTANCE_LOW
)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(serviceChannel)
manager.createNotificationChannel(serviceChannel2)
}
}
fun reminderForNow(context: Context) : ArrayList<Reminder> {
var listOfReminder : ArrayList<Reminder> = ArrayList()
var timetoday = takeTimeNow()
var dateToday = takeTodayDate()
val dbHelper = SQLConector(context)
val allRemindersList = dbHelper.getAllReminders()
for (i: Reminder in allRemindersList) {
if (i.reminderDate == dateToday && i.ReminderTime == timetoday) {
var reminder = Reminder(
i.idReminder,
i.medicineName,
i.reminderDate,
i.ReminderTime
)
listOfReminder.add(reminder)
}
}
return listOfReminder
}
private fun createReminderMessage(p0: Context) : String{
var message = "Nadszedł czas by zażyć: "
var listOfReminders = reminderForNow(p0)
if(listOfReminders.count() > 0){
for (i: Reminder in listOfReminders) {
message += i.medicineName + ", "
}
}
return message
}
private fun takeTodayDate():String{
val current = LocalDateTime.now()
val formatDate = DateTimeFormatter.ofPattern("yyyy-MM-dd")
var currentDate = current.format(formatDate).toString()
return currentDate
}
private fun takeTimeNow() : String{
val current = LocalDateTime.now()
val formatTime = DateTimeFormatter.ofPattern("HH:mm")
return current.format(formatTime).toString()
}
}
**Main Activity**
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
binding.buttonStart.setOnClickListener { startService() }
binding.buttonStop.setOnClickListener {stopService() }
startService()
}
private fun startService() {
val serviceIntent = Intent(this, ForegroundService::class.java)
serviceIntent.putExtra("time", 1)
ContextCompat.startForegroundService(this, serviceIntent)
}
private fun stopService() {
val serviceIntent = Intent(this, ForegroundService::class.java)
stopService(serviceIntent)
}
}
*Manifest*
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:allowBackup="false"
android:icon="#drawable/pills"
android:label="#string/nameOfApplications"
android:roundIcon="#drawable/icon"
android:supportsRtl="true"
android:theme="#style/AppTheme">
<service
android:name=".ForegroundService"
android:enabled="true"
android:exported="true"/>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>

Android Kotlin Foregeground Service + Notifications, Why works wrong?

In my application, I need a foreground service that will check a certain condition every minute, and if it is correct, it triggers a notification reminder. The user determines in advance what time and day he would like to have the reminder to. Data is saved in the database. Then the service every minute checks if it has a reminder for a given hour and day and if so sends a notification. The service must work when the user uses the application, when the application runs in the background and when it is closed. Could someone tell me why this code works on one phone but not on others? So-called, if I set a reminder, up to 20 minutes, it works (in all 3 states that I wrote about earlier), but once I set the reminder for the next days, it doesn't work anymore. I am surprised that sometimes the reminder on another phone works and sometimes it doesn't. I checked, the permission for the application to run in the background is selected in the settings. Please help.
Manifest
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:allowBackup="false"
android:icon="#drawable/pills"
android:label="#string/nameOfApplications"
android:roundIcon="#drawable/icon"
android:supportsRtl="true"
android:theme="#style/AppTheme">
<service
android:name=".ForegroundService"
android:enabled="true"
android:exported="true"/>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
ForegroundService
class ForegroundService : Service() {
companion object {
val CHANNEL_ID = "ForegroundServiceChannel"
val CHANNEL_ID_CHILD = "ForegroundServiceChannelCHILD"
private var isRunning = false
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val input = intent.getIntExtra("time",15)
createNotificationChannel()
val notificationIntent = Intent(this, Menu::class.java)
val pendingIntent = PendingIntent.getActivity(
this,
0, notificationIntent, 0
)
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("MotherNotification")
.setContentText("Message")
.setOnlyAlertOnce(true)
.build()
startForeground(1, notification)
isRunning = true
val context = this
val intent = Intent(this, ShowAll::class.java)
val pendingIntentNotification = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
doAsync {
while(isRunning)
{
var message : String = createReminderMessage(context)
//SystemClock.sleep(input * 10_000L)
SystemClock.sleep(50000)
uiThread {
if(isRunning && (message != "Nadszedł czas by zażyć: ")) {
val notification = NotificationCompat.Builder(context, CHANNEL_ID_CHILD)
.setContentTitle("Title")
.setContentText(message)
.setContentIntent(pendingIntentNotification)
.setAutoCancel(true)
.build()
with(NotificationManagerCompat.from(context)) {
notificationManager.notify(2, notification)
}
}
}
}
}
return START_NOT_STICKY
}
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onDestroy() {
super.onDestroy()
isRunning = false
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel(
CHANNEL_ID,
"Foreground Service Channel",
NotificationManager.IMPORTANCE_DEFAULT
)
val serviceChannel2 = NotificationChannel(
CHANNEL_ID_CHILD,
"Foreground Service ChannelChild ",
NotificationManager.IMPORTANCE_DEFAULT
//NotificationManager.IMPORTANCE_LOW
)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(serviceChannel)
manager.createNotificationChannel(serviceChannel2)
}
}
fun reminderForNow(context: Context) : ArrayList<Reminder> {
var listOfReminder : ArrayList<Reminder> = ArrayList()
var timetoday = takeTimeNow()
var dateToday = takeTodayDate()
val dbHelper = SQLConector(context)
val allRemindersList = dbHelper.getAllReminders()
for (i: Reminder in allRemindersList) {
if (i.reminderDate == dateToday && i.ReminderTime == timetoday) {
var reminder = Reminder(
i.id,
i.Name,
i.reminderDate,
i.ReminderTime
)
listOfReminder.add(reminder)
}
}
return listOfReminder
}
private fun createReminderMessage(p0: Context) : String{
var message : String = "title : "
var listOfReminders = reminderForNow(p0)
if(listOfReminders.count() > 0){
for (i: Reminder in listOfReminders) {
message += i.Name + ", "
}
}
return message
}
private fun takeTodayDate():String{
val current = LocalDateTime.now()
val formatDate = DateTimeFormatter.ofPattern("yyyy-MM-dd")
var dateResult = current.format(formatDate).toString()
return dateResult
}
private fun takeTimeNow() : String{
val current = LocalDateTime.now()
val formatTime = DateTimeFormatter.ofPattern("HH:mm")
var timeResult = current.format(formatTime).toString()
return timeResult
}
}
Main Activity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
binding.buttonStart.setOnClickListener { startService() }
binding.buttonStop.setOnClickListener {stopService() }
startService()
}
private fun startService() {
val serviceIntent = Intent(this, ForegroundService::class.java)
serviceIntent.putExtra("time", 1)
ContextCompat.startForegroundService(this, serviceIntent)
}
private fun stopService() {
val serviceIntent = Intent(this, ForegroundService::class.java)
stopService(serviceIntent)
}
}
The correct way to handle tasks which require exact fire time is to use the AlarmManagerCompat class.
You can use setExactAndAllowWhileIdle(...) method to force the alarm to start your service even when the device is in Doze mode and you will need a BroadcastReceiver to re-schedule the alarms if the device is rebooted.
You can find some references online on how to implement that.

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)

Categories

Resources