Version 1.5.11
账户处理
选择账户获取认证信息
- 在MainActivity中,onConnect()是由MainSettings中为connected控件设置的点击事件触发。
@Subscribe
public void onConnect(AccountConnectionChangedEvent event) {
if (event.connected) {
startActivityForResult(new Intent(this, AccountManagerAuthActivity.class), REQUEST_PICK_ACCOUNT);
} else {
showDialog(DISCONNECT);
}
}
updateConnected().setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference preference, Object change) {
//将会触发MainActivity中的onConnect方法,该方法会打开AccountManagerAuthActivity账户管理界面。
App.post(new AccountConnectionChangedEvent((Boolean) change));
return false; // will be set later
}
});
- AccountManagerAuthActivity 账户管理
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
accountManager = AccountManager.get(this);
if (needsGetAccountPermission()) {//是否需要权限,该权限为GET_ACCOUNTS
//需要请求权限就请求权限
requestGetAccountsPermission();
} else {
//不需要权限直接走到选择账户的逻辑
checkAccounts();
}
}
//选择账户的逻辑
public static final String GOOGLE_TYPE = "com.google";
private void checkAccounts() {
//获取账户
Account[] accounts = accountManager.getAccountsByType(GOOGLE_TYPE);
if (accounts == null || accounts.length == 0) {
//如果账户获取失败就关闭Activity,返回到MainActivity中提示通过Web方式获取账户。
Log.d(TAG, "no google accounts found on this device, using standard auth");
setResult(RESULT_OK, new Intent(ACTION_FALLBACK_AUTH));
finish();
} else {
//获取账户成功之后,就会将账户通过Dialog展示出来,供用户选择。
Bundle args = new BundleBuilder().putParcelableArray(ACCOUNTS, accounts).build();
((DialogFragment)Fragment.instantiate(this, AccountDialogs.class.getName(), args)).show(getSupportFragmentManager(), null);
}
}
- AccountDialogs中用户选择账户
@Override @NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
//账户数组
final Account[] accounts = (Account[]) getArguments().getParcelableArray(ACCOUNTS);
//被选择账户的索引
final int[] checkedItem = {0};
final ColorStateList colorStateList = ContextCompat.getColorStateList(getContext(), R.color.secondary_text);
return new AlertDialog.Builder(getContext())
.setTitle(R.string.select_google_account)
.setIcon(getTinted(getResources(), R.drawable.ic_account_circle, colorStateList.getDefaultColor()))
.setSingleChoiceItems(getNames(accounts), checkedItem[0], new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
checkedItem[0] = which;
}
})
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int which) {
//将被选择的账户信息通过onAccountSelected处理后发送到主页
((AccountManagerAuthActivity)getActivity()).onAccountSelected(accounts[checkedItem[0]]);
}
})
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
((AccountManagerAuthActivity)getActivity()).onCanceled();
}
})
.create();
}
- AccountManagerAuthActivity 账户管理中对被选择的账户进行处理
//处理被选择的Account信息
private void onAccountSelected(final Account account) {
new AccessTokenProgress().show(getSupportFragmentManager(), null);
accountManager.getAuthToken(account, AUTH_TOKEN_TYPE, null, this, new AccountManagerCallback<Bundle>() {
public void run(AccountManagerFuture<Bundle> future) {
try {
//具体处理逻辑
useToken(account, future.getResult().getString(KEY_AUTHTOKEN));
} catch (OperationCanceledException e) {
onAccessDenied();
} catch (Exception e) {
handleException(e);
}
}
}, null);
}
//通过Intent返回到MainActivity中
private void useToken(Account account, String token) {
Log.d(TAG, "obtained token for " + account + " from AccountManager");
Intent result = new Intent(ACTION_ADD_ACCOUNT)
.putExtra(EXTRA_ACCOUNT, account.name)
.putExtra(EXTRA_TOKEN, token);
setResult(RESULT_OK, result);
finish();
}
- 当MainActivity在接到选择账户方式获取用户账户认证信息成功之后,将对Account账户进一步进行处理
//处理用户认证信息
private void handleAccountManagerAuth(@NonNull Intent data) {
final String token = data.getStringExtra(EXTRA_TOKEN);
final String account = data.getStringExtra(EXTRA_ACCOUNT);
if (!TextUtils.isEmpty(token) && !TextUtils.isEmpty(account)) {
//具体处理逻辑
authPreferences.setOauth2Token(account, token, null);
onAuthenticated();
} else {
String error = data.getStringExtra(AccountManagerAuthActivity.EXTRA_ERROR);
if (!TextUtils.isEmpty(error)) {
//获取用户账户认证信息失败
showDialog(ACCOUNT_MANAGER_TOKEN_ERROR);
}
}
}
//具体的处理用户账户认证信息就是将他们存储在SP文件中了
public void setOauth2Token(String username, String accessToken, String refreshToken) {
preferences.edit()
.putString(OAUTH2_USER, username)
.commit();
getCredentials().edit()
.putString(OAUTH2_TOKEN, accessToken)
.commit();
getCredentials().edit()
.putString(OAUTH2_REFRESH_TOKEN, refreshToken)
.commit();
}
Web方式获取用户认证信息
- 所有的通过选择账户方式获取账户认证信息的方式失败之后都会去调用MainActivity中的handleFallbackAuth方法,该方法会提示通过Web方式获取用户账户认证信息。
//oauth2Client.requestUrl() 该方法很重要,通过该方法可以获取到URI
fallbackAuthIntent = new Intent(this, OAuth2WebAuthActivity.class).setData(oauth2Client.requestUrl());
//会通过发送Event消息视图调用该方法
@Subscribe
public void handleFallbackAuth(FallbackAuthEvent event) {
if (event.showDialog) {//是否弹出Dialog提示
//调用Dialog弹窗提示通过Web方式获取,最终调用的还是startActivityForResult(fallbackAuthIntent, REQUEST_WEB_AUTH);,将进入到OAuth2WebAuthActivity中去。
showDialog(WEB_CONNECT);
} else {
startActivityForResult(fallbackAuthIntent, REQUEST_WEB_AUTH);
}
}
- OAuth2Client类中requestUrl()方法是创建一个Uri,通过该Uri来打开Web浏览器
//OAuth2Client中存储有clientId,该Id需要自己创建。
public OAuth2Client(String clientId) {
if (TextUtils.isEmpty(clientId)) {
throw new IllegalArgumentException("empty client id");
}
this.clientId = clientId;
}
//将这些信息拼凑,最终得到一个URI
public Uri requestUrl() {
return Uri.parse(AUTH_URL)
.buildUpon()
.appendQueryParameter(SCOPE, DEFAULT_SCOPE)
.appendQueryParameter(CLIENT_ID, clientId)
.appendQueryParameter(RESPONSE_TYPE, "code")
.appendQueryParameter(REDIRECT_URI, REDIRECT_URL.toString()).build();
}
//需要注意一个地方Uri REDIRECT_URL = Uri.parse("applicationID:/oauth2redirect");
创建OAuth 客户端ID 地址
- OAuth2WebAuthActivity中会使Web浏览器加载上一步拼接好的Uri
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
//上一步拼接好的URI
final Uri urlToLoad = getIntent().getData();
App.register(this);
//Web浏览器加载
startActivity(new Intent(Intent.ACTION_VIEW, urlToLoad));
}
@Override
protected void onRestart() {
super.onRestart();
//用户不予授权的时候,会回调Activity中的onRestart()方法,将返回到MainActivity中,将会提示“无法获取访问权限,请确认您选择了“授权访问”。”
setResult(RESULT_CANCELED);
finish();
}
- 通过Web浏览器加载Uri后,如果用户授权则专门负责重定向的RedirectReceiverActivity将收到消息
//RedirectReceiverActivity 中Manifest文件中filter过滤器的设置,当用户允许授权之时,该Activity将接收到一个消息。如果用户不予授权,则无法接收到消息。
<activity android:name=".activity.auth.RedirectReceiverActivity" android:launchMode="singleTask">
<!-- handle oauth browser callbacks (legacy auth support) -->
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<action android:name="android.intent.action.EXTERNAL_APPLICATIONS_AVAILABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="com.zegoggles.smssync" android:path="/oauth2redirect"
tools:ignore="GoogleAppIndexingUrlError"/>
</intent-filter>
</activity>
//浏览器界面,用户授权之后,RedirectReceiverActivity中onCreate方法会被回调,在此会接收到一些信息,下面会去处理该信息。
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "onCreate: " +savedInstanceState);
super.onCreate(savedInstanceState);
handleRedirectIntent(getIntent());
}
//处理从浏览器中返回的信息
private void handleRedirectIntent(Intent intent) {
if (OAuth2Client.REDIRECT_URL.getScheme().equals(intent.getScheme())) {
//将code与error通过Event发送出去,最终会触发OAuth2WebAuthActivity中的onBrowserAuthResult方法
final String code = intent.getData().getQueryParameter("code");
final String error = intent.getData().getQueryParameter("error");
App.post(new BrowserAuthResult(code, error));
}
//如果没有fini,则RedirectReceiverActivity将把MainActivity覆盖
finish();
}
- OAuth2WebAuthActivity中的onBrowserAuthResult()方法最终会将浏览器中返回的Code码传递到MainActivity当中去。
@Subscribe
public void onBrowserAuthResult(RedirectReceiverActivity.BrowserAuthResult event) {
//event对象是由handleRedirectIntent()方法传递过来的
//该方法最终会触发MainActivity中的onActivityResult->case REQUEST_WEB_AUTH
if (!TextUtils.isEmpty(event.code)) {
setResult(RESULT_OK, new Intent().putExtra(EXTRA_CODE, event.code));
} else {
setResult(RESULT_OK, new Intent().putExtra(EXTRA_ERROR, event.error));
}
finish();
}
- 在MainActivity中处理从用户Web授权认证信息中的code码
case REQUEST_WEB_AUTH: {
//无法获取访问权限,请确认您选择了“授权访问”。
if (resultCode == RESULT_CANCELED) {
Toast.makeText(this, R.string.ui_dialog_access_token_error_msg, LENGTH_LONG).show();
return;
}
//获取Code码
final String code = data == null ? null : data.getStringExtra(OAuth2WebAuthActivity.EXTRA_CODE);
//如果Code码不为null
if (!TextUtils.isEmpty(code)) {
//旋转的进度条提示框
showDialog(OAUTH2_ACCESS_TOKEN_PROGRESS);
//oauth2Client中存储有clientId,并且也拿到了用户授权获取的Code码,这里开始执行AsyncTask异步任务
new OAuth2CallbackTask(oauth2Client).execute(code);
} else {
//授权失败的提示,最终弹出OAuth2AccessTokenError 对话框
showDialog(OAUTH2_ACCESS_TOKEN_ERROR);
}
break;
}
public static class OAuth2AccessTokenError extends BaseFragment {
@Override @NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
//认证失败
final String title = getString(R.string.ui_dialog_access_token_error_title);
//无法获取访问权限,请确认您选择了“授权访问”。
final String msg = getString(R.string.ui_dialog_access_token_error_msg);
return createMessageDialog(title, msg, ic_dialog_alert);
}
}
- OAuth2CallbackTask 继承自AsyncTask,用来处理用户从Web端授权之后返回的授权码,使用授权码获取用户信息认证。
@Override
protected OAuth2Token doInBackground(String... code) {
if (code == null || code.length == 0 || TextUtils.isEmpty(code[0])) {
Log.w(TAG, "invalid input: "+ Arrays.toString(code));
return null;
}
try {
//获取账户认证Token,code[0]是用户通过Web授权返回的code码
return oauth2Client.getToken(code[0]);
} catch (IOException e) {
Log.w(TAG, e);
}
return null;
}
- OAuth2Client.class该类是用于处理账户授权认证相关的工作,特别是getToken()方法
//获取token
public OAuth2Token getToken(String code) throws IOException {
HttpsURLConnection connection = postTokenEndpoint(getAccessTokenPostData(code));
final int responseCode = connection.getResponseCode();
if (responseCode == HttpsURLConnection.HTTP_OK) {
OAuth2Token token = parseResponse(connection.getInputStream());
String username = getUsernameFromContacts(token);
Log.d(TAG, "got token " + token.getTokenForLogging()+ ", username="+username);
//获取到token之后,会通过Event将OAuth2Token对象发送到MainActivity中onOAuth2Callback接收
return new OAuth2Token(token.accessToken, token.tokenType, token.refreshToken, token.expiresIn, username);
} else {
Log.e(TAG, "error: " + responseCode);
throw new IOException("Invalid response from server:" + responseCode);
}
}
- MainActivity中处理通过Web用户授权获取到的Token信息
@Subscribe
public void onOAuth2Callback(OAuth2CallbackTask.OAuth2CallbackEvent event) {
if (event.valid()) {//检查Token是否为null
//将token信息保存到SP文件中
authPreferences.setOauth2Token(event.token.userName, event.token.accessToken, event.token.refreshToken);
//该方法是用来更新UI,与测试用户是否是第一次备份,如果是第一次备份,当用户授权了账户认证之后,就提示用户进行备份,如果非第一次备份,当用户获取账户授权之后,UI还是必须要更改,提示备份就无需提示了。
onAuthenticated();
} else {
showDialog(OAUTH2_ACCESS_TOKEN_ERROR);
}
}
短信通话记录备份
- 自定义控件StatusPreference中onClick方法。
@Override
public void onClick(View which) {
if (which == backupButton) {
//短信通话记录备份按钮点击事件
onBackup();
} else if (which == restoreButton) {
//短信通话记录还原按钮点击事件
onRestore();
}
}
//短信通话记录备份方法
private void onBackup() {
if (!SmsBackupService.isServiceWorking()) {//检测备份服务是否正在运行
if (LOCAL_LOGV) Log.v(TAG, "user requested sync");
//该方法最终回调MainActivity中的performAction(action)方法
App.post(new PerformAction(Backup, preferences.confirmAction()));
} else {
//当备份服务正在进行,关闭短信联系人备份任务
if (LOCAL_LOGV) Log.v(TAG, "user requested cancel");
// Sync button will be restored on next status update.
backupButton.setText(R.string.ui_sync_button_label_canceling);//正在停止
backupButton.setEnabled(false);
//该方法最终回调BackupTask中的canceled(cancelEvent)方法,目的是关闭AsyncTask异步任务
App.post(new CancelEvent());
}
}
//关闭BackupTask的异步任务
@Subscribe
public void canceled(CancelEvent cancelEvent) {
if (LOCAL_LOGV) {
Log.v(TAG, "canceled("+cancelEvent+")");
}
cancel(cancelEvent.mayInterruptIfRunning());
}
- MainActivity中开始执行备份任务的逻辑
@Subscribe
public void performAction(PerformAction action) {
if (authPreferences.isLoginInformationSet()) {//判断是否获取到用户认证授权
if (action.confirm) {
//提示是否确认此操作的Dialog对话框,最终会执行doPerform()方法
showDialog(CONFIRM_ACTION, new BundleBuilder().putString(ACTION, action.action.name()).build());
} else if (preferences.isFirstBackup() && action.action == Backup) {
//第一次备份Dialog对话框提示,最终会执行doPerform()方法
showDialog(FIRST_SYNC);
} else {
//最终执行的方法
doPerform(action.action);
}
} else {
showDialog(MISSING_CREDENTIALS);//用户没有授权,弹出缺失凭证的提示
}
}
//执行备份还原的逻辑
@Subscribe
public void doPerform(Actions action) {
switch (action) {
case Backup:
case BackupSkip:
//备份数据
startBackup(action == Backup ? MANUAL : SKIP);//手动或者跳过
break;
case Restore:
//还原数据
startRestore();
break;
}
}
//备份服务开启
private void startBackup(BackupType backupType) {
startService(new Intent(this, SmsBackupService.class)
//该参数告诉Service备份的类型
.setAction(backupType.name()));
}
- 备份服务SmsBackupService运行逻辑
@Override
protected void handleIntent(final Intent intent) {
if (intent == null) return; // NB: should not happen with START_NOT_STICKY
//获取备份类型,备份/跳过/手动/第三方等等
final BackupType backupType = BackupType.fromIntent(intent);
appLog(R.string.app_log_backup_requested, getString(backupType.resId));
// Only start a backup if there's no other operation going on at this time.
if (!isWorking() && SmsRestoreService.isServiceIdle()) {//检测备份服务是否运行
//开始备份
backup(backupType);
} else {
appLog(R.string.app_log_skip_backup_already_running);//Log日志打印,提示已经在备份中,跳过这次执行逻辑
}
}
private void backup(BackupType backupType) {
getNotifier().cancel(NOTIFICATION_ID_WARNING);
try {
// set initial state
state = new BackupState(INITIAL, 0, 0, backupType, null, null);
//备份类型 短信,彩信,通话记录
EnumSet<DataType> enabledTypes = getEnabledBackupTypes();
//权限检查,检查这几种备份权限是否获取到了
checkPermissions(enabledTypes);
if (backupType != SKIP) {
checkCredentials();//检查凭证
if (getPreferences().isUseOldScheduler()) {//Google Play是否可用,如果不可用使用旧有的程序
legacyCheckConnectivity();
}
}
//Logo 开始备份
appLog(R.string.app_log_start_backup, backupType);
//getBackupImapStore()方法获取IMAP备份仓库
//getBackupTask() 获取AsyncTask对象
//getBackupConfig()获取备份设置信息
getBackupTask().execute(getBackupConfig(backupType, enabledTypes, getBackupImapStore()));
} catch (MessagingException e) {
//捕获异常更新状态提示信息
Log.w(TAG, e);
moveToState(state.transition(ERROR, e));
} catch (ConnectivityException e) {
moveToState(state.transition(ERROR, e));
} catch (RequiresLoginException e) {
appLog(R.string.app_log_missing_credentials);
moveToState(state.transition(ERROR, e));
} catch (BackupDisabledException e) {
moveToState(state.transition(FINISHED_BACKUP, e));
} catch (MissingPermissionException e) {
moveToState(state.transition(ERROR, e));
}
}
//获取备份设置信息
private BackupConfig getBackupConfig(BackupType backupType, EnumSet<DataType> enabledTypes, BackupImapStore imapStore) {
return new BackupConfig(
imapStore,//IMAP仓库
0,
getPreferences().getMaxItemsPerSync(),每次备份时候最大的数据量,这个是通过手动设置的
getPreferences().getBackupContactGroup(),//备份联系人
backupType,//备份方式,手动,自动等
enabledTypes,//数据类型,短信,彩信,通话记录
getPreferences().isAppLogDebug()
);
}
- BackupTask构造方法中涉及到了好几个类,比较重要,一一分析。
//preferences.getDataTypePreferences() 涉及到DataTypePreferences.java
/**
* 返回以毫秒为单位的最后一步同步日期
* @return returns the last synced date in milliseconds (epoch)
*/
public long getMaxSyncedDate(DataType dataType) {
final long maxSynced = sharedPreferences.getLong(dataType.maxSyncedPreference, MAX_SYNCED_DATE);
if (dataType == MMS && maxSynced > 0) {
return maxSynced * 1000L;
} else {
return maxSynced;
}
}
//new BackupQueryBuilder(preferences.getDataTypePreferences()) 涉及到BackupQueryBuilder.java中比较重要的两个方法
/**
* 通过数据类型构建全新,没有任何限制类型的查询
* @param type
* @return
*/
public @Nullable
Query buildMostRecentQueryForDataType(DataType type) {
switch (type) {
case MMS:
return new Query(
Consts.MMS_PROVIDER,
new String[]{Telephony.BaseMmsColumns.DATE},
null,
null,
Telephony.BaseMmsColumns.DATE + DESC_LIMIT_1);
case SMS:
return new Query(
Consts.SMS_PROVIDER,
new String[]{Telephony.TextBasedSmsColumns.DATE},
Telephony.TextBasedSmsColumns.TYPE + " <> ?",
new String[]{String.valueOf(Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT)},
Telephony.TextBasedSmsColumns.DATE + DESC_LIMIT_1);
case CALLLOG:
return new Query(
Consts.CALLLOG_PROVIDER,
new String[]{CallLog.Calls.DATE},
null,
null,
CallLog.Calls.DATE + DESC_LIMIT_1);
default:
return null;
}
}
/**
* "%s > ?" 表示CallLog.Calls.DATE日期要大于最大同步数据的日期
* @param max 该参数是用来记录每次备份还原数据的最大数量
* @return
*/
private Query getQueryForCallLog(int max) {
return new Query(
Consts.CALLLOG_PROVIDER,
CALLLOG_PROJECTION,
String.format(Locale.ENGLISH, "%s > ?", CallLog.Calls.DATE),
new String[]{
String.valueOf(preferences.getMaxSyncedDate(CALLLOG))
},
max);
}
//BackupItemsFetcher.java
/**
* 获取数据类型该有的Cursor游标
* @param dataType
* @param group
* @param max
* @return
*/
public @NonNull Cursor getItemsForDataType(DataType dataType, ContactGroupIds group, int max) {
if (LOCAL_LOGV) Log.v(TAG, "getItemsForDataType(type=" + dataType + ", max=" + max + ")");
return performQuery(queryBuilder.buildQueryForDataType(dataType, group, max));
}
/**
* Gets the most recent timestamp for given datatype.
* 获取给定数据类型最新时间戳。
* @param dataType the data type
* @return timestamp
* @throws SecurityException if app does not hold necessary permissions
*/
public long getMostRecentTimestamp(DataType dataType) {
return getMostRecentTimestampForQuery(queryBuilder.buildMostRecentQueryForDataType(dataType));
}
/**
* @param query
* @return
*/
private long getMostRecentTimestampForQuery(BackupQueryBuilder.Query query) {
Cursor cursor = performQuery(query);
try {
if (cursor.moveToFirst()) {
return cursor.getLong(0);
} else {
return DataType.Defaults.MAX_SYNCED_DATE;
}
} finally {
cursor.close();
}
}
//PersonLookup.java中的lookupPerson()方法比较重要
/**
* 在MessageConverter里的messageToContentValues()方法中,
* lookupPerson()方法主要是用来将com.fsck.k9.mail.Message对象中的Header也就是address值,拿到PersonRecord对象
* PersonRecord对象是用来记录姓名电话Email的
* look up a person
* @throws SecurityException if the caller does not hold READ_CONTACTS
*/
public @NonNull PersonRecord lookupPerson(final String address) {
if (TextUtils.isEmpty(address)) {
return new PersonRecord(0, null, null, "-1");
} else if (!personCache.containsKey(address)) {
final Uri personUri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(address));
Cursor c = null;
try {
c = resolver.query(personUri, new String[] {
PhoneLookup._ID,
PhoneLookup.DISPLAY_NAME
}, null, null, null);
} catch (IllegalArgumentException e) {
// https://github.com/jberkel/sms-backup-plus/issues/870
Log.wtf(TAG, "avoided a crash with address: " + address, e);
}
final PersonRecord record;
if (c != null && c.moveToFirst()) {
final long id = c.getLong(0);
record = new PersonRecord(
id,
c.getString(1),
getPrimaryEmail(id),
address
);
} else {
if (LOCAL_LOGV) Log.v(TAG, "Looked up unknown address: " + address);
record = new PersonRecord(0, null, null, address);
}
//personCache 通过地址作为键 PersonRecord对象作为值得Map集合
personCache.put(address, record);
if (c != null) c.close();
}
//获取PersonRecord 对象返回
return personCache.get(address);
}
//ContactAccessor.java
/**
* 获取联系人组,该方法在AdvancedSettings 页面的时候,点击选择备份组的时候会调用,用来查看手机中有哪些联系人组。
* 查询完联系人组之后,会用列表展示出来供用户选择设置。
* @param resolver the resolver
* @param resources the resources
* @return the ids and groups a user has
* @throws SecurityException if READ_CONTACTS permission is missing
*/
public Map<Integer, Group> getGroups(ContentResolver resolver, Resources resources) {
final Map<Integer, Group> map = new LinkedHashMap<Integer, Group>();
map.put(EVERYBODY_ID, new Group(EVERYBODY_ID, resources.getString(R.string.everybody), 0));
final Cursor c = resolver.query(
Groups.CONTENT_SUMMARY_URI,
new String[]{Groups._ID, Groups.TITLE, Groups.SUMMARY_COUNT},
null,
null,
Groups.TITLE + " ASC");
while (c != null && c.moveToNext()) {
map.put(c.getInt(0), new Group(c.getInt(0), c.getString(1), c.getInt(2)));
}
if (c != null) c.close();
return map;
}
/**
* 通过组类别_id,查看族中所有的ID
* @param resolver the resolver
* @param group the group
* @return all contacts from a group
* @throws SecurityException if READ_CONTACTS permission is missing
*/
public ContactGroupIds getGroupContactIds(ContentResolver resolver, ContactGroup group) {
if (group.isEveryBody()) return null;
final ContactGroupIds contactIds = new ContactGroupIds();
Cursor c = resolver.query(
Data.CONTENT_URI,
new String[]{
GroupMembership.CONTACT_ID,
GroupMembership.RAW_CONTACT_ID,
GroupMembership.GROUP_ROW_ID
},
GroupMembership.GROUP_ROW_ID + " = ? AND " + GroupMembership.MIMETYPE + " = ?",
new String[]{String.valueOf(group._id), GroupMembership.CONTENT_ITEM_TYPE},
null);
while (c != null && c.moveToNext()) {
contactIds.add(c.getLong(0), c.getLong(1));
}
if (c != null) c.close();
return contactIds;
}
//MessageConverter.java中重要的方法
/**
* 通过cursor和dataType得到转换后的结果
* @param cursor
* @param dataType
* @return
* @throws MessagingException
*/
public @NonNull ConversionResult convertMessages(final Cursor cursor, DataType dataType) throws MessagingException {
final Map<String, String> msgMap = getMessageMap(cursor);
final Message m = messageGenerator.messageForDataType(msgMap, dataType);
final ConversionResult result = new ConversionResult(dataType);
if (m != null) {
m.setFlag(Flag.SEEN, markAsSeen(dataType, msgMap));
result.add(m, msgMap);
}
return result;
}
//TokenRefresher.java 刷新Token
public void refreshOAuth2Token() throws TokenRefreshException{
final String token = authPreferences.getOauth2Token();
final String refreshToken = authPreferences.getOauth2RefreshToken();
final String name = authPreferences.getOauth2Username();
if (isEmpty(token)) {
throw new TokenRefreshException("no current token set");
}
//如果refreshToken 不为null,那么该授权方式肯定是通过Web浏览器授权的,那么就通过Web浏览器的方式刷新。
//如果refreshToken 是null,那么该授权方式肯定是通过账户授权,那么就通过账户授权方式刷新。
if (!isEmpty(refreshToken)) {
//使用Web方式让用户授权认证
// user authenticated using webflow
refreshUsingOAuth2Client(name, refreshToken);
} else {
//使用账户管理器刷新
refreshUsingAccountManager(token, name);
}
}
- BackupTask备份异步任务的逻辑
//看看BackupTask构造方法
BackupTask(@NonNull SmsBackupService service) {
final Context context = service.getApplicationContext();
this.service = service;
this.authPreferences = service.getAuthPreferences();
this.preferences = service.getPreferences();
//这个比较的重要
//preferences.getDataTypePreferences() 该SP文件中保存了短信,彩信,通话记录最大同步日期
//BackupQueryBuilder 用来构造查询Sql时候需要的语句
//BackupItemsFetcher 主要是用来获取该有数据类型的Cursor游标或者用来获取该有数据类型的最新时间戳
this.fetcher = new BackupItemsFetcher(context.getContentResolver(), new BackupQueryBuilder(preferences.getDataTypePreferences()));
//查找某人
PersonLookup personLookup = new PersonLookup(service.getContentResolver());
//联系人访问器
this.contactAccessor = new ContactAccessor();
this.converter = new MessageConverter(context, service.getPreferences(), authPreferences.getUserEmail(), personLookup, contactAccessor);
//日历访问
if (preferences.isCallLogCalendarSyncEnabled()) {
calendarSyncer = new CalendarSyncer(
CalendarAccessor.Get.instance(service.getContentResolver()),
preferences.getCallLogCalendarId(),
personLookup,
new CallFormatter(context.getResources())
);
} else {
calendarSyncer = null;
}
//认证授权刷新
this.tokenRefresher = new TokenRefresher(service, new OAuth2Client(authPreferences.getOAuth2ClientId()), authPreferences);
}
//fetchAndBackupItems()方法开始获取和备份条目
private BackupState acquireLocksAndBackup(BackupConfig config) {
try {
//获取所
service.acquireLocks();
return fetchAndBackupItems(config);
} finally {
//释放锁
service.releaseLocks();
}
}
private BackupState fetchAndBackupItems(BackupConfig config) {
BackupCursors cursors = null;
try {
获取不同组类的所有联系人id
final ContactGroupIds groupIds = contactAccessor.getGroupContactIds(service.getContentResolver(), config.groupToBackup);
//获取游标集合
cursors = new BulkFetcher(fetcher).fetch(config.typesToBackup, groupIds, config.maxItemsPerSync);
//通过游标集合获取总数目
final int itemsToSync = cursors.count();
if (itemsToSync > 0) {
//正在备份 (%1$d 条短信, %2$d 条彩信, %3$d 条通话记录)
appLog(R.string.app_log_backup_messages, cursors.count(SMS), cursors.count(MMS), cursors.count(CALLLOG));
if (config.debug) {
appLog(R.string.app_log_backup_messages_with_config, config);
}
//开始备份
return backupCursors(cursors, config.imapStore, config.backupType, itemsToSync);
} else {
//已跳过 (没有找到项目)
appLog(R.string.app_log_skip_backup_no_items);
if (preferences.isFirstBackup()) {
// If this is the first backup we need to write something to MAX_SYNCED_DATE
// such that we know that we've performed a backup before.
preferences.getDataTypePreferences().setMaxSyncedDate(SMS, MAX_SYNCED_DATE);
preferences.getDataTypePreferences().setMaxSyncedDate(MMS, MAX_SYNCED_DATE);
}
Log.i(TAG, "Nothing to do.");
return transition(FINISHED_BACKUP, null);
}
} catch (XOAuth2AuthenticationFailedException e) {
return handleAuthError(config, e);
} catch (AuthenticationFailedException e) {
return transition(ERROR, e);
} catch (MessagingException e) {
return transition(ERROR, e);
} catch (SecurityException e) {
return transition(ERROR, e);
} finally {
if (cursors != null) {
cursors.close();
}
}
}
//fetchAndBackupItems()方法中涉及到了几个重要的方法这里说明下。
//config.groupToBackup 该值可以追一下,SmsBackupService中方法getBackupConfig(),
private BackupConfig getBackupConfig(BackupType backupType, EnumSet<DataType> enabledTypes, BackupImapStore imapStore) {
return new BackupConfig(
imapStore,//IMAP仓库
0,
getPreferences().getMaxItemsPerSync(),//每次备份时候最大的数据量,这个是通过手动设置的
getPreferences().getBackupContactGroup(),//备份联系人组信息
backupType,//备份方式,手动,自动等
enabledTypes,//数据类型,短信,彩信,通话记录
getPreferences().isAppLogDebug()
);
}
//创建ContactGroup对象
public ContactGroup getBackupContactGroup() {
return new ContactGroup(getStringAsInt(BACKUP_CONTACT_GROUP, -1));
}
//ContactGroup 中存储了组_id与数据获取方式是按照组获取的还是获取所有
public final long _id;
public final Type type;
public enum Type {
EVERYBODY,
GROUP
}
//开始备份的backupCursors(cursors, config.imapStore, config.backupType, itemsToSync)
private BackupState backupCursors(BackupCursors cursors, BackupImapStore store, BackupType backupType, int itemsToSync) throws MessagingException {
Log.i(TAG, String.format(Locale.ENGLISH, "Starting backup (%d messages)", itemsToSync));
publish(LOGIN);
store.checkSettings();
try {
publish(CALC);
int backedUpItems = 0;
while (!isCancelled() && cursors.hasNext()) {
BackupCursors.CursorAndType cursor = cursors.next();
if (LOCAL_LOGV) Log.v(TAG, "backing up: " + cursor);
//通过该方法将cursors中的数据取出来,并进行了二次转换
ConversionResult result = converter.convertMessages(cursor.cursor, cursor.type);
if (!result.isEmpty()) {
//
List<Message> messages = result.getMessages();
if (LOCAL_LOGV) {
Log.v(TAG, String.format(Locale.ENGLISH, "sending %d %s message(s) to server.", messages.size(), cursor.type));
}
//把得到得消息都存储在k9 mail这个文件夹中了
store.getFolder(cursor.type, preferences.getDataTypePreferences()).appendMessages(messages);
//这里可以去除,只是将记录插入到了Google 日历中
if (cursor.type == CALLLOG && calendarSyncer != null) {
//如果corsor type为通话记录,那么就将通话记录同步到Google 日历中去
calendarSyncer.syncCalendar(result);
}
//设置最大同步日期
preferences.getDataTypePreferences().setMaxSyncedDate(cursor.type, result.getMaxDate());
backedUpItems += messages.size();
} else {
Log.w(TAG, "no messages converted");
itemsToSync -= 1;
}
//发布进展 进度条
publishProgress(new BackupState(BACKUP, backedUpItems, itemsToSync, backupType, cursor.type, null));
}
return new BackupState(FINISHED_BACKUP,
backedUpItems,
itemsToSync,
backupType, null, null);
} finally {
store.closeFolders();
}
}
- ServiceBase.java中关于getBackupImapStore()方法解读
/**
* 创建IMAP备份协议的仓库
* @return
* @throws MessagingException
*/
protected BackupImapStore getBackupImapStore() throws MessagingException {
final String uri = getAuthPreferences().getStoreUri();
if (!BackupImapStore.isValidUri(uri)) {
throw new MessagingException("No valid IMAP URI: "+uri);
}
//创建一个IMAP备份协议的仓库
return new BackupImapStore(getApplicationContext(), uri, getAuthPreferences().isTrustAllCertificates());
}
//AuthPreferences中getStoreUri()方法
/**
* 获取存储仓库的URI
* @return
*/
public String getStoreUri() {
if (useXOAuth()) {
if (hasOAuth2Tokens()) {
return formatUri(
AuthType.XOAUTH2, //身份认证类型
DEFAULT_SERVER_PROTOCOL,//默认服务器协议
getOauth2Username(),//获取授权名称
generateXOAuth2Token(),//生成授权token
DEFAULT_SERVER_ADDRESS);//默认服务器地址
} else {
Log.w(TAG, "No valid xoauth2 tokens");
return null;
}
} else {
return formatUri(AuthType.PLAIN,
getServerProtocol(),
getImapUsername(),
getImapPassword(),
getServerAddress());
}
}
// useXOAuth() 主要是用来判断获取认证得类型,是通过Gmail还是自己设置的邮箱。
public boolean useXOAuth() {
return getAuthMode() == AuthMode.XOAUTH;
}
private AuthMode getAuthMode() {
return getDefaultType(preferences, SERVER_AUTHENTICATION, AuthMode.class, AuthMode.XOAUTH);
}
//查看SP文件中是否保存了SERVER_AUTHENTICATION该键所对应的值,如果不存在则该认证模式为AuthMode.XOAUTH,
static <T extends Enum<T>> T getDefaultType(SharedPreferences preferences, String pref, Class<T> tClazz, T defaultType) {
try {
final String s = preferences.getString(pref, null);
return s == null ? defaultType : Enum.valueOf(tClazz, s.toUpperCase(Locale.ENGLISH));
} catch (IllegalArgumentException e) {
Log.e(TAG, "getDefaultType(" + pref + ")", e);
return defaultType;
}
}
未完待续…