AccountManager简介与使用

  关于账号存储的处理,每个不同的APP都有着不同的处理方式。最为常见的就是使用数据库作为账号管理。使用本地SQLite存储账号的用户名,头像,Token等关于账号的一系列信息。但是,因为数据存储在本地,或许有心术不正的人获取到本地存储的数据库信息(当然了,本地存储的数据库是很容易拿到的),就能够拿到账号的所有信息。信息就这样泄漏了。

  另外一种比较安全的方法就是使用Android系统提供的AccountManager存储账号相关信息。So,google一下发现,国内似乎好像很少有关于这个类的介绍,只查到了一篇翻译国外的一篇文章。所有我想把AccountManager的介绍以及如何使用AccountManager,还有使用过程中需要注意的地方。


附:(翻译) Android Accounts Api使用指南

下边就先介绍一下相关的类

1.Accout介绍

Accont其实就是我们需要添加的账号,里边只有两个字段。

public final String name;
public final String type;

  其中name表示该账户的用户名,当然了,有很多应用它的用户名是隐藏的,你也可以把它作为昵称来看待。type则代表账户的类型,这个是type必须是独一无二的,当然了,如果说你的两个应用使用的是同一个账户管理体系,这个是可以相同的。

public Account(String name, String type) {
if (TextUtils.isEmpty(name)) {
throw new IllegalArgumentException("the name must not be empty: " + name);
}
if (TextUtils.isEmpty(type)) {
throw new IllegalArgumentException("the type must not be empty: " + type);
}
this.name = name;
this.type = type;
}

  通过传递name和type完成对Account的创建。这个时候可能会疑问,不是说好的存储呢token呢,还有头像的信息,还有其他一些数据呢?其实,本身Account的本身是不存储这样的东西的,如果想存储关于账号的其他数据,需要通过AccountManager.addAccountExplicitly(account, password, userData)的方式来添加用户的附加信息的。其中userData是一个Bundle类型的数据,通过bundle.put(key,object)的方式添加用户所需要的附加数据。关于AccountManager的介绍及常用方法,接下来会详细讲解。

2.AccountManager介绍

关于AccountManager类,Android官方的开发文档是这么介绍的

This class provides access to a centralized registry of the user’s online accounts. The user enters credentials (username and password) once per account, granting applications access to online resources with “one-click” approval.

简单的来翻译一下吧

  这个类提供了访问用户的网上帐户的统一的管理。每个用户只需要要输入一次凭据(用户名和密码),一键式的授予应用程序访问网络资源。(英语不好,如有错误,请指正)。

通俗的来说,可以使用这个类来完成对账号的增、删、改、查。

3.添加一个账户信息

  刚才上文中也已经提到了,就是使用AccountManager.addAccountExplicitly(account, password, userData)的方式进行添加的。看一下这个方法的源码。

private final IAccountManager mService;
//省略中间部分代码
public boolean addAccountExplicitly(Account account, String password, Bundle userdata) {
if (account == null) throw new IllegalArgumentException("account is null");
try {
return mService.addAccountExplicitly(account, password, userdata);
} catch (RemoteException e) {
// Can happen if there was a SecurityException was thrown.
throw new RuntimeException(e);
}
}

  其中IAccountManager是一个aidl接口类,不难猜测出其中的具体添加账户的服务由Android系统底层服务添加,很抱歉,我在翻阅了大量的资料后,并没有找到相关的源码。不过,通过这样一个方法就可以添加一个账户进入系统的账户。

4.删除一个账户

  删除账户有对应的方法在API21前后略有不同,在API21以前采用的是removeAccount(Account account, AccountManagerCallback<Boolean> callback, Handler handler),而在API21之后则改成了removeAccount(Account account, Activity activity, AccountManagerCallback<Bundle> callback, Handler handler),另外还有一个方法是removeAccountExplicitly(Account account)
  其中前两个是异步方式进行的,最后一个方法看官网的方法描述Removes an account directly.(直接删除账户)(目前我使用的是API22以前异步的方法,其他的两个方法暂时未进行测试,欢迎各路大神测试吐槽)。

关于三个参数介绍:

  • Account:表示带移除的账户,看源码之后发现,当account传入null的时候,会抛出一个异常,所以要确保出传入的account不能为null
  • AccountManagerCallback<Boolean>:表示移除完成后的回调,通过future.getResult()获取回调的返回结果
  • Handler:对应回调的线程,当然了,如果你传入为null的话,他的默认回调线程则是为主线程
    对于这个方法,我是这样使用的。
    public static void removeCurrentAccount() {
    Account account = getCurrentAccount();
    if (account == null)
    return;
    manager.removeAccount(account, new AccountManagerCallback<Boolean>() {
    @Override
    public void run(AccountManagerFuture<Boolean> future) {
    try {
    Logger.i("移除账号" + future.getResult());
    //send EventBus or BroaderCast or RxBus
    RxBus.getDefault().send(new AccountSignStatus(false));
    } catch (OperationCanceledException e) {
    e.printStackTrace();
    } catch (AuthenticatorException e) {
    e.printStackTrace();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }, null);

  以上两个是比较基础的添加和删除账号信息,剩下的获取账号信息的部分还有修改账号用户名(昵称)我将会用超长的篇幅类介绍和使用关于这些方法的相信使用,因为这些方法是相对来说调用比较频繁的,特别是获取账户信息的方法,同时,在使用这些方法时,我遇到的坑还是相当多的。而同时,在获取账号token也是整个账户管理的核心,同时也是最复杂的。

5.获取用户token信息及用户数据

先简单介绍一下获取token的流程,下面是关于获取账户token的流程示意图

这看起来似乎是一团乱麻,但是其实相当的简单。我会解释一下当我们第一次在设备上用一个账号登入时出现的情况。

第一次登陆

  1. app向AccountManager请求一个AuthToken。
  2. AccountManager询问相关的AccountAuthenticiator是否有缓存的AuthToken。
  3. 因为没有缓存的AuthToken(还没有登入),AccountManager为我们开启一个AccountAuthenticatorActivity,引导用户登入。
  4. 用户成功登陆,AuthToken从服务端返回。
  5. Accountmanager将AuthToken缓存起来以便于将来使用。
  6. app获得她请求的AuthToken.
  7. 众人皆喜!

如果用户已经登入,那么我们就会在第二步时获得AuthToken。(摘抄(翻译) Android Accounts Api使用指南原文)
看起来是很复杂的事情,不过我会一步一步来解释并实现这样的一个过程。

####第一步 创建属于自己的账号服务
首先我需要编写出一个Authenticator认证器的类,让这个类继承AbstractAccountAuthenticator,并实现其中的方法。具体的实现会在接下来进行介绍。
然后创建一个服务类,并在mainifist如下代码所示

/**
* Created by lisao on 2016/3/19.
*/
public class AccountService extends Service {
private Authenticator authenticator;
@Override
public void onCreate() {
super.onCreate();
authenticator = new Authenticator(this);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return authenticator.getIBinder();
}
}

第三步,在res文件夹下创建xml文件夹,并添加authenticator.xml文件

<?xml version="1.0" encoding="utf-8"?>
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="定义的唯一账户类型,建议使用公司域名,如果你贵司有多个产品并共享一套数据库的话"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:smallIcon="@mipmap/ic_launcher" />

最后配置mainifest清单文件中添加

<!--用户认证 service-->
<service
android:name=".account.AccountService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>

经过以上的几个步骤,运行APP就可以在Android的设置界面点击添加账户看到定义的账号名称了。类似于这样。(补充一点,某系机型可能看不到,例如说某米,某族等)

OK,上文也提到了,会对实现的Authenticator进行详解。其实我们主要关注的就是其中的两个方法,一个是addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options),另外一个就是getAuthToken(final AccountAuthenticatorResponse response, final Account account, String authTokenType, Bundle options)第一个是在设置界面点击添加账户是跳转到登录页的方法,另外一个就是使用AccountManager.getAuthToken调用的方法。
对于addAccount方法我的实现是这样的

@Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException {
final Intent intent = new Intent(mContext, SignActivity.class);
intent.putExtra(SignActivity.ARG_ACCOUNT_TYPE, accountType);
intent.putExtra(SignActivity.ARG_AUTH_TYPE, authTokenType);
intent.putExtra(SignActivity.ARG_IS_ADDING_NEW_ACCOUNT, true);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
final Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;
}

SignAcitiviy需要继承AccountAuthenticatorActivity,当然了如果你项目中与BaseActivity的时候,你讲AccountAuthenticatorActivity中的相关变量和方法复制到SignAcitiviy也是可以的。 具体的SignAcitivity实现请参考本文头部提到的原文
对于getAuthToken我的实现是这样的

@Override
public Bundle getAuthToken(final AccountAuthenticatorResponse response, final Account account, String authTokenType, Bundle options) throws NetworkErrorException {
String authToken = AccountUtil.peekAuthToken(account, authTokenType);
if (TextUtils.isEmpty(authToken)) {
final String password = AccountUtil.getPassword(account);
if (password != null) {
//使用账户密码进行登录
}
}
if (!TextUtils.isEmpty(authToken)) {
final 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, authToken);
return result;
}
final Intent intent = new Intent(mContext, SignActivity.class);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
final Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;
}

Ok,经过以上的代码编写,你可以使用

AccountManager.getAuthToken(Account account, String authTokenType, Bundle options, Activity activity, AccountManagerCallback<Bundle> callback, Handler handler)


AccountManager.getAuthTokenByFeatures(String accountType, String authTokenType, String[] features, Activity activity, Bundle addAccountOptions, Bundle getAuthTokenOptions, AccountManagerCallback<Bundle> callback, Handler handler)

两个方法了(妈蛋!需要传递这么多的参数,到底鬼什么意思?)
先来介绍一下getAuthToken这个方法.
根据官方文档的解释

  Gets an auth token of the specified type for a particular account, prompting the user for credentials if necessary. This method is intended for applications running in the foreground where it makes sense to ask the user directly for a password.

  If a previously generated auth token is cached for this account and type, then it is returned. Otherwise, if a saved password is available, it is sent to the server to generate a new auth token. Otherwise, the user is prompted to enter a password.

官方文档的解释也印证了上边提到的当获取AuthToken的流程(如果忘记了,请返回上文仔细观看)。实际上这也是在AuthenticatorgetAuthToken的流程逻辑。

最后解释一下传递参数的详细解释

参数名称 参数释义
account 需指定的账户
authTokenType 账号token的类别,不是账号的类别并且不可以为null
options 身份认证专用的请求选项可以为null
activity 不解释
callback 调用完成时的回调,如果为null则不回掉
handle 回调的线程,如果传null则回调为主线程

接下来介绍一下getAuthTokenByFeatures这个方法。还是先看一下官方文档

This method gets a list of the accounts matching the specified type and feature set; if there is exactly one, it is used; if there are more than one, the user is prompted to pick one; if there are none, the user is prompted to add one. Finally, an auth token is acquired for the chosen account.

翻译一下就是,这个方法会根据传递的账户类型匹配是否有该类型的账户,如果恰好有一个,则返回改账户的token,如果有多个,则弹出一个对话框让你选择一个,如果没有,会让你新建一个指定类型的账户,最后返回这个账户的token。

其实toke的获取流程还是和getAuthToken的流程相同。唯一的一点区别在于getAuthToken需要传递的是一个已知的账户,而getAuthTokenByFeatures传递的是一个账户类型,由系统帮我们选定账户。
最后还是列举一下传递参数的意思

参数名称 参数释义
accountType 账户类型
authTokenType 账户token类型
features 该账户需要授权的功能,可以为null
activity 不解释
addAccountOptions 身份认证专用的请求选项可以为null 用于添加账户时使用
getAuthTokenOptions 身份认证专用的请求选项可以为null 用于获取账户时使用
callback 调用完成时的回调,如果为null则不回掉
handle 回调的线程,如果传null则回调为主线程

上述就是获取账户信息Token的常用方法。下面就来介绍一下获取账户数据的方法,就是像获取用户头像信息等。

获取用户数据比较上来说就简单多了。直接调用manager.getUserData(Account account, String key)就可以了。这个方法是同步获取的,相比上述获取token的方式简单太多了。参数释义太简单了不再一一赘述。

6.更改用户信息

先解释最简单吧,修改用户数据,也就是修改用户的头像信息这些东西了。调用manager.setUserData(Account account, String key,String value);就可以了。具体参数释义不解释了。
其实最坑的一点就是修改账号的昵称(用户名)。闲话不多说,先上代码。

public interface AccountCallBack {
void done();
void error(String msg);
}
public static void renameAccount(final String newName, final AccountCallBack callback) {
if (TextUtils.isEmpty(getCurrentAccountToken())) return;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
manager.renameAccount(getCurrentAccount(), newName, new AccountManagerCallback<Account>() {
@Override
public void run(AccountManagerFuture<Account> future) {
try {
if (future.getResult().name.equals(newName)) {
callback.done();
} else {
callback.error("昵称修改失败,请稍后重试");
}
} catch (OperationCanceledException e) {
e.printStackTrace();
callback.error("昵称修改失败,请稍后重试");
} catch (IOException e) {
e.printStackTrace();
callback.error("昵称修改失败,请稍后重试");
} catch (AuthenticatorException e) {
e.printStackTrace();
callback.error("昵称修改失败,请稍后重试");
}
}
}, null);
} else {
String avatar = manager.getUserData(getCurrentAccount(), ACCOUNT_AVATAR);
String joinTime = manager.getUserData(getCurrentAccount(), ACCOUNT_JOIN_TIME);
String mobile = manager.getUserData(getCurrentAccount(), ACCOUNT_MOBILE);
String userId = manager.getUserData(getCurrentAccount(), ACCOUNT_USER_ID);
final String password = manager.getPassword(getCurrentAccount());
final String token = getCurrentAccountToken();
final Bundle bundle = new Bundle();
bundle.putString(ACCOUNT_AVATAR, avatar);
bundle.putString(ACCOUNT_JOIN_TIME, joinTime);
bundle.putString(ACCOUNT_MOBILE, mobile);
bundle.putString(ACCOUNT_USER_ID, userId);
manager.removeAccount(getCurrentAccount(), new AccountManagerCallback<Boolean>() {
@Override
public void run(AccountManagerFuture<Boolean> future) {
try {
if (future.getResult()) {
addAccountExplicitly(newName, password, bundle);
setAuthToken(getCurrentAccount(), AUTH_TOKEN_TYPE_FULL_ACCESS, token);
callback.done();
} else {
callback.error("昵称修改失败,请稍后重试");
}
} catch (OperationCanceledException e) {
e.printStackTrace();
callback.error("昵称修改失败,请稍后重试");
} catch (IOException e) {
e.printStackTrace();
callback.error("昵称修改失败,请稍后重试");
} catch (AuthenticatorException e) {
e.printStackTrace();
callback.error("昵称修改失败,请稍后重试");
}
}
}, null);
}
}

代码的逻辑解释一下,当在API21之后使用manager.renameAccount的方法直接重名昵称(用户名),在API21之前,先把用户数据存储下来,移除账户后添加一个新名字的账户。(目前我是这么做的,希望有更好的方法大神指点)
这么做会遇到一个问题,就是当新名字和旧的名字是相同的时候。出现一个奇怪的现象,重命名成功后,账户列表不显示账户,并且这个账号永久性的添加不成功,只有当卸载APP之后才可以。所有目前的做法是如果是相同昵称,直接返回重命名成功就好了。

7.总结需要注意的几点。

  1. 不同的账户体系,accounttype千万不要相同。
  2. 在AccountManager存储token的时候也是以明文进行存储的,保险起见,加一下密。
  3. 注意重命名昵称的时候不要和旧名字相同。
  4. 关于账号数据服务器返回数据的能存的尽量存下来,以备后续版本可能会用到。

关于这篇介绍AccountManager,因为本人水平有限,在介绍的时候难免会有错误,欢迎各路大神指正。

Created By Lisao
2016-8-22