Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to keep HTTP requests running in background? - Android #14

Closed
JuanDeLeon opened this issue Mar 10, 2021 · 26 comments
Closed

How to keep HTTP requests running in background? - Android #14

JuanDeLeon opened this issue Mar 10, 2021 · 26 comments

Comments

@JuanDeLeon
Copy link

JuanDeLeon commented Mar 10, 2021

First of all, awesome plugin. Now, I understand this is not exactly a plugin's issue, but probably a very common scenario for it.
I'm trying to watch and send the location via HTTP Post request to server as in the following code:

BackgroundGeolocation.addWatcher(
      {
        backgroundMessage:
          "Location is still tracked in background.",
        requestPermissions: true,
        stale: true,
        distanceFilter: 200,
      },
      async function (location) {
        if (location) {
          let formData = new FormData();
          formData.append("location", JSON.stringify(location));
          await ApiService.sendLocation(formData); // Does a simple HTTP post request using Axios.
        }
      }
    );

This is working fine in iOS indefinitely (as long as I grant the 'Always' location permission).

However for Android, this is not the case. I granted all battery/data usage permissions for my app in particular.
After 5 minutes in background, the server will stop receiving locations.
As soon as I enter the app again (it is not being killed btw, I can tell because it does not reload the splash screen) it will start sending all the buffered requests to server.
So the location is being tracked correctly, but it seems like the HTTP requests get blocked and start to pile up.

Does this mean that I should actually do the HTTP request inside the native callback code?
public void onLocationResult(LocationResult locationResult)

I would appreciate any insight, but as I said, I understand this is not precisely this plugin's issue.

@diachedelic
Copy link
Collaborator

May I ask what device(s) you are experiencing this issue with?

As a starting point, could you please insert console.log(new Date()); at the start of your watcher callback, and then connect your device via USB and attach a debugger via chrome://inspect. After reproducing the problem, look for a large gap in the logged timestamps. This would indicate that the WebView stopped receiving updates from the plugin at some point.

@JuanDeLeon
Copy link
Author

JuanDeLeon commented Mar 11, 2021

Samsung Galaxy Tab A SM-T510 on Android 10.

I made a quick test editing the Android plugin code, and put an additional HTTP POST call inside the onLocationResult() callback method.
It's interesting because after 5 minutes, both calls start to throttle down. But it seems like the native HTTP is able to "push" the Axios one. (The records with 0.0000 are native, the others are axios).

Captura de Pantalla 2021-03-10 a la(s) 19 04 20

Then after 5-10 more minutes, I stop getting the Axios call, but keep getting the native one, which is what I want to achieve anyway. So I guess I'll just fork this.

Captura de Pantalla 2021-03-10 a la(s) 19 06 05

@diachedelic
Copy link
Collaborator

There looks to be some throttling going on. The plugin seems to receive location updates every ~10s initially, slowing to one per minute or less. So the operating system might be throttling updates from the location provider to the plugin. But it also looks like the operating system is throttling execution of the WebView, which is disturbing (however I can't tell from your logs whether the watcher callback ceased being called or was just unable to send off HTTP requests).

@diachedelic
Copy link
Collaborator

Where do the timestamps in the rightmost column come from? Are they from the location object, or generated by the server?

@JuanDeLeon
Copy link
Author

That's the timestamp from server, when it receives the request.
I'll try to test it out as you mentioned, just to confirm if it's throttling the callback or the HTTP requests.
Since entering into the App again immediately starts sending out all "buffered" HTTP requests, I would think it's throttling those, and not the callback.

@diachedelic
Copy link
Collaborator

Thanks. It might be illuminating to see the difference between the time the location was generated and the time it was received by the server.

@JuanDeLeon
Copy link
Author

JuanDeLeon commented Mar 11, 2021

So this is what I got in Android Studio console (couldn't use Chrome devtools).
After 5-6 minutes I stopped receiving the Axios calls, that's exactly when console shows "A resource failed to call end" and "FinalizerDaemon".
At this point, the callback keeps being called opportunely, but not the Axios HTTP. (Native HTTP keeps working fine).
*Notice the "mutating IsBusy" log, which is triggered by my API/Axios services.

Captura de Pantalla 2021-03-10 a la(s) 23 00 18

Finally, when I open the App again, it starts to fire off all queued requests, and logs the following to make it clear:
2021-03-10 23:09:56.183 1124-1124/com.corvustrack.ionic I/Capacitor/Console: File: http://localhost/home - Line 0 - Msg: Some resource load requests were throttled while the tab was in background, and no request was sent from the queue in the last 1 minute. This means previously requested in-flight requests haven't received any response from servers. See https://www.chromestatus.com/feature/5527160148197376 for more details

Captura de Pantalla 2021-03-10 a la(s) 23 11 03

In conclusion, seems like this is intended by Chrome and I don't pretend my users having to turn this off, so I guess native HTTP is the way to go 👍🏻

@diachedelic
Copy link
Collaborator

WebSockets, WebRTC's data channel, Fetch API, XMLHttpRequest, EventSource, Video and Audio are excluded today to allow keep-alive connections.

From the link you pasted above, it appears your Axios requests should not be throttled.

This means previously requested in-flight requests haven't received any response from servers.

The error message seems to indicate that your requests are not receiving responses. Are you sure your server is responding to your requests correctly?

@JuanDeLeon
Copy link
Author

Oh I totally missed that. You're right.
However, I cannot think of why my server would just start rejecting axios requests after a few minutes... while actually accepting the "native" requests.
Both have pretty much the same headers and even the same authorization credentials.
Not to mention this all just worked on iOS, with Axios only 🤔

@diachedelic
Copy link
Collaborator

Is it possible your server is accepting & processing the requests, but is not sending out any response?

@JuanDeLeon
Copy link
Author

Just tested this by putting a log server-side, right before returning the 200 response. It definitely sent the response back.

@diachedelic
Copy link
Collaborator

Well I am bamboozled then.

@JuanDeLeon
Copy link
Author

Yeah, thanks for keeping in touch.
I'll just use the http plugin for this specifically.
Hopefully someone else will find this useful.

@gbrits
Copy link

gbrits commented Mar 19, 2021

I am experiencing this issue on Android only exactly as described in the opening post of this thread, I implemented an enqueueing process which I initially thought helped, but then on Android the problem (as described above) persisted. I considered setting up job queueing on my Laravel backend, but that doesn't rectify the fact that after some time Android appears to start spamming GPS locations after adding them up -- it's almost as if backgrounding works temporarily, then stalls, collects GPS to send but doesn't use the callback to send them until the app is foregrounded - then it spams all of the GPS points into your database all at once, causing throttling to occur. @JuanDeLeon did you solve this somehow?

@JuanDeLeon
Copy link
Author

@gbrits Yeah I switched from Axios to the Http plugin for this specific call.

So on this plugin's callback I do:

async function (location) {
          if (location) {
            setNewMarkerPosition(location);
            sendLocation(location);
          }
        }

And this method will use Http community plugin, not axios.

const sendLocation = async (location) => {
      try {
        const access_token = await Storage.get({ key: "access_token" });

        await Http.request({
          method: "POST",
          url: process.env.VUE_APP_API_BASE_URL + "/sendLocation",
          headers: {
            Authorization: "Bearer " + access_token.value,
            Accept: "application/json",
            "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
          },
          data: {
            location: JSON.stringify(location),
          },
        });
      } catch (e) {
        console.log(e);
      }
    };

@gbrits
Copy link

gbrits commented Mar 19, 2021

Hey thanks for responding @JuanDeLeon I'm seriously keen to get this fixed, thanks for sharing your code. I'm using Ionic, I'm not using Axios (as far as I'm aware) I'm using @angular/common/http ...is this similar to Axios in its failing, do you know?

@gbrits
Copy link

gbrits commented Mar 19, 2021

I see you're referring to https://github.com/capacitor-community/http -- @angular's common http must be a javascript implementation of http requests, much like Axios, whereas it appears https://github.com/capacitor-community/http is a native http requester - maybe that's why it works better with the backgrounding for you? This is going to be painful to refactor but worth it, if it's guaranteed to work...

@JuanDeLeon
Copy link
Author

Yeah should be the same root issue. You want native http, not browser based.
"Implements an HTTP client API for Angular apps that relies on the XMLHttpRequest interface exposed by browsers."

After switching to native http, I kept my tablet turned on for 12+ hours and definitely worked as I expected.

@JuanDeLeon
Copy link
Author

JuanDeLeon commented Mar 19, 2021

If you need a quick try, you may overwrite this plugin's native callback function and add something like the following.

HttpURLConnection con;
try {
    URL url = new URL("https://yourserver/sendLocation");

    Map<String,Object> locationJson = new LinkedHashMap<>();
    Map<String,Object> params = new LinkedHashMap<>();

    params.put("location", "{\"latitude\": " + location.getLatitude() + ", \"longitude\": " + location.getLongitude() + "}");

    StringBuilder postData = new StringBuilder();
    for (Map.Entry<String,Object> param : params.entrySet()) {
        if (postData.length() != 0) postData.append('&');
        postData.append(URLEncoder.encode(param.getKey(), "UTF-8"));
        postData.append('=');
        postData.append(URLEncoder.encode(String.valueOf(param.getValue()), "UTF-8"));
    }
    byte[] postDataBytes = postData.toString().getBytes("UTF-8");

    con = (HttpURLConnection) url.openConnection();
    con.setRequestMethod("POST");
    con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
    con.setRequestProperty("Accept", "application/json");
    con.setRequestProperty("Authorization", "Bearer " + "tokenRawValueHere");
    con.setDoInput(true);
    con.setDoOutput(true);
    con.setUseCaches(false);
    con.connect();

    con.getOutputStream().write(postDataBytes);

    Reader in = new BufferedReader(new InputStreamReader(con.getInputStream(), "UTF-8"));

    for (int c; (c = in.read()) >= 0;)
        System.out.print((char)c);


} catch (ProtocolException e) {
    e.printStackTrace();
} catch (MalformedURLException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}

@gbrits
Copy link

gbrits commented Mar 19, 2021

Thank you so much! I will give it a whirl

@asiermusa
Copy link

@gbrits Yeah I switched from Axios to the Http plugin for this specific call.

So on this plugin's callback I do:

async function (location) {
          if (location) {
            setNewMarkerPosition(location);
            sendLocation(location);
          }
        }

And this method will use Http community plugin, not axios.

const sendLocation = async (location) => {
      try {
        const access_token = await Storage.get({ key: "access_token" });

        await Http.request({
          method: "POST",
          url: process.env.VUE_APP_API_BASE_URL + "/sendLocation",
          headers: {
            Authorization: "Bearer " + access_token.value,
            Accept: "application/json",
            "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
          },
          data: {
            location: JSON.stringify(location),
          },
        });
      } catch (e) {
        console.log(e);
      }
    };

You saved my day!

@HarelM
Copy link
Contributor

HarelM commented Jul 19, 2022

Hi! I'm currently maintaining a fork of mauron85 plugin and am looking into migration to capacitor. I generally don't need all this http stuff but am interested to know how to solve the current problem of webview being stopped while the app is in the background (the 5 minutes described in this issue).
My solution right now is to delete all locations when going to background and fetching them from the SQLITE DB when returning to foreground (I believe this capability is not supported here according to the code I read).
What's the solution in this plugin for this issue?
I can open a different issue of needed, I haven't seen a discussion option in this repo...

@HarelM
Copy link
Contributor

HarelM commented Aug 13, 2022

@diachedelic any comments on the above question?
It would be great if I could migrate to this plugin (and maybe help maintain it) assuming it has the functionality I need...

@diachedelic
Copy link
Collaborator

This issue is about HTTP requests being throttled in the background. As far as I am aware, the WebView does not stop after 5 minutes in the background. Please create a new issue if that is happening for you.

@HarelM
Copy link
Contributor

HarelM commented Aug 14, 2022

Thanks for the info @diachedelic!
I haven't integrated the plugin yet in my app so I don't know if it is working as expected or not.
I know that Cordova used to stop the JS events in the webview after some time in the background, I still don't know what is the case for capacitor.
I would like to avoid opening an issue if it doesn't exists but rather understand what are the know limitations of this plugin if at all...
In any case, I'll take it for a spin now that I finished migrating to Capacitor and report an issue in case I find one.

@diachedelic
Copy link
Collaborator

diachedelic commented Oct 27, 2022

The sum up, HTTP requests initiated within the WebView are not sent reliably in the background. To fix this, use a native HTTP plugin such as https://capacitorjs.com/docs/apis/http.

@capacitor-community capacitor-community locked as resolved and limited conversation to collaborators Oct 27, 2022
@diachedelic diachedelic unpinned this issue Sep 29, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants