紧接上期

账号系统建立 Account Authenticator

如果只需要借助系统更新服务(SyncAdapter)来做定期维护, 那么通过前两部分的介绍, 已经可以达到所预期的目标了.

本期话题将会解决

  • 添加账号
  • 获得授权

这些服务全部都是可以跨进程的操作, 完成了这些操作, 我们就可以完成像QQ 新浪微博 一样的功能, 账号系统可以为第三方应用授权.

实现AccountService 以及 Authenticator

先看看所提供的重写方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class AccountService extends Service {

private Authenticator authenticator;
public static final String ACCOUNT_TYPE = "org.weyoung.notebook";
public static final String TOKEN_TYPE = "org.weyoung.notebook.admin";

@Override
public void onCreate() {
authenticator = new Authenticator(this);
}
@Override
public IBinder onBind(Intent intent) {
return authenticator.getIBinder();
}


class Authenticator extends AbstractAccountAuthenticator {
private Context context;
private AccountManager accountManager;

public Authenticator(Context context) {
super(context);
this.context = context;
accountManager = AccountManager.get(context);

}

@Override
public Bundle editProperties(AccountAuthenticatorResponse accountAuthenticatorResponse,
String s) {
throw new UnsupportedOperationException();
}

@Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options)
throws NetworkErrorException {
//添加账号
}

@Override
public Bundle confirmCredentials(AccountAuthenticatorResponse accountAuthenticatorResponse,
Account account, Bundle bundle)
throws NetworkErrorException {
return null;
}

@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String s, Bundle bundle)
throws NetworkErrorException {
//获取认证
}

@Override
public String getAuthTokenLabel(String s) {
throw new UnsupportedOperationException();
}

@Override
public Bundle updateCredentials(AccountAuthenticatorResponse accountAuthenticatorResponse,
Account account, String s, Bundle bundle)
throws NetworkErrorException {
throw new UnsupportedOperationException();
}

@Override
public Bundle hasFeatures(AccountAuthenticatorResponse accountAuthenticatorResponse,
Account account, String[] strings)
throws NetworkErrorException {
throw new UnsupportedOperationException();
}
}
}

通过Authenticator所提供的方法可以看出, 最核心的就是实现addAccountgetAuthToken方法, 这样我们的AccountService就可以获得授权添加账号功能了

添加账号

先看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
addAccount() {
final Bundle bundle = new Bundle();
//限制绑定账号只能有一个
if(accountManager.getAccountsByType(ACCOUNT_TYPE).length > 0) {
final Intent intent = new Intent(context, FailActivity.class);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;
}
final Intent intent = new Intent(context, AuthenticatorActivity.class);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);

bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;
}

这里返回的addAccount返回的Bundle必须带有一个Key为AccountManager.KEY_INTENT, 并且这个Intent含有一个Key为AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, Value为参数AccountAuthenticatorResponse. 这个response主要用来回调成功与否, 内部还可以传递生成账号的信息(Name,Type,Token), 将会在启动的用于注册的AuthenticatorActivity里使用.

关于AuthenticatorActivity的实现, 其实很简单, 其继承AccountAuthenticatorActivity, 这样父类自己帮你在onCreate的时候获取到了之前传入的AccountAuthenticatorResponse, 然后在注册成功后调用setAccountAuthenticatorResult来传出一个Bundle, 内部其实调用了前面所指的response对象的onResult或者onError方法.

这个Bundle可以含有若干重要的字段, AccountManager.KEY_ACCOUNT_NAME AccountManager.KEY_ACCOUNT_TYPE AccountManager.KEY_AUTHTOKEN

下面看看AuthenticatorActivity的核心实现吧.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
submit() {
final Account account = new Account(name, AccountService.ACCOUNT_TYPE);

//添加账号到系统Account中心, 这里输入的password可以做转换之类的
......认证 生成token 的逻辑
accountManager.addAccountExplicitly(account, password, null);
//添加token
accountManager.setAuthToken(account, AccountService.TOKEN_TYPE, authToken);

//返回信息, 可以供getAuthToken使用
Bundle data = new Bundle();
data.putString(AccountManager.KEY_ACCOUNT_NAME, name);
data.putString(AccountManager.KEY_ACCOUNT_TYPE, AccountService.ACCOUNT_TYPE);
data.putString(AccountManager.KEY_AUTHTOKEN, authToken);
Intent intent = new Intent();
intent.putExtras(data);
//如果不设置, 相当于调用response的onCancel取消注册
setAccountAuthenticatorResult(data);
finish();

获得认证

先看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
getAuthToken() {
//获得一个已存在的token
String authToken = accountManager.peekAuthToken(account, TOKEN_TYPE);

//如果没有可能过期了 拿到密码 重新认证一次
if (TextUtils.isEmpty(authToken)) {
final String password = accountManager.getPassword(account);
if (password != null) {
try {
authToken = 生成token的逻辑;
} catch (Exception e) {
e.printStackTrace();
}
}
}

//如果有token了, 传递
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;
}

//如果没有token, 也没有密码, 需要重新登录, 账户名可一并传递进认证的Activity, 如果有的话
final Intent intent = new Intent(context, AuthenticatorActivity.class);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
intent.putExtra(AuthenticatorActivity.ARG_ACCOUNT_NAME, account.name);
final Bundle b = new Bundle();
b.putParcelable(AccountManager.KEY_INTENT, intent);
return b;
}

由上面的代码可以看见, 对于需要获取token(getAuthToken)的操作, 我们的步骤是从简到繁, 最差情况就是需要重新的登录认证来获得token

跟添加账号相同, 最终账号的信息(Name Type Token)都是通过神奇的Bundle来传递的

####声明账号服务

与同步服务一样, 账号服务同样需要声明, 以便通过系统的Accout功能操作, 或者为其他第三方程序提供支持.

1
2
3
4
5
6
7
8
9
10
11
<service
android:name=".sync.AccountService"
android:exported="true"
android:process=":auth" >
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>

这里跟上一节SyncService类似, 必须声明为exported, process不是必须的, 并且需要添加android约定的过滤器android.accounts.AccountAuthenticator

关于Authenticator的配置, 也类似的有一个独立的xml.

1
2
3
4
5
6
<account-authenticator 
xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="org.weyoung.notebook"
android:icon="@drawable/ic_launcher"
android:smallIcon="@drawable/ic_launcher"
android:label="@string/app_name"/>

这里声明的labelicon都会在系统设置Account里面看到, 如上一节最后面图片的上部分所示.


使用账号服务

构建好了自己的Service跟账号认证Adapter, 下面就剩如何使用了. 使用大致分为两种, 一种是直接申请Token, 一种是添加账号. 当然如果没申请到, 结果跟后面一种类似.

所有的操作都来自于AccountManager,申请Token主要是通过AccountManager#getAuthToken 系列方法. 这里使用了适合第三方应用使用的AccountManager#getAuthTokenByFeatures, 通过账号类型与Token类型来申请.

添加账号则通过AccountManager#addAccount, 添加指定类型的账号.

当然先通过AccountManager#getAccountsByType)查看是否存在账号, 决定是否是添加, 还是获取

当然这些操作都需要一定的权限声明, 如下所示

1
2
3
4
5
<!-- getAuthTokenByFeatures -->
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<!-- getAccountsByType -->
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />

本文通过三次的介绍, 应该可以为想构建自己的账号同步系统的同学指明了道路, 所有代码中间可能做过稍许节选, 添加了中文的注释, 并有一定的伪代码. 整个程序的代码可以通过这里来获得, 进一步了解整个功能的实现.


春节将至, 祝快乐


Reference: