6

So, my question restated is when you go to Settings -> Accounts & Sync and select the an account that was created that your SyncAdapter is syncing with a cloud server, and select remove account, what happens as far as your SyncAdapter is concerned? There is a dialog that displays asking you to confirm and that the data on the phone associated with that account will be removed. I cannot easily believe that the framework can automatically remove the data my SyncAdapter has stored in the local database, but it seems to imply that removing the account will (and I would agree that is should) remove that data. Is there a addition to my SyncAdapter that will serve sort of as the callback for the account removal to handle deleting all the appropriate data from the local database? Maybe it has to be done through the AccountManager instead; my AccountManager gets notified when the account gets removed and from there I can trigger the data deletion without the SyncAdapter.

EDIT: On a related note, is the sync manager calling my SyncAdapter for each account that it synchronizes when a new account is added? I see a onPerformSync(...) being executed for previously added accounts along with the just added account when I add an account, and would like to stop that.

Dandre Allison
  • 5,975
  • 5
  • 42
  • 56

3 Answers3

7

I discovered the solution is to make the app's ContentProvider implement OnAccountsUpdateListener. Attach the ContentProvider as a listener in its onCreate method with account_manager.addOnAccountsUpdatedListener(this, null, false) and then implement the interface method like

@Override
public void onAccountsUpdated(final Account[] accounts) {
    Ln.i("Accounts updated.");
    final Iterable<String> account_list = new Iterable<String>() {
        @Override
        public Iterator<String> iterator() {
            return new Iterator<String>() {
                private final Iterator<Account> account_list = Arrays.asList(accounts).iterator();

                @Override
                public boolean hasNext() {
                    return account_list.hasNext();
                }

                /** Extracts the next account name and wraps it in single quotes. */
                @Override
                public String next() {
                    return "'" + account_list.next().name + "'";
                }

                @Override
                public void remove() { throw new UnsupportedOperationException("Not implemented"); }
            };
        }
    };
    final String account_set = TextUtils.join(", ", account_list);
    Ln.i("Current accounts: %s", account_set);

    // Removes content that is associated with accounts that are not currently connected
    final SelectionBuilder builder = new SelectionBuilder();
    builder.table(Tables.CALENDARS)
           .where(Calendars.CALENDAR_USER + " NOT IN (?)", account_set);

    new SafeAsyncTask() {
        @Override
        public Void call() throws Exception {
            _model.openWritableDatabase();
            _model.delete(builder);
            return null;
        }
    }.execute();


    getContext().getContentResolver().notifyChange(Calendars.NO_SYNC_URI, null, false);
}

I construct a String of the currently connected accounts, then build a SQL query with that String. I perform a delete on the database in a background thread on that query to remove the data associated with accounts not currently connected. And I notify that content changed, but does not need to synchronized with the server.

Dandre Allison
  • 5,975
  • 5
  • 42
  • 56
  • Could you elaborate a little on your answer? What `onCreate` are you talking about? – akirk Oct 03 '12 at 12:40
  • ...The ContentProvider, read the first line closer. I don't know what more you want "elaborate"d – Dandre Allison Oct 03 '12 at 21:59
  • Is it possible to remove the OnAccountsUpdateListener at the relevant point in the ContentProviders lifecycle? – fr1550n May 20 '13 at 11:51
  • Are you sure that your listener gets called at all times? Which onCreate method are you referring to? That of the service? – RaB Jun 23 '13 at 06:06
  • @RaB you need to give me more than "are you sure...", are you having issues with it? As to which `onCreate`, it's in the answer and I repeated it in the comments here: the `ContentProvider`. – Dandre Allison Jun 23 '13 at 14:59
  • @DandreAllison I didnt experience problems with this yet. I was contemplating to implement your solution, but was wondering whether there was a chance in which this approach might fail to work (e.g. when you reinstall the app so that noone is actively using your content provider, then the listener might not be registered). Sorry about the dumb question about which onCreate method. I guess I didnt read thoroughly enough. – RaB Jun 24 '13 at 16:41
  • @RaB don't feel like your question is dumb, I thought it should have been clear enough in my statement, but since people were confused I restated it to hopefully be even clearer. Your other question requires answering "when is the `ContentProvider` created". The framework must be creating it, since it can be used (sometimes) by external apps, and it must be created before an account is deleted if you are suppose to remove content from the `ContentProvider` when accounts are deleted. My answer is a guess that adding the listener in the `ContentProvider.onCreate` should not fail. – Dandre Allison Jun 25 '13 at 23:18
  • @DandreAllison I was indeed able to verify that even in case noone is using the content provider, it still gets initialized. However, architecturally, I believe it might make more sense to put this code into the AuthService, since this where the accounts are managed. – RaB Jun 26 '13 at 05:43
  • @RaB, I think architectually it makes more sense to say that the `ContentProvider` is listening to account changes to know when it should remove an accounts stored data. I'm glad you were able to verify that. It tends to not makes sense for something to listen to itself. And I'd prefer to keep the account stuff from knowing or caring about the `ContentProvider` since it has no bearing on managing accounts. – Dandre Allison Jun 26 '13 at 15:21
  • Why should the listener be on the ContentProvider? Presumably you already have a Service running to provide authentication when you added the account. Couldn't the service add the listener on its OnCreate() and remove it on its OnDestroy()? – Patrick Brennan Jul 07 '15 at 00:54
  • I agree with Patrick, I think Service makes more sense for this use case. I'm going to implement this as well. – Qylin Jan 10 '17 at 14:10
5

No, but your Authenticator does[1]. This method is called before the account is removed:

AbstractAccountAuthenticator.getAccountRemovalAllowed(AccountAuthenticatorResponse, Account) 

the Account param is the account being deleted - the default behaviour is to allow removal of the account:

return super.getAccountRemovalAllowed(response, account); // returns Bundle[{booleanResult=true}]

..but I guess it's a hook that you can use to tidy things up or block the account being removed should you wish to.

[1] - this is a dirty hack; please see Dandre's comment.

fr1550n
  • 1,055
  • 12
  • 23
  • 2
    I do not believe that method is intended to be used to "tidy things up". Given its name and location in the `AbstractAccountAuthenticator`, it is likely used to prevent internal accounts you might use for your app to be deletable outside of your control. I've not found a use case personally. The big intent to me is "get" as the prefix. You should avoid side effects in "get" methods, makes the flow difficult to follow. Would you suspect a "get" to delete all of your data? – Dandre Allison Jun 23 '13 at 15:08
  • No doubt you are correct and it's a dirty hack. I shall update my answer as such. – fr1550n Jun 27 '13 at 08:58
4

Another option is to register for the android.accounts.LOGIN_ACCOUNTS_CHANGED broadcast that the AccountManager sends out. Unfortunately, this broadcast is sent out whenever any account is changed and the broadcast does not deliver further information what has changed either.

So you'd have to query the account manager and look how many of "your" accounts it has left and delete the data of the missing ones.

Thomas Keller
  • 5,933
  • 6
  • 48
  • 80
  • Well, the solution above this by @Dandre Allison is based on this. So this answer should be preferred. When you call `addOnAccountsUpdatedListener`, AccountManager is registering a dynamic broadcast receiver which call the listener in its `onReceive`. So, it's basically the same. Both solutions are notified whenever any account in the device (Google account, Twitter account, etc) is changed. – blindOSX Jan 19 '15 at 00:52