首页 » Android程序设计:第2版 » Android程序设计:第2版全文在线阅读

《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;}@Overridepublic Bundle confirmCredentials(AccountAuthenticatorResponse response,    Account account, Bundle options) {    return null;}@Overridepublic Bundle editProperties(AccountAuthenticatorResponse response,        String accountType) {    return null;}@Overridepublic Bundle getAuthToken(AccountAuthenticatorResponse response,        Account account, String authTokenType, Bundle loginOptions) {    return null;}@Overridepublic String getAuthTokenLabel(String authTokenType) {    return null;}@Overridepublic 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方法,示例如下:


@Overridepublic 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;}