Custom Account Type with Android AccountManager

2020-05-29 09:11发布

问题:

I have an account type "mypackage.account" and a content authority "mypackage". I have a Service that provides an implementation of AbstractAccountAuthenticator, the addAccount method is implemented like this:

    /**
     * The user has requested to add a new account to the system. We return an intent that will launch our login
     * screen if the user has not logged in yet, otherwise our activity will just pass the user's credentials on to
     * the account manager.
     */
    @Override
    public Bundle addAccount(AccountAuthenticatorResponse response, String account_type, String auth_token_type,
                             String[] required_features, Bundle options) throws NetworkErrorException {
        final Intent intent = new Intent(_context, ConnectAccountActivity.class);
        intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
        final Bundle reply = new Bundle();
        reply.putParcelable(AccountManager.KEY_INTENT, intent);

        return reply;
    }

I provide an authenticator.xml

<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
                   android:accountType="mypackage.account"
                   android:icon="@drawable/ic_launcher"
                   android:smallIcon="@drawable/ic_launcher"
                   android:label="@string/app_name"
                   android:accountPreferences="@xml/account_preferences" />

and I define this Service in AndroidManifest.xml like this:

<!-- Account authentication service that provides the methods for authenticating KeepandShare accounts to the
     AccountManager framework -->
<service android:exported="true" android:name=".authenticator.AccountAuthenticatorService" android:process=":auth" tools:ignore="ExportedService">
    <intent-filter>
        <action android:name="android.accounts.AccountAuthenticator"/>
    </intent-filter>
    <meta-data android:name="android.accounts.AccountAuthenticator" android:resource="@xml/authenticator"/>
</service>

That's the setup, now when I want to have a screen with the list of my account type accounts on the device with an action to add a new account, I have the add account action that looks like this:

final Intent intent = new Intent(Settings.ACTION_ADD_ACCOUNT);
intent.putExtra(Settings.EXTRA_AUTHORITIES, new String[]{ "mypackage" });
startActivity(intent);

At this point I'm led to an account type picker that shows "mypackage.account" and "anotherpackage.account" as options. ("anotherpackage.account" is defined in another app I work on) This doesn't seem to be like the intended behavior. I've checked about 20 times that the authorities defined by both apps are different - and they are. Can someone show me what I'm missing?

回答1:

Android decoupling came to bite me again. It appears that both apps needed to also have a sync_adapter.xml like:

<!-- The attributes in this XML file provide configuration information for the SyncAdapter. -->
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
          android:contentAuthority="mypackage"
          android:accountType="mypackage.account"
          android:supportsUploading="true"
          android:userVisible="true"
          android:allowParallelSyncs="false"
          android:isAlwaysSyncable="true"/>

and connect that to the sync service in the AndroidManifest.xml:

<!-- Data sync service that provides the SyncAdapter to the SyncManager framework. The SyncAdapter is used to
     maintain that the set of data on the device is a subset of the data on the server -->
<service android:exported="true" android:name=".data.sync.SyncService" tools:ignore="ExportedService">
    <intent-filter>
        <action android:name="android.content.SyncAdapter"/>
    </intent-filter>
    <meta-data android:name="android.content.SyncAdapter" android:resource="@xml/sync_adapter"/>
</service>

For completeness, my Service is implemented as follows:

/**
 * Service that provides sync functionality to the SyncManager through the {@link SyncAdapter}.
 */
public class SyncService extends Service {

    @Override
    public void onCreate() {
        synchronized (_sync_adapter_lock) {
            if (_sync_adapter == null)
                _sync_adapter = new SyncAdapter(getApplicationContext(), false);
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return _sync_adapter.getSyncAdapterBinder();
    }

    private static final Object _sync_adapter_lock = new Object();
    private static SyncAdapter _sync_adapter = null;
}

and the SyncAdapter:

/**
 * Sync adapter for KeepandShare data.
 */
public class SyncAdapter extends AbstractThreadedSyncAdapter {

    public SyncAdapter(Context context, boolean should_auto_initialize) {
        super(context, should_auto_initialize);

        //noinspection ConstantConditions,PointlessBooleanExpression
        if (!BuildConfig.DEBUG) {
            Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
                @Override
                public void uncaughtException(Thread thread, Throwable throwable) {
                    Log.e("Uncaught sync exception, suppressing UI in release build.", throwable);
                }
            });
        }
    }

    @Override
    public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider,
                              SyncResult sync_result) {
        // TODO: implement sync
    }
}

Even though I'm not actually syncing any data (the apps are not even linked to any server right now), the Android framework appears to be using the settings of the SyncAdapter to figure out which account authenticator respond to the add account request.



回答2:

I know the question is old and closed nevertheless...

Documentation of Settings.ACTION_ADD_ACCOUNT:

The account types available to add may be restricted by adding an EXTRA_AUTHORITIES extra to the Intent with one or more syncable content provider's authorities. Only account types which can sync with that content provider will be offered to the user.

I assume that´s why you had to implement an empty sync-adapter. Refering to the docs this should not have been necessary when using Settings.EXTRA_ACCOUNT_TYPES instead (I did not try it for two projects like you did but it works like a charm in a project I am currently developing and I do not need sync-adapters at all):

intent.putExtra(Settings.EXTRA_ACCOUNT_TYPES, new String[] { "mypackage.account" });


回答3:

Instead of calling the intent...

final Intent intent = new Intent(Settings.ACTION_ADD_ACCOUNT);
intent.putExtra(Settings.EXTRA_AUTHORITIES, new String[]{ "mypackage" });
startActivity(intent);

You can use the following to go straight into the Authentication process given the account type. This would not require you to have a SyncAdapter.

AccountManager accountManager = AccountManager.get(this);
accountManager.addAccount(AccountAuthenticator.ACCOUNT_TYPE, AccountAuthenticator.AUTHTOKEN_TYPE_FULL_ACCESS, null, null, this, new AccountManagerCallback<Bundle>() {
        @Override
        public void run(AccountManagerFuture<Bundle> future) {

        }
}, new Handler());