nextcloud / nextcloud/ios

Background location doesn't work - Significant-location-change background trigger never fires

Open
#4,305 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
Swift
Stars
2.5k
Forks
1k
Avg merge
2d 18h
Merged PRs (30d)
13

Description

Bug report

How to use GitHub
  • Please use the πŸ‘ reaction to show that you are affected by the same issue.
  • Please don't comment if you have no relevant information to add. It's just extra noise for everyone subscribed to this issue.
  • Subscribe to receive notifications on status change and new comments.
Steps to reproduce
  1. Enable background location and allow app to receive them in settings
  2. Activate auto-upload
  3. Foreground app once, don't force quit it
  4. use your phone normally and move around for a few days without rebooting your phone
  5. foreground app
Expected behaviour

At least one photo should have been uploaded, should be visible on the server

Actual behaviour

Nothing happens

Logs

See below

Reasoning or why should it be changed/implemented?

Make background update work. Bonus: with this and (#4304) solved, there is probably no reason to keep the "keep app active" banner, as no more unintended transfer interruption can occur.

Environment data

Device: iPhone 12 mini, iOS 26.6.1, "Always" location authorization, location background mode present,
auto-upload on, Wi-Fi off during the outings unless stated.

Code references are against upstream master at 204f9e23b1 (2026-08-23). The log lines quoted come
from a build with diagnostic logging added locally (Location update delivered, Re-arming location monitoring at launch, getCameraRollAssets: counts); upstream only logs Triggered by location change.

Analysis

This really is one single issue, but wrote up as two parts simply because it took two rounds to discover the whole of it, and I got lazy over merging them...

Note 1: also, the use of .fitness is questionable, as I believe .other makes more sense for everyday use:

Use this activity type to describe positioning in activities that aren’t covered by one of the other activity types. This includes activities without a specific user intention, for example, positioning while a user sits on a bench interacting with a device.

Note 2: when this is solved, we're one fix away (#4304) from removing the need for the _keep_running_ notification banner (since Location trigger can actually relaunch an force-quit app).

Part 1 β€” CLLocationManager is created on a thread with no run loop, so its delegate is never called

Apple's contract

CLLocationManager:

Core Location calls the methods of your delegate object using the RunLoop of the thread on which you
initialized the CLLocationManager object. That thread must itself have an active RunLoop, like the one
found in your app's main thread.

CLLocationManagerDelegate:

Core Location calls the methods of your delegate object on the runloop from the thread on which you
initialized CLLocationManager. That thread must itself have an active run loop, like the one found in
your app's main thread.

startMonitoringSignificantLocationChanges():

The first event to be delivered is usually the most recently cached location event (if any) but may be a
newer event in some circumstances. […] When you restart location services, the current event is
delivered to your delegate immediately.

So every call to start… on a correctly-created manager should produce at least one delegate call β€” the
initial cached location β€” immediately.

Where the code departs from it

iOSClient/NCBackgroundLocationUploadManager.swift (master, lines 9–24): the manager is a lazily-created
singleton whose initialiser creates the CLLocationManager and sets delegate = self:

static let shared = NCBackgroundLocationUploadManager()
…
private let locationManager = CLLocationManager()
…
private override init() {
    super.init()
    locationManager.delegate = self
    locationManager.activityType = .fitness
    locationManager.allowsBackgroundLocationUpdates = true
}

iOSClient/SceneDelegate.swift (master, lines 273–289): the only place in normal use that touches
.shared β€” and therefore the thread the manager is created on β€” is a group.addTask { … } closure
inside sceneDidEnterBackground:

let didFinish = await withTaskGroup(of: Bool.self) { group -> Bool in
    group.addTask {
        …
        // LOCATION MANAGER
        if CLLocationManager().authorizationStatus == .authorizedAlways && NCPreferences().location && tblAccount.autoUploadStart {
            NCBackgroundLocationUploadManager.shared.start()
        } else {
            NCBackgroundLocationUploadManager.shared.stop()
        }

A task-group child runs on a Swift cooperative-pool thread, which has no run loop. The only other caller
is the settings screen (NCAutoUploadModel.swift:250), which is not reached in ordinary use.

Field evidence

Before: across every log from 2026-09-10 to 2026-09-15 β€” five days, walks, a 300 km drive, a 5 km
drive, force-quit and left-alive variants β€” monitoring was armed over a hundred times and
Triggered by location change appeared zero times. Not even the initial cached-location delivery
Apple describes.

Log Span Location monitoring started Triggered by location change
log.txt 09-10 18:42 β†’ 09-11 14:47 17 0
log-2 09-11 15:07 β†’ 16:19 4 0
log-3 (300 km drive) 09-11 15:07 β†’ 23:39 7 0
log-4 09-11 23:40 β†’ 09-12 11:03 9 0
log-5b 09-13 06:22 β†’ 22:57 19 0
log-6 09-13 23:32 β†’ 23:59 18 0
log-8 09-14 00:06 β†’ 00:32 14 0
log-9 09-14 00:34 β†’ 08:27 8 0
log-b 09-14 08:50 β†’ 11:11 2 0
log-c 09-14 11:40 β†’ 09-15 07:42 4 0
log-d (1 km walk) 09-15 08:07 β†’ 10:17 1 0
log-e (1 km walk) 09-15 08:07 β†’ 12:47 5 0

iOS itself reported location use: the system's "Nextcloud has used your location in the background"
notice was shown on the device during this period, i.e. the OS considered the registration active. The
app simply had nowhere to deliver to.

After creating the manager on the main thread (everything else unchanged), the very first arming
delivered β€” log-f, 2026-09-15:

12:57:04  [DEBUG] Location monitoring started
12:57:04  [LOCATION] Location update delivered: <redacted>      ← same second: the initial delivery
12:57:04  [LOCATION] Triggered by location change: <redacted>
…
14:45:42  [DEBUG] Location monitoring started
14:45:42  [LOCATION] Location update delivered: <redacted>      ← second arming, second immediate delivery

and every arming since (log-g/h/i/j: 15 deliveries over the following day and a half).

Test scripts for log-d / log-e (the two "before" 1km+ walks on the day of the diagnosis)

log-d:

- force kill shortly after 8:00, WiFi still on
- deactivate WiFi at 8:35
- start moving at 8:40, arrive at 8:55 1 km away
- resume moving at 9:45, arrive at 10 back at the starting point
- at 10:15, reactivate WiFi, wait a bit, foreground app, wait for popup, extract log-d.txt
...taking photos, videos, and briefly activating the phone from time to time throughout the experiment.

log-e:

- leave 11:03
- arrival 11:15
- start coming back at 11:48
- arrival 12:08
- check server: nothing uploaded yet since approx 2 hours ago
- connect to wifi at 12:23
- foreground app at 12:46

Part 2 β€” a process relaunched by iOS never re-arms monitoring, so relaunches for location events do nothing

Apple's contract

startMonitoringSignificantLocationChanges():

If you start this service and your app is subsequently terminated, the system automatically relaunches
the app into the background if a new event arrives. In such a case, the options dictionary passed to the
application(_:willFinishLaunchingWithOptions:) and application(_:didFinishLaunchingWithOptions:)
methods of your app delegate contains the key location to indicate that your app was launched because
of a location event. Upon relaunch, you must still configure a location manager object and call this
method to continue receiving location events.
When you restart location services, the current event is
delivered to your delegate immediately.

UIApplication.LaunchOptionsKey.location:

You should use the presence of this key as a signal to create a CLLocationManager object and start
location services again. Location data is delivered only to the location manager delegate and not using
this key.

Where the code departs from it

iOSClient/AppDelegate.swift (master) contains no reference to NCBackgroundLocationUploadManager,
CLLocationManager, or LaunchOptionsKey.location. The only arming site is sceneDidEnterBackground
(above), which does not run for a process that begins life in the background. A process iOS relaunches
for a location event β€” or for any other reason (BGTask, background URLSession) β€” therefore has no manager
and no delegate for the lifetime of that process.

Field evidence

Bare relaunches with nothing after them. In every outing before the re-arm, the log shows iOS
relaunching the app mid-trip ([START] Start session with no preceding foreground and no user action),
followed by nothing location-related at all. log-e, walk of 11:03–12:08:

11:16:00  [START] Start session with level verbose Nextcloud …   ← relaunched mid-walk; nothing follows
11:58:15  [START] Start session with level verbose Nextcloud …   ← relaunched on the way back; nothing follows
12:46:57  [START] Start session with level verbose Nextcloud …
12:46:59  [DEBUG] Application will enter in foreground           ← tester opens the app
12:47:00  [DEBUG] Automatic upload, new 18 assets found          ← the whole walk's photos, found at once

log-d, walk of 08:40–10:00: relaunches at 09:52:24 and 10:16:41, nothing after either. log-f (5 km
drive, Part 1 already fixed): relaunches at 13:55:26 and 14:24:56, nothing after either; the drive's 16
photos were found by a BGAppRefreshTask at 14:41:30, not by location.

launchOptions[.location] was never observed. With a re-arm keyed on that option, the log line it
emits (Launched for a location event) appears zero times across every subsequent log (log-f through
log-j, 2026-09-15 β†’ 09-16), including the relaunches below that are unambiguously location-triggered.
Under this app's scene-based lifecycle the key documented above did not arrive in
application(_:didFinishLaunchingWithOptions:) on this device.

After re-arming on every launch β€” log-g, 2026-09-15, app force-quit before the walk:

15:18:31  [DEBUG] App is terminating                              ← tester force-kills; walk starts 15:22
15:35:11  [START] Start session with level verbose Nextcloud …    ← relaunched ~13 min / ~500 m into the walk
15:35:11  [LOCATION] Re-arming location monitoring at launch
15:35:11  [DEBUG] Location monitoring started
15:35:11  [LOCATION] Location update delivered: <redacted>
15:35:12  [LOCATION] Triggered by location change: <redacted>
16:01:06  [START] Start session with level verbose Nextcloud …    ← relaunched on arrival home
16:01:06  [LOCATION] Re-arming location monitoring at launch
16:01:06  [DEBUG] Location monitoring started
16:01:06  [LOCATION] Location update delivered: <redacted>
16:01:08  [LOCATION] Triggered by location change: <redacted>

No BGAppRefreshTask ran between 15:09:00 and 16:09:50 and no upload was in flight, so neither relaunch
is attributable to a BGTask or a background-URLSession wake; both coincide with ~500 m of movement and with
arrival, matching the documented SLC behaviour, after a force-quit. These are the first
movement-triggered relaunches on record for this app.

Subsequent outings (log-h 17:13:54 relaunch three minutes after leaving home; log-i 20:54:33 relaunch;
log-j four deliveries into a never-terminated process over 3Β½ h) show the same chain each time.

Test script for log-g
- 15:18 : force kill the backgrounded app, take a few pictures, take wifi off, lock screen
- 15:22 start walking -- approx 700m away in straight line and back, but I walked over 1,5km total
- 16:01-16:03 : elevator, home
- 16:04 : charge. At this stage there is no visible (complete) upload on the server
- 16:10-16:11 : pictures uploaded
- 16:30 : foreground and extract log

Appendix β€” the whole record in one table

"Armed" = Location monitoring started; "Launched for location event" = a diagnostic line emitted only
when launchOptions[.location] != nil; "Re-armed" = the every-launch re-arm's line; "Delivered" = a
diagnostic line emitted at the top of didUpdateLocations, before any guard (present from 09-15 ~08:00);
"Triggered" = upstream's own Triggered by location change; "Bare starts" = [START] Start session
lines, i.e. process launches.

Log Span Armed Launched for location event Re-armed Delivered Triggered Bare starts
log.txt 09-10 18:42 β†’ 09-11 14:47 17 β€” β€” β€” 0 7
log-2 09-11 15:07 β†’ 16:19 4 β€” β€” β€” 0 2
log-3 09-11 15:07 β†’ 23:39 7 β€” β€” β€” 0 4
log-4 09-11 23:40 β†’ 09-12 11:03 9 β€” β€” β€” 0 5
log-5b 09-13 06:22 β†’ 22:57 19 β€” β€” β€” 0 3
log-6 09-13 23:32 β†’ 23:59 18 β€” β€” β€” 0 1
log-8 09-14 00:06 β†’ 00:32 14 β€” β€” β€” 0 1
log-9 09-14 00:34 β†’ 08:27 8 β€” β€” β€” 0 3
log-b 09-14 08:50 β†’ 11:11 2 β€” β€” β€” 0 1
log-c 09-14 11:40 β†’ 09-15 07:42 4 β€” β€” β€” 0 5
log-d 09-15 08:07 β†’ 10:17 1 β€” β€” 0 0 3
log-e 09-15 08:07 β†’ 12:47 5 β€” β€” 0 0 9
log-f (Part 1 fixed) 09-15 12:47 β†’ 14:45 3 0 β€” 2 2 3
log-g (both fixed) 09-15 14:52 β†’ 16:31 6 0 4 4 4 4
log-h 09-15 16:47 β†’ 17:52 5 0 2 3 2 2
log-i 09-15 17:52 β†’ 09-16 08:50 8 0 3 5 3 3
log-j 09-16 09:03 β†’ 12:27 3 0 0 4 4 0

(log-h/i/j "Delivered" exceeds "Triggered" where a delivery arrived while the app was in the foreground and
was intentionally ignored.)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up β€” it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Read iOSClient/NCBackgroundLocationUploadManager.swift and iOSClient/SceneDelegate.swift to trace where CLLocationManager is created and monitoring is armed. Then inspect iOSClient/AppDelegate.swift for launch handling and NCAutoUploadModel.swift for the other arming path. Done means location updates reach the delegate during background use and a relaunch re-arms monitoring; verify with the issue's reproduction steps and logs.

Written by the indexing model from the issue text.

Assessment

Tech stack
ios, swift
Domain
mobile
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.