Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1967,9 +1967,16 @@ private void askForACodeIfTheSessionNeedsOne() {
// reason when a session goes stale, and each failure calls this - so without the latch a
// six-tag account would ask Apple for six codes, or sign out six times over. The check
// and the set are both on the main thread, which is why a plain boolean is enough.
if (this.appleService == null
|| this.alreadyAskingAboutTheSession
if (this.appleService == null) {
Log.d(TAG, "Not checking the session: there is no Apple service");
return;
}
if (this.alreadyAskingAboutTheSession
|| (this.twoFactorAgain != null && this.twoFactorAgain.isShowing())) {
// **Logged, because a silent return here used to be indistinguishable from
// "checked, the session is fine".** Both left a failed fetch with no follow-up
// line in the log, and only one of them says anything about the session.
Log.d(TAG, "Not re-checking the session: a check is already in flight or on screen");
return;
}
this.alreadyAskingAboutTheSession = true;
Expand All @@ -1979,7 +1986,15 @@ private void askForACodeIfTheSessionNeedsOne() {
.observeOn(AndroidSchedulers.mainThread())
.subscribe(needed -> {
if (needed.isEmpty()) {
return; // nothing wrong with this session
// **Logged, because this answer is half the diagnosis.** A fetch that
// was refused while the session answers as healthy is not a dead
// session - the reports endpoint itself is refusing (rate limiting,
// flagging), and no re-login fixes that. Without the line, this
// outcome and a check suppressed by the latch above produce the same
// empty log.
Log.i(TAG, "The session answers as healthy after a failed fetch, so the"
+ " refusal is specific to fetching rather than a dead session");
return;
}

final List<PythonAuthService.AuthMethod> methods = needed.get();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,11 @@ public static Observable<byte[]> retrieveAuthData(@NonNull PythonAuthResponse au
public static Observable<PythonAppleAccount> restoreAccount(
final AppleUserData appleUserData, final AnisetteSource localAnisette) {
return Observable.fromCallable(() -> {
// **The two lines bracket the Python call so a stall is visible.** A capture that
// shows the first and never the second means the restore is sitting inside Python
// (anisette provisioning, the network) rather than failed - and a failure is the
// only one of those the exception below distinguishes today.
Log.i(TAG, "Restoring the stored account");
var data = AppCryptographyUtil.AppEncryptedData.fromFlattened(appleUserData.getData());
var account = new AppCryptographyUtil().decrypt(data, KEYSTORE_ALIAS_ACCOUNT);

Expand All @@ -308,6 +313,7 @@ public static Observable<PythonAppleAccount> restoreAccount(
throw new PythonAccountLoginException("Error occurred while restoring account! Check python logs for more details");
}

Log.i(TAG, "Account restore returned from Python");
return new PythonAppleAccount(returned);
}).subscribeOn(Schedulers.io());
}
Expand Down
12 changes: 12 additions & 0 deletions app/src/main/python/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1945,6 +1945,18 @@ def getLastReports(

print(f"Got {len(reports)} raw reports for {beaconId}")

# Log the age of the newest one, not only the count. A fetch that succeeds while
# Apple's newest report is hours old means the network genuinely holds nothing
# newer - the "stale map with no error anywhere" case - and a count alone cannot
# tell that from a fetch that returned something fresh.
if reports:
newest_ms = max(int(r.timestamp.timestamp() * 1000) for r in reports)
age_min = max(0, (now_ms - newest_ms) // 60000)
age = f"{age_min} min" if age_min < 180 else f"{age_min / 60:.1f} h"
newest_iso = datetime.fromtimestamp(
newest_ms / 1000, tz=timezone.utc).isoformat(timespec="seconds")
print(f" -> newest report is {age} old ({newest_iso})")

# Measured here because this is the one place both halves are in scope: what the
# alignment said before anything was fetched, and what the fetch made of it.
aligned_after = _alignmentOf(airtag)
Expand Down
6 changes: 0 additions & 6 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,6 @@ uv run python -m exporter.cli # the same thing in a terminal
uv run python -m exporter.cli --help
```

> [!IMPORTANT]
> **Pass `--no-password` to the CLI for now**, or use the window, which does not lock bundles.
> The CLI encrypts by default and no released version of the Android app can open an encrypted
> zip — its reader cannot decrypt anything. See
> [the handover doc](../docs/android-import-handover.md).

**tkinter** is what draws the window, and it is not part of a stock Python on macOS or Linux:

```shell
Expand Down
Loading