认证和同步

从Android 2.0(API level 5)开始,可以定制同步provider,把系统通讯录、日历等整合起来。遗憾的是,通过远程服务执行同步是不可靠的,因为任意一点的失误都可能导致Android系统崩溃和重启(很少能够看出哪个地方做错了)。理想情况下,随着Android的发展,同步将变得更加简单,不那么复杂。现在,同步这个过程包含两个部分——认证(账户认证)和同步(Sync provider)。

在深入细节之前,要指出的是,这里提供的例子有两个组成部分:服务器端和Android客户端。服务器端是一个基本的Web服务,它接收特定的GET请求,返回JSON格式的响应。在每个小节中都提供了相关的GET URI及响应示例。本书的源代码中包含了完整的服务器端源代码。

要注意的另外一点是,在这个示例中,选择的是同步账户信息。这不是唯一可以执行同步的数据,可以同步任何可以访问的内容提供者,甚至是应用特定的存储数据。

认证

为了使客户端能够通过Android账户认证系统和远程服务端进行认证,必须提供3种信息:

·android.accounts.AccountAuthenticator intent所触发的服务,其在onBind方法中返回AbstractAccountAuthenticator子类。

·提示用户输入其校验信息的活动。

·描述账户数据如何显示的XML文件。

我们先来探讨服务。在manifest文件中,需要启用android.permission.AUTHENTICATE_ACCOUNTS。


<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
  

然后,在manifest文件中描述服务。注意,在intent-filter描述符中包含了android.accounts.AccountAuthenticator intent。该manifest文件还描述了AccountAuthenticator的资源:


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

在manifest文件中标示的资源见下文。其中包含的accountType,可以区分不同的Authenticator。修改该XML文件时要十分小心(例如不要直接把字符串赋值给android:label或包含不存在的drawable),因为如果内容不正确,当你添加一个新的账户时,Android会崩溃(在Account&Sync设置中):


<?xml version="1.0" encoding="utf-8"?>
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
    android:accountType="com.oreilly.demo.pa.ch17.sync"
    android:icon="@drawable/icon"
    android:smallIcon="@drawable/icon"
    android:label="@string/authlabel"
/>
  

因为在manifest文件中已经描述了服务,所以现在转而考虑service本身。注意,onBind方法返回的是Authenticator类。该Authenticator类继承了AbstractAccount-Authenticator类:


package com.oreilly.demo.android.pa.clientserver.client.sync.authsync;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
public class AuthenticationService extends Service {
    private static final Object lock = new Object;
    private Authenticator auth;
    @Override
    public void onCreate {
        synchronized (lock) {
            if (auth == null) {
                auth = new Authenticator(this);
            }
        }
    }
    @Override
    public IBinder onBind(Intent intent) {
        return auth.getIBinder;
    }
}
  

在探讨Authenticator类的全部源代码之前,先来看看在AbstractAccountAuthenticator中包含的一个重要方法——addAccount。当用户从Add Account屏幕中选择自定义账户时,最终会调用这个方法。LoginActivity(我们自定义的Activity,在用户登录时会弹出对话框)是在Intent内描述的,Intent在返回的Bundle中。在intent中包含的AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE键是至关重要的,因为它包含AccountAuthenticatorResponse对象,一旦用户在远程服务上通过认证,会通过AccountAuthenticatorResponse对象返回账户密钥:


public class Authenticator extends AbstractAccountAuthenticator {
    public Bundle addAccount(AccountAuthenticatorResponse response,
            String accountType, String authTokenType,
            String requiredFeatures, Bundle options) {
        Intent intent = new Intent(context, LoginActivity.class);
        intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
        Bundle bundle = new Bundle;
        bundle.putParcelable(AccountManager.KEY_INTENT, intent);
        return bundle;
    }
}
  

以下是完整的Authenticator activity,它继承了AbstractAccountAuthenticator:


package com.oreilly.demo.android.pa.clientserver.client.sync.authsync;
import com.oreilly.demo.android.pa.clientserver.client.sync.LoginActivity;
import android.accounts.AbstractAccountAuthenticator;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
public class Authenticator extends AbstractAccountAuthenticator {
    public static final String AUTHTOKEN_TYPE
                    = "com.oreilly.demo.android.pa.clientserver.client.sync";
    public static final String AUTHTOKEN_TYPE
                        = "com.oreilly.demo.android.pa.clientserver.client.sync";
    private final Context context;
    public Authenticator(Context context) {
        super(context);
        this.context = context;
    }
    @Override
    public Bundle addAccount(AccountAuthenticatorResponse response,
            String accountType, String authTokenType,
            String requiredFeatures, Bundle options) {
        Intent intent = new Intent(context, LoginActivity.class);
        intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
        Bundle bundle = new Bundle;
        bundle.putParcelable(AccountManager.KEY_INTENT, intent);
        return bundle;
}
@Override
public Bundle confirmCredentials(AccountAuthenticatorResponse response,
    Account account, Bundle options) {
    return null;
}
@Override
public Bundle editProperties(AccountAuthenticatorResponse response,
        String accountType) {
    return null;
}
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response,
        Account account, String authTokenType, Bundle loginOptions) {
    return null;
}
@Override
public String getAuthTokenLabel(String authTokenType) {
    return null;
}
@Override
public Bundle hasFeatures(AccountAuthenticatorResponse response,
        Account account, String features) {
    return null;
}
    @Override
    public Bundle updateCredentials(AccountAuthenticatorResponse response,
            Account account, String authTokenType, Bundle loginOptions) {
        return null;
    }
}
  

在这个示例中,访问远程服务器需要调用登录API(通过HTTP URI访问),其参数包括username和password。如果登录成功,会返回包含token的JSON字符串:


uri: http://<serverBaseUrl>:<port>/login?username=<name>&password=<pass>
response: { "token" : "someAuthenticationToken" }
  

LoginActivity请求用户为该账户输入用户名和密码,然后和远程服务器通信。一旦返回了期望的JSON字符串,会调用handleLoginResponse方法,并把账户的相关信息传回AccountManager:


package com.oreilly.demo.android.pa.clientserver.sync;
import org.json.JSONObject;
import com.oreilly.demo.android.pa.clientserver.client.R;
import com.oreilly.demo.android.pa.clientserver.client.sync.authsync.Authenticator;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorActivity;
import android.accounts.AccountManager;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.ContentResolver;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.provider.ContactsContract;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.EditText;
import android.widget.Toast;
public class LoginActivity extends AccountAuthenticatorActivity {
    public static final String PARAM_AUTHTOKEN_TYPE              = "authtokenType";
    public static final String PARAM_USERNAME               = "username";
    public static final String PARAM_PASSWORD               = "password";
    private String username;
    private String password;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getVars;
        setupView;
    }
    @Override
    protected Dialog onCreateDialog(int id) {
        final ProgressDialog dialog = new ProgressDialog(this);
        dialog.setMessage("Attemping to login");
        dialog.setIndeterminate(true);
        dialog.setCancelable(false);
        return dialog;
    }
    private void getVars {
        username = getIntent.getStringExtra(PARAM_USERNAME);
    }
    private void setupView {
        setContentView(R.layout.login);
        findViewById(R.id.login).setOnClickListener(new OnClickListener {
            @Override
            public void onClick(View v) {
                login;
            }
        });
        if(username != null) {
            ((EditText) findViewById(R.id.username)).setText(username);
        }
    }
    private void login {
        if(((EditText) findViewById(R.id.username)).getText == null ||
              ((EditText) findViewById(R.id.username)).getText.toString.
                  trim.length
                    < 1) {
            Toast.makeText(this, "Please enter a Username",
                Toast.LENGTH_SHORT).show;
            return;
        }
        if(((EditText) findViewById(R.id.password)).getText == null ||
            ((EditText) findViewById(R.id.password)).getText.toString.
                trim.length
            < 1) {
            Toast.makeText(this, "Please enter a Password",
                Toast.LENGTH_SHORT).show;
            return;
        }
        username = ((EditText) findViewById(R.id.username)).getText.toString;
        password = ((EditText) findViewById(R.id.password)).getText.toString;
        showDialog(0);
        Handler loginHandler = new Handler {
            @Override
            public void handleMessage(Message msg) {
                if(msg.what == NetworkUtil.ERR) {
                    dismissDialog(0);
                    Toast.makeText(LoginActivity.this, "Login Failed: "+
                                    msg.obj, Toast.LENGTH_SHORT).show;
                } else if(msg.what == NetworkUtil.OK) {
                    handleLoginResponse((JSONObject) msg.obj);
                }
            }
        };
        NetworkUtil.login(getString(R.string.baseurl),
                          username, password, loginHandler);
    }
    private void handleLoginResponse(JSONObject resp) {
        dismissDialog(0);
        final Account account = new Account(username, Authenticator.ACCOUNT_TYPE);
        if (getIntent.getStringExtra(PARAM_USERNAME) == null) {
            AccountManager.get(this).addAccountExplicitly(account, password, null);
            ContentResolver.setSyncAutomatically(account,
                ContactsContract.AUTHORITY, true);
        } else {
            AccountManager.get(this).setPassword(account, password);
        }
        Intent intent = new Intent;
        intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, username);
        intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE,
                        Authenticator.ACCOUNT_TYPE);
        if (resp.has("token")) {
            intent.putExtra(AccountManager.KEY_AUTHTOKEN, resp.optString("token"));
        }
        setAccountAuthenticatorResult(intent.getExtras);
        setResult(RESULT_OK, intent);
        finish;
    }
}
  

LoginActivity的layout XML文件如下:


<?xml version="1.0" encoding="utf-8" ?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_
    android:layout_
    android:background="#fff">
      <ScrollView
        android:layout_
        android:layout_
        android:layout_weight="1">
            <LinearLayout
            android:layout_
            android:layout_
            android:layout_weight="1"
            android:orientation="vertical"
            android:paddingTop="5dip"
            android:paddingBottom="13dip"
            android:paddingLeft="20dip"
            android:paddingRight="20dip">
                  <EditText
                android:id="@+id/username"
                android:singleLine="true"
                android:layout_
                android:layout_
                android:minWidth="250dip"
                android:scrollHorizontally="true"
                android:capitalize="none"
                android:hint="Username"
                android:autoText="false" />
                  <EditText
                android:id="@+id/password"
                android:singleLine="true"
                android:layout_
                android:layout_
                android:minWidth="250dip"
                android:scrollHorizontally="true"
                android:capitalize="none"
                android:autoText="false"
                android:password="true"
                android:hint="Password"
                android:inputType="textPassword" />
            </LinearLayout>
      </ScrollView>
      <FrameLayout
        android:layout_
        android:layout_
        android:background="#fff"
        android:minHeight="54dip"
        android:paddingTop="4dip"
        android:paddingLeft="2dip"
        android:paddingRight="2dip">
            <Button
            android:id="@+id/login"
            android:layout_
            android:layout_
            android:layout_gravity="center_horizontal"
            android:minWidth="100dip"
            android:text="Login" />
      </FrameLayout>
</LinearLayout>
  

账户建立好了,接下来可以同步数据了。

同步

为了同步账户数据,还需要处理3个模块:一是注册的service,它监听android.content.SyncAdapter intent,并在onBind方法上返回继承AbstractThreadedSyncAdapter的类;二是XML描述符,它描述要查看和同步的数据结构;三是继承AbstractThreaded-SyncAdapter的类,它处理实际的同步操作。

在我们这个例子中,希望同步之前章节中所描述的账户的联系信息。注意,通讯录信息不是唯一可以执行同步的信息。可以和能够访问的任何内容提供者执行同步,甚至是应用特定的存储数据。

以下许可是在manifest文件中给出的:


<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
  

现在,描述要使用的服务。注意,它包含了android.content.SyncAdapter intent,并且描述了通讯录数据和SyncAdapter的结构:


<service android:name=".sync.authsync.SyncService">
      <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 XML资源中,要注意accountType描述符。我们希望使用的Android通讯录数据如下:


<?xml version="1.0" encoding="utf-8"?>
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
    android:contentAuthority="com.android.contacts"
    android:accountType="com.oreilly.demo.android.pa.clientserver.client.sync"
/>
  

以下是通讯录描述符XML。注意各个字段的名称:


<?xml version="1.0" encoding="utf-8"?>
<ContactsSource xmlns:android="http://schemas.android.com/apk/res/android">
      <ContactsDataKind
        android:mimeType=
"vnd.android.cursor.item/vnd.com.oreilly.demo.android.pa.clientserver.sync.profile"
        android:icon="@drawable/icon"
        android:summaryColumn="data2"
        android:detailColumn="data3"
        android:detailSocialSummary="true" />
</ContactsSource>
  

所创建的SyncService会返回SyncAdapter类。该自定义类继承AbstractThreadedSync-Adapter:


package com.oreilly.demo.android.pa.clientserver.client.sync.authsync;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
public class SyncService extends Service {
    private static final Object lock = new Object;
    private static SyncAdapter adapter = null;
    @Override
    public void onCreate {
        synchronized (lock) {
            if (adapter == null) {
                adapter = new SyncAdapter(getApplicationContext, true);
            }
        }
    }
    @Override
    public void onDestroy {
        adapter = null;
    }
    @Override
    public IBinder onBind(Intent intent) {
        return adapter.getSyncAdapterBinder;
    }
    }
  

继续该示例,我们在远程服务端创建了getfriends方法。它会接收上一节成功登录所传递回来的token,以及表示最近一次调用是第几次调用(如果是第一次调用,会传递0值)的时间。响应是另一个JSON字符串,它描述了朋友(ID、name和phone)、调用时间(服务器端的UNIX时间),以及该账户增删朋友的历史记录。在历史记录中,type字段值0表示增加,1表示删除。字段who是朋友ID,time是操作的时间:


uri: http://<serverBaseUrl>:<port>/getfriends?token=<token>&time=<lasttime>
response:
{
    "time" : 1295817666232,
    "history" : [
        {
            "time" : 1295817655342,
            "type" : 0,
            "who" : 1
        }
    ],
    "friend" : [
        {
            "id" : 1,
            "name" : "Mary",
            "phone" : "8285552334"
        }
    ]
}
  

AbstractThreadedSyncAdapter类继承SyncAdapter类,如下:


public class SyncAdapter extends AbstractThreadedSyncAdapter {
    private final Context context;
    private static long lastsynctime = 0;
    public SyncAdapter(Context context, boolean autoInitialize) {
        super(context, autoInitialize);
        this.context = context;
    }
    @Override
    public void onPerformSync(Account account, Bundle extras, String authority,
                ContentProviderClient provider, SyncResult syncResult) {
        String authtoken = null;
          try {
              authtoken = AccountManager.get(context).blockingGetAuthToken(account,
                                    Authenticator.AUTHTOKEN_TYPE, true);
              ListFriends friendsdata =
                ListFriends.fromJSON(
                    NetworkUtil.getFriends(context.getString(R.string.baseurl),
                    authtoken, lastsynctime, null));
              lastsynctime = friendsdata.time;
              sync(account, friendsdata);
        } catch (Exception e) {
            e.printStackTrace;
        }
    }
    private void sync(Account account, ListFriends data) {
        // MAGIC HAPPENS
    }
}
 

SyncAdapter类的完整代码如下,包括当sync方法接收数据时发生的各种操作。它包含通讯录信息的各种增删操作。在前面的章节中涵盖了Contact和ContentProvider操作。


package com.oreilly.demo.android.pa.clientserver.client.sync.authsync;
import java.util.ArrayList;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentUris;
import android.content.Context;
import android.content.SyncResult;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.provider.ContactsContract.RawContacts;
import com.oreilly.demo.android.pa.clientserver.client.R;
import com.oreilly.demo.android.pa.clientserver.client.sync.NetworkUtil;
import com.oreilly.demo.android.pa.clientserver.client.sync.dataobjects.Change;
import com.oreilly.demo.android.pa.clientserver.client.sync.dataobjects.ListFriends;
import com.oreilly.demo.android.pa.clientserver.client.sync.dataobjects.User;
public class SyncAdapter extends AbstractThreadedSyncAdapter {
    private final Context context;
    private static long lastsynctime = 0;
    public SyncAdapter(Context context, boolean autoInitialize) {
        super(context, autoInitialize);
        this.context = context;
    }
    @Override
    public void onPerformSync(Account account, Bundle extras, String authority,
                    ContentProviderClient provider, SyncResult syncResult) {
        String authtoken = null;
          try {
                // get accounttoken. this eventually calls our Authenticator
                // getAuthToken
            authtoken = AccountManager.get(context).blockingGetAuthToken(account,
                          Authenticator.AUTHTOKEN_TYPE, true);
            ListFriends friendsdata =
              ListFriends.fromJSON(
                NetworkUtil.getFriends(context.getString(R.string.baseurl),
            authtoken, lastsynctime, null));
            lastsynctime = friendsdata.time;
            sync(account, friendsdata);
        } catch (Exception e) {
            e.printStackTrace;
        }
    }
    // where the magic happens
    private void sync(Account account, ListFriends data) {
        User self = new User;
        self.username = account.name;
        ArrayList<ContentProviderOperation> ops =
                                    new ArrayList<ContentProviderOperation>;
        // cycle through the history to find the deletes
        if(data.history != null && !data.history.isEmpty) {
            for(Change change : data.history) {
                if(change.type == Change.ChangeType.DELETE) {
                    ContentProviderOperation op = delete(account, change.who);
                    if(op != null) ops.add(op);
                }
            }
        }
        // cycle through the friends to find ones we do not already have and add them
        if(data.friends != null && !data.friends.isEmpty) {
            for(User f : data.friends) {
                ArrayList<ContentProviderOperation> op = add(account, f);
                if(op != null) ops.addAll(op);
            }
        }
        if(!ops.isEmpty) {
            try {
                context.getContentResolver.applyBatch(ContactsContract.AUTHORITY,
                                                        ops);
            } catch (Exception e) {
                e.printStackTrace;
            }
        }
    }
    // adding a contact. note we are storing the id referenced in the response
    // from the server in the SYNC1 field - this way we can find it with this
    // server based id
    private ArrayList<ContentProviderOperation> add(Account account, User f) {
        long rawid = lookupRawContact(f.id);
        if(rawid != 0) return null;
        ArrayList<ContentProviderOperation> ops =
          new ArrayList<ContentProviderOperation>;
        ops.add(ContentProviderOperation.newInsert(
                    ContactsContract.RawContacts.CONTENT_URI)
                .withValue(RawContacts.SOURCE_ID, 0)
                .withValue(RawContacts.SYNC1, f.id)
                .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE,
                            Authenticator.ACCOUNT_TYPE)
                .withValue(ContactsContract.RawContacts.ACCOUNT_NAME,
                            account.name)
                .build);
        if(f.name != null && f.name.trim.length > 0) {
            ops.add(ContentProviderOperation.newInsert(
                        ContactsContract.Data.CONTENT_URI)
                .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID,
                                        0)
                .withValue(ContactsContract.Data.MIMETYPE,
                   ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
                .withValue(ContactsContract.CommonDataKinds.
                   StructuredName.DISPLAY_NAME, f.name)
                .build);
        }
        if(f.phone != null && f.phone.trim.length > 0) {
            ops.add(ContentProviderOperation.newInsert
                (ContactsContract.Data.CONTENT_URI)
                    .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
                    .withValue(ContactsContract.Data.MIMETYPE,
                            ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
                    .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, f.phone)
                    .withValue(ContactsContract.CommonDataKinds.Phone.TYPE,
                            ContactsContract.CommonDataKinds.Phone.TYPE_HOME)
                    .build);
        }
        ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
                .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
                .withValue(ContactsContract.Data.MIMETYPE,
                  "vnd.android.cursor.item/vnd.com.oreilly.demo.android.pa.clientserver.client.sync.profile")
                .withValue(ContactsContract.Data.DATA2, "Ch15 Profile")
                .withValue(ContactsContract.Data.DATA3, "View profile")
                .build
                );
        return ops;
    }
    // delete contact via the server based id
    private ContentProviderOperation delete(Account account, long id) {
        long rawid = lookupRawContact(id);
        if(rawid == 0) return null;
        return ContentProviderOperation.newDelete(
                ContentUris.withAppendedId(
                            ContactsContract.RawContacts.CONTENT_URI,
                rawid))
                .build;
    }
    // look up the actual raw id via the id we have stored in the SYNC1 field
    private long lookupRawContact(long id) {
        long rawid = 0;
        Cursor c = context.getContentResolver.query(
                        RawContacts.CONTENT_URI, new String {RawContacts._ID},
                        RawContacts.ACCOUNT_TYPE + "='" +
                        Authenticator.ACCOUNT_TYPE + "' AND "+
                        RawContacts.SYNC1 + "=?",
                        new String {String.valueOf(id)},
                        null);
        try {
            if(c.moveToFirst) {
                rawid = c.getLong(0);
            }
        } finally {
            if (c != null) {
                c.close;
                c = null;
            }
        }
        return rawid;
    }
}
  

在前面的SyncAdapter类中可能缺失了一个重要的详细信息:在执行onPerformSync调用时,我们希望通过blockingGetAuthToken方法从AccountManager中获取authtoken。它最终会调用和该账户关联的AbstractAccountAuthenticator类。在这个例子中,它调用的是我们在前一节中提到过的Authenticator类。在Authenticator类中,会调用getAuthToken方法,示例如下:


@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,
                        String authTokenType, Bundle loginOptions) {
    // check and make sure it is the right token type we want
    if (!authTokenType.equals(AUTHTOKEN_TYPE)) {
        final Bundle result = new Bundle;
        result.putString(AccountManager.KEY_ERROR_MESSAGE,
          "invalid authTokenType");
        return result;
    }
    // if we have the password, let's try and get the current
    // authtoken from the server
    String password = AccountManager.get(context).getPassword(account);
    if (password != null) {
        JSONObject json = NetworkUtil.login(context.getString(R.string.baseurl),
                                        account.name, password, true, null);
        if(json != null) {
            Bundle result = new Bundle;
            result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
            result.putString(AccountManager.KEY_ACCOUNT_TYPE, ACCOUNT_TYPE);
            result.putString(AccountManager.KEY_AUTHTOKEN,
                            json.optString("token"));
            return result;
        }
    }
    // if all else fails let's see about getting the user to log in
    Intent intent = new Intent(context, LoginActivity.class);
    intent.putExtra(LoginActivity.PARAM_USERNAME, account.name);
    intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
    Bundle bundle = new Bundle;
    bundle.putParcelable(AccountManager.KEY_INTENT, intent);
    return bundle;
}
  

《Android程序设计:第2版》