简介
我们在手机系统设置里会有“帐户”一项功能,点击进去会发现有一些我们认识的APP在里头,如QQ、微信、邮箱等。没错,这就是Android系统提供的帐户同步功能。任何第三方APP都可以通过此功能将数据在一定时间内同步到服务器中去。比如微信会将当前系统中登录的微信帐户的信息、聊天记录同步到微信服务器上去,又比如Android原生系统中可以使用Google账号进行数据的同步。这里顺便提一下,系统在将APP帐户同步时,也会将深睡中的APP进程拉活,这点我们在文章后面也会说到。好了,下面我们将一步一步地来实现一个帐户同步的Demo。
创建帐户服务
第一步,定义一个action为android.accounts.AccountAuthenticator的Intent的Service,并在meta-data的resource属性指定一定说明该Account基本显示信息的xml文件,AndroidMainifest.xml添加代码如:
<service
android:name="project.test.com.myapplication.AuthenticationService"
android:enabled="true"
android:exported="true">
<intent-filter>
<actionandroid:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>
authenticator.xml:
<?xml version="1.0"encoding="utf-8"?>
<account-authenticatorxmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="project.test.com.myapplication.account.type"
android:icon="@mipmap/ic_launcher"
android:smallIcon="@mipmap/ic_launcher"
android:label="@string/app_name"
/>
注意:
android:accountType表示的是您的Account的类型,它必须是唯一的
第二步,创建帐户Service,并在Service的onBind中调AbstractAccountAuthenticator的getIBinder()返回其用于远程调用的IBinder:
public class AuthenticationService extends Service {
privateAuthenticationService.AccountAuthenticator mAuthenticator;
private AuthenticationService.AccountAuthenticator getAuthenticator() {
if(mAuthenticator == null)
mAuthenticator = new AuthenticationService.AccountAuthenticator(this);
return mAuthenticator;
}
@Override
public void onCreate() {
mAuthenticator = new AuthenticationService.AccountAuthenticator(this);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return getAuthenticator().getIBinder();
}
class AccountAuthenticator extends AbstractAccountAuthenticator {
public AccountAuthenticator(Context context) {
super(context);
}
@Override
public Bundle editProperties(AccountAuthenticatorResponse response, StringaccountType) {
return null;
}
@Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType,String authTokenType, String[] requiredFeatures, Bundle options) throwsNetworkErrorException {
return null;
}
@Override
public Bundle confirmCredentials(AccountAuthenticatorResponse response, Accountaccount, Bundle options) throws NetworkErrorException {
return null;
}
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,String authTokenType, Bundle options) throws NetworkErrorException {
return null;
}
@Override
public String getAuthTokenLabel(String authTokenType) {
return null;
}
@Override
public Bundle updateCredentials(AccountAuthenticatorResponse response, Accountaccount, String authTokenType, Bundle options) throws NetworkErrorException {
return null;
}
@Override
public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account,String[] features) throws NetworkErrorException {
return null;
}
}
}
说明:
我们在AuthenticationService类中定义了一个继承于AbstractAccountAuthenticator的内部类AccountAuthenticator,然后在AuthenticationService的onBind方法中返回了AccountAuthenticator类对象的getIBinder()。
通过这简单的两步,我们运行程序后再在“设置”-“帐户”-“添加帐户”中的列表中可以发现到我们的app了。
如图:
添加帐户
您会发现,像微信,如果当前手机微信没有登录过的话,在帐户列表中点击微信会自动添转到一个登录微信的Activity,但点击我们刚才app是跳转到一个空白没有任务操作,那是因为我们只是创建了帐户服务,还没有对其进行添加帐户的代码处理。
第一步,声明两个必要的权限:
<uses-permissionandroid:name="android.permission.GET_ACCOUNTS" /> // 查看帐户需要权限
<uses-permissionandroid:name="android.permission.AUTHENTICATE_ACCOUNTS" /> // 添加帐户需要权限
第二步,添加AuthenticatorActivity:
public class AuthenticatorActivity extendsAppCompatActivity {
public static final String ACCOUNT_TYPE ="project.test.com.myapplication.account.type"; // TYPE必须与account_preferences.xml中的TYPE保持一致
private AccountManager mAccountManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_authenticator);
mAccountManager = (AccountManager)getSystemService(ACCOUNT_SERVICE);
Account[] accounts =mAccountManager.getAccountsByType(ACCOUNT_TYPE); // 获取系统帐户列表中已添加的帐户是否存在我们的帐户,用TYPE做为标识
if(accounts.length > 0) {
Toast.makeText(this, "已添加当前登录的帐户",Toast.LENGTH_SHORT).show();
finish();
}
ButtonbtnAddAccount = (Button)findViewById(R.id.btn_add_account);
btnAddAccount.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Account account = new Account(getString(R.string.app_name),ACCOUNT_TYPE);
mAccountManager.addAccountExplicitly(account, null, null); // 帐户密码和信息这里用null演示
finish();
}
});
}
}
说明:
1、Activity中定义的成员变量ACCOUNT_TYPE用于我们当前APP获取系统帐户的唯一标识,这个在前面account_preferences.xml中也有提过,两处的声明必须是一致;
2、在onCreate中去获取系统帐户列表中已添加的帐户是否存在我们的帐户,用我们的TYPE做为标识,如果存在表示已经添加过,则退出当前页面;
3、如若从未添加过帐户,则模拟了一个登户操作,在操作成功后使用AccountManager的addAccountExplicitly方法往系统帐户中添加我们的帐户。
第三步,在AccountAuthenticator类中的addAccount方法里添加跳转登录的Activity:
@Override
public Bundle addAccount(AccountAuthenticatorResponseresponse, String accountType, String authTokenType, String[] requiredFeatures,Bundle options)
throwsNetworkErrorException {
final Bundle bundle = new Bundle();
final Intent intent = new Intent(mContext, AuthenticatorActivity.class);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE,response);
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;
}
我们再次运行程序,在“添加帐户”的点击和模拟登录后,便可在“设置”-“帐户”中看到我们的APP已添加了帐户了,如图:
同步帐户数据
当我们都完成了上面添加帐户的操作后,试着在帐户中点击会跳转到一个页面,如图:
这个页面只是列出了我们APP的图标和名称,并没有其它的操作和我们预期中的同步功能。所以接下来我们继续为APP加入同步的服务代码。
第一步,定义一个action为android.content.SyncAdapter的Intent的Service,并在meta-data的resource属性指定一定说明该同步基本显示信息的xml文件,AndroidMainifest.xml添加代码如:
<service
android:name=".SyncService"
android:exported="true">
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/sync_adapter" />
</service>
sync_adapter.xml:
<?xml version="1.0"encoding="utf-8"?>
<sync-adapterxmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="project.test.com.myapplication.account.type"
android:allowParallelSyncs="false"
android:contentAuthority="project.test.com.myapplication.account.provide"
android:isAlwaysSyncable="true"
android:supportsUploading="false"
android:userVisible="true"/>
说明:
android:accountType表示的是您的Account的类型,一定要跟前面保持一致
android:allowParallelSyncs 是否支持多账号同时同步
android:contentAuthority 指定要同步的ContentProvider
android:isAlwaysSyncable 设置所有账号的isSyncable为1
android:supportsUploading 设置是否必须notifyChange通知才能同步
android:userVisible 设置是否在“设置”中显示
第二步,创建同步Service,并在Service的onBind中调AbstractThreadedSyncAdapter的getIBinder()返回其用于远程调用的IBinder:
public class SyncService extends Service {
private static final Object sSyncAdapterLock = new Object();
private static SyncAdapter sSyncAdapter = null;
@Override
public voidonCreate() {
synchronized (sSyncAdapterLock) {
if(sSyncAdapter == null) {
sSyncAdapter = new SyncAdapter(getApplicationContext(), true);
}
}
}
@Nullable
@Override
public IBinderonBind(Intent intent) {
returnsSyncAdapter.getSyncAdapterBinder();
}
class SyncAdapter extends AbstractThreadedSyncAdapter {
public SyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
}
@Override
public void onPerformSync(Account account, Bundle extras, String authority,ContentProviderClient provider, SyncResult syncResult) {
//TODO 实现数据同步
// getContext().getContentResolver().notifyChange(AccountProvider.CONTENT_URI,null, false); }
}
}
说明:
1、我们在SyncService类中定义了一个继承于AbstractThreadedSyncAdapter的内部类SyncAdapter,然后在SyncService的onBind方法中返回了SyncAdapter类对象的getIBinder()。
2、SyncAdapter类中onPerformSync方法就是实现同步数据到服务器的代码,这里省略
第三步,上面我们在sync_adapter.xml中指定了一个ContentProvider,所以此刻当然是创建用于上传的ContentProvider:
<provider
android:name=".AccountProvider"
android:authorities="project.test.com.myapplication.account.provide"
android:exported="false"
android:syncable="true"/>
public class AccountProvider extends ContentProvider{
publicstatic final String AUTHORITY ="project.test.com.myapplication.account.provide";
publicstatic final String CONTENT_URI_BASE = "content://" + AUTHORITY;
publicstatic final String TABLE_NAME = "data";
publicstatic final Uri CONTENT_URI = Uri.parse(CONTENT_URI_BASE + "/" +TABLE_NAME);
@Override
publicboolean onCreate() {
returnfalse;
}
@Nullable
@Override
publicCursor query(Uri uri, String[] projection, String selection, String[]selectionArgs, String sortOrder) {
returnnull;
}
@Nullable
@Override
publicString getType(Uri uri) {
returnnull;
}
@Nullable
@Override
public Uriinsert(Uri uri, ContentValues values) {
returnnull;
}
@Override
public intdelete(Uri uri, String selection, String[] selectionArgs) {
return0;
}
@Override
public intupdate(Uri uri, ContentValues values, String selection, String[] selectionArgs){
return0;
}
}
说明:
这里我们只是用于演示同步数据,并没有对ContentProvider进行实际操作,大家在实际开发中可以自行去完善。
最后一步,执行同步。我们来修改AuthenticatorActivity的模拟登录代码,使在在登录成功后将其设置为自动同步:
……
Button btnAddAccount =(Button)findViewById(R.id.btn_add_account);
btnAddAccount.setOnClickListener(newView.OnClickListener() {
@Override
public voidonClick(View v) {
Accountaccount = new Account(getString(R.string.app_name), ACCOUNT_TYPE);
mAccountManager.addAccountExplicitly(account, null, null); // 帐户密码和信息这里用null演示
// 自动同步
Bundle bundle= new Bundle();
ContentResolver.setIsSyncable(account, AccountProvider.AUTHORITY, 1);
ContentResolver.setSyncAutomatically(account, AccountProvider.AUTHORITY,true);
ContentResolver.addPeriodicSync(account, AccountProvider.AUTHORITY,bundle, 30); // 间隔时间为30秒
// 手动同步
//ContentResolver.requestSync(account, AccountProvider.AUTHORITY, bundle);
finish();
}
});
说明:
账户信息同步其实有两种方式,一种是自动同步,就是在预设定的时间间隔,让Android系统帮我们完成自动同步数据。这里要注意的是,因为Android本身为了考虑同步所带来的消耗和减少唤醒设备的次数,所以这里预设的时间并一定准确,系统其实在内部做了一些同步的算法处理,例如尽量将所有需要同步数据都安排在某个时间点,所以我们预设的那个时间值仅仅是作为一个参考罢了。另一种方式是手动同步,手动同步则是在应用中调用某个方法直接告诉设备,通知系统同步数据。
当我们再次在已添加的帐户列表中点击我们的APP后会变成这样,如图:
更改同步页面
如果您希望在已添加的帐户列表中点击我们的APP后出现一个自定义的页面,那么就得
修改authenticator.xml文件,加入accountPreferences属性:
<?xml version="1.0"encoding="utf-8"?>
<account-authenticatorxmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="project.test.com.myapplication.account.type"
android:icon="@mipmap/ic_launcher"
android:smallIcon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:accountPreferences="@xml/account_preferences"
/>
account_preferences.xml文件:
<?xml version="1.0"encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategoryandroid:title="PreferenceCategory_title" />
<PreferenceScreen
android:key="key1"
android:title="PreferenceScreen_title"
android:summary="PreferenceScreen_summary">
<intent
android:action="key1.ACTION"
android:targetPackage="key1.package"
android:targetClass="key1.class" />
</PreferenceScreen>
</PreferenceScreen>
我们再次运行APP,然后往已添加的帐户列表中点击我们的APP后便会出现一个中间页面,如图:
拉活机制
对于某些APP来说,当然都希望自己的进程尽量的不被杀死。于是乎,就出现的各种进程保活的方法,其中帐户同步也算为其中的一种。因为系统帐户自动同步会在特定时间时同步APP数据到服务器上去。上面实现也看到,登录帐户和执行同步帐户数据都是通过我们自己的代码来实现,所以在同步数据时会把已退出或完全结束的APP再次拉活起来。笔者在部分手机中验证过我们今天介绍的Demo,发现效果还是行得通的。当然由于国内手机厂商的各种订制和阉割,也会出现某部分手机是行不通的,大家有时间可以自行去尝试。这点也算是Android系统的一个小漏洞。希望大家能善用帐户同步功能,养成良好的开发信仰,尊重用户意愿。因为并不是所有用户都是愿意让不相关的APP任其后台常驻的!
总结
本文只为简单介绍和演示Android帐户同步的实现步骤,大家如若需要更完整的帐户同步实现,有兴趣可以自行去研究一下在Anroid SDK中samples目录下也有一个叫SampleSyncAdapter的帐号与同步的实例。另外上面介绍的Demo源码可以点击这里下载