package org.andstatus.app.account;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import org.andstatus.app.account.MyAccount.Builder;
import org.andstatus.app.account.MyAccount.CredentialsVerificationStatus;
import org.andstatus.app.backup.MyBackupDataInput;
import org.andstatus.app.backup.MyBackupDataOutput;
import org.andstatus.app.backup.MyBackupDescriptor;
import org.andstatus.app.context.MyContext;
import org.andstatus.app.context.MyPreferences;
import org.andstatus.app.data.DbUtils;
import org.andstatus.app.database.FriendshipTable;
import org.andstatus.app.origin.Origin;
import org.andstatus.app.util.CollectionsUtil;
import org.andstatus.app.util.I18n;
import org.andstatus.app.util.MyLog;
import org.andstatus.app.util.Permissions;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
public class PersistentAccounts {
/**
* Name of "current" account: it is not stored when application is killed
*/
private volatile String currentAccountName = "";
private final MyContext myContext;
private final List<MyAccount> mAccounts = new CopyOnWriteArrayList<>();
private int distinctOriginsCount = 0;
private volatile Set<Long> myFriends = null;
private PersistentAccounts(MyContext myContext) {
this.myContext = myContext;
}
/**
* Get list of all persistent accounts
* for the purpose of using these "accounts" elsewhere. Value of
* {@link MyAccount#getCredentialsVerified()} is the main differentiator.
*
* @return not null
*/
public List<MyAccount> list() {
return mAccounts;
}
public boolean isEmpty() {
return mAccounts.isEmpty();
}
public int size() {
return mAccounts.size();
}
public PersistentAccounts initialize() {
myFriends = null;
android.accounts.Account[] aa = getAccounts(myContext.context());
List<MyAccount> myAccounts = new ArrayList<>();
for (android.accounts.Account account : aa) {
MyAccount ma = Builder.fromAndroidAccount(myContext, account).getAccount();
if (ma.isValid()) {
myAccounts.add(ma);
} else {
MyLog.e(this, "The account is not valid: " + ma);
}
}
CollectionsUtil.sort(myAccounts);
mAccounts.clear();
mAccounts.addAll(myAccounts);
calculateDistinctOriginsCount();
MyLog.v(this, "Account list initialized, " + mAccounts.size() + " accounts in " + distinctOriginsCount + " origins");
return this;
}
public MyAccount getDefaultAccount() {
return mAccounts.isEmpty() ? MyAccount.EMPTY : list().get(0);
}
public int getDistinctOriginsCount() {
return distinctOriginsCount;
}
private void calculateDistinctOriginsCount() {
Set<Long> originIds = new HashSet<>();
for (MyAccount ma : mAccounts) {
originIds.add(ma.getOriginId());
}
distinctOriginsCount = originIds.size();
}
public static PersistentAccounts newEmpty(MyContext myContext) {
return new PersistentAccounts(myContext);
}
/**
* Delete everything about the MyAccount
*
* @return Was the MyAccount (and Account) deleted?
*/
public boolean delete(MyAccount ma) {
boolean isDeleted = false;
// Delete the User's object from the list
MyAccount toDelete = null;
for (MyAccount persistentAccount : mAccounts) {
if (persistentAccount.equals(ma)) {
toDelete = persistentAccount;
break;
}
}
if (toDelete != null) {
MyAccount.Builder.fromMyAccount(myContext, ma, "delete", false).deleteData();
// And delete the object from the list
mAccounts.remove(toDelete);
isDeleted = true;
MyPreferences.onPreferencesChanged();
}
return isDeleted;
}
/**
* Find persistent MyAccount by accountName in local cache AND in Android
* AccountManager
*
* @return Invalid account if was not found
*/
@NonNull
public MyAccount fromAccountName(String accountNameString) {
AccountName accountName = AccountName.fromAccountName(myContext, accountNameString);
if (!accountName.isValid()) {
return MyAccount.EMPTY;
}
for (MyAccount persistentAccount : mAccounts) {
if (persistentAccount.getAccountName().equals(accountName.toString())) {
return persistentAccount;
}
}
for (android.accounts.Account androidAccount : getAccounts(myContext.context())) {
if (accountName.toString().equals(androidAccount.name)) {
MyAccount myAccount = Builder.fromAndroidAccount(myContext, androidAccount).getAccount();
mAccounts.add(myAccount);
CollectionsUtil.sort(mAccounts);
MyPreferences.onPreferencesChanged();
return myAccount;
}
}
return MyAccount.EMPTY;
}
@NonNull
public MyAccount fromOriginAndOid(long originId, String myUserOid) {
for (MyAccount persistentAccount : mAccounts) {
if (persistentAccount.getOriginId() == originId && persistentAccount.getUserOid().equals(myUserOid)) {
return persistentAccount;
}
}
return MyAccount.EMPTY;
}
/**
* Get instance of current MyAccount (MyAccount selected by the user). The account isPersistent.
* As a side effect the function changes current account if old value is not valid.
* @return Invalid account if no persistent accounts exist
*/
@NonNull
public MyAccount getCurrentAccount() {
MyAccount ma = fromAccountName(currentAccountName);
if (ma.isValid()) {
return ma;
}
currentAccountName = "";
ma = getDefaultAccount();
if (!ma.isValid()) {
for (MyAccount myAccount : mAccounts) {
if (myAccount.isValid()) {
ma = myAccount;
break;
}
}
}
if (ma.isValid()) {
// Correct Current Account if needed
if (TextUtils.isEmpty(currentAccountName)) {
setCurrentAccount(ma);
}
}
return ma;
}
/**
* Get Guid of current MyAccount (MyAccount selected by the user). The account isPersistent
*/
public String getCurrentAccountName() {
return getCurrentAccount().getAccountName();
}
/**
* @return 0 if no valid persistent accounts exist
*/
public long getCurrentAccountUserId() {
return getCurrentAccount().getUserId();
}
public boolean isAccountUserId(long userId) {
if (userId == 0) {
return false;
}
return fromUserId(userId).isValid();
}
/**
* Get MyAccount by the UserId.
* Please note that a valid User may not have an Account (in AndStatus)
* @return Invalid account if was not found
*/
@NonNull
public MyAccount fromUserId(long userId) {
MyAccount ma = MyAccount.EMPTY;
if (userId != 0) {
for (MyAccount persistentAccount : mAccounts) {
if (persistentAccount.getUserId() == userId) {
ma = persistentAccount;
break;
}
}
}
return ma;
}
@NonNull
public MyAccount getFirstSucceeded() {
return getFirstSucceededForOriginId(0);
}
@NonNull
public MyAccount getFirstSucceededForOrigin(@NonNull Origin origin) {
return getFirstSucceededForOriginId(origin.getId());
}
/**
* Return first verified and autoSynced MyAccount of the provided originId.
* If not auto synced, at least verified and succeeded,
* If there is no verified account, any account of this Origin is been returned.
* Otherwise invalid account is returned;
* @param originId May be 0 to search in any Origin
* @return Invalid account if not found
*/
@NonNull
public MyAccount getFirstSucceededForOriginId(long originId) {
MyAccount ma = MyAccount.EMPTY;
for (MyAccount persistentAccount : mAccounts) {
if (originId==0 || persistentAccount.getOriginId() == originId) {
if (!ma.isValid()) {
ma = persistentAccount;
}
if (persistentAccount.isValidAndSucceeded()) {
if (!ma.isValidAndSucceeded()) {
ma = persistentAccount;
}
if (persistentAccount.isSyncedAutomatically()) {
ma = persistentAccount;
break;
}
}
}
}
return ma;
}
public boolean hasSyncedAutomatically() {
for (MyAccount ma : mAccounts) {
if (ma.isValidAndSucceeded() && ma.isSyncedAutomatically()) {
return true;
}
}
return false;
}
/** Should not be called from UI thread
* Find MyAccount, which may be linked to this message.
* First try two supplied user IDs, then try any other existing account
* @return Invalid account if nothing suitable found
*/
@NonNull
public MyAccount getAccountForThisMessage(long originId, long messageId, MyAccount firstUser,
MyAccount preferredUser, boolean succeededOnly) {
final String method = "getAccountForThisMessage";
MyAccount ma = firstUser == null ? MyAccount.EMPTY : firstUser;
if (!accountFits(ma, originId, succeededOnly)) {
ma = betterFit(ma, preferredUser == null ? MyAccount.EMPTY : preferredUser, originId, succeededOnly);
}
if (!accountFits(ma, originId, succeededOnly)) {
ma = betterFit(ma, getFirstSucceededForOriginId(originId), originId, succeededOnly);
}
if (MyLog.isVerboseEnabled()) {
MyLog.v(this, method + "; msgId=" + messageId
+ "; user1=" + firstUser
+ "; user2=" + preferredUser
+ (succeededOnly ? "; succeeded only" : "")
+ " -> account=" + ma.getAccountName());
}
return ma;
}
private boolean accountFits(MyAccount ma, long originId, boolean succeededOnly) {
return ma != null
&& (succeededOnly ? ma.isValidAndSucceeded() : ma.isValid())
&& (originId == 0 || ma.getOriginId() == originId);
}
@NonNull
private MyAccount betterFit(@NonNull MyAccount oldMa, @NonNull MyAccount newMa, long originId, boolean succeededOnly) {
if (accountFits(oldMa, originId, succeededOnly) || !accountFits(newMa, originId, false)) {
return oldMa;
}
if (!oldMa.isValid() && newMa.isValid()) {
return newMa;
}
return oldMa;
}
/**
* Set provided MyAccount as Current one.
* Current account selection is not persistent
*/
public void setCurrentAccount(MyAccount ma) {
if (ma != null && !currentAccountName.equals(ma.getAccountName()) ) {
MyLog.v(this, "Changing current account from '" + currentAccountName + "' to '" + ma.getAccountName() + "'");
currentAccountName = ma.getAccountName();
}
}
public void onDefaultSyncFrequencyChanged() {
long syncFrequencySeconds = MyPreferences.getSyncFrequencySeconds();
for (MyAccount ma : mAccounts) {
if (ma.getSyncFrequencySeconds() <= 0) {
Account account = ma.getExistingAndroidAccount();
if (account != null) {
AccountData.setSyncFrequencySeconds(account, syncFrequencySeconds);
}
}
}
}
public List<MyAccount> accountsToSync(MyAccount myAccount, boolean forAllAccounts) {
boolean hasSyncedAutomatically = hasSyncedAutomatically();
List<MyAccount> accounts = new ArrayList<>();
if (forAllAccounts) {
for (MyAccount account : list()) {
addMyAccountToSync(accounts, account, hasSyncedAutomatically);
}
} else {
addMyAccountToSync(accounts, myAccount, false);
}
return accounts;
}
private void addMyAccountToSync(List<MyAccount> accounts, MyAccount account, boolean hasSyncedAutomatically) {
if ( !account.isValidAndSucceeded()) {
MyLog.v(this, "Account '" + account.getAccountName() + "' skipped as invalid authenticated account");
return;
}
if (hasSyncedAutomatically && !account.isSyncedAutomatically()) {
MyLog.v(this, "Account '" + account.getAccountName() + "' skipped as it is not synced automatically");
return;
}
accounts.add(account);
}
public static final String KEY_ACCOUNT = "account";
public long onBackup(MyBackupDataOutput data, MyBackupDescriptor newDescriptor) throws IOException {
long backedUpCount = 0;
JSONArray jsa = new JSONArray();
try {
for (MyAccount ma : mAccounts) {
jsa.put(ma.toJson());
backedUpCount++;
}
byte[] bytes = jsa.toString(2).getBytes("UTF-8");
data.writeEntityHeader(KEY_ACCOUNT, bytes.length, ".json");
data.writeEntityData(bytes, bytes.length);
} catch (JSONException e) {
throw new IOException(e);
}
newDescriptor.setAccountsCount(backedUpCount);
return backedUpCount;
}
/** Returns count of restores objects */
public long onRestore(MyBackupDataInput data, MyBackupDescriptor newDescriptor) throws IOException {
long restoredCount = 0;
final String method = "onRestore";
MyLog.i(this, method + "; started, " + I18n.formatBytes(data.getDataSize()));
byte[] bytes = new byte[data.getDataSize()];
int bytesRead = data.readEntityData(bytes, 0, bytes.length);
try {
JSONArray jsa = new JSONArray(new String(bytes, 0, bytesRead, "UTF-8"));
for (int ind = 0; ind < jsa.length(); ind++) {
MyLog.v(this, method + "; restoring " + (ind+1) + " of " + jsa.length());
MyAccount.Builder builder = Builder.fromJson(data.getMyContext(), (JSONObject) jsa.get(ind));
CredentialsVerificationStatus verified = builder.getAccount().getCredentialsVerified();
if (verified != CredentialsVerificationStatus.SUCCEEDED) {
newDescriptor.getLogger().logProgress("Account " + builder.getAccount().getAccountName() + " was not successfully verified");
builder.setCredentialsVerificationStatus(CredentialsVerificationStatus.SUCCEEDED);
}
if (builder.saveSilently().success) {
MyLog.v(this, method + "; restored " + (ind+1) + ": " + builder.toString());
restoredCount++;
if (verified != CredentialsVerificationStatus.SUCCEEDED) {
builder.setCredentialsVerificationStatus(verified);
builder.saveSilently();
}
} else {
MyLog.e(this, method + "; failed to restore " + (ind+1) + ": " + builder.toString());
}
}
if (restoredCount != newDescriptor.getAccountsCount()) {
throw new FileNotFoundException("Restored only " + restoredCount + " accounts of " + newDescriptor.getAccountsCount());
}
newDescriptor.getLogger().logProgress("Restored " + restoredCount + " accounts");
} catch (JSONException e) {
throw new IOException(method, e);
}
return restoredCount;
}
@Override
public String toString() {
return "PersistentAccounts{" +
"mAccounts=" + mAccounts +
'}';
}
@Override
public int hashCode() {
return mAccounts.hashCode();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PersistentAccounts other = (PersistentAccounts) o;
return mAccounts.equals(other.mAccounts);
}
public boolean isMeOrMyFriend(long inReplyToUserId) {
if (isAccountUserId(inReplyToUserId)) {
return true;
}
return isMyFriend(inReplyToUserId);
}
private boolean isMyFriend(long userId) {
if (myFriends == null) {
initializeMyFriends();
}
return myFriends.contains(userId);
}
private void initializeMyFriends() {
Set<Long> friends = new HashSet<>();
String sql = "SELECT DISTINCT " + FriendshipTable.FRIEND_ID + " FROM " + FriendshipTable.TABLE_NAME
+ " WHERE " + FriendshipTable.FOLLOWED + "=1";
SQLiteDatabase db = myContext.getDatabase();
Cursor cursor = null;
try {
cursor = db.rawQuery(sql, null);
while (cursor.moveToNext()) {
friends.add(cursor.getLong(0));
}
} catch (Exception e) {
MyLog.i(this, "SQL:'" + sql + "'", e);
} finally {
DbUtils.closeSilently(cursor);
}
myFriends = friends;
}
public void reorderAccounts(List<MyAccount> reorderedItems) {
int order = 0;
boolean changed = false;
for (MyAccount myAccount : reorderedItems) {
order++;
if (myAccount.getOrder() != order) {
changed = true;
MyAccount.Builder builder = Builder.fromMyAccount(myContext, myAccount, "reorder", false);
builder.setOrder(order);
builder.save();
}
}
if (changed) {
CollectionsUtil.sort(mAccounts);
MyPreferences.onPreferencesChanged();
}
}
@NonNull
public static Account[] getAccounts(Context context) {
if (Permissions.checkPermission(context, Permissions.PermissionType.GET_ACCOUNTS) ) {
AccountManager am = AccountManager.get(context);
return am.getAccountsByType(AuthenticatorService.ANDROID_ACCOUNT_TYPE);
}
return new Account[]{};
}
}