SyncAdapter not getting called on "Network tickle" - android

Overview
I follwed Google's tutorial on using SyncAdapter without using ContentProvider, Authenticator..etc. It works perfectly when I call onPerformSync(...) when I need to do an "upload" to the server via de SyncAdapter.
Now, as you can imagine, I need to do downloads from the server as well (yes I understand that it would be better to use Google's Cloud Messaing system, but this is the set up I was given, and I can't change that). For that, instead of doing periodical syncs, I want to make use of the "Network tickle" Android system carries out when there is a network available. For that I state the following:
ContentResolver.setIsSyncable(accounts[0], AUTHORITY, 1);
ContentResolver.setSyncAutomatically(accounts[0], AUTHORITY, true);
But my SyncAdapter is just not getting called. Looking into other stackOverFlow questions, there seem to be a problem if targetting API 10 or below with SyncAdapter and that you must add an account explicitly before calling the before methods. So I ended up with this:
AccountManager accountManager = (AccountManager) context.getSystemService(ACCOUNT_SERVICE);
Account[] accounts = accountManager.getAccounts();
if(accounts.length == 0){ //ADD DUMMY ACCOUNT
Account newAccount = new Account(ACCOUNT, ACCOUNT_TYPE);
ContentResolver.setIsSyncable(accounts[0], AUTHORITY, 1);
ContentResolver.setSyncAutomatically(accounts[0], AUTHORITY, true);
accountManager.addAccountExplicitly(newAccount, null, null);
}else{
accounts = accountManager.getAccounts();
ContentResolver.setIsSyncable(accounts[0], AUTHORITY, 1);
ContentResolver.setSyncAutomatically(accounts[0], AUTHORITY, true);
}
Now this code gets executed when the user signs in, or if the application was killed and is started up again. I am wondering, should I call setIsSyncable and setSyncAutomatically only when I add the dummyAccount the very first time?
Also, part of the "goodiness" of the SyncAdapter is that it will keep on making the calls in case of an exception. But I don't quite understand how this goes about, so instead I have this:
private void profileUpdate(){
TableAccounts db = TableAccounts.getInstance(getContext());
boolean isRecordDirty = db.isRecordDirty(signedInUser);
if(isRecordDirty){
if(server.upDateUserProfile(signedInUser)){
db.cleanDirtyRecord(signedInUser);
turnOffPeriodicSync();
}else{
this.turnOnPeriodicSync(this.sync_bundle);
}
}else
turnOffPeriodicSync();
}
As you can see, depending on the result of my upload to the server, I turn on or off a periodic sync.

Since the accountManager.getAccounts[] return every account on the device, I think nothing guarantee that the account[0] is your app's account (aka, has the ACCOUNT_TYPE of your package name).
-- You could call addAccountExplicitly() in any case, if it is existed, then nothing happens.
Account account = new Account(ACCOUNT, ACCOUNT_TYPE);
AccountManager accountManager = (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
accountManager.addAccountExplicitly(account, null, null)
context.getContentResolver().setSyncAutomatically(account, AUTHORITY, true);

Disclaimer: I might be mistaken.
On top everything you did, you also have to call ContentResolver.requestSync() from within your app every time you need a sync to run. The sync will not run immediately though, because Android is trying to cluster network activity.
Or you can use Googles Cloud Messaging API to request a sync, but I don't know a whole lot about that.

Related

Android AccountPicker 'Add account' doesen't refresh the options

When my app starts, I'd like to ask my users either to create an Account or to choose from existing ones. I've implemented an Authenticator (extended AccountAuthenticatorActivity, AbstractAccountAuthenticator, made a Service) It seems to be working, I can create new Accounts from Settings/Accounts.
When I start an AccountPicker, I get a list of already created Accounts. When I click Add acccount it shows up my Account creation Activity. But when I'm done with account creation, finishing that Activity, and going back to the AccountPicker I dont see a new option of the newly created Account. Although if I restart the app, the recently created Account is in the list.
How I start the AccountPicker:
Intent intent = accountManager.newChooseAccountIntent(null, null, new String[]{"test_namespace"}, null, null, null, null);
startActivityForResult(intent, TEST_CODE);
My questions:
Is it supposed to work like this?
Can I reload the content of the AccountPicker after I created a new
Account?
Can I just simply return an Intent with the newly created Account when I
return from my Account creation Activity?
In my authenticator activity, after the user authenticates on the server I check the existing accounts and explicitly add the account if it's not there:
boolean accountRegistered = false;
Account account = new Account(username, AccountAuthenticator.ACCOUNT_TYPE_MYAPP);
AccountManager acctMgr = AccountManager.get(this);
Account[] accounts = acctMgr.getAccountsByType(AccountAuthenticator.ACCOUNT_TYPE_MYAPP);
for (Account acct : accounts) {
if (acct.equals(account)) {
accountRegistered = true;
break;
}
}
if (accountRegistered) {
acctMgr.setPassword(account, password);
} else {
acctMgr.addAccountExplicitly(account, password, null);
}
After I do this, I see the account in the account picker.
I can't guarantee this is 100% correct; with the undocumented authentication classes, we're all flying blind.

onPerformSync called on emulator but not physical device

I've built an Account sync adapter to show contacts from the app in local contact book.
There is a fake authonticator that provides account. Also account is syncable
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1);
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true);
This account is displayed in all accounts on the device:
I've tried to trigger onPerformSync by system - from settings in menu press 'sync now' and programatically:
public static void triggerRefresh() {
Bundle b = new Bundle();
b.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
b.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
Account account = AccountAuthenticatorService.GetAccount();
if (ContentResolver.isSyncPending(account, ContactsContract.AUTHORITY) ||
ContentResolver.isSyncActive(account, ContactsContract.AUTHORITY)) {
ContentResolver.cancelSync(account, ContactsContract.AUTHORITY);
}
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1);
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true);
ContentResolver.requestSync(
account, // Sync account
ContactsContract.AUTHORITY,authority
b);
}
It works fine on emulator, but on several devices (sony, samsung) it not triggered at all (I've tried to log smth in onPerformSync method but never see this log).
I've tried to find such problem, but nothing helps, I can't make onPerformSync force to be called.
What the main difference between emulator and device according to syncAdapter?
Finally I've found the reason of this issue:
onPerformSync method will never be called until you don't have internet connection. On several devices and emulators there was an internet connection but on others you can't trigger this method.

Account.setPassword causing SyncAdapter infinite loop

There are quite a few questions considering infinite loop of android's SyncAdapter: [1]
[2]
[3], but none described the problem I encountered.
I am setting up my sync as:
ContentResolver.setIsSyncable(account, AppConstants.AUTHORITY, 1);
ContentResolver.setSyncAutomatically(account, AppConstants.AUTHORITY, true);
ContentResolver.addPeriodicSync(account, AppConstants.AUTHORITY, Bundle.EMPTY, 60);
My sync adapter supports uploading (android:supportsUploading="true"), which means that in my ContentProvider I have to check whether the data change comes from my SyncAdapter, and if it does, then I notify change without requesting sync to network.
boolean syncToNetwork = false;
getContext().getContentResolver().notifyChange(uri, null, syncToNetwork);
Still my sync adapter runs in a constant loop, what another reason could there be for triggering another sync?
In each sync I request the server for data. For each request I get an access token from my custom Account Authenticator. Instead of saving a password in my account, I decided to save the Oauth2 refresh token, which can then be use to refresh the access token. With each refreshed access token the server also send a new refresh token, which I then update to my account:
accountManager.setPassword(account, refreshToken);
And THAT was the problem. Going through the AOSP codes I discovered the following BroadcastReceiver in the SyncManager:
private BroadcastReceiver mAccountsUpdatedReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
updateRunningAccounts();
// Kick off sync for everyone, since this was a radical account change
scheduleSync(null, UserHandle.USER_ALL, null, null, 0 /* no delay */, false);
}
};
So what it does, on each account change (adding, deleting, setting password) a broadcast in send to trigger sync for all SyncAdapters, not just your own!
I honestly don't know what what the reasoning for that, but I can see it as exploitable - I let my phone (with my app stuck in infinite loop) run over night, in the morning the battery was drained, but also my FUP - only the Google's Docs, Slides and Sheets apps consumed 143MB each.

First time sync loops indefinitely

I'm having a situation with SyncAdapter I don't know how to fix.
I'm using periodic syncs. The onPerformSync method just logs some info for me to know that the process is working (no calls to notifyChanges in content providers or anything else).
The project consists of two apps: The first one creates a user account (for testing purposes only). The second holds the sync adapter. Note that this is perfectly legal for the scope of the project.
I first install the app with the account.
I can see the account has been created.
Then I install the app with the sync adapter and the first time it runs the synchronization hangs. Seeing the account sync settings, the spinner icon is continuously running and no log messages are registered (meaning it does not reach onPerformSync).
However, I can cancel the sync in the Settings and then the sync process starts working normally. This means the wiring between Account, Content Provider and SyncService is properly set.
I'm aware that adding/removing an account triggers other sync processes so I let a good lapse of time to go before installing the app with the sync adapter.
Any hints on why this is happening?
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mAccountManager = AccountManager.get(this);
// No worries here. The account exists and it's the one I want
Account[] accounts = mAccountManager.getAccountsByType(Constants.ACCOUNT_TYPE);
// Just first account for TESTING purposes
if (accounts != null && accounts.length > 0)
account = accounts[0];
else {
Log.e(TAG, "No accounts set!!");
return;
}
// Set sync for this account.
Bundle extras = new Bundle();
extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false);
extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, false);
extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
ContentResolver.setIsSyncable(account, authority, 1); // Mandatory since 3.1
// Allows the provider to sync when internet connection is back
ContentResolver.setSyncAutomatically(account, authority, true);
// Add a periodic synchronization
ContentResolver.addPeriodicSync(account, authority, extras, POLL_FREQUENCY);
}
EDIT
I found out that calling cancel on the sync, makes it work. Not the best solution but it fixes the problem by now. I put this line combined with a "isFirstUse" flag.
ContentResolver.cancelSync(account, authority);

Never get AccountManager.KEY_INTENT from getAuthToken request

On JellyBean device.
I'm following this to request an oauth2 token, e.g.
AccountManager am = AccountManager.get(getActivity());
am.invalidateAuthToken(MY_AUTH_TOKEN_TYPE, null);
am.getAuthToken(aGoogleAccount, MY_AUTH_TOKEN_TYPE, null, this,
new OnTokenAcquired(), new Handler(new OnError()));
and then make the check as per the later code sample:
private class OnTokenAcquired implements AccountManagerCallback<Bundle> {
#Override
public void run(AccountManagerFuture<Bundle> result) {
Bundle bundle = result.getResult();
...
Intent launch = (Intent) bundle.get(AccountManager.KEY_INTENT);
if (launch != null) {
startActivityForResult(launch, 0);
return;
}
}
}
I never get a KEY_INTENT. I understand the following:
There may be many reasons for the authenticator to return an Intent. It may be the first time the user has logged in to this account. Perhaps the user's account has expired and they need to log in again, or perhaps their stored credentials are incorrect. Maybe the account requires two-factor authentication or it needs to activate the camera to do a retina scan. It doesn't really matter what the reason is. If you want a valid token, you're going to have to fire off the Intent to get it.
However, the getAuthToken always results in the permission screen, or login screen, appearing before the code hits the run method at which point the token is valid. I've tried:
Turning on 2 step authentication. Account login is requested before run so always have the token in run.
Changing the password on the server. Again account login is requested before run so always have the token in run.
Don't have the ability to try a retina scan so somewhat at a loss.
EDIT 1 The problem I have is that there may be a circumstance where I will get a KEY_INTENT and so I'd rather test this code path now rather when when it's out in the field.
Thanks in advance.
Peter.
Had a chance to do something similar on a project. This not the exactly the same as your code, and I still say that the callback docs have too many 'maybes' to be certain of how it should work, but if you use this method passing false for notifyAuthFailure, you will get an intent with the re-login screen if you change the password or enable 2FA. This is for ClientLogin, but should work similarly for OAuth 2 (not tested though). Something like:
// using Calendar ClientLogin for simplicity
Bundle authResult = am.getAuthToken(account, "cl", false, null, null).getResult();
if (authResult.containsKey(AccountManager.KEY_INTENT)) {
Intent authIntent = authResult.getParcelable(AccountManager.KEY_INTENT);
// start activity or show notification
}
I think you need to call getResult(), like this:
Intent launch = (Intent)result.getResult().get(AccountManager.KEY_INTENT);
You're using the version of getAuthToken which uses an Activity to invoke the access authorization prompt. That version of getAuthToken does not return an intent since the supplied activity is used to launch the corresponding intent. If you want to manually launch an intent, use the version of getAuthToken that was deprecated in API level 14. See the following for more information:
http://developer.android.com/reference/android/accounts/AccountManager.html#getAuthToken%28android.accounts.Account,%20java.lang.String,%20boolean,%20android.accounts.AccountManagerCallback%3Candroid.os.Bundle%3E,%20android.os.Handler%29

Categories

Resources