讀古今文學網 > Android程序設計:第2版 > 認證和同步 >

認證和同步

從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;
}