package com.lody.virtual.server.accounts; import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.AuthenticatorDescription; import android.accounts.IAccountAuthenticator; import android.accounts.IAccountAuthenticatorResponse; import android.accounts.IAccountManagerResponse; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; import android.os.Parcel; import android.os.RemoteException; import android.os.SystemClock; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; import android.util.Xml; import com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.helper.compat.AccountManagerCompat; import com.lody.virtual.helper.utils.VLog; import com.lody.virtual.os.VBinder; import com.lody.virtual.os.VEnvironment; import com.lody.virtual.os.VUserHandle; import com.lody.virtual.server.IAccountManager; import com.lody.virtual.server.am.VActivityManagerService; import com.lody.virtual.server.pm.VPackageManagerService; import org.xmlpull.v1.XmlPullParser; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import mirror.com.android.internal.R_Hide; import static android.accounts.AccountManager.ERROR_CODE_BAD_ARGUMENTS; /** * @author Lody */ public class VAccountManagerService extends IAccountManager.Stub { private static final AtomicReference<VAccountManagerService> sInstance = new AtomicReference<>(); private static final long CHECK_IN_TIME = 30 * 24 * 60 * 1000L; private static final String TAG = VAccountManagerService.class.getSimpleName(); private final SparseArray<List<VAccount>> accountsByUserId = new SparseArray<>(); private final LinkedList<AuthTokenRecord> authTokenRecords = new LinkedList<>(); private final LinkedHashMap<String, Session> mSessions = new LinkedHashMap<>(); private final AuthenticatorCache cache = new AuthenticatorCache(); private Context mContext = VirtualCore.get().getContext(); private long lastAccountChangeTime = 0; public static VAccountManagerService get() { return sInstance.get(); } public static void systemReady() { VAccountManagerService service = new VAccountManagerService(); service.readAllAccounts(); sInstance.set(service); } private static AuthenticatorDescription parseAuthenticatorDescription(Resources resources, String packageName, AttributeSet attributeSet) { TypedArray array = resources.obtainAttributes(attributeSet, R_Hide.styleable.AccountAuthenticator.get()); try { String accountType = array.getString(R_Hide.styleable.AccountAuthenticator_accountType.get()); int label = array.getResourceId(R_Hide.styleable.AccountAuthenticator_label.get(), 0); int icon = array.getResourceId(R_Hide.styleable.AccountAuthenticator_icon.get(), 0); int smallIcon = array.getResourceId(R_Hide.styleable.AccountAuthenticator_smallIcon.get(), 0); int accountPreferences = array.getResourceId(R_Hide.styleable.AccountAuthenticator_accountPreferences.get(), 0); boolean customTokens = array.getBoolean(R_Hide.styleable.AccountAuthenticator_customTokens.get(), false); if (TextUtils.isEmpty(accountType)) { return null; } return new AuthenticatorDescription(accountType, packageName, label, icon, smallIcon, accountPreferences, customTokens); } finally { array.recycle(); } } @Override public AuthenticatorDescription[] getAuthenticatorTypes(int userId) { synchronized (cache) { AuthenticatorDescription[] descArray = new AuthenticatorDescription[cache.authenticators.size()]; int i = 0; for (AuthenticatorInfo info : cache.authenticators.values()) { descArray[i] = info.desc; i++; } return descArray; } } @Override public void getAccountsByFeatures(int userId, IAccountManagerResponse response, String type, String[] features) { if (response == null) throw new IllegalArgumentException("response is null"); if (type == null) throw new IllegalArgumentException("accountType is null"); AuthenticatorInfo info = getAuthenticatorInfo(type); if (info == null) { Bundle bundle = new Bundle(); bundle.putParcelableArray(AccountManager.KEY_ACCOUNTS, new Account[0]); try { response.onResult(bundle); } catch (RemoteException e) { e.printStackTrace(); } return; } if (features == null || features.length == 0) { Bundle bundle = new Bundle(); bundle.putParcelableArray(AccountManager.KEY_ACCOUNTS, getAccounts(userId, type)); try { response.onResult(bundle); } catch (RemoteException e) { e.printStackTrace(); } } else { new GetAccountsByTypeAndFeatureSession(response, userId, info, features).bind(); } } @Override public final String getPreviousName(int userId, Account account) { if (account == null) throw new IllegalArgumentException("account is null"); synchronized (accountsByUserId) { String previousName = null; VAccount vAccount = getAccount(userId, account); if (vAccount != null) { previousName = vAccount.previousName; } return previousName; } } @Override public Account[] getAccounts(int userId, String type) { List<Account> accountList = getAccountList(userId, type); return accountList.toArray(new Account[accountList.size()]); } private List<Account> getAccountList(int userId, String type) { synchronized (accountsByUserId) { List<Account> accounts = new ArrayList<>(); List<VAccount> vAccounts = accountsByUserId.get(userId); if (vAccounts != null) { for (VAccount vAccount : vAccounts) { if (type == null || vAccount.type.equals(type)) { accounts.add(new Account(vAccount.name, vAccount.type)); } } } return accounts; } } @Override public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { try { return super.onTransact(code, data, reply, flags); } catch (Throwable e) { e.printStackTrace(); throw e; } } @Override public final void getAuthToken(final int userId, final IAccountManagerResponse response, final Account account, final String authTokenType, final boolean notifyOnAuthFailure, boolean expectActivityLaunch, final Bundle loginOptions) { if (response == null) { throw new IllegalArgumentException("response is null"); } try { if (account == null) { VLog.w(TAG, "getAuthToken called with null account"); response.onError(ERROR_CODE_BAD_ARGUMENTS, "account is null"); return; } if (authTokenType == null) { VLog.w(TAG, "getAuthToken called with null authTokenType"); response.onError(ERROR_CODE_BAD_ARGUMENTS, "authTokenType is null"); return; } } catch (RemoteException e) { e.printStackTrace(); return; } AuthenticatorInfo info = getAuthenticatorInfo(account.type); if (info == null) { try { response.onError(ERROR_CODE_BAD_ARGUMENTS, "account.type does not exist"); } catch (RemoteException e) { e.printStackTrace(); } return; } // Get the calling package. We will use it for the purpose of caching. final String callerPkg = loginOptions.getString(AccountManagerCompat.KEY_ANDROID_PACKAGE_NAME); final boolean customTokens = info.desc.customTokens; loginOptions.putInt(AccountManager.KEY_CALLER_UID, VBinder.getCallingUid()); loginOptions.putInt(AccountManager.KEY_CALLER_PID, Binder.getCallingPid()); if (notifyOnAuthFailure) { loginOptions.putBoolean(AccountManagerCompat.KEY_NOTIFY_ON_FAILURE, true); } if (!customTokens) { VAccount vAccount; synchronized (accountsByUserId) { vAccount = getAccount(userId, account); } String authToken = vAccount != null ? vAccount.authTokens.get(authTokenType) : null; if (authToken != null) { Bundle result = new Bundle(); result.putString(AccountManager.KEY_AUTHTOKEN, authToken); result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type); onResult(response, result); return; } } if (customTokens) { String authToken = getCustomAuthToken(userId, account, authTokenType, callerPkg); if (authToken != null) { Bundle result = new Bundle(); result.putString(AccountManager.KEY_AUTHTOKEN, authToken); result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type); onResult(response, result); return; } } new Session(response, userId, info, expectActivityLaunch, false, account.name) { @Override protected String toDebugString(long now) { return super.toDebugString(now) + ", getAuthToken" + ", " + account + ", authTokenType " + authTokenType + ", loginOptions " + loginOptions + ", notifyOnAuthFailure " + notifyOnAuthFailure; } @Override public void run() throws RemoteException { mAuthenticator.getAuthToken(this, account, authTokenType, loginOptions); } @Override public void onResult(Bundle result) throws RemoteException { if (result != null) { String authToken = result.getString(AccountManager.KEY_AUTHTOKEN); if (authToken != null) { String name = result.getString(AccountManager.KEY_ACCOUNT_NAME); String type = result.getString(AccountManager.KEY_ACCOUNT_TYPE); if (TextUtils.isEmpty(type) || TextUtils.isEmpty(name)) { onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, "the type and name should not be empty"); return; } if (!customTokens) { synchronized (accountsByUserId) { VAccount account = getAccount(userId, name, type); if (account == null) { List<VAccount> accounts = accountsByUserId.get(userId); if (accounts == null) { accounts = new ArrayList<>(); accountsByUserId.put(userId, accounts); } account = new VAccount(userId, new Account(name, type)); accounts.add(account); saveAllAccounts(); } } } long expiryMillis = result.getLong( AccountManagerCompat.KEY_CUSTOM_TOKEN_EXPIRY, 0L); if (customTokens && expiryMillis > System.currentTimeMillis()) { AuthTokenRecord record = new AuthTokenRecord(userId, account, authTokenType, callerPkg, authToken, expiryMillis); synchronized (authTokenRecords) { authTokenRecords.remove(record); authTokenRecords.add(record); } } } Intent intent = result.getParcelable(AccountManager.KEY_INTENT); if (intent != null && notifyOnAuthFailure && !customTokens) { // TODO: send Signin error Notification } } super.onResult(result); } }.bind(); } @Override public void setPassword(int userId, Account account, String password) { if (account == null) throw new IllegalArgumentException("account is null"); setPasswordInternal(userId, account, password); } private void setPasswordInternal(int userId, Account account, String password) { synchronized (accountsByUserId) { VAccount vAccount = getAccount(userId, account); if (vAccount != null) { vAccount.password = password; vAccount.authTokens.clear(); saveAllAccounts(); synchronized (authTokenRecords) { Iterator<AuthTokenRecord> iterator = authTokenRecords.iterator(); while (iterator.hasNext()) { AuthTokenRecord record = iterator.next(); if (record.userId == userId && record.account.equals(account)) { iterator.remove(); } } } sendAccountsChangedBroadcast(userId); } } } @Override public void setAuthToken(int userId, Account account, String authTokenType, String authToken) { if (account == null) throw new IllegalArgumentException("account is null"); if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null"); synchronized (accountsByUserId) { VAccount vAccount = getAccount(userId, account); if (vAccount != null) { // FIXME: cancelNotification vAccount.authTokens.put(authTokenType, authToken); this.saveAllAccounts(); } } } @Override public void setUserData(int userId, Account account, String key, String value) { if (key == null) throw new IllegalArgumentException("key is null"); if (account == null) throw new IllegalArgumentException("account is null"); VAccount vAccount = getAccount(userId, account); if (vAccount != null) { synchronized (accountsByUserId) { vAccount.userDatas.put(key, value); saveAllAccounts(); } } } @Override public void hasFeatures(int userId, IAccountManagerResponse response, final Account account, final String[] features) { if (response == null) throw new IllegalArgumentException("response is null"); if (account == null) throw new IllegalArgumentException("account is null"); if (features == null) throw new IllegalArgumentException("features is null"); AuthenticatorInfo info = this.getAuthenticatorInfo(account.type); if (info == null) { try { response.onError(ERROR_CODE_BAD_ARGUMENTS, "account.type does not exist"); } catch (RemoteException e) { e.printStackTrace(); } return; } new Session(response, userId, info, false, true, account.name) { @Override public void run() throws RemoteException { try { mAuthenticator.hasFeatures(this, account, features); } catch (RemoteException e) { onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, "remote exception"); } } @Override public void onResult(Bundle result) throws RemoteException { IAccountManagerResponse response = getResponseAndClose(); if (response != null) { try { if (result == null) { response.onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, "null bundle"); return; } Log.v(TAG, getClass().getSimpleName() + " calling onResult() on response " + response); final Bundle newResult = new Bundle(); newResult.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT, false)); response.onResult(newResult); } catch (RemoteException e) { // if the caller is dead then there is no one to care about remote exceptions Log.v(TAG, "failure while notifying response", e); } } } }.bind(); } @Override public void updateCredentials(int userId, final IAccountManagerResponse response, final Account account, final String authTokenType, final boolean expectActivityLaunch, final Bundle loginOptions) { if (response == null) throw new IllegalArgumentException("response is null"); if (account == null) throw new IllegalArgumentException("account is null"); if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null"); AuthenticatorInfo info = this.getAuthenticatorInfo(account.type); if (info == null) { try { response.onError(ERROR_CODE_BAD_ARGUMENTS, "account.type does not exist"); } catch (RemoteException e) { e.printStackTrace(); } return; } new Session(response, userId, info, expectActivityLaunch, false, account.name) { @Override public void run() throws RemoteException { mAuthenticator.updateCredentials(this, account, authTokenType, loginOptions); } @Override protected String toDebugString(long now) { if (loginOptions != null) loginOptions.keySet(); return super.toDebugString(now) + ", updateCredentials" + ", " + account + ", authTokenType " + authTokenType + ", loginOptions " + loginOptions; } }.bind(); } @Override public String getPassword(int userId, Account account) { if (account == null) throw new IllegalArgumentException("account is null"); synchronized (accountsByUserId) { VAccount vAccount = getAccount(userId, account); if (vAccount != null) { return vAccount.password; } return null; } } @Override public String getUserData(int userId, Account account, String key) { if (account == null) throw new IllegalArgumentException("account is null"); if (key == null) throw new IllegalArgumentException("key is null"); synchronized (accountsByUserId) { VAccount vAccount = getAccount(userId, account); if (vAccount != null) { return vAccount.userDatas.get(key); } return null; } } @Override public void editProperties(int userId, IAccountManagerResponse response, final String accountType, final boolean expectActivityLaunch) { if (response == null) throw new IllegalArgumentException("response is null"); if (accountType == null) throw new IllegalArgumentException("accountType is null"); AuthenticatorInfo info = this.getAuthenticatorInfo(accountType); if (info == null) { try { response.onError(ERROR_CODE_BAD_ARGUMENTS, "account.type does not exist"); } catch (RemoteException e) { e.printStackTrace(); } return; } new Session(response, userId, info, expectActivityLaunch, true, null) { @Override public void run() throws RemoteException { mAuthenticator.editProperties(this, mAuthenticatorInfo.desc.type); } @Override protected String toDebugString(long now) { return super.toDebugString(now) + ", editProperties" + ", accountType " + accountType; } }.bind(); } @Override public void getAuthTokenLabel(int userId, IAccountManagerResponse response, final String accountType, final String authTokenType) { if (accountType == null) throw new IllegalArgumentException("accountType is null"); if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null"); AuthenticatorInfo info = getAuthenticatorInfo(accountType); if (info == null) { try { response.onError(ERROR_CODE_BAD_ARGUMENTS, "account.type does not exist"); } catch (RemoteException e) { e.printStackTrace(); } return; } new Session(response, userId, info, false, false, null) { @Override public void run() throws RemoteException { mAuthenticator.getAuthTokenLabel(this, authTokenType); } @Override public void onResult(Bundle result) throws RemoteException { if (result != null) { String label = result.getString(AccountManager.KEY_AUTH_TOKEN_LABEL); Bundle bundle = new Bundle(); bundle.putString(AccountManager.KEY_AUTH_TOKEN_LABEL, label); super.onResult(bundle); } else { super.onResult(null); } } }.bind(); } public void confirmCredentials(int userId, IAccountManagerResponse response, final Account account, final Bundle options, final boolean expectActivityLaunch) { if (response == null) throw new IllegalArgumentException("response is null"); if (account == null) throw new IllegalArgumentException("account is null"); AuthenticatorInfo info = getAuthenticatorInfo(account.type); if (info == null) { try { response.onError(ERROR_CODE_BAD_ARGUMENTS, "account.type does not exist"); } catch (RemoteException e) { e.printStackTrace(); } return; } new Session(response, userId, info, expectActivityLaunch, true, account.name, true, true) { @Override public void run() throws RemoteException { mAuthenticator.confirmCredentials(this, account, options); } }.bind(); } @Override public void addAccount(int userId, final IAccountManagerResponse response, final String accountType, final String authTokenType, final String[] requiredFeatures, final boolean expectActivityLaunch, final Bundle optionsIn) { if (response == null) throw new IllegalArgumentException("response is null"); if (accountType == null) throw new IllegalArgumentException("accountType is null"); AuthenticatorInfo info = getAuthenticatorInfo(accountType); if (info == null) { try { response.onError(ERROR_CODE_BAD_ARGUMENTS, "account.type does not exist"); } catch (RemoteException e) { e.printStackTrace(); } return; } new Session(response, userId, info, expectActivityLaunch, true, null, false, true) { @Override public void run() throws RemoteException { mAuthenticator.addAccount(this, mAuthenticatorInfo.desc.type, authTokenType, requiredFeatures, optionsIn); } @Override protected String toDebugString(long now) { return super.toDebugString(now) + ", addAccount" + ", accountType " + accountType + ", requiredFeatures " + (requiredFeatures != null ? TextUtils.join(",", requiredFeatures) : null); } }.bind(); } @Override public boolean addAccountExplicitly(int userId, Account account, String password, Bundle extras) { if (account == null) throw new IllegalArgumentException("account is null"); return insertAccountIntoDatabase(userId, account, password, extras); } @Override public boolean removeAccountExplicitly(int userId, Account account) { return account != null && removeAccountInternal(userId, account); } @Override public void renameAccount(int userId, IAccountManagerResponse response, Account accountToRename, String newName) { if (accountToRename == null) throw new IllegalArgumentException("account is null"); Account resultingAccount = renameAccountInternal(userId, accountToRename, newName); Bundle result = new Bundle(); result.putString(AccountManager.KEY_ACCOUNT_NAME, resultingAccount.name); result.putString(AccountManager.KEY_ACCOUNT_TYPE, resultingAccount.type); try { response.onResult(result); } catch (RemoteException e) { Log.w(TAG, e.getMessage()); } } @Override public void removeAccount(final int userId, IAccountManagerResponse response, final Account account, boolean expectActivityLaunch) { if (response == null) throw new IllegalArgumentException("response is null"); if (account == null) throw new IllegalArgumentException("account is null"); AuthenticatorInfo info = this.getAuthenticatorInfo(account.type); if (info == null) { try { response.onError(ERROR_CODE_BAD_ARGUMENTS, "account.type does not exist"); } catch (RemoteException e) { e.printStackTrace(); } return; } // FIXME: Cancel Notification new Session(response, userId, info, expectActivityLaunch, true, account.name) { @Override protected String toDebugString(long now) { return super.toDebugString(now) + ", removeAccount" + ", account " + account; } @Override public void run() throws RemoteException { mAuthenticator.getAccountRemovalAllowed(this, account); } @Override public void onResult(Bundle result) throws RemoteException { if (result != null && result.containsKey(AccountManager.KEY_BOOLEAN_RESULT) && !result.containsKey(AccountManager.KEY_INTENT)) { final boolean removalAllowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT); if (removalAllowed) { removeAccountInternal(userId, account); } IAccountManagerResponse response = getResponseAndClose(); if (response != null) { Log.v(TAG, getClass().getSimpleName() + " calling onResult() on response " + response); Bundle result2 = new Bundle(); result2.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, removalAllowed); try { response.onResult(result2); } catch (RemoteException e) { e.printStackTrace(); } } } super.onResult(result); } }.bind(); } @Override public void clearPassword(int userId, Account account) { if (account == null) throw new IllegalArgumentException("account is null"); setPasswordInternal(userId, account, null); } private boolean removeAccountInternal(int userId, Account account) { List<VAccount> accounts = accountsByUserId.get(userId); if (accounts != null) { Iterator<VAccount> iterator = accounts.iterator(); while (iterator.hasNext()) { VAccount vAccount = iterator.next(); if (userId == vAccount.userId && TextUtils.equals(vAccount.name, account.name) && TextUtils.equals(account.type, vAccount.type)) { iterator.remove(); saveAllAccounts(); sendAccountsChangedBroadcast(userId); return true; } } } return false; } @Override public boolean accountAuthenticated(int userId, final Account account) { if (account == null) { throw new IllegalArgumentException("account is null"); } synchronized (accountsByUserId) { VAccount vAccount = getAccount(userId, account); if (vAccount != null) { vAccount.lastAuthenticatedTime = System.currentTimeMillis(); saveAllAccounts(); return true; } return false; } } @Override public void invalidateAuthToken(int userId, String accountType, String authToken) { if (accountType == null) throw new IllegalArgumentException("accountType is null"); if (authToken == null) throw new IllegalArgumentException("authToken is null"); synchronized (accountsByUserId) { List<VAccount> accounts = accountsByUserId.get(userId); if (accounts != null) { boolean changed = false; for (VAccount account : accounts) { if (account.type.equals(accountType)) { account.authTokens.values().remove(authToken); changed = true; } } if (changed) { saveAllAccounts(); } } synchronized (authTokenRecords) { Iterator<AuthTokenRecord> iterator = authTokenRecords.iterator(); while (iterator.hasNext()) { AuthTokenRecord record = iterator.next(); if (record.userId == userId && record.authTokenType.equals(accountType) && record.authToken.equals(authToken)) { iterator.remove(); } } } } } private Account renameAccountInternal(int userId, Account accountToRename, String newName) { // TODO: Cancel Notification synchronized (accountsByUserId) { VAccount vAccount = getAccount(userId, accountToRename); if (vAccount != null) { vAccount.previousName = vAccount.name; vAccount.name = newName; saveAllAccounts(); Account newAccount = new Account(vAccount.name, vAccount.type); synchronized (authTokenRecords) { for (AuthTokenRecord record : authTokenRecords) { if (record.userId == userId && record.account.equals(accountToRename)) { record.account = newAccount; } } } sendAccountsChangedBroadcast(userId); return newAccount; } } return accountToRename; } @Override public String peekAuthToken(int userId, Account account, String authTokenType) { if (account == null) throw new IllegalArgumentException("account is null"); if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null"); synchronized (accountsByUserId) { VAccount vAccount = getAccount(userId, account); if (vAccount != null) { return vAccount.authTokens.get(authTokenType); } return null; } } private String getCustomAuthToken(int userId, Account account, String authTokenType, String packageName) { AuthTokenRecord record = new AuthTokenRecord(userId, account, authTokenType, packageName); String authToken = null; long now = System.currentTimeMillis(); synchronized (authTokenRecords) { Iterator<AuthTokenRecord> iterator = authTokenRecords.iterator(); while (iterator.hasNext()) { AuthTokenRecord one = iterator.next(); if (one.expiryEpochMillis > 0 && one.expiryEpochMillis < now) { iterator.remove(); } else if (record.equals(one)) { authToken = record.authToken; } } } return authToken; } private void onResult(IAccountManagerResponse response, Bundle result) { try { response.onResult(result); } catch (RemoteException e) { // if the caller is dead then there is no one to care about remote // exceptions e.printStackTrace(); } } private AuthenticatorInfo getAuthenticatorInfo(String type) { synchronized (cache) { return type == null ? null : cache.authenticators.get(type); } } private VAccount getAccount(int userId, Account account) { return this.getAccount(userId, account.name, account.type); } private boolean insertAccountIntoDatabase(int userId, Account account, String password, Bundle extras) { if (account == null) { return false; } synchronized (accountsByUserId) { VAccount vAccount = new VAccount(userId, account); vAccount.password = password; // convert the [Bundle] to [Map<String, String>] if (extras != null) { for (String key : extras.keySet()) { Object value = extras.get(key); if (value instanceof String) { vAccount.userDatas.put(key, (String) value); } } } List<VAccount> accounts = accountsByUserId.get(userId); if (accounts == null) { accounts = new ArrayList<>(); accountsByUserId.put(userId, accounts); } accounts.add(vAccount); saveAllAccounts(); sendAccountsChangedBroadcast(vAccount.userId); return true; } } private void sendAccountsChangedBroadcast(int userId) { Intent intent = new Intent(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION); VActivityManagerService.get().sendBroadcastAsUser(intent, new VUserHandle(userId)); broadcastCheckInNowIfNeed(userId); } private void broadcastCheckInNowIfNeed(int userId) { long time = System.currentTimeMillis(); if (Math.abs(time - lastAccountChangeTime) > CHECK_IN_TIME) { lastAccountChangeTime = time; saveAllAccounts(); Intent intent = new Intent("android.server.checkin.CHECKIN_NOW"); VActivityManagerService.get().sendBroadcastAsUser(intent, new VUserHandle(userId)); } } /** * Serializing all accounts */ private void saveAllAccounts() { File accountFile = VEnvironment.getAccountConfigFile(); Parcel dest = Parcel.obtain(); try { dest.writeInt(1); List<VAccount> accounts = new ArrayList<>(); for (int i = 0; i < this.accountsByUserId.size(); i++) { List<VAccount> list = this.accountsByUserId.valueAt(i); if (list != null) { accounts.addAll(list); } } dest.writeInt(accounts.size()); for (VAccount account : accounts) { account.writeToParcel(dest, 0); } dest.writeLong(lastAccountChangeTime); FileOutputStream fileOutputStream = new FileOutputStream(accountFile); fileOutputStream.write(dest.marshall()); fileOutputStream.close(); } catch (Exception e) { e.printStackTrace(); } dest.recycle(); } /** * Read all accounts from file. */ private void readAllAccounts() { File accountFile = VEnvironment.getAccountConfigFile(); refreshAuthenticatorCache(null); if (accountFile.exists()) { accountsByUserId.clear(); Parcel dest = Parcel.obtain(); try { FileInputStream is = new FileInputStream(accountFile); byte[] bytes = new byte[(int) accountFile.length()]; int readLength = is.read(bytes); is.close(); if (readLength != bytes.length) { throw new IOException(String.format(Locale.ENGLISH, "Expect length %d, but got %d.", bytes.length, readLength)); } dest.unmarshall(bytes, 0, bytes.length); dest.setDataPosition(0); dest.readInt(); // skip the magic int size = dest.readInt(); // the VAccount's size we need to read boolean invalid = false; while (size-- > 0) { VAccount account = new VAccount(dest); VLog.d(TAG, "Reading account : " + account.type); AuthenticatorInfo info = cache.authenticators.get(account.type); if (info != null) { List<VAccount> accounts = accountsByUserId.get(account.userId); if (accounts == null) { accounts = new ArrayList<>(); accountsByUserId.put(account.userId, accounts); } accounts.add(account); } else { invalid = true; } } lastAccountChangeTime = dest.readLong(); if (invalid) { saveAllAccounts(); } } catch (Exception e) { e.printStackTrace(); } finally { dest.recycle(); } } } private VAccount getAccount(int userId, String accountName, String accountType) { List<VAccount> accounts = accountsByUserId.get(userId); if (accounts != null) { for (VAccount account : accounts) { if (TextUtils.equals(account.name, accountName) && TextUtils.equals(account.type, accountType)) { return account; } } } return null; } public void refreshAuthenticatorCache(String packageName) { cache.authenticators.clear(); Intent intent = new Intent(AccountManager.ACTION_AUTHENTICATOR_INTENT); if (packageName != null) { intent.setPackage(packageName); } generateServicesMap( VPackageManagerService.get().queryIntentServices(intent, null, PackageManager.GET_META_DATA, 0), cache.authenticators, new AppAccountParser()); } private void generateServicesMap(List<ResolveInfo> services, Map<String, AuthenticatorInfo> map, IAccountParser accountParser) { for (ResolveInfo info : services) { XmlResourceParser parser = accountParser.getParser(mContext, info.serviceInfo, AccountManager.AUTHENTICATOR_META_DATA_NAME); if (parser != null) { try { AttributeSet attributeSet = Xml.asAttributeSet(parser); int type; while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) { // Nothing to do } if (AccountManager.AUTHENTICATOR_ATTRIBUTES_NAME.equals(parser.getName())) { AuthenticatorDescription desc = parseAuthenticatorDescription( accountParser.getResources(mContext, info.serviceInfo.applicationInfo), info.serviceInfo.packageName, attributeSet); if (desc != null) { map.put(desc.type, new AuthenticatorInfo(desc, info.serviceInfo)); } } } catch (Exception e) { e.printStackTrace(); } } } } final static class AuthTokenRecord { public int userId; public Account account; public long expiryEpochMillis; public String authToken; private String authTokenType; private String packageName; AuthTokenRecord(int userId, Account account, String authTokenType, String packageName, String authToken, long expiryEpochMillis) { this.userId = userId; this.account = account; this.authTokenType = authTokenType; this.packageName = packageName; this.authToken = authToken; this.expiryEpochMillis = expiryEpochMillis; } AuthTokenRecord(int userId, Account account, String authTokenType, String packageName) { this.userId = userId; this.account = account; this.authTokenType = authTokenType; this.packageName = packageName; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; AuthTokenRecord that = (AuthTokenRecord) o; return userId == that.userId && account.equals(that.account) && authTokenType.equals(that.authTokenType) && packageName.equals(that.packageName); } @Override public int hashCode() { return ((this.userId * 31 + this.account.hashCode()) * 31 + this.authTokenType.hashCode()) * 31 + this.packageName.hashCode(); } } private final class AuthenticatorInfo { final AuthenticatorDescription desc; final ServiceInfo serviceInfo; AuthenticatorInfo(AuthenticatorDescription desc, ServiceInfo info) { this.desc = desc; this.serviceInfo = info; } } private final class AuthenticatorCache { final Map<String, AuthenticatorInfo> authenticators = new HashMap<>(); } private abstract class Session extends IAccountAuthenticatorResponse.Stub implements IBinder.DeathRecipient, ServiceConnection { final int mUserId; final AuthenticatorInfo mAuthenticatorInfo; private final boolean mStripAuthTokenFromResult; public int mNumResults; IAccountAuthenticator mAuthenticator; private IAccountManagerResponse mResponse; private boolean mExpectActivityLaunch; private long mCreationTime; private String mAccountName; private boolean mAuthDetailsRequired; private boolean mUpdateLastAuthenticatedTime; private int mNumRequestContinued; private int mNumErrors; Session(IAccountManagerResponse response, int userId, AuthenticatorInfo info, boolean expectActivityLaunch, boolean stripAuthTokenFromResult, String accountName, boolean authDetailsRequired, boolean updateLastAuthenticatedTime) { if (info == null) throw new IllegalArgumentException("accountType is null"); this.mStripAuthTokenFromResult = stripAuthTokenFromResult; this.mResponse = response; this.mUserId = userId; this.mAuthenticatorInfo = info; this.mExpectActivityLaunch = expectActivityLaunch; this.mCreationTime = SystemClock.elapsedRealtime(); this.mAccountName = accountName; this.mAuthDetailsRequired = authDetailsRequired; this.mUpdateLastAuthenticatedTime = updateLastAuthenticatedTime; synchronized (mSessions) { mSessions.put(toString(), this); } if (response != null) { try { response.asBinder().linkToDeath(this, 0 /* flags */); } catch (RemoteException e) { mResponse = null; binderDied(); } } } Session(IAccountManagerResponse response, int userId, AuthenticatorInfo info, boolean expectActivityLaunch, boolean stripAuthTokenFromResult, String accountName) { this(response, userId, info, expectActivityLaunch, stripAuthTokenFromResult, accountName, false, false); } IAccountManagerResponse getResponseAndClose() { if (mResponse == null) { // this session has already been closed return null; } IAccountManagerResponse response = mResponse; close(); // this clears mResponse so we need to save the response before this call return response; } private void close() { synchronized (mSessions) { if (mSessions.remove(toString()) == null) { // the session was already closed, so bail out now return; } } if (mResponse != null) { // stop listening for response deaths mResponse.asBinder().unlinkToDeath(this, 0 /* flags */); // clear this so that we don't accidentally send any further results mResponse = null; } unbind(); } @Override public void onServiceConnected(ComponentName name, IBinder service) { mAuthenticator = IAccountAuthenticator.Stub.asInterface(service); try { run(); } catch (RemoteException e) { onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, "remote exception"); } } @Override public void onRequestContinued() { mNumRequestContinued++; } @Override public void onError(int errorCode, String errorMessage) { mNumErrors++; IAccountManagerResponse response = getResponseAndClose(); if (response != null) { Log.v(TAG, getClass().getSimpleName() + " calling onError() on response " + response); try { response.onError(errorCode, errorMessage); } catch (RemoteException e) { Log.v(TAG, "Session.onError: caught RemoteException while responding", e); } } else { Log.v(TAG, "Session.onError: already closed"); } } @Override public void onServiceDisconnected(ComponentName name) { mAuthenticator = null; IAccountManagerResponse response = getResponseAndClose(); if (response != null) { try { response.onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, "disconnected"); } catch (RemoteException e) { Log.v(TAG, "Session.onServiceDisconnected: " + "caught RemoteException while responding", e); } } } @Override public void onResult(Bundle result) throws RemoteException { mNumResults++; if (result != null) { boolean isSuccessfulConfirmCreds = result.getBoolean( AccountManager.KEY_BOOLEAN_RESULT, false); boolean isSuccessfulUpdateCredsOrAddAccount = result.containsKey(AccountManager.KEY_ACCOUNT_NAME) && result.containsKey(AccountManager.KEY_ACCOUNT_TYPE); // We should only update lastAuthenticated time, if // mUpdateLastAuthenticatedTime is true and the confirmRequest // or updateRequest was successful boolean needUpdate = mUpdateLastAuthenticatedTime && (isSuccessfulConfirmCreds || isSuccessfulUpdateCredsOrAddAccount); if (needUpdate || mAuthDetailsRequired) { synchronized (accountsByUserId) { VAccount account = getAccount(mUserId, mAccountName, mAuthenticatorInfo.desc.type); if (needUpdate && account != null) { account.lastAuthenticatedTime = System.currentTimeMillis(); saveAllAccounts(); } if (mAuthDetailsRequired) { long lastAuthenticatedTime = -1; if (account != null) { lastAuthenticatedTime = account.lastAuthenticatedTime; } result.putLong(AccountManagerCompat.KEY_LAST_AUTHENTICATED_TIME, lastAuthenticatedTime); } } } } if (result != null && !TextUtils.isEmpty(result.getString(AccountManager.KEY_AUTHTOKEN))) { // String accountName = result.getString(AccountManager.KEY_ACCOUNT_NAME); // String accountType = result.getString(AccountManager.KEY_ACCOUNT_TYPE); // if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) { // Account account = new Account(accountName, accountType); // FIXME: Cancel Notification // } } Intent intent = null; if (result != null) { intent = result.getParcelable(AccountManager.KEY_INTENT); } IAccountManagerResponse response; if (mExpectActivityLaunch && result != null && result.containsKey(AccountManager.KEY_INTENT)) { response = mResponse; } else { response = getResponseAndClose(); } if (response != null) { try { if (result == null) { Log.v(TAG, getClass().getSimpleName() + " calling onError() on response " + response); response.onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, "null bundle returned"); } else { if (mStripAuthTokenFromResult) { result.remove(AccountManager.KEY_AUTHTOKEN); } Log.v(TAG, getClass().getSimpleName() + " calling onResult() on response " + response); if ((result.getInt(AccountManager.KEY_ERROR_CODE, -1) > 0) && (intent == null)) { // All AccountManager error codes are greater than 0 response.onError(result.getInt(AccountManager.KEY_ERROR_CODE), result.getString(AccountManager.KEY_ERROR_MESSAGE)); } else { response.onResult(result); } } } catch (RemoteException e) { // if the caller is dead then there is no one to care about remote exceptions Log.v(TAG, "failure while notifying response", e); } } } public abstract void run() throws RemoteException; void bind() { Log.v(TAG, "initiating bind to authenticator type " + mAuthenticatorInfo.desc.type); Intent intent = new Intent(); intent.setAction(AccountManager.ACTION_AUTHENTICATOR_INTENT); intent.setClassName(mAuthenticatorInfo.serviceInfo.packageName, mAuthenticatorInfo.serviceInfo.name); intent.putExtra("_VA_|_user_id_", mUserId); if (!mContext.bindService(intent, this, Context.BIND_AUTO_CREATE)) { Log.d(TAG, "bind attempt failed for " + toDebugString()); onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, "bind failure"); } } protected String toDebugString() { return toDebugString(SystemClock.elapsedRealtime()); } protected String toDebugString(long now) { return "Session: expectLaunch " + mExpectActivityLaunch + ", connected " + (mAuthenticator != null) + ", stats (" + mNumResults + "/" + mNumRequestContinued + "/" + mNumErrors + ")" + ", lifetime " + ((now - mCreationTime) / 1000.0); } private void unbind() { if (mAuthenticator != null) { mAuthenticator = null; mContext.unbindService(this); } } @Override public void binderDied() { mResponse = null; close(); } } private class GetAccountsByTypeAndFeatureSession extends Session { private final String[] mFeatures; private volatile Account[] mAccountsOfType = null; private volatile ArrayList<Account> mAccountsWithFeatures = null; private volatile int mCurrentAccount = 0; public GetAccountsByTypeAndFeatureSession(IAccountManagerResponse response, int userId, AuthenticatorInfo info, String[] features) { super(response, userId, info, false /* expectActivityLaunch */, true /* stripAuthTokenFromResult */, null /* accountName */); mFeatures = features; } @Override public void run() throws RemoteException { mAccountsOfType = getAccounts(mUserId, mAuthenticatorInfo.desc.type); // check whether each account matches the requested features mAccountsWithFeatures = new ArrayList<Account>(mAccountsOfType.length); mCurrentAccount = 0; checkAccount(); } public void checkAccount() { if (mCurrentAccount >= mAccountsOfType.length) { sendResult(); return; } final IAccountAuthenticator accountAuthenticator = mAuthenticator; if (accountAuthenticator == null) { // It is possible that the authenticator has died, which is indicated by // mAuthenticator being set to null. If this happens then just abort. // There is no need to send back a result or error in this case since // that already happened when mAuthenticator was cleared. Log.v(TAG, "checkAccount: aborting session since we are no longer" + " connected to the authenticator, " + toDebugString()); return; } try { accountAuthenticator.hasFeatures(this, mAccountsOfType[mCurrentAccount], mFeatures); } catch (RemoteException e) { onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, "remote exception"); } } @Override public void onResult(Bundle result) { mNumResults++; if (result == null) { onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, "null bundle"); return; } if (result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT, false)) { mAccountsWithFeatures.add(mAccountsOfType[mCurrentAccount]); } mCurrentAccount++; checkAccount(); } public void sendResult() { IAccountManagerResponse response = getResponseAndClose(); if (response != null) { try { Account[] accounts = new Account[mAccountsWithFeatures.size()]; for (int i = 0; i < accounts.length; i++) { accounts[i] = mAccountsWithFeatures.get(i); } if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, getClass().getSimpleName() + " calling onResult() on response " + response); } Bundle result = new Bundle(); result.putParcelableArray(AccountManager.KEY_ACCOUNTS, accounts); response.onResult(result); } catch (RemoteException e) { // if the caller is dead then there is no one to care about remote exceptions Log.v(TAG, "failure while notifying response", e); } } } @Override protected String toDebugString(long now) { return super.toDebugString(now) + ", getAccountsByTypeAndFeatures" + ", " + (mFeatures != null ? TextUtils.join(",", mFeatures) : null); } } }