Search code examples
c#xamarin.androidalarmmanagerforeground-service

AlarmManager not firing when in sleep mode


As the title says, my AlarmManager often does not fire, or better said, seems to go on-pause, and thus the alarm will be fired with delays (I'm using SetExactAndAllowWhileIdle), sometimes really high delays which breaks the purpose of my app.

My app has a foreground process, I've already added the app to IgnoreBatteryOptimizations and also disabled the OEM battery optimization.

I've these problem on my Samsung S7 Edges, but on the emulator (tested also with doze activated) everything works fine, so I'm aware that this is an issue caused by the OEMs, but does someone know how to resolve this?

Below some relevant code:

Alarm BroadcastReceiver:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

using Android.Content;
using Android.Util;

namespace CGSJDSportsNotification.Droid {
    public partial class JDMonitoring {
        [BroadcastReceiver]
        public partial class BackgroundWorker : BroadcastReceiver {
            Helper H { get; } = new Helper(60 * SharedSettings.Entries.Get.Int32("searchRefresh"));

            public override void OnReceive(Context context, Intent intent) {
                FetchTickets();
            }

            async void FetchTickets() {
                Log.Debug("jd_foo", "STARTED");

                // Reinitialize the Browser class
                if (H.browser == null) {
                    Log.Debug("jd_foo", "BROWSER == NULL");

                    H.wc = new Browser.CustomWebViewClient();
                    H.wc.OnPageStart += BrowserOnPageStarted;
                    H.wc.OnPageLoaded += BrowserOnPageLoaded;
                    await H.BrowserInit();

                    Log.Debug("jd_foo", "BROWSER_INIT_SUCCESS");
                }

                if (await H.Net_Or_FgServiceAreNotActive())
                    goto CleanUpWebView;

                Log.Debug("jd_foo", "NET_OR_FGSERVICE_OK");

                UserNotification.Remove(12029); // Removes the 'No Internet' notification when the connection is back
                UserNotification.Remove(503); // Removes the 'Server took too long to respond' notification when is back
                UserNotification.Remove(0); // Notification warning group id

                // Resets the process cycle
                H.ProcessPhase = 0;
                H.InDepthProcess = false;

                H.browser.LoadUrl(H.UrlQueuePage);

                Log.Debug("jd_foo", "PREPARING_WHILE_PHASE_0");

                // Waits for the log-in page to fully load
                while (H.ProcessPhase == 0) {
                    if (await H.Net_Or_FgServiceAreNotActive())
                        goto CleanUpWebView;

                    await Task.Delay(100);
                }

                Log.Debug("jd_foo", "WHILE_PHASE_O_PASSED");

                bool? loginPage = await H.IsOnLoginPage();

                Log.Debug("jd_foo", $"ON_LOGIN_PAGE = {loginPage}");

                if (loginPage == true) {
                    bool? loginResponse = await H.Login();
                    Log.Debug("jd_foo", $"LOGIN_RESPONSE = {loginResponse}");

                    if (loginResponse == false) {
                        if (await H.Net_Or_FgServiceAreNotActive())
                            goto CleanUpWebView;

                        H.BackgroundWorkerReset();
                        goto CleanUpWebView;

                    } else if (loginResponse == null)
                        // If Login function returns null means that there's no internet connection or the server is down
                        goto CleanUpWebView;

                } else if (loginPage == false) {
                    if (await H.Net_Or_FgServiceAreNotActive())
                        goto CleanUpWebView;

                    // If at this point ProcessPhase is 1 this means that the browser session was cleaned and the we loggedin again
                    if (H.ProcessPhase == 1)
                        H.browser.LoadUrl(H.UrlQueuePage);
                } else
                    // If null is returned, this means that there's no internet connection or the server is down
                    goto CleanUpWebView;

                Log.Debug("jd_foo", "PREPARING_WHILE_PROCESS_PHASE_1");

                // Waits for the queue page to fully load
                while (H.ProcessPhase == 1) {
                    if (await H.Net_Or_FgServiceAreNotActive())
                        goto CleanUpWebView;

                    await Task.Delay(100);
                }

                Log.Debug("jd_foo", "WHILE_PHASE_1_PASSED");

                if (await H.JDServerIsOk() == false)
                    goto CleanUpWebView;

                Log.Debug("jd_foo", "JD_SERVER_OK");

                // Checks if the queue HTML table element is present
                if (await H.browser.ElementExists("document.getElementsByClassName('ticket-list collection-as-table')[0]")) {
                    Log.Debug("jd_foo", "QUEUE_TABLE_OK");

                    List<Ticket> tkts = await H.GetTickets();

                    Log.Debug("jd_foo", "GOT_TKTS_OK");

                    string country = SharedSettings.Entries.Get.String("searchCountrySelected").ToLower();

                    // Fetch all the tickets inside the time frame chosen by the user
                    // Starts from the end of the list in order to show the recent tkts on top
                    for (int i = tkts.Count - 1; i >= 0; i--) {
                        Ticket t = tkts[i];

                        // Checks only the tkts with the 'open' or 'new' status
                        //if (t.Status.ToLower() == "open" || t.Status.ToLower() == "new") {
                        if (t.Status.Length > 0) { //-- Used for testing
                            // The first and fastest step is to check the tkt title
                            if (t.Title.ToLower().Contains(country)) {
                                H.DisplayNotification(t.ID, t.LastUpdated, t.Title, t.Status, t.Owner, country, t.Link);
                            } else if (H.TktIsWithoutCountry(t.Title)) {
                                if (await H.JDServerIsOk() == false)
                                    goto CleanUpWebView;

                                // Country not found in the title, we must dig deeper by opening the tkt link
                                H.InDepthProcess = true;

                                H.browser.LoadUrl(t.Link);

                                // Waits for the tkt url to load
                                while (H.ProcessPhase == 2) {
                                    if (await H.Net_Or_FgServiceAreNotActive())
                                        goto CleanUpWebView;

                                    await Task.Delay(100);
                                }

                                // Gets the page's body
                                string pBody = await H.browser.EvalJS("document.body.textContent");

                                // If the selected country by the user is present somewhere into the tkt page body
                                if (pBody.IndexOf(country, StringComparison.OrdinalIgnoreCase) > -1) {
                                    H.DisplayNotification(t.ID, t.LastUpdated, t.Title, t.Status, t.Owner, country, t.Link);
                                } else if (H.TktIsWithoutCountry(pBody)) {
                                    // If the country string isn't present and the user has chosen to check also for tkts with unknown countries
                                    if (SharedSettings.Entries.Get.Bool("searchUnknownCountry"))
                                        H.DisplayNotification(t.ID, t.LastUpdated, t.Title, t.Status, t.Owner, "unknown", t.Link);
                                }

                                // Ready for the next ticket
                                H.ProcessPhase = 2;
                            }
                        }
                    }
                }

                Log.Debug("jd_foo", "STARTING_CLEAN_UP");
            CleanUpWebView:
                // Tries to free the memory asap
                H.FreeMemory();
                // Reschedules the alarm
                H.BackgroundWorkerReset();

                Log.Debug("jd_foo", "CLEAN_UP_OK");
            }

            public async void BrowserOnPageStarted(object sender, string url) {
                if (await H.Net_Or_FgServiceAreNotActive())
                    return;
            }

            async void BrowserOnPageLoaded(object sender, string url) {
                if (await H.Net_Or_FgServiceAreNotActive())
                    return;
                
                // When the login page has loaded
                if (url.StartsWith(H.UrlLoginPage)) {
                    // Login again in order to access the tkt page
                    if (H.InDepthProcess)
                        await H.Login();
                    else
                        // First login
                        H.ProcessPhase = 1;

                    // Saves the login session
                    Android.Webkit.CookieManager.Instance.Flush();
                }

                // When the queue page has loaded
                if (url.Equals(H.UrlQueuePage))
                    H.ProcessPhase = 2;

                // When the tkt page has loaded
                if (url.StartsWith("https://support.jdplc.com/rt4/Ticket/Display.html?id=") && H.InDepthProcess) {
                    while (await H.browser.EvalJS("document.getElementById('TitleBox--_Helpers_TicketHistory------SGlzdG9yeQ__---0')") == null) {
                        if (await H.Net_Or_FgServiceAreNotActive() || await H.JDServerIsOk() == false)
                            return;

                        await Task.Delay(1000);
                    }

                    H.ProcessPhase = 3;
                }
            }
        }
    }
}

Alarm helper methods:

public static void BackgroundWorkerFire(int alarmMilliseconds) {
((AlarmManager)(new ContextWrapper(AndroidContext)).GetSystemService(Context.AlarmService))
    .SetExactAndAllowWhileIdle(
        AlarmType.ElapsedRealtimeWakeup,
        SystemClock.ElapsedRealtime() + alarmMilliseconds,
        PendingIntent.GetBroadcast(
        AndroidContext,
        98,
        new Intent(
            AndroidContext,
            typeof(JDMonitoring.BackgroundWorker)),
    PendingIntentFlags.UpdateCurrent));
}

public void BackgroundWorkerReset(int seconds = -1) {
    BackgroundWorkerStop();
    BackgroundWorkerFire(seconds == -1 ? RefreshRate : 1000 * seconds);
}

Example of not firing in time:

06-27 21:19:54.962 25634 25634 D jd_foo  : STARTED
06-27 21:19:54.962 25634 25634 D jd_foo  : BROWSER == NULL
06-27 21:19:55.766 25634 25634 D jd_foo  : BROWSER_INIT_SUCCESS
06-27 21:19:55.842 25634 25634 D jd_foo  : NET_OR_FGSERVICE_OK
06-27 21:19:55.851 25634 25634 D jd_foo  : PREPARING_WHILE_PHASE_0
06-27 21:19:59.699 25634 25634 D jd_foo  : WHILE_PHASE_O_PASSED
06-27 21:19:59.791 25634 25634 D jd_foo  : ON_LOGIN_PAGE = True
06-27 21:20:05.093 25634 25634 D jd_foo  : LOGIN_RESPONSE = True
06-27 21:20:05.093 25634 25634 D jd_foo  : PREPARING_WHILE_PROCESS_PHASE_1
06-27 21:20:05.795 25634 25634 D jd_foo  : WHILE_PHASE_1_PASSED
06-27 21:20:05.803 25634 25634 D jd_foo  : JD_SERVER_OK
06-27 21:20:05.814 25634 25634 D jd_foo  : QUEUE_TABLE_OK
06-27 21:20:06.411 25634 25634 D jd_foo  : GOT_TKTS_OK
06-27 21:20:08.666 25634 25634 D jd_foo  : STARTING_CLEAN_UP
06-27 21:20:08.714 25634 25634 D jd_foo  : CLEAN_UP_OK
06-27 21:33:37.282 25634 25634 D jd_foo  : STARTED
06-27 21:33:37.284 25634 25634 D jd_foo  : BROWSER == NULL
06-27 21:33:37.415 25634 25634 D jd_foo  : BROWSER_INIT_SUCCESS
06-27 21:33:37.429 25634 25634 D jd_foo  : NET_OR_FGSERVICE_OK
06-27 21:33:37.437 25634 25634 D jd_foo  : PREPARING_WHILE_PHASE_0
06-27 21:33:39.572 25634 25634 D jd_foo  : WHILE_PHASE_O_PASSED
06-27 21:33:39.599 25634 25634 D jd_foo  : ON_LOGIN_PAGE = True
06-27 21:33:44.871 25634 25634 D jd_foo  : LOGIN_RESPONSE = True
06-27 21:33:44.871 25634 25634 D jd_foo  : PREPARING_WHILE_PROCESS_PHASE_1
06-27 21:33:44.872 25634 25634 D jd_foo  : WHILE_PHASE_1_PASSED
06-27 21:33:44.890 25634 25634 D jd_foo  : JD_SERVER_OK
06-27 21:33:44.909 25634 25634 D jd_foo  : QUEUE_TABLE_OK
06-27 21:33:45.732 25634 25634 D jd_foo  : GOT_TKTS_OK
06-27 21:33:47.383 25634 25634 D jd_foo  : STARTING_CLEAN_UP
06-27 21:33:47.452 25634 25634 D jd_foo  : CLEAN_UP_OK
06-27 21:48:37.318 25634 25634 D jd_foo  : STARTED
06-27 21:48:37.318 25634 25634 D jd_foo  : BROWSER == NULL
06-27 21:48:37.442 25634 25634 D jd_foo  : BROWSER_INIT_SUCCESS
06-27 21:48:37.463 25634 25634 D jd_foo  : NET_OR_FGSERVICE_OK
06-27 21:48:37.470 25634 25634 D jd_foo  : PREPARING_WHILE_PHASE_0
06-27 21:48:39.523 25634 25634 D jd_foo  : WHILE_PHASE_O_PASSED
06-27 21:48:39.578 25634 25634 D jd_foo  : ON_LOGIN_PAGE = True
06-27 21:48:44.943 25634 25634 D jd_foo  : LOGIN_RESPONSE = True
06-27 21:48:44.944 25634 25634 D jd_foo  : PREPARING_WHILE_PROCESS_PHASE_1
06-27 21:48:44.944 25634 25634 D jd_foo  : WHILE_PHASE_1_PASSED
06-27 21:48:44.962 25634 25634 D jd_foo  : JD_SERVER_OK
06-27 21:48:44.980 25634 25634 D jd_foo  : QUEUE_TABLE_OK
06-27 21:48:46.221 25634 25634 D jd_foo  : GOT_TKTS_OK
06-27 21:48:47.869 25634 25634 D jd_foo  : STARTING_CLEAN_UP
06-27 21:48:47.931 25634 25634 D jd_foo  : CLEAN_UP_OK
06-27 22:03:37.396 25634 25634 D jd_foo  : STARTED
06-27 22:03:37.398 25634 25634 D jd_foo  : BROWSER == NULL
06-27 22:03:37.565 25634 25634 D jd_foo  : BROWSER_INIT_SUCCESS
06-27 22:03:37.581 25634 25634 D jd_foo  : NET_OR_FGSERVICE_OK
06-27 22:03:37.586 25634 25634 D jd_foo  : PREPARING_WHILE_PHASE_0
06-27 22:03:39.581 25634 25634 D jd_foo  : WHILE_PHASE_O_PASSED
06-27 22:03:39.620 25634 25634 D jd_foo  : ON_LOGIN_PAGE = True
06-27 22:03:44.860 25634 25634 D jd_foo  : LOGIN_RESPONSE = True
06-27 22:03:44.861 25634 25634 D jd_foo  : PREPARING_WHILE_PROCESS_PHASE_1
06-27 22:03:44.861 25634 25634 D jd_foo  : WHILE_PHASE_1_PASSED
06-27 22:03:44.879 25634 25634 D jd_foo  : JD_SERVER_OK
06-27 22:03:44.893 25634 25634 D jd_foo  : QUEUE_TABLE_OK
06-27 22:03:45.754 25634 25634 D jd_foo  : GOT_TKTS_OK
06-27 22:03:47.549 25634 25634 D jd_foo  : STARTING_CLEAN_UP
06-27 22:03:47.625 25634 25634 D jd_foo  : CLEAN_UP_OK
06-27 22:18:37.288 25634 25634 D jd_foo  : STARTED
06-27 22:18:37.289 25634 25634 D jd_foo  : BROWSER == NULL
06-27 22:18:37.422 25634 25634 D jd_foo  : BROWSER_INIT_SUCCESS
06-27 22:18:37.438 25634 25634 D jd_foo  : NET_OR_FGSERVICE_OK
06-27 22:18:37.443 25634 25634 D jd_foo  : PREPARING_WHILE_PHASE_0
06-27 22:18:39.343 25634 25634 D jd_foo  : WHILE_PHASE_O_PASSED
06-27 22:18:39.400 25634 25634 D jd_foo  : ON_LOGIN_PAGE = True
06-27 22:18:44.643 25634 25634 D jd_foo  : LOGIN_RESPONSE = True
06-27 22:18:44.644 25634 25634 D jd_foo  : PREPARING_WHILE_PROCESS_PHASE_1
06-27 22:18:44.644 25634 25634 D jd_foo  : WHILE_PHASE_1_PASSED
06-27 22:18:44.651 25634 25634 D jd_foo  : JD_SERVER_OK
06-27 22:18:44.658 25634 25634 D jd_foo  : QUEUE_TABLE_OK
06-27 22:18:45.483 25634 25634 D jd_foo  : GOT_TKTS_OK
06-27 22:18:47.087 25634 25634 D jd_foo  : STARTING_CLEAN_UP
06-27 22:18:47.167 25634 25634 D jd_foo  : CLEAN_UP_OK

As you can see the first firing was at 21:19, then at 21:33 and then at 21:48.
But should have been fired every 10 minutes

I really don't understand why :/

P.S: I've also noticed just now, that the screen does not wake up, even if I'm using AlarmType.ElapsedRealtimeWakeup)

[EDIT]
Now that I've tested a little bit longer, looks like that the alarm after the first firing, is fired every 15 minutes, this is not a big issue for my app, but why?


Solution

  • From Android documentation:

    To reduce abuse, there are restrictions on how frequently these alarms will go off for a particular application. Under normal system operation, it will not dispatch these alarms more than about every minute (at which point every such pending alarm is dispatched); when in low-power idle modes this duration may be significantly longer, such as 15 minutes.

    So, that's normal, "exact" is not so exact, it's name is misleading.