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.
Related
I am making an app in which I have a local database and I'm using a SyncAdapter to sync this local database with the server, I don't have much experience with a SyncAdapter and I cannot seem to figure something out. So far I've implemented the "Run the sync adapter when content provider data changes" section from the Android documentation (https://developer.android.com/training/sync-adapters/running-sync-adapter), and initially it worked great but I started to notice something. When calling requestSync from inside the ContentObserver a new SyncRequest is queued by Android (I guess?) and then a little later executed but when my onPerformSync method from my SyncAdapter is executing, I find that if I make a new SyncRequest, this SyncRequest is completely ignored and not even executed later on. This is kinda annoying because when I update my database while my database is being synced, then it could happen that my updates do not reach my server (because the updates occured while the old data was being synced already). I cannot find much information about this behaviour, is this normal behaviour and if so how could I avoid this (without needing to write an entire queing system by myself)?
Here is the code from my ContentObserver (AppLogger is some custom logging system I made):
ContentObserver observer = new ContentObserver(null) {
#Override
public void onChange(boolean selfChange) {
super.onChange(selfChange, null);
}
#Override
public void onChange(boolean selfChange, #Nullable Uri uri) {
AppLogger.log(context, "AppBroadCastReceiver", "Requesting a sync for the Datamanager from the observer...");
ContentResolver.requestSync(mAccount, ApplicationProvider.AUTHORITY, new Bundle());
}
};
The onPerformSync method I used to test this behaviour:
#Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
AppLogger.log(context, "DataManagerSyncAdapter", "Starting a sync attempt");
try{
Timer timer = new Timer();
CountDownLatch latch = new CountDownLatch(1);
timer.schedule(new TimerTask() {
#Override
public void run() {
latch.countDown();
}
}, 10000);
latch.await();
} catch(InterruptedException e){}
AppLogger.log(context, "DataManagerSyncAdapter", "Finishing a sync attempt");
}
And then the syncadapter xml:
<?xml version="1.0" encoding="utf-8"?>
<sync-adapter
xmlns:android="http://schemas.android.com/apk/res/android"
android:contentAuthority="com.example.getrequest.Providers.application_provider"
android:accountType="example.com"
android:userVisible="true"
android:supportsUploading="false"
android:allowParallelSyncs="false"
android:isAlwaysSyncable="true"/>
And I got as result in the log (when trying to test this behaviour):
AppBroadCastReceiver: Requesting a sync for the Datamanager from the observer...
DataManagerSyncAdapter: Starting a sync attempt
AppBroadCastReceiver: Requesting a sync for the Datamanager from the observer...
DataManagerSyncAdapter: Finishing a sync attempt
And then onPerformSync is never run again (atleast not in the next 20 minutes, after that I lost my patience). I also noticed setting android:supportsUploading="true" kinda solved my problem but then a ton of useless SyncRequests are handled which I don't even ask for (like one every minute almost).
I've also thought about maybe blocking access to the database until my SyncRequest is completely done but is this common practice? If I want to update multiple tables in the server based on multiple tables in my local database then isn't it better to only lock the database per table instead of locking everything until the SyncRequest is completed and added to this, does this really solve anything? At which point in the onPerformSync should I then unlock my database again? It looks to me that unlocking the database in the onPerformSync could always result in a database call being executed while the onPerformSync is still busy (even if the possibility is very small)? Any help or information about this would be greatly appreciated!
Edit:
When digging through the source code of the SyncManager (https://android.googlesource.com/platform/frameworks/base/+/master/services/core/java/com/android/server/content/SyncManager.java) I came across this:
// Check currently running syncs
for (ActiveSyncContext asc: mActiveSyncContexts) {
if (asc.mSyncOperation.key.equals(syncOperation.key)) {
if (isLoggable) {
Log.v(TAG, "Duplicate sync is already running. Not scheduling "
+ syncOperation);
}
return;
}
}
So I guess this is expected behaviour, which from my point of view does not make any sense at all but maybe I don't have enough experience with these sort of things. So how should I ensure that data updated during a onPerformSync is still getting updated (without writing tons of code myself) or how can I ensure that data is not being updated while onPerformSync is busy?
I have an app in which user authentificates in Office365 with AzureAD library for Android.
It works well, users can authentificate and work with the app. Unfortunately, after a while they start hitthing AuthenticationException with ADALError.AUTH_REFRESH_FAILED_PROMPT_NOT_ALLOWED as an error code.
I checked the source code of AzurelAD. The only place, which is throughing this issue is acquireTokenAfterValidation() method:
private AuthenticationResult acquireTokenAfterValidation(CallbackHandler callbackHandle,
final IWindowComponent activity, final boolean useDialog,
final AuthenticationRequest request) {
Logger.v(TAG, "Token request started");
// BROKER flow intercepts here
// cache and refresh call happens through the authenticator service
if (mBrokerProxy.canSwitchToBroker()
&& mBrokerProxy.verifyUser(request.getLoginHint(),
request.getUserId())) {
.......
Logger.v(TAG, "Token is not returned from backgroud call");
if (!request.isSilent() && callbackHandle.callback != null && activity != null) {
....
} else {
// User does not want to launch activity
String msg = "Prompt is not allowed and failed to get token:";
Logger.e(TAG, msg, "", ADALError.AUTH_REFRESH_FAILED_PROMPT_NOT_ALLOWED);
callbackHandle.onError(new AuthenticationException(
ADALError.AUTH_REFRESH_FAILED_PROMPT_NOT_ALLOWED, msg));
}
// It will start activity if callback is provided. Return null here.
return null;
} else {
return localFlow(callbackHandle, activity, useDialog, request);
}
}
My source code:
authenticator.getAccessTokenSilentSync(getMailService());
public class Authenticator {
..............
public String getAccessTokenSilentSync(ServiceInfo serviceInfo) {
throwIfNotInitialized();
return getAuthenticationResultSilentSync(serviceInfo).getAccessToken();
}
private AuthenticationResult getAuthenticationResultSilentSync(ServiceInfo serviceInfo) {
try {
return authenticationContext.acquireTokenSilentSync(
serviceInfo.ServiceResourceId,
Client.ID,
userIdentity.getAdUserId());
} catch (AuthenticationException ex) {
// HERE THE EXCEPTION IS HANDLED.
}
}
..............
}
Stacktrace I'm getting:
<package name>.data_access.error_handler.AuthenticationExceptionWithServiceInfo: Refresh token is failed and prompt is not allowed
at com.microsoft.aad.adal.AuthenticationContext.localFlow(AuthenticationContext.java:1294)
at com.microsoft.aad.adal.AuthenticationContext.acquireTokenAfterValidation(AuthenticationContext.java:1229)
at com.microsoft.aad.adal.AuthenticationContext.acquireTokenLocalCall(AuthenticationContext.java:1123)
at com.microsoft.aad.adal.AuthenticationContext.refreshToken(AuthenticationContext.java:1609)
at com.microsoft.aad.adal.AuthenticationContext.localFlow(AuthenticationContext.java:1261)
at com.microsoft.aad.adal.AuthenticationContext.acquireTokenAfterValidation(AuthenticationContext.java:1229)
at com.microsoft.aad.adal.AuthenticationContext.acquireTokenLocalCall(AuthenticationContext.java:1123)
at com.microsoft.aad.adal.AuthenticationContext.refreshToken(AuthenticationContext.java:1609)
at com.microsoft.aad.adal.AuthenticationContext.localFlow(AuthenticationContext.java:1261)
at com.microsoft.aad.adal.AuthenticationContext.acquireTokenAfterValidation(AuthenticationContext.java:1229)
at com.microsoft.aad.adal.AuthenticationContext.acquireTokenLocalCall(AuthenticationContext.java:1123)
at com.microsoft.aad.adal.AuthenticationContext.access$600(AuthenticationContext.java:58)
at com.microsoft.aad.adal.AuthenticationContext$4.call(AuthenticationContext.java:1072)
at com.microsoft.aad.adal.AuthenticationContext$4.call(AuthenticationContext.java:1067)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
Version of AzureAD library I'm using: 1.1.7 (to prevent blaming too old version - I've checked the changelist since from 1.1.7 to 1.1.11 and haven't found anything related to question)
Problem: Right now, I'm treating this error, as a signal to through the user to the login screen. In my opinion, it leads to a poor experience for the user. The fact that it happens very often and affects many users make it even worse.
Question: Is there anything I can do different to avoid this AuthenticationException or workaround it somehow (i.e. avoid user enters credentials once again).
Have you verified that AuthenticationContext.acquireTokenSilentSync() is truly the method that you wish to invoke?
The docs indicate that this method will explicitly not show a prompt. From the docs:
This is sync function. It will first look at the cache and automatically checks for the token expiration. Additionally, if no suitable access token is found in the cache, but refresh token is available, the function will use the refresh token automatically. This method will not show UI for the user. If prompt is needed, the method will return an exception.
The refresh token you are issued should last two weeks per this AAD book. After the refresh token expires users are expected to reauthenticate. Can you inspect net traffic with Fiddler or Charles and inspect the expiry of the tokens? If you can verify that the tokens are failing to refresh before their expiry it may indicate a bug in the AD library.
To clarify the difference in methods on AuthenticationContext - there are two categories of methods: "silent" methods (which will not present a dialog to user in the event that they need to reauthenticate), and non-silent. Non-silent methods will, in the event of requiring reauthentication (or consent) from the user, start a new Activity containing the AAD login. At that point the authentication flow is restarted.
Additionally, if you make changes to your application's registration in Azure such as adding new permission scopes your users will be required to re-grant consent for the application to continue to handle their data.
This is because you need to refresh your token and implement this in your code so the user won't be prompt to login every time the access token is expired. please check out how to implement refresh token here:
https://msdn.microsoft.com/en-us/library/azure/dn645538.aspx
Hope this helps.
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.
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);
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