关于Android帐户管理:允许在Android设备上登录相关帐户,如skype,facebook等,可对日历、联系人的数据进行同步。

想要添加一个自己的帐户,本地端要实现两大部分。1是添加帐户的功能。2是同步的功能。

PS:服务器端忽略,SampleSyncAdapter中有相关的服务器代码,未研究。


PART.1 添加帐户的功能

首先,我们关注AccountManager,它为我们管理帐户提供了接口,详情见API文档。

常用的接口有

addAccount() :添加一个帐户。

addAccountExplicitly(): 直接添加一个帐户到AccountManager。

getAccounts():获取所有帐户。

removeAccount():删除帐户

--进入正题--

我们想添加一个自己的帐户到设备中,所需权限如下:



<uses-sdk android:minSdkVersion="5"/> <uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/></uses-permission> <uses-permission android:name="android.permission.ACCOUNT_MANAGER"></uses-permission> <uses-permission android:name="android.permission.GET_ACCOUNTS"></uses-permission> <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"></uses-permission>


首先需要实现一个AccountAuthenticatorActivity 的类来供用户实现输入。此输入界面即你在setting中点击添加帐户,选择完相应的类型之后跳出来的对话框。

之后,需要添加一个账户服务(Service)和一个验证器(AbstractAccountAuthenticator)。我们首先需要构建一个authenticator.xml



<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
    android:accountType="com.example.android.samplesync"
    android:icon="@drawable/icon"
    android:smallIcon="@drawable/icon"
    android:label="@string/label"
/>

然后,在AndroidManifest.xml文件中AuthenticationService向AccountManager注册一个账户类型。同时,类似widget的声明方式,在meta-data中要声明一个xml,在这个xml中描述账户的类型,图标,显示名,等等 。 这样我们可以在setting中的添加帐户中添加一个自己的帐户类型。

<service
            android:name=".authenticator.AuthenticationService"
            android:exported="true">
            <intent-filter>
                <action
                    android:name="android.accounts.AccountAuthenticator" />
            </intent-filter>
            <meta-data
                android:name="android.accounts.AccountAuthenticator"
                android:resource="@xml/authenticator" />
        </service>


Service的工作其实很简单,代码如下,主要负责返回一个IBinder 接口。

public class AuthenticationService extends Service {

    private static final String TAG = "AuthenticationService";
    private Authenticator mAuthenticator;
    @Override
    public void onCreate() {
         mAuthenticator = new Authenticator(this);
    }
    @Override
    public IBinder onBind(Intent intent) {
        return mAuthenticator.getIBinder();
    }
}

最后,最重要的是AbstractAccountAuthenticator类的实现,因为在添加、操作账户信息时会通过AbstractAccountAuthenticator实现异步调用。下面是实现的addAccount方法



@Override
    public Bundle addAccount(AccountAuthenticatorResponse response, String accountType,
            String authTokenType, String[] requiredFeatures, Bundle options) {
        Log.v(TAG, "addAccount()");
        final Intent intent = new Intent(mContext, AuthenticatorActivity.class);
        intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
        final Bundle bundle = new Bundle();
        bundle.putParcelable(AccountManager.KEY_INTENT, intent);
        return bundle;
    }

这个Bundle中都会有个包含登录界面的Activity的Intent,然后通过这个Bundle返回给AccountManager,来启动一个登录界面添加账户。


之后在验证完用户名和密码之后添加帐户,代码如下



private void finishLogin(String authToken) {

        Log.i(TAG, "finishLogin()");
        final Account account = new Account(mUsername, Constants.ACCOUNT_TYPE);
        if (mRequestNewAccount) {
            mAccountManager.addAccountExplicitly(account, mPassword, null);
            // Set contacts sync for this account.
            ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true);
        } else {
            mAccountManager.setPassword(account, mPassword);
        }
        final Intent intent = new Intent();
        intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, mUsername);
        intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, Constants.ACCOUNT_TYPE);
        setAccountAuthenticatorResult(intent.getExtras());
        setResult(RESULT_OK, intent);
        finish();
    }

直接向AccountManager添加一个帐户,并设置让其能够自动同步。

到此为止,你已经成功添加了一个属于自己的帐户,可是仅仅是一个帐户,可是却没有同谷歌帐户一样的选项去选择同步联系人,同步日历等等的选项,那就是接下去需要实现的第二步SyncAdapter。


PART.2 同步的功能

SyncAdapter同样需要一个服务(Service)和一个同步适配器(AbstractThreadedSyncAdapter)。

SyncAdapter的Service 需要在AndroidManifest里面声明一个带有Intent:android.content.SyncAdapter的Service来达到向系统注册一个具有同步功能的账户适配器(sync-adapter)。 同时,类似widget的声明方式,在meta-data中要声明一个xml,在这个xml中描述适配器绑定的账户,所要同步的区域(Authority)(如com.android.contacts,com.android.calendar com.android.email)等信息,一个适配器只能同步一个Authority,若想使一个账户同步多个Authority,可以向系统注册多个绑定同一账户的sync-adapter。


<service
            android:name=".syncadapter.SyncService"
            android:exported="true">
            <intent-filter>
                <action
                    android:name="android.content.SyncAdapter" />
            </intent-filter>
            <meta-data
                android:name="android.content.SyncAdapter"
                android:resource="@xml/syncadapter" />
            <meta-data
                android:name="android.provider.CONTACTS_STRUCTURE"
                android:resource="@xml/contacts" />
        </service>


<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
    android:contentAuthority="com.android.contacts"
    android:accountType="com.example.android.samplesync"
    android:supportsUploading="false"
    android:userVisible="true"
/>



而SyncAdapter的Service的功能也很简单,实现基本和帐户服务的Service一样,主要返回一个IBinder。SyncAdapter主要通过实现父类AbstractThreadedSyncAdapter 的onPerformSync()的方法来实现帐户同步功能。


public class SyncService extends Service {

    private static final Object sSyncAdapterLock = new Object();
    private static SyncAdapter sSyncAdapter = null;

    @Override
    public void onCreate() {
        synchronized (sSyncAdapterLock) {
            if (sSyncAdapter == null) {
                sSyncAdapter = new SyncAdapter(getApplicationContext(), true);
            }
        }
    }

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


帐户同步适配器(SyncAdapter)里面最主要的方法代码实现如下, 在此SampleSyncAdapter中,实现的主要是联系人的同步。

@Override
    public void onPerformSync(Account account, Bundle extras, String authority,
        ContentProviderClient provider, SyncResult syncResult) {

        try {
            // see if we already have a sync-state attached to this account. By handing
            // This value to the server, we can just get the contacts that have
            // been updated on the server-side since our last sync-up
            long lastSyncMarker = getServerSyncMarker(account);

            // By default, contacts from a 3rd party provider are hidden in the contacts
            // list. So let's set the flag that causes them to be visible, so that users
            // can actually see these contacts.
            if (lastSyncMarker == 0) {
                ContactManager.setAccountContactsVisibility(getContext(), account, true);
            }

            List<RawContact> dirtyContacts;
            List<RawContact> updatedContacts;

            // Use the account manager to request the AuthToken we'll need
            // to talk to our sample server.  If we don't have an AuthToken
            // yet, this could involve a round-trip to the server to request
            // and AuthToken.
            final String authtoken = mAccountManager.blockingGetAuthToken(account,
                    Constants.AUTHTOKEN_TYPE, NOTIFY_AUTH_FAILURE);

            // Make sure that the sample group exists
            final long groupId = ContactManager.ensureSampleGroupExists(mContext, account);

            // Find the local 'dirty' contacts that we need to tell the server about...
            // Find the local users that need to be sync'd to the server...
            dirtyContacts = ContactManager.getDirtyContacts(mContext, account);

            // Send the dirty contacts to the server, and retrieve the server-side changes
            updatedContacts = NetworkUtilities.syncContacts(account, authtoken,
                    lastSyncMarker, dirtyContacts);

            // Update the local contacts database with the changes. updateContacts()
            // returns a syncState value that indicates the high-water-mark for
            // the changes we received.
            Log.d(TAG, "Calling contactManager's sync contacts");
            long newSyncState = ContactManager.updateContacts(mContext,
                    account.name,
                    updatedContacts,
                    groupId,
                    lastSyncMarker);

            // This is a demo of how you can update IM-style status messages
            // for contacts on the client. This probably won't apply to
            // 2-way contact sync providers - it's more likely that one-way
            // sync providers (IM clients, social networking apps, etc) would
            // use this feature.

            ContactManager.updateStatusMessages(mContext, updatedContacts);

            // This is a demo of how you can add stream items for contacts on
            // the client. This probably won't apply to
            // 2-way contact sync providers - it's more likely that one-way
            // sync providers (IM clients, social networking apps, etc) would
            // use this feature. This is only supported in ICS MR1 or above.

            if (Build.VERSION.SDK_INT >=
                    Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
                ContactManager.addStreamItems(mContext, updatedContacts,
                    account.name, account.type);
            }

            // Save off the new sync marker. On our next sync, we only want to receive
            // contacts that have changed since this sync...
            setServerSyncMarker(account, newSyncState);

            if (dirtyContacts.size() > 0) {
                ContactManager.clearSyncFlags(mContext, dirtyContacts);
            }

        } catch (final AuthenticatorException e) {
            Log.e(TAG, "AuthenticatorException", e);
            syncResult.stats.numParseExceptions++;
        } catch (final OperationCanceledException e) {
            Log.e(TAG, "OperationCanceledExcetpion", e);
        } catch (final IOException e) {
            Log.e(TAG, "IOException", e);
            syncResult.stats.numIoExceptions++;
        } catch (final AuthenticationException e) {
            Log.e(TAG, "AuthenticationException", e);
            syncResult.stats.numAuthExceptions++;
        } catch (final ParseException e) {
            Log.e(TAG, "ParseException", e);
            syncResult.stats.numParseExceptions++;
        } catch (final JSONException e) {
            Log.e(TAG, "JSONException", e);
            syncResult.stats.numParseExceptions++;
        }
    }


PART.3

到此为止,应该已经成功添加了一个帐户类型,可以有同步联系人的功能。如果我们想添加一个新的帐户,可以进行如下操作:

1、直接通过AccountManager添加,AccountManager.getInstance().addcount,在该接口中传入,账户等信息,系统就会调用那个账户的登录界面。

2、有时可能想调用所有可以同步该应用的账户接口(如日历可以使用Exchange账户和Google账户),或者特定的一组账户,这时可以传入Intent: Settings.ACTION_ADD_ACCOUNT or android.settings.ADD_ACCOUNT_SETTINGS,这个Intent会调用系统Settings的AddAccount的Activity。如果没有其他参数,那么这个activity和从设置->账户与同步进入的activity没有区别。如果使用putExtra添加额外的参数的话,那么就可以启动一组特定的账户了,具体如下:

Extra: (“authorities”,String[])指定一组authority 用字符串数组传入

Extra:(“account_types”,String[])指定一组账户类型,用字符串数组传入


THE LAST:

我研究帐户管理主要是想用于添加一个本地帐户用于存储SIM卡联系人,因此我不需要用到真正的同步功能因此也就没有研究如何搭建同步服务器。 但用于本地帐户的时候发现,如果不联网则不会出现同步联系人的选项,具体原因需要研究源码。

在进行添加帐户的时候,很多人经常会没有实现authenticator.xml 而导致UID 报错的情况,这也是我刚开始的时候遇到的问题。