android賬號管理與同步機制

在應用中,很多app都有登陸註冊功能,這樣可以更好的管理個人信息,很多時候人們會使用sharepreference保存賬戶信息,把他經過加密寫入文件中,這樣既方便有簡單。但是這樣真的好嗎?當服務器數據更新時,當一個應用具有多個賬號時候,管理起來很不方便,並且安全性也不可靠。在Android2.0中加入了一個新的包android.accounts,該包功能十分強大,主要包括了集中式的賬戶管理API,用以安全地存儲和訪問認證的令牌和密碼,可以在同一個設備中管理同一應用的多個不同賬號,能夠自動批量的同步服務器更新賬戶,甚至可以和不同服務器進行數據同步和安全認證。現在進入主題,這篇文章將從賬戶的管理和賬戶的更新兩大方面進行展開介紹。涉及到如下核心類:

    AccountManager
    AbstractAccountAuthenticator
    AuthenticatorService
    AccountAuthenticatorActivity
    AbstractThreadedSyncAdapter
    SyncService

賬戶管理
一.AccountManager

AccountManager是一個賬戶管理類,來實現賬戶的管理,常用的方法如下:

            addAccount() :添加一個帳戶。
            addAccountExplicitly(): 直接添加一個帳戶到AccountManager。
            getAccounts():獲取所有帳戶。
            getAccountsByType(String package):獲取指定的賬號
            removeAccount():刪除帳戶

二.AbstractAccountAuthenticator

AbstractAccountAuthenticator的實現類用戶賬戶添加,登陸接口認證操作,他是一個抽象類,需要寫出他的實現類。在實現類中需要複寫他的抽象方法,一共7個。如下:
    1.editProperties:返回一個Bundle,其中包含可用於編輯屬性的活動的Intent,如果無返回,可以直接返回null,或者拋出常,例如new UnsupportedOperationException()。
    2.addAccount:添加指定的accountType的帳戶。(重要)
    當用戶在添加賬戶頁面選擇賬戶進行添加或者調用accountManager.addAccount 的時候,AbstractAccountAuthenticator中的addAccount 方法會被默認調用,因此需要重寫addAccount 方法。
    3.confirmCredentials:檢查用戶是否知道帳戶的憑據。連接服務器進行身份校驗。如果無校驗可以返回返回null,或者拋出常。
    4.getAuthToken:獲得一個賬戶authtoken。(重要)
    當執行AccountAuthenticatorActivity中的mAccountManager.blockingGetAuthToken(account,Constants.AUTHTOKEN_TYPE, NOTIFY_AUTH_FAILURE);時調用該方法。如果之前成功獲取過AuthenToken會緩存,之後不會在調用getAuthenToken()方法,除非調用invalidateAuthenToken()。
    5.getAuthTokenLabel:向認證者詢問給定的authTokenType的本地化標籤。如果無,可以直接返回null,或者拋出異常。
    6.updateCredentials:更新帳戶的本地存儲的憑據。如果無更新,可以直接返回null,或者拋出異常。
    7.hasFeatures:檢查帳戶是否支持所有指定的驗證器特定功能。如果不需要可以直接返回null,或者拋出異常。
    AccountManager和Authenticator的方法相對應,比如,AccountManager的addAccount()方法會調用Authenticator的addAccount()方法,Authenticator的方法會返回一個Bundle給AccountManager處理。
    接下來介紹兩個比較重要的方法:addAccount,getAuthToken

        @Override
        public Bundle addAccount(AccountAuthenticatorResponse accountAuthenticatorResponse, String s, String s1,
                                 String[] strings, Bundle bundle) throws NetworkErrorException {
            /**
             *  這裏是跳轉到新的頁面讓用戶添加賬戶,還可以直接添加賬戶,方法如下
             *  Account account = new Account(String name, String type);
             *  accountManager.addAccountExplicitly(account,password,userdata);
             */
            Intent intent = new Intent(ctx, RegisterActivty.class);
            intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, accountAuthenticatorResponse);
            Bundle b = new Bundle();
            bundle.putParcelable(AccountManager.KEY_INTENT, intent);
            return b;
        }

        @Override
        public Bundle getAuthToken(AccountAuthenticatorResponse accountAuthenticatorResponse, Account account, String s, Bundle options) throws NetworkErrorException {
            //可以請求服務器獲取token,這裏爲了簡單直接返回
            Bundle bundle;
            if (!s.equals(ConstantsGlobal.AUTH_TOKEN_TYPE)) {
                // 通過blockingGetAuthToken方法傳來的Constants.AUTHTOKEN_TYPE進行比較
                bundle = new Bundle();
                bundle.putInt(AccountManager.KEY_ERROR_CODE, 1);
                bundle.putString(AccountManager.KEY_ERROR_MESSAGE, "invalid authToken");
                return bundle;
            }
     
            AccountManager am = AccountManager.get(ctx);
            String psw = am.getPassword(account);
            if (!TextUtils.isEmpty(psw)) {
                //根據服務器接口根據賬戶密碼獲取authToken
                // String authToken = NetworkUtilities.authenticate(account.name, psw);
     
                //這裏爲了測試使用隨機數
                Random random = new Random();
                bundle = new Bundle();
                String authToken = random.nextLong() + "";
     
                //如果已經到服務器驗證過賬號並保存到AccountManager中,並且返回
                if (!TextUtils.isEmpty(authToken)) {
                    Bundle result = new Bundle();
                    //result.putString(AccountManager.KEY_AUTHTOKEN, authToken);
                    bundle.putString(AccountManager.KEY_AUTHTOKEN, authToken);
                    //不返回name和type會報錯“the type and name should not be empty”
                    bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
                    bundle.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
                    return bundle;
                }
            }
     
                //如果沒有到服務器驗證過賬號並保存到AccountManager中,則重新倒添加賬號頁面中驗證。
                bundle = new Bundle();
                Intent intent = new Intent(ctx, AuthenticatorActivity.class);
                bundle.putParcelable(AccountManager.KEY_INTENT, intent);
                intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, account.type);
                intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, account.name);
                intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, accountAuthenticatorResponse);
                return bundle;
        }

上面兩個方法每一步的標註非常清楚了,這裏不在詳細敘述。
三.AuthenticatorService

    1.爲Authenticator創建Service

Authenticator說完了,那麼他在什麼地方使用呢?接下來介紹AuthenticatorService,Authenticator就在AuthenticatorService裏面使用,AuthenticatorService他是一個服務繼承自Server。組需要在onBind方法中返回Authenticator的IBinder對象。

        @Override
        public IBinder onBind(Intent intent) {
            return new Authenticator(getApplicationContext()).getIBinder();
        }

    2.清單文件配置:

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

其中<action android:name="android.accounts.AccountAuthenticator" />必須配置。

    3.添加Metadata組件,在res/xml/目錄下聲明組件

        <account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
            android:accountType="com.account.accountmanagerdemo"
            android:icon="@mipmap/ic_launcher"
            android:smallIcon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            />

注意:ccountType很重要,用來唯一標識Authenticator,AccountManager的方法中有accountType的參數需要和此
處保持一致。一般爲包名稱,否則創建不成功。
四.AccountAuthenticatorActivity

在前期準備寫好之後,寫下來需要在activity裏面寫入登陸,註冊等相關功能了,這時候需要用到了AccountAuthenticatorActivity他是實現一個用於幫助實現一個AbstractAccountAuthenticator的Activity的基類。如果AbstractAccountAuthenticator需要用一個Activity去處理請求,就可以使用一個擴展的AccountAuthenticatorActivity來實現賬戶管理的Activity,這裏創建AuthenticatorActivity類讓他繼承自AccountAuthenticatorActivity,作爲操作賬戶的界面,可以進行賬戶的註冊,登陸查詢,更新等。

1.註冊,核心代碼如下:

        //在這裏調用接口訪問服務器進行註冊。這裏爲了方便,不進行網絡操作
        String mAccount = account.getText().toString().trim();
        String mPwd = pwd.getText().toString().trim();
        if (TextUtils.isEmpty(mAccount) || TextUtils.isEmpty(mPwd)) {
            return;
        }
        //如果註冊成功,則執行以下代碼,否則重新註冊,這裏默認註冊成功
        Account account = new Account(mAccount, ConstantsGlobal.ACCOUNT_TYPE);
        AccountManager am = AccountManager.get(RegisterActivty.this);
        am.addAccountExplicitly(account, mPwd, null);

    2.登陸,核心代碼如下:

        //請求接口,從服務器獲取數據token;mAccount爲登陸的賬戶名稱
        Account account = new Account(mAccount, ConstantsGlobal.ACCOUNT_TYPE);
        AccountManagerCallback<Bundle> callback = new AccountManagerCallback<Bundle>() {
            @Override
            public void run(AccountManagerFuture<Bundle> future) {
                try {
                    //獲取token
                    String token = future.getResult().getString(AccountManager.KEY_AUTHTOKEN);
                    Intent intent = new Intent(AuthenticatorActivity.this, MainActivity.class);
                    Account account1 = new Account(future.getResult().getString(AccountManager.KEY_ACCOUNT_NAME),
                            future.getResult().getString(AccountManager.KEY_ACCOUNT_TYPE));
                    intent.putExtra(ConstantsGlobal.KEY_ACCOUNT, account1);
                    startActivity(intent);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };
        accountManager.getAuthToken(account, ConstantsGlobal.AUTH_TOKEN_TYPE, null, AuthenticatorActivity.this, callback, null);

    3.退出登陸

        AccountManagerCallback<Bundle> callback = new AccountManagerCallback<Bundle>() {
            @Override
            public void run(AccountManagerFuture<Bundle> future) {
                try {
                    String token = future.getResult().getString(AccountManager.KEY_AUTHTOKEN);
                    accountManager.invalidateAuthToken(ConstantsGlobal.ACCOUNT_TYPE, token);
                    finish();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };
        //account爲要退出登錄的登陸名稱;this爲當前頁面的Activity
        accountManager.getAuthToken(account, ConstantsGlobal.AUTH_TOKEN_TYPE, null, this, callback, null);

    到此爲止,賬號登陸的相關功能介紹結束,接下來看看賬號的更新功能。
賬戶更新
一.SyncService

賬號更新功能需要一個服務來提供,讓SyncService繼承自Service來實現賬號更新功能,和上面所說的AuthenticatorService一樣需要在onBind方法中返回一個IBinder,這裏需要藉助於SyncAdapter,關於SyncAdapter稍後在做描述,通過syncAdapter.getSyncAdapterBinder()方法就可以獲取到IBinder對象。

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

     需要注意的是實例化syncAdapter的過程要保證線程安全,以免同步框架會將多次同步響應添加到隊列中。清單文件中配置如下:

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

    爲了便於數據的傳輸和更新,在這裏使用provider。一個適配器只能同步一個Authority,若想使一個賬戶同步多個Authority,可以向系統註冊多個綁定同一賬戶的sync-adapter,syncadapter配置如下:

        <?xml version="1.0" encoding="utf-8"?>
        <sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
            android:accountType="com.account.accountmanagerdemo"
            android:allowParallelSyncs="false"
            android:contentAuthority="com.android.contacts"
            android:isAlwaysSyncable="true"
            android:supportsUploading="false"
            android:userVisible="false" >
        </sync-adapter>

    1.accountType賬號類型,需要與前面在代碼段中的ACCOUNT_TYPE 常量還有authenticator的元數據文件中定義的保持一致。
    2.allowParallelSyncs是否支持上傳到雲端,否則僅支持下載。
    3.userVisible是否支持在設置中可見。
    4.設置是否允許SyncAdapter多實例同時運行。
    5.指定同步框架是否可以在任意時間運行你的SyncAdapter。

二.AccountContentProvider

1.賬戶更新數據的存儲獲取可以與ContentProvider框架協作,來存儲更新數據,不僅是方便使用SyncAdapter,而且它也具有更好的安全性。這裏可以使用一個虛擬的ContentProvider,可以全部返回null或者0。

        public class AccountContentProvider extends ContentProvider {
            @Override
            public boolean onCreate() {
                return false;
            }
     
            @Nullable
            @Override
            public Cursor query(@NonNull Uri uri, @Nullable String[] strings, @Nullable String s, @Nullable String[] strings1, @Nullable String s1) {
                return null;
            }
     
            @Nullable
            @Override
            public String getType(@NonNull Uri uri) {
                return null;
            }
     
            @Nullable
            @Override
            public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
                return null;
            }
     
            @Override
            public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) {
                return 0;
            }
     
            @Override
            public int update(@NonNull Uri uri, @Nullable ContentValues contentValues, @Nullable String s, @Nullable String[] strings) {
                return 0;
            }
        }

    2.清單文件配置

        <provide
            android:name=".accountrefresh.AccountContentProvider"
            android:authorities="com.account.accountmanagerdemo.accountrefresh.provider"
            android:exported="false"
            android:syncable="true">

      (1)authorities用來唯一指定一個ContentProvider的URI authority。這個值最好設置爲“包名 + .provider”。
      (2)指定實現ContentProvider的完整類名。
      (3)設置外部應用是否可以訪問。因爲我們的Provider並不需要別的應用訪問,所以設置爲”false”。這個值並不影響同步框架的訪問。android:exported="false"。
      (4)設置是否可同步。如果設置爲true 就不需要在代碼中再調用setIsSyncable() 。這個值決定了同步框架可以與Provider傳輸數據,但是也僅在你明確調用的時候才傳輸。android:syncable="true"。
三.AbstractThreadedSyncAdapter

AbstractThreadedSyncAdapter是一個抽象類,用於賬戶的同步操作,它是對 Account的內容進行同步操作的適配器。當他收到同步請求時,產生一個線程來進行Account指定內容的同步處理。同步框架用的ContentPrivder框架協作,同時需要SyncAdapter做支持,否則崩潰這裏,定義一個類SyncAdapter,讓他繼承自AbstractThreadedSyncAdapter。
    1.原理
    SyncAdapter不會自動做數據傳輸,它只是封裝你的代碼,以便框架可以在後臺調用,而不需要你的應用介入。同步框架準備要同步應用數據的時候,它會調用SyncAdapter中實現的onPerformSync()方法。應該通過定期任務或是根據一些事件的結果來運行SyncAdapter。比如,隔一段時間或在每天某個特殊的時間運行,或是在本地數據變化後運行。
    2.調用時機
     (1)服務端數據變化時
        服務端數據變化時,根據服務端發送的消息運行。這樣可以避免輪詢服務器影響性能和功耗。
     (2)本地數據變化時
        本地數據變化後同步可以將本地變化的數據發送到服務端,適合用來確保服務端數據最新。如果數據真的是用ContentProvider保存的,那這方式是很容易實現的(譯者注:在ContentProvider中使用ContentResolver的 notifyChange(android.net.Uri, android.database.ContentObserver, boolean)方法);如果是僞造的ContentProvider,那可能要麻煩一些。
     (3)系統發送網絡消息時
        當系統發出保持TCP/IP連接開啓的網絡消息時發起,這個網絡消息是網絡框架的一部分。這是自動同步的一種方式,可以考慮和基於時間間隔同步結合起來使用。
     (4)固定時間間隔
        自定義一個固定的時間間隔,或者是每天的某個時間點發起。
     (5)即時發起
        由用戶手動操作發起。但是,爲了有更好的體驗,最好還是以自動同步爲主,這樣可以降低電池和網絡資源的消耗。
      3.更新
      更新調用ContentResolver的requestSync(Account account, String authority, Bundle extras)方法進行數據更新。強烈建議使用自動刷新賬號以及SyncAdapter中其他刷新方式,爲了測試期間,這裏舉例手動刷新,在AuthenticatorActivity界面調用手動調用刷新:

           private void refreshAccount() {
               Bundle bundle = new Bundle();
               bundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
               bundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
     
               ContentResolver.requestSync(CreateSyncAccount(), ConstantsGlobal.AUTHORITY, bundle);
           }

            private static final String oldAccountName = "中央電視臺";//此數據一般是賬號變更或者服務器返回新賬號
            public Account CreateSyncAccount() {
                // 創建賬戶類型和默認賬戶名稱
                Account newAccount = new Account(oldAccountName, ConstantsGlobal.ACCOUNT_TYPE);
                /*
                 * Add the account and account type, no password or user data
                 * If successful, return the Account object, otherwise report an error.
                 */
                if (accountManager.addAccountExplicitly(newAccount, null, null)) {
                    /*
                     * If you don't set android:syncable="true" in
                     * in your <provider> element in the manifest,
                     * then call context.setIsSyncable(account, AUTHORITY, 1)
                     * here.
                     */
                   Log.e("AuthenticatorActivity", "賬戶更新成功");
                    Toast.makeText(this,"更新成功,請重新點擊查看賬戶列表",Toast.LENGTH_SHORT).show();
                } else {
                    /*
                     * The account exists or some other error occurred. Log this, report it,
                     * or handle it internally.
                     */
                    Log.e("AuthenticatorActivity", "賬戶更新失敗,或者此賬戶已經存在");
                    Toast.makeText(this,"更新成功,數據沒有變化,請查看列表",Toast.LENGTH_SHORT).show();
                }
                return newAccount;
            }

    4.onPerformSync方法

        @Override
        public void onPerformSync(Account account, Bundle bundle, String s, ContentProviderClient contentProviderClient, SyncResult syncResult) {
     
        }

    4.1 參數含義:
    (1)與本次觸發事件關聯的Account對象,如果你的服務器不需要賬號,直接無視就可以。
    (2)包含一些標誌位的Bundle對象。
    (3)系統中ContentProvider的authority,一般是你自己應用中的ContentProvider對應的authority。
    (4)authority對應的ContentProviderClient,它是ContentProvider的一個輕量接口,具有與ContentResolver相同的功能。如果是用ContentProvider保存的數據,你可以用這個對象連接到ContentProvider,否則無視就好。
    (5)SyncResult對象,可以用來將同步的結果傳給同步框架。
    4.2 操作步驟
     (1)連接服務器
     雖然同步開始時你可以認爲網絡是通暢的,但是同步框架並不會自動幫你連接服務器
     (2)下載上傳數據
     SyncAdapter不會自動做數據傳輸。如果你要從服務端取數據存到本地,那你必須提供請求、下載、插入數據的代碼。同樣,如果需要上傳數據,也要讀數據、發送數據請求。除此之外,還需要處理數據傳輸中發生的網絡錯誤。
     (3)處理數據衝突
     SyncAdapter不會自動處理服務端和本地的數據衝突。而且,也不會檢測本地和服務端的數據哪一個更新。你必須自己提供算法處理這種場景。
     (4)清理
     在傳輸結束後關閉與服務器的鏈接,清理臨時文件和緩存。
     注意: 同步框架自動將onPerformSync()放在後臺線程,因此不需要自己設置後臺運。

    需要添加權限如下:

        <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
        <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
        <uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/>
        <uses-permission android:name="android.permission.USE_CREDENTIALS"/>
        <uses-permission android:name="android.permission.INTERNET"></uses-permission>
        <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>

    最後附上源碼地址:
    http://download.csdn.net/download/yoonerloop/10130204

 

 
————————————————
版權聲明:本文爲CSDN博主「一杯清泉」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/yoonerloop/article/details/78211022

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章