2

I have an app that generates data that needs to be sent to my server eventually (several hours of delay are no problem at all). I would like to do this with minimal impact to the user, which means to me:

  • Minimal battery use
  • No extra permissions, especially not "sketchy" ones
  • No other strange changes to the system (see example later)

So far I've found three approaches, that all have some serious problems:

  1. Using a SyncAdapter triggered by a network message:
    Technically, this sounded like the perfect answer to the problem: Have the system call a service in my app whenever it's doing something on the network anyways. That way, the network is guaranteed to be available, there's barely any extra battery use, since the radio is already on, and the whole thing happens in the background...
    And then I tried actually implementing it...
    Unfortunately, this approach requires some weird modifications to the system: My app needs to create a new dummy account for the sync-process, even if my app doesn't otherwise use any accounts. And unfortunately, this account cannot be hidden from the user, so now my app is listed when the user clicks "Add Account" in the System Settings, but inside the app, there is no indication of user accounts anywhere... Also, I need to request all sorts of strange permissions, like "Create accounts and set passwords", "Toggle sync on and off", and "Read sync settings". Why would an app that doesn't even provide me with a user account require a permission to set passwords on my device??? Not cool... Delete!!!

  2. Using a BroadcastReceiver and checking for connectivity when it fires:
    This requires permission to "view network connections" and possibly even "view information about Wi-Fi networking, such as whether Wi-Fi is enabled and names of connected Wi-Fi devices" ... Why would this app care about my wi-fi or network neighborhood? Creepy... Not getting installed on my device...
    The nice thing about this approach is that a network connectivity change likely only happens while the radios are turned on (at least a change to the "available" state). So, if I use this event to trigger an upload (attempt), the battery-impact will be minimal as I won't cause the radio to turn on just for the upload. Unfortunately, this event will probably not be called very often. And to make matters worse, from the docs it sounds like the BroadcastReceiver will only be called while my app is in the resumed-state. So, I won't even be able to make use of events while my app is paused... I.e. using these broadcast events will only be slightly better than option 3:

  3. Just blindly initiating the upload at regular intervals and, if it fails, retrying it again later:
    This approach doesn't require any strange permissions and doesn't mess with any system settings. But, clearly, it is the worst I can do for battery use. So, while it's workable, I'd rather find a smarter alternative...

Is there a way to fix the issues of one of the first two approaches? The optimal solution would be a SyncAdapter without a dummy user-account, or, at least, with a hidden one, or one that already exists in the system... But many hours of searching didn't yield any usable answers...

Or is there another better way altogether?

Community
  • 1
  • 1
Markus A.
  • 12,349
  • 8
  • 52
  • 116
  • 2
    4. Use `JobScheduler` on Android 5.0+, configuring the job to only be invoked if there is an Internet connection. 5. Use `GcmNetworkManager`, which is a `JobScheduler`-workalike that works on older Android versions (not sure how far back) but requires Play Services. With regards to #2, I am not aware that you need any special permission to listen for `CONNECTIVITY_CHANGE` broadcasts (leastways, [nothing is documented for it](http://developer.android.com/reference/android/net/ConnectivityManager.html#CONNECTIVITY_ACTION)). – CommonsWare Nov 17 '15 at 21:01
  • @CommonsWare I think you're correct that CONNECTIVITY_CHANGE doesn't require any permissions... But it seems to have some other issues, like only being fired if the application is in the resumed state... I should elaborate on those in the question a bit... Unfortunately the `JobScheduler` itself is out, since I need my app to run on more than just 5.0+ devices. But I'll take a look at it's source. Maybe I can learn from its implementation. I'll also take a look at GCM... At first glance, it's not clear to me whether users need to install anything extra or what permissions my app needs. Thanks. – Markus A. Nov 17 '15 at 21:15
  • @CommonsWare GcmNetworkManager for the win! It works beautifully! No permissions! No weird changes to the system like dummy user accounts, etc.! Sweet! Supposedly it works all the way back to Android 2.3, but it requires Google Play Services installed, which it is on most devices... If you like, why don't you turn your comment into an actual answer, so I can +1 and accept it! :) Thanks! – Markus A. Nov 19 '15 at 00:32
  • "which it is on most devices" -- that depends on your distribution channels. "why don't you turn your comment into an actual answer" -- I would recommend that you answer it yourself, as you can better explain how it is meeting your requirements. – CommonsWare Nov 19 '15 at 00:34
  • @CommonsWare Will do! Thank you! :) Just wanted to give you the opportunity for the rep. If you'd like, you can also just literally copy your comment into an answer and I'll accept it and add a second answer to expand on how I actually used it. – Markus A. Nov 19 '15 at 00:50

1 Answers1

2

@CommonsWare pointed out two further approaches:

  1. Using JobScheduler:
    This allows scheduling jobs for future execution that depend on certain network connectivity or even a certain charging status of the device. It says that it tries to batch job executions for all applications on the system, which may save battery by avoiding additional radio activation, if other apps also have jobs executing that access the network. But if my app is the only one needing to do a network request, I don't think the JobScheduler provides quite the same amount of battery-saving as a network-tickle-driven SyncAdapter would. This is just a guess based on my current understanding, though.
    The big issue with using the JobScheduler is that it requires API level 21, i.e. Android 5.0+. For some apps, this might be acceptable, but for me it's not...

  2. Using Google Play Services' Cloud Messenger API, specifically GcmNetworkManager: This seems to provide pretty much identically the same functionality (except for controlling the retry-backoff-strategy) that the JobScheduler does, except it is available on all devices that have Google Play Services v7.5 or higher installed. Google Play Services is available for devices as far back as Android 2.3, but v7.5 was only released end of May 2015. So, while v7.5 is also available for Android 2.3, it is not guaranteed that it is installed.
    To use it, the (supposedly very lightweight) Google Play Services API library needs to be added to the app, which provides the method GoogleApiAvailability.isGooglePlayServicesAvailable(...) that can check if Google Play Services is indeed installed and updated to the version required by the API library. To maximize the chances that this is the case, the API library v7.5 can be added to the app (see here and here for how to get it). If the check fails due to a user-resolvable problem, the library even provides the required dialog to prompt the user to fix the issue (e.g. run an update, ...).
    The major advantage of this approach is that it does not require any extra permissions for the app and it doesn't rely on any other changes to the system (mock user accounts, ...). So, it is entirely transparent to the user.

I also found one additional approach, somewhat related to point 3 above:

  1. Using AlarmManager.setInexactRepeating(...):
    The AlarmManager also tries to batch callbacks together, just like the JobScheduler and GcmNetworkManager do. In fact, looking at LogCat that sometimes reads "AlarmManager: Checking for alarms... com.google.android.gms", it seems that GCM, and probably also JobScheduler, use the AlarmManager to trigger their execution. The features missing from the AlarmManager are: Suppressing a callback based on network connectivity or battery charging status, and automatic retries of failed executions with a back-off schedule. Technically, one can easily add these features, but some of them, like checking for network connectivity, might require additional permissions.
    The major advantage of this approach is: It's available on almost all devices (starting API level 3).

Currently I have the GcmNetworkManager approach implemented (point 5) and it works as expected.

But I'm actually considering moving to AlarmManager.setInexactRepeating(...) (point 6) to maximize compatibility. Checking for the device charging state seems to be possible without extra permissions and rather than checking for network connectivity, I can just fire off the http request and check whether it failed... The only feature I would be missing is determining whether the user is on a metered connection or not. And, of course, it will be a bit of work to implement retries, back-offs, ...

Update:

It seems like JobScheduler actually does detect existing network activity (see this JobScheduler introduction), which might make it superior to just using the AlarmManager, and pretty much as good or better than SyncAdapters... The GCM documentation also claims callback optimization based on current network activity (not just availability)... So, I guess the optimal solution would be to use the JobScheduler where available and fall back to GCM where it's not and to the AlarmManager where GCM isn't available either... Yuck...

Community
  • 1
  • 1
Markus A.
  • 12,349
  • 8
  • 52
  • 116