Skip to content

Fix "Device" contacts vulnerable to deletion via unrelated account changes (#477) - #495

Closed
Adam-Winwood wants to merge 2 commits into
you-apps:mainfrom
Adam-Winwood:fix/issue-477-local-contact-account
Closed

Fix "Device" contacts vulnerable to deletion via unrelated account changes (#477)#495
Adam-Winwood wants to merge 2 commits into
you-apps:mainfrom
Adam-Winwood:fix/issue-477-local-contact-account

Conversation

@Adam-Winwood

Copy link
Copy Markdown
  • Root cause: "Device" contacts were created with a fake account_type/account_name pair that never corresponds to a real registered account, making them vulnerable to Android's ContactsProvider2 wiping them whenever any account is added or removed anywhere on the device - unrelated to ConnectYou or even to contacts syncing.

  • Fix: create local contacts/groups with null/null instead — Android's actual documented local-contact convention.

  • Second commit: one-time migration for contacts already affected on existing installs, with a safe-fail check against the (extremely unlikely) case where that account pair happens to be a real registered account on some device.

Fixes #477
Fixes #275

Adam W added 2 commits July 25, 2026 00:17
DeviceContactsRepository created local contacts and groups with
account_type="com.android.contacts", account_name="DEVICE" -
AccountType.androidDefault. This string pair has never corresponded
to a real registered AccountManager account. Android's
ContactsProvider2 reconciles raw_contacts/data/groups rows against
the current real account list on every account-list change (any
account, any type, added or removed anywhere on the device) and
deletes rows whose account no longer exists. Since "DEVICE" never
existed, every "Device" contact ConnectYou has ever created was
permanently exposed to this cleanup.

Confirmed on emulator (repeatable, both add and remove of an
unrelated throwaway account) and on my actual phone.

Fix: create local contacts/groups with account_type=null,
account_name=null instead - Android's (AOSP anyway) actual documented
convention for local/unsynced contacts (RawContacts.getLocalAccountType()/
getLocalAccountName() resolve to null on most devices). Verified via
the real createContact() path that a null-account contact survives
the same account add/remove cycles that reliably wiped the old
fake-account ones, checked over a full 2-minute window each direction.
The previous commit stopped new "Device" contacts/groups from being
created under the fake account_type=AccountType.androidDefault.type,
account_name=AccountType.androidDefault.name pair. Contacts already
created that way on existing installs were still exposed to the bug and
needed moving to the real null/null local-contact convention too.

Handles the case where the pair might legitimately be a real account on
some device rather than our fake one: checks AccountManager for a real
account matching that exact identity first, and skips migration entirely
if one exists, since individual raw contacts can't be distinguished by
creating app via public API. This fails safe either way - if the pair is
genuinely fake (the normal case), migrating is a strict improvement; if
a real account exists, nothing happens at all, so there's no action for
a backup to protect against.

Runs once from App.onCreate(), gated by a SharedPreferences flag
that's only set once a migration attempt actually completes (not on
a missing-permission or ambiguous-collision skip), so either
condition naturally retries on a later launch once resolved.

Verified all three paths on an emulator: normal migration (fake
contacts get nulled, flag set), collision present (contacts
untouched, flag stays unset), and recovery after the collision
resolves (previously-protected contacts correctly migrated on the
next launch).

@Bnyro Bnyro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the pull request!

I couldn't reproduce the contact deletion bug on my end by changing accounts, but makes sense that Android cleans up all accounts that don't exist. I've looked at the Fossify Contacts app and it also just stores a null value for account name and type.

Please clarify if and how you used AI to work on this pull request. Fyi, AI contributions are not allowed on this repository.

// makes ContactsProvider2 treat these raw contacts as belonging to an account
// that doesn't exist, so any unrelated account being added/removed anywhere on
// the device wipes them. null/null is Android's actual local-contact convention.
val isLocalAccount = resolvedAccountType == AccountType.androidDefault.type

@Bnyro Bnyro Jul 25, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using AccountType.androidDefault here is not really the cleanest solution, instead we should remove AccountType.androidDefault and just always pass null around as the account name and type. So the idea would be to modify setLastChosenAccount and getLastChosenAccount to also handle null values. Then we don't have to do this case distinction here and can just run getCreateAction(resolvedAccountType, resolvedAccountName).

return@withContext
}

val sentinelType = AccountType.androidDefault.type

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you call this sentinel, it's not really a sentinel?

}
if (realAccountExists) return@withContext

runCatching {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the runCatching? In theory this should never fail/raise an exception, or in which case should it?

@Adam-Winwood

Copy link
Copy Markdown
Author

AI clarification

Many apologies, yes I have used AI in the writing of this. If I had known, I would not have used it on this repo.

Specifically, I have used AI for first drafts of commit messages (lazy yes I know), and the prototyping of code. Perhaps 40% of the writing, and 80% of the code could be attributed to AI.

Whilst I am happy to have a stab at redoing it all fresh and hand writing the code etc. (likely as a new PR), I understand if you would prefer I'm not involved in fixing this particular bug at this point.

Reproduction of the issue

I am surprised to hear that you haven't managed to reproduce the bug on your end - did you add a contact under DEVICE specifically, and add/remove an account? On my actual device I just deleted a couple of old unused accounts from Settings -> "Accounts and backup" ->"Manage accounts". Whilst on the emulator I added a bit of debug code that I could poke with adb shell am broadcast -a com.bnyro.contacts.debug.ADD_TEST_ACCOUNT that would get the AccountManager from the context and then call addAccountExplicitly on it.

Reproducing on my actual device is sad as it vaporises all my contacts and I have to re-import them from backups I make whenever I add new contacts, hence my focus on reproducing this on the emulator.

Summary

Anyway, let me know what you want me to do (or not do) now.

@Bnyro

Bnyro commented Jul 25, 2026

Copy link
Copy Markdown
Member

I am surprised to hear that you haven't managed to reproduce the bug on your end - did you add a contact under DEVICE specifically, and add/remove an account?

I just removed an account I no longer used. But possibly this just varies depending on the ROM / Android version, so it's not an issue that I can't reproduce it.

Whilst I am happy to have a stab at redoing it all fresh and hand writing the code etc.

Yes, I think that creating a new pull request from the ground up would be cool. We should probably fully remove ContactsHelper.androidAccountType and rewrite all its uses to use null values instead, which will make the code simpler :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Contact auto lost / couldn't read contact list Connect You seems to be DELETING ALL CONTACTS

2 participants