1. 程式人生 > >Android 帳戶同步機制

Android 帳戶同步機制

裝過Android版的Facebook、lastfm的同學是否對於這些應用的功能感到驚喜,它們可以定期更新朋友的最新資訊,將最新近況和心情短語整合入聯絡人中。這些應用全部是以Android2.0後的賬戶和同步機制為基礎的。Google的例程中給出了名為SampleSyncAdpater的例子,通過分析該例子可以學會Android中的Account驗證、同步Adapter的使用。

詳細例子程式碼可以看sdk samples中提供的原始碼,現在拿2.2中的版本來簡要說明。

首先是 class Authenticator extends AbstractAccountAuthenticator ,該類是賬戶認證類,開啟手機的Setting裡,有Account&Sync 一項,Authenticator就是實現其中的賬號功能的類。

  1. // in Authenticator.java
  2.     public Bundle addAccount(AccountAuthenticatorResponse response,  
  3.         String accountType, String authTokenType, String[] requiredFeatures,  
  4.         Bundle options) {  
  5.         final Intent intent = new Intent(mContext, AuthenticatorActivity.class);  
  6.         intent.putExtra(AuthenticatorActivity.PARAM_AUTHTOKEN_TYPE,  
  7.             authTokenType);  
  8.         intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE,  
  9.             response);  
  10.         final Bundle bundle = new Bundle();  
  11.         bundle.putParcelable(AccountManager.KEY_INTENT, intent);  
  12.         return bundle;  
  13.     }  

其中addAccount方法用來定義需要增加賬號時的操作,如呼叫AuthenticatorActivity來進行賬號的新增認證。

在AuthenticatorActivity.java中定義了handleLogin(),此方法由login_activity.xml中的android:onClick="handleLogin"定義與ui中的okbutton的關聯。

  1. // in layout/login_activity.xml
  2. <Button  
  3.             android:id="@+id/ok_button"
  4.             android:layout_width="wrap_content"
  5.             android:layout_height="wrap_content"
  6.             android:layout_gravity="center_horizontal"
  7.             android:minWidth="100dip"
  8.             android:text="@string/login_activity_ok_button"
  9.             android:onClick="handleLogin" />  

handleLogin()將ui中的使用者名稱和密碼取得,並建立一個試圖認證的執行緒,通過網路去服務端驗證。

NetworkUtilities.java中的 public static boolean authenticate(String username, String password, Handler handler, final Context context)方法展示了通過網路驗證的具體流程。得到服務端驗證結果後,在sendResult()中通過handler.post呼叫來實現 onAuthenticationResult()在AuthenticatorActivity中的執行。 onAuthenticationResult()判斷驗證通過則結束AuthenticatorActivity,否則報出使用者名稱密碼錯,讓使用者在 AuthenticatorActivity中再次嘗試驗證。

  1. // AuthenticatorActivity.java中的handleLogin()方法
  2.     /** 
  3.      * Handles onClick event on the Submit button. Sends username/password to 
  4.      * the server for authentication. 
  5.      *  
  6.      * @param view The Submit button for which this method is invoked 
  7.      */
  8.     publicvoid handleLogin(View view) {  
  9.         if (mRequestNewAccount) {  
  10.             mUsername = mUsernameEdit.getText().toString();  
  11.         }  
  12.         mPassword = mPasswordEdit.getText().toString();  
  13.         if (TextUtils.isEmpty(mUsername) || TextUtils.isEmpty(mPassword)) {  
  14.             mMessage.setText(getMessage());  
  15.         } else {  
  16.             showProgress();  
  17.             // Start authenticating...
  18.             mAuthThread =  
  19.                 NetworkUtilities.attemptAuth(mUsername, mPassword, mHandler,  
  20.                     AuthenticatorActivity.this);  
  21.         }  
  22.     }  
  1. // NetworkUtilities中的authenticate()方法通過網路訪問具體來實現服務端的驗證,sendResult()來使呼叫結果被AuthenticatorActivity的onAuthenticationResult()呼叫。
  2.     /** 
  3.      * Connects to the Voiper server, authenticates the provided username and 
  4.      * password. 
  5.      *  
  6.      * @param username The user's username 
  7.      * @param password The user's password 
  8.      * @param handler The hander instance from the calling UI thread. 
  9.      * @param context The context of the calling Activity. 
  10.      * @return boolean The boolean result indicating whether the user was 
  11.      *         successfully authenticated. 
  12.      */
  13.     publicstaticboolean authenticate(String username, String password,  
  14.         Handler handler, final Context context) {  
  15.         final HttpResponse resp;  
  16.         final ArrayList<NameValuePair> params = new ArrayList<NameValuePair>();  
  17.         params.add(new BasicNameValuePair(PARAM_USERNAME, username));  
  18.         params.add(new BasicNameValuePair(PARAM_PASSWORD, password));  
  19.         HttpEntity entity = null;  
  20.         try {  
  21.             entity = new UrlEncodedFormEntity(params);  
  22.         } catch (final UnsupportedEncodingException e) {  
  23.             // this should never happen.
  24.             thrownew AssertionError(e);  
  25.         }  
  26.         final HttpPost post = new HttpPost(AUTH_URI);  
  27.         post.addHeader(entity.getContentType());  
  28.         post.setEntity(entity);  
  29.         maybeCreateHttpClient();  
  30.         try {  
  31.             resp = mHttpClient.execute(post);  
  32.             if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {  
  33.                 if (Log.isLoggable(TAG, Log.VERBOSE)) {  
  34.                     Log.v(TAG, "Successful authentication");  
  35.                 }  
  36.                 sendResult(true, handler, context);  
  37.                 returntrue;  
  38.             } else {  
  39.                 if (Log.isLoggable(TAG, Log.VERBOSE)) {  
  40.                     Log.v(TAG, "Error authenticating" + resp.getStatusLine());  
  41.                 }  
  42.                 sendResult(false, handler, context);  
  43.                 returnfalse;  
  44.             }  
  45.         } catch (final IOException e) {  
  46.             if (Log.isLoggable(TAG, Log.VERBOSE)) {  
  47.                 Log.v(TAG, "IOException when getting authtoken", e);  
  48.             }  
  49.             sendResult(false, handler, context);  
  50.             returnfalse;  
  51.         } finally {  
  52.             if (Log.isLoggable(TAG, Log.VERBOSE)) {  
  53.                 Log.v(TAG, "getAuthtoken completing");  
  54.             }  
  55.         }  
  56.     }  
  57.     /** 
  58.      * Sends the authentication response from server back to the caller main UI 
  59.      * thread through its handler. 
  60.      *  
  61.      * @param result The boolean holding authentication result 
  62.      * @param handler The main UI thread's handler instance. 
  63.      * @param context The caller Activity's context. 
  64.      */
  65.     privatestaticvoid sendResult(final Boolean result, final Handler handler,  
  66.         final Context context) {  
  67.         if (handler == null || context == null) {  
  68.             return;  
  69.         }  
  70.         handler.post(new Runnable() {  
  71.             publicvoid run() {  
  72.                 ((AuthenticatorActivity) context).onAuthenticationResult(result);  
  73.             }  
  74.         });  
  75.     }  
  1. // AuthenticatorActivity.java中的onAuthenticationResult,來根據驗證結果來選擇結束認證或重新嘗試。
  2.     /** 
  3.      * Called when the authentication process completes (see attemptLogin()). 
  4.      */
  5.     publicvoid onAuthenticationResult(boolean result) {  
  6.         Log.i(TAG, "onAuthenticationResult(" + result + ")");  
  7.         // Hide the progress dialog
  8.         hideProgress();  
  9.         if (result) {  
  10.             if (!mConfirmCredentials) {  
  11.                 finishLogin();  
  12.             } else {  
  13.                 finishConfirmCredentials(true);  
  14.             }  
  15.         } else {  
  16.             Log.e(TAG, "onAuthenticationResult: failed to authenticate");  
  17.             if (mRequestNewAccount) {  
  18.                 // "Please enter a valid username/password.
  19.                 mMessage  
  20.                     .setText(getText(R.string.login_activity_loginfail_text_both));  
  21.             } else {  
  22.                 // "Please enter a valid password." (Used when the
  23.                 // account is already in the database but the password
  24.                 // doesn't work.)
  25.                 mMessage  
  26.                     .setText(getText(R.string.login_activity_loginfail_text_pwonly));  
  27.             }  
  28.         }  
  29.     }  

Account的驗證完畢後,就生成了賬號,可以開始使用同步功能了。同步的主要邏輯在public class SyncAdapter extends AbstractThreadedSyncAdapter中實現。

  1. // SyncAdapter.java中的OnPerformSync方法,主要的同步邏輯
  2.     @Override
  3.     publicvoid onPerformSync(Account account, Bundle extras, String authority,  
  4.         ContentProviderClient provider, SyncResult syncResult) {  
  5.         List<User> users;  
  6.         List<Status> statuses;  
  7.         String authtoken = null;  
  8.          try {  
  9.              // use the account manager to request the credentials
  10.              authtoken =  
  11.                 mAccountManager.blockingGetAuthToken(account,  
  12.                     Constants.AUTHTOKEN_TYPE, true/* notifyAuthFailure */);  
  13.              // fetch updates from the sample service over the cloud
  14.              users =  
  15.                 NetworkUtilities.fetchFriendUpdates(account, authtoken,  
  16.                     mLastUpdated);  
  17.             // update the last synced date.
  18.             mLastUpdated = new Date();  
  19.             // update platform contacts.
  20.             Log.d(TAG, "Calling contactManager's sync contacts");  
  21.             ContactManager.syncContacts(mContext, account.name, users);  
  22.             // fetch and update status messages for all the synced users.
  23.             statuses = NetworkUtilities.fetchFriendStatuses(account, authtoken);  
  24.             ContactManager.insertStatuses(mContext, account.name, statuses);  
  25.         } catch (final AuthenticatorException e) {  
  26.             syncResult.stats.numParseExceptions++;  
  27.             Log.e(TAG, "AuthenticatorException", e);  
  28.         } catch (final OperationCanceledException e) {  
  29.             Log.e(TAG, "OperationCanceledExcetpion", e);  
  30.         } catch (final IOException e) {  
  31.             Log.e(TAG, "IOException", e);  
  32.             syncResult.stats.numIoExceptions++;  
  33.         } catch (final AuthenticationException e) {  
  34.             mAccountManager.invalidateAuthToken(Constants.ACCOUNT_TYPE,  
  35.                 authtoken);  
  36.             syncResult.stats.numAuthExceptions++;  
  37.             Log.e(TAG, "AuthenticationException", e);  
  38.         } catch (final ParseException e) {  
  39.             syncResult.stats.numParseExceptions++;  
  40.             Log.e(TAG, "ParseException", e);  
  41.         } catch (final JSONException e) {  
  42.             syncResult.stats.numParseExceptions++;  
  43.             Log.e(TAG, "JSONException", e);  
  44.         }  
  45.     }  

onPerformSync中的執行流程中,使用NetworkUtilities中的fetchFriendUpdates和 fetchFriendStatuses來訪問服務端的聯絡人更新,並使用了例程中自己封裝的ContactManager來讀取、更新聯絡人資訊。

那Account和SyncAdapter及其Service和xml定義之間是如何關聯的呢? AndroidManifest.xml中定義了AccountAuthenticator,SyncAdapter及對應的Service和xml定義的關聯。

  1. <application  
  2.     android:icon="@drawable/icon"
  3.     android:label="@string/label">  
  4.     <!-- The authenticator service -->  
  5.     <service  
  6.         android:name=".authenticator.AuthenticationService"
  7.         android:exported="true">  
  8.         <intent-filter>  
  9.             <action  
  10.                 android:name="android.accounts.AccountAuthenticator" />  
  11.         </intent-filter>  
  12.         <meta-data  
  13.             android:name="android.accounts.AccountAuthenticator"
  14.             android:resource="@xml/authenticator" />  
  15.     </service>  
  16.     <service  
  17.         android:name=".syncadapter.SyncService"
  18.         android:exported="true">  
  19.         <intent-filter>  
  20.             <action  
  21.                 android:name="android.content.SyncAdapter" />  
  22.         </intent-filter>  
  23.         <meta-data  
  24.             android:name="android.content.SyncAdapter"
  25.             android:resource="@xml/syncadapter" />  
  26.         <meta-data  
  27.             android:name="android.provider.CONTACTS_STRUCTURE"
  28.             android:resource="@xml/contacts" />  
  29.     </service>  
  30.     <activity  
  31.         android:name=".authenticator.AuthenticatorActivity"
  32.         android:label="@string/ui_activity_title"
  33.         android:theme="@android:style/Theme.Dialog"
  34.         android:excludeFromRecents="true"
  35.         >  
  36.         <!--  
  37.             No intent-filter here! This activity is only ever launched by  
  38.             someone who explicitly knows the class name  
  39.         -->  
  40.     </activity>  
  41. </application>  

更詳細的程式碼細節和執行流程,可以去把SDK中的SampleSyncAdapter程式碼執行起來體會一下,不過要實現整個流程,必須搭建聯絡人的伺服器端,例程中在目錄samplesyncadapter_server中也提供了簡單的server端python程式碼,需要搭建在google app engine上。搭建過程遇到一些問題,由於對python不熟我弄了幾天才解決好搭建成功,其中遇到的一個model moudle找不到的問題需要你在model中新建一個__init__.py的空檔案,來說明是一個python模組,如果你也遇到此問題,希望對你有幫助。

以上是從別人那裡看到的一篇不錯的文章,以下是自己在原始碼開發過程中的一些總結:

在AccountManagerService.java和RegisteredServicesCache.java的generateServicesMap()方法裡會解析所有的功能清單裡配置的有關帳戶同步的 service。如果沒有解析到會將當前加入的帳戶資訊刪除。


帳戶資訊儲存在:data/system/account.db
                                data/system/sync
                                data/system/registered-services下面

涉到的service包括:

<service
            android:name=".myservices.ContactsSyncAdapterService"
            android:exported="true">
            <intent-filter>
                <action android:name="android.content.SyncAdapter" />
            </intent-filter>
            <meta-data android:name="android.content.SyncAdapter"
                       android:resource="@xml/syncadapter_contacts" />
          </service>
        
          <service
            android:name=".myservices.CalendarSyncAdapterService"
            android:exported="true">
            <intent-filter>
                <action android:name="android.content.SyncAdapter" />
            </intent-filter>
            <meta-data android:name="android.content.SyncAdapter"
                       android:resource="@xml/syncadapter_calendar" />
          </service>   
        
          <service
            android:name=".myservices.SyncManager"
            android:enabled="true"
            >
          </service>   
        
          <service
            android:name=".service.EasAuthenticatorService"
            android:exported="true"
            android:enabled="true"
            >
            <intent-filter>
                <action android:name="android.accounts.AccountAuthenticator" />
            </intent-filter>
            <meta-data
                android:name="android.accounts.AccountAuthenticator"
                android:resource="@xml/authenticator"
                />
          </service>
                  
          <service
            android:name=".myservices.EasAuthenticatorServiceAlternate"
            android:exported="true"
            >
            <intent-filter>
                <action android:name="android.accounts.AccountAuthenticator" />
            </intent-filter>
            <meta-data
                android:name="android.accounts.AccountAuthenticator"
                android:resource="@xml/authenticator_alternate"
                />
          </service>