From 0b25179dab10dc7dfb91210cabfe637f3067d777 Mon Sep 17 00:00:00 2001 From: Martin Hibdon Date: Tue, 3 Dec 2013 16:55:48 -0800 Subject: [PATCH] Allow database to hold oauth credentials Change-Id: I127297fd78c7676995f1dcfa59fbbcafe4e72e8e --- .../android/emailcommon/provider/Account.java | 52 ++++-- .../emailcommon/provider/Credential.java | 158 ++++++++++++++++++ .../emailcommon/provider/EmailContent.java | 14 ++ .../emailcommon/provider/HostAuth.java | 64 ++++++- src/com/android/email/provider/DBHelper.java | 37 +++- .../android/email/provider/EmailProvider.java | 14 ++ 6 files changed, 319 insertions(+), 20 deletions(-) create mode 100644 emailcommon/src/com/android/emailcommon/provider/Credential.java diff --git a/emailcommon/src/com/android/emailcommon/provider/Account.java b/emailcommon/src/com/android/emailcommon/provider/Account.java index 1f152cf83..18e37b255 100755 --- a/emailcommon/src/com/android/emailcommon/provider/Account.java +++ b/emailcommon/src/com/android/emailcommon/provider/Account.java @@ -31,11 +31,9 @@ import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; -import android.text.TextUtils; import com.android.emailcommon.provider.EmailContent.AccountColumns; import com.android.emailcommon.utility.Utility; -import com.android.mail.utils.LogUtils; import java.util.ArrayList; import java.util.List; @@ -207,9 +205,6 @@ public final class Account extends EmailContent implements AccountColumns, Parce MailboxColumns.TYPE + " = " + Mailbox.TYPE_INBOX + " AND " + MailboxColumns.ACCOUNT_KEY + " =?"; - /** - * no public constructor since this is a utility class - */ public Account() { mBaseUri = CONTENT_URI; @@ -739,22 +734,55 @@ public final class Account extends EmailContent implements AccountColumns, Parce int index = 0; int recvIndex = -1; + int recvCredentialsIndex = -1; int sendIndex = -1; + int sendCredentialsIndex = -1; - // Create operations for saving the send and recv hostAuths + // Create operations for saving the send and recv hostAuths, and their credentials. // Also, remember which operation in the array they represent ArrayList ops = new ArrayList(); if (mHostAuthRecv != null) { + if (mHostAuthRecv.mCredential != null) { + recvCredentialsIndex = index++; + ops.add(ContentProviderOperation.newInsert(mHostAuthRecv.mCredential.mBaseUri) + .withValues(mHostAuthRecv.mCredential.toContentValues()) + .build()); + } + recvIndex = index++; - ops.add(ContentProviderOperation.newInsert(mHostAuthRecv.mBaseUri) - .withValues(mHostAuthRecv.toContentValues()) - .build()); + final ContentProviderOperation.Builder b = ContentProviderOperation.newInsert( + mHostAuthRecv.mBaseUri); + b.withValues(mHostAuthRecv.toContentValues()); + if (recvCredentialsIndex >= 0) { + final ContentValues cv = new ContentValues(); + cv.put(HostAuth.CREDENTIAL_KEY, recvCredentialsIndex); + b.withValueBackReferences(cv); + } + ops.add(b.build()); } if (mHostAuthSend != null) { + if (mHostAuthSend.mCredential != null) { + if (mHostAuthRecv.mCredential != null && + mHostAuthRecv.mCredential.equals(mHostAuthSend.mCredential)) { + // These two credentials are identical, use the same row. + sendCredentialsIndex = recvCredentialsIndex; + } else { + sendCredentialsIndex = index++; + ops.add(ContentProviderOperation.newInsert(mHostAuthRecv.mCredential.mBaseUri) + .withValues(mHostAuthRecv.mCredential.toContentValues()) + .build()); + } + } sendIndex = index++; - ops.add(ContentProviderOperation.newInsert(mHostAuthSend.mBaseUri) - .withValues(mHostAuthSend.toContentValues()) - .build()); + final ContentProviderOperation.Builder b = ContentProviderOperation.newInsert( + mHostAuthSend.mBaseUri); + b.withValues(mHostAuthSend.toContentValues()); + if (sendCredentialsIndex >= 0) { + final ContentValues cv = new ContentValues(); + cv.put(HostAuth.CREDENTIAL_KEY, sendCredentialsIndex); + b.withValueBackReferences(cv); + } + ops.add(b.build()); } // Now do the Account diff --git a/emailcommon/src/com/android/emailcommon/provider/Credential.java b/emailcommon/src/com/android/emailcommon/provider/Credential.java new file mode 100644 index 000000000..dbb5932a9 --- /dev/null +++ b/emailcommon/src/com/android/emailcommon/provider/Credential.java @@ -0,0 +1,158 @@ +package com.android.emailcommon.provider; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.emailcommon.provider.EmailContent; +import com.android.emailcommon.utility.Utility; +import com.google.common.base.Objects; + +public class Credential extends EmailContent implements Parcelable { + + public static final String TABLE_NAME = "Credential"; + public static Uri CONTENT_URI; + + public static final Credential EMPTY = new Credential(-1, "", "", 0); + + public static void initCredential() { + CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/credential"); + } + + public static final String TYPE_OAUTH = "oauth"; + + public String mAccessToken; + public String mRefreshToken; + public long mExpiration; + + // Name of the authentication provider. + public static final String PROVIDER_COLUMN = "provider"; + // Access token. + public static final String ACCESS_TOKEN_COLUMN = "accessToken"; + // Refresh token. + public static final String REFRESH_TOKEN_COLUMN = "refreshToken"; + // Expiration date for these credentials. + public static final String EXPIRATION_COLUMN = "expiration"; + + + public interface CredentialQuery { + public static final int ID_COLUMN_INDEX = 0; + public static final int PROVIDER_COLUMN_INDEX = 1; + public static final int ACCESS_TOKEN_COLUMN_INDEX = 2; + public static final int REFRESH_TOKEN_COLUMN_INDEX = 3; + public static final int EXPIRATION_COLUMN_INDEX = 4; + + public static final String[] PROJECTION = new String[] { + RECORD_ID, + PROVIDER_COLUMN, + ACCESS_TOKEN_COLUMN, + REFRESH_TOKEN_COLUMN, + EXPIRATION_COLUMN + }; + } + + public Credential() { + mBaseUri = CONTENT_URI; + } + + public Credential(long id, String accessToken, String refreshToken, long expiration) { + mBaseUri = CONTENT_URI; + mId = id; + mAccessToken = accessToken; + mRefreshToken = refreshToken; + mExpiration = expiration; + } + + /** + * Restore a Credential from the database, given its unique id + * @param context + * @param id + * @return the instantiated Credential + */ + public static Credential restoreCredentialsWithId(Context context, long id) { + return EmailContent.restoreContentWithId(context, Credential.class, + Credential.CONTENT_URI, CredentialQuery.PROJECTION, id); + } + + @Override + public void restore(Cursor cursor) { + mBaseUri = CONTENT_URI; + mId = cursor.getLong(CredentialQuery.ID_COLUMN_INDEX); + mAccessToken = cursor.getString(CredentialQuery.ACCESS_TOKEN_COLUMN_INDEX); + mRefreshToken = cursor.getString(CredentialQuery.REFRESH_TOKEN_COLUMN_INDEX); + mExpiration = cursor.getInt(CredentialQuery.EXPIRATION_COLUMN_INDEX); + } + + /** + * Supports Parcelable + */ + @Override + public int describeContents() { + return 0; + } + + /** + * Supports Parcelable + */ + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public Credential createFromParcel(Parcel in) { + return new Credential(in); + } + + @Override + public Credential[] newArray(int size) { + return new Credential[size]; + } + }; + + @Override + public void writeToParcel(Parcel dest, int flags) { + // mBaseUri is not parceled + dest.writeLong(mId); + dest.writeString(mAccessToken); + dest.writeString(mRefreshToken); + dest.writeLong(mExpiration); + } + + /** + * Supports Parcelable + */ + public Credential(Parcel in) { + mBaseUri = CONTENT_URI; + mId = in.readLong(); + mAccessToken = in.readString(); + mRefreshToken = in.readString(); + mExpiration = in.readLong(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Credential)) { + return false; + } + Credential that = (Credential)o; + return Utility.areStringsEqual(mAccessToken, that.mAccessToken) + && Utility.areStringsEqual(mRefreshToken, that.mRefreshToken) + && mExpiration == that.mExpiration; + } + + @Override + public int hashCode() { + return Objects.hashCode(mAccessToken, mRefreshToken, mExpiration); + } + + @Override + public ContentValues toContentValues() { + ContentValues values = new ContentValues(); + values.put(ACCESS_TOKEN_COLUMN, mAccessToken); + values.put(REFRESH_TOKEN_COLUMN, mRefreshToken); + values.put(EXPIRATION_COLUMN, mExpiration); + return values; + } + +} diff --git a/emailcommon/src/com/android/emailcommon/provider/EmailContent.java b/emailcommon/src/com/android/emailcommon/provider/EmailContent.java index 0abaa7975..6e13f0477 100755 --- a/emailcommon/src/com/android/emailcommon/provider/EmailContent.java +++ b/emailcommon/src/com/android/emailcommon/provider/EmailContent.java @@ -27,12 +27,14 @@ import android.content.res.Resources; import android.database.Cursor; import android.net.Uri; import android.os.Environment; +import android.os.Looper; import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; import com.android.emailcommon.utility.TextUtilities; import com.android.emailcommon.utility.Utility; +import com.android.emailcommon.Logging; import com.android.emailcommon.R; import com.android.mail.providers.UIProvider; import com.android.mail.utils.LogUtils; @@ -160,6 +162,7 @@ public abstract class EmailContent { Mailbox.initMailbox(); QuickResponse.initQuickResponse(); HostAuth.initHostAuth(); + Credential.initCredential(); Policy.initPolicy(); Message.initMessage(); MessageMove.init(); @@ -169,6 +172,14 @@ public abstract class EmailContent { } } + + private static void warnIfUiThread() { + if (Looper.getMainLooper().getThread() == Thread.currentThread()) { + LogUtils.w(Logging.LOG_TAG, "Method called on the UI thread", + new Throwable()); + } + } + public static boolean isInitialSyncKey(final String syncKey) { return syncKey == null || syncKey.isEmpty() || syncKey.equals("0"); } @@ -197,6 +208,7 @@ public abstract class EmailContent { */ public static T restoreContentWithId(Context context, Class klass, Uri contentUri, String[] contentProjection, long id) { + warnIfUiThread(); Uri u = ContentUris.withAppendedId(contentUri, id); Cursor c = context.getContentResolver().query(u, contentProjection, null, null, null); if (c == null) throw new ProviderUnavailableException(); @@ -1718,6 +1730,8 @@ public abstract class EmailContent { static final String ACCOUNT_KEY = "accountKey"; // A blob containing an X509 server certificate static final String SERVER_CERT = "serverCert"; + // The credentials row this hostAuth should use. Currently only set if using OAuth. + static final String CREDENTIAL_KEY = "credentialKey"; } public interface PolicyColumns { diff --git a/emailcommon/src/com/android/emailcommon/provider/HostAuth.java b/emailcommon/src/com/android/emailcommon/provider/HostAuth.java index 5e17a5106..8c822a392 100644 --- a/emailcommon/src/com/android/emailcommon/provider/HostAuth.java +++ b/emailcommon/src/com/android/emailcommon/provider/HostAuth.java @@ -52,8 +52,9 @@ public final class HostAuth extends EmailContent implements HostAuthColumns, Par public static final int FLAG_TLS = 0x02; // Use TLS public static final int FLAG_AUTHENTICATE = 0x04; // Use name/password for authentication public static final int FLAG_TRUST_ALL = 0x08; // Trust all certificates + public static final int FLAG_OAUTH = 0x10; // Use OAuth for authentication // Mask of settings directly configurable by the user - public static final int USER_CONFIG_MASK = 0x0b; + public static final int USER_CONFIG_MASK = 0x1b; public String mProtocol; public String mAddress; @@ -65,6 +66,9 @@ public final class HostAuth extends EmailContent implements HostAuthColumns, Par public String mClientCertAlias = null; // NOTE: The server certificate is NEVER automatically retrieved from EmailProvider public byte[] mServerCert = null; + public long mCredentialKey; + + public transient Credential mCredential; public static final int CONTENT_ID_COLUMN = 0; public static final int CONTENT_PROTOCOL_COLUMN = 1; @@ -75,16 +79,15 @@ public final class HostAuth extends EmailContent implements HostAuthColumns, Par public static final int CONTENT_PASSWORD_COLUMN = 6; public static final int CONTENT_DOMAIN_COLUMN = 7; public static final int CONTENT_CLIENT_CERT_ALIAS_COLUMN = 8; + public static final int CONTENT_CREDENTIAL_KEY_COLUMN = 9; public static final String[] CONTENT_PROJECTION = new String[] { RECORD_ID, HostAuthColumns.PROTOCOL, HostAuthColumns.ADDRESS, HostAuthColumns.PORT, HostAuthColumns.FLAGS, HostAuthColumns.LOGIN, - HostAuthColumns.PASSWORD, HostAuthColumns.DOMAIN, HostAuthColumns.CLIENT_CERT_ALIAS + HostAuthColumns.PASSWORD, HostAuthColumns.DOMAIN, HostAuthColumns.CLIENT_CERT_ALIAS, + HostAuthColumns.CREDENTIAL_KEY }; - /** - * no public constructor since this is a utility class - */ public HostAuth() { mBaseUri = CONTENT_URI; @@ -92,6 +95,41 @@ public final class HostAuth extends EmailContent implements HostAuthColumns, Par mPort = PORT_UNKNOWN; } + /** + * getOrCreateCredentials + * Return the credential object for this HostAuth, creating it if it does not yet exist. + * This should not be called on the main thread. + * @param context + * @return the credential object for this HostAuth + */ + public Credential getOrCreateCredentials(Context context) { + + if (mCredential == null) { + if (mCredentialKey >= 0) { + mCredential = Credential.restoreCredentialsWithId(context, mCredentialKey); + } else { + mCredential = new Credential(); + } + } + return mCredential; + } + + /** + * getCredentials + * Return the credential object for this HostAuth, or null if it does not exist. + * This should not be called on the main thread. + * @param context + * @return + */ + public Credential getCredentials(Context context) { + if (mCredential == null) { + if (mCredentialKey >= 0) { + mCredential = Credential.restoreCredentialsWithId(context, mCredentialKey); + } + } + return mCredential; + } + /** * Restore a HostAuth from the database, given its unique id * @param context @@ -181,6 +219,7 @@ public final class HostAuth extends EmailContent implements HostAuthColumns, Par mPassword = cursor.getString(CONTENT_PASSWORD_COLUMN); mDomain = cursor.getString(CONTENT_DOMAIN_COLUMN); mClientCertAlias = cursor.getString(CONTENT_CLIENT_CERT_ALIAS_COLUMN); + mCredentialKey = cursor.getLong(CONTENT_CREDENTIAL_KEY_COLUMN); } @Override @@ -194,6 +233,7 @@ public final class HostAuth extends EmailContent implements HostAuthColumns, Par values.put(HostAuthColumns.PASSWORD, mPassword); values.put(HostAuthColumns.DOMAIN, mDomain); values.put(HostAuthColumns.CLIENT_CERT_ALIAS, mClientCertAlias); + values.put(HostAuthColumns.CREDENTIAL_KEY, mCredentialKey); values.put(HostAuthColumns.ACCOUNT_KEY, 0); // Need something to satisfy the DB return values; } @@ -330,6 +370,12 @@ public final class HostAuth extends EmailContent implements HostAuthColumns, Par dest.writeString(mPassword); dest.writeString(mDomain); dest.writeString(mClientCertAlias); + dest.writeLong(mCredentialKey); + if (mCredential == null) { + Credential.EMPTY.writeToParcel(dest, flags); + } else { + mCredential.writeToParcel(dest, flags); + } } /** @@ -346,6 +392,11 @@ public final class HostAuth extends EmailContent implements HostAuthColumns, Par mPassword = in.readString(); mDomain = in.readString(); mClientCertAlias = in.readString(); + mCredentialKey = in.readLong(); + mCredential = new Credential(in); + if (mCredential.equals(Credential.EMPTY)) { + mCredential = null; + } } @Override @@ -362,7 +413,8 @@ public final class HostAuth extends EmailContent implements HostAuthColumns, Par && Utility.areStringsEqual(mLogin, that.mLogin) && Utility.areStringsEqual(mPassword, that.mPassword) && Utility.areStringsEqual(mDomain, that.mDomain) - && Utility.areStringsEqual(mClientCertAlias, that.mClientCertAlias); + && Utility.areStringsEqual(mClientCertAlias, that.mClientCertAlias) + && mCredentialKey == that.mCredentialKey; // We don't care about the server certificate for equals } diff --git a/src/com/android/email/provider/DBHelper.java b/src/com/android/email/provider/DBHelper.java index 3a29c4a31..d066bd6e8 100644 --- a/src/com/android/email/provider/DBHelper.java +++ b/src/com/android/email/provider/DBHelper.java @@ -32,6 +32,7 @@ import com.android.email.R; import com.android.email2.ui.MailActivityEmail; import com.android.emailcommon.mail.Address; import com.android.emailcommon.provider.Account; +import com.android.emailcommon.provider.Credential; import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.EmailContent.AccountColumns; import com.android.emailcommon.provider.EmailContent.Attachment; @@ -94,6 +95,15 @@ public final class DBHelper { " where " + EmailContent.RECORD_ID + "=old." + AccountColumns.POLICY_KEY + "; end"; + private static final String TRIGGER_HOST_AUTH_DELETE = + "create trigger host_auth_delete after delete on " + HostAuth.TABLE_NAME + + " begin delete from " + Credential.TABLE_NAME + + " where " + Credential.RECORD_ID + "=old." + HostAuth.CREDENTIAL_KEY + + " and (select count(*) from " + HostAuth.TABLE_NAME + " where " + + HostAuth.CREDENTIAL_KEY + "=old." + HostAuth.CREDENTIAL_KEY + ")=0" + + "; end"; + + // Any changes to the database format *must* include update-in-place code. // Original version: 3 // Version 4: Database wipe required; changing AccountManager interface w/Exchange @@ -163,7 +173,8 @@ public final class DBHelper { // Version 122: Need to update Message_Updates and Message_Deletes to match previous. // Version 123: Changed the duplicateMesage deletion trigger to ignore accounts that aren't // exchange accounts. - public static final int DATABASE_VERSION = 123; + // Version 124: Add credentials table for OAuth. + public static final int DATABASE_VERSION = 124; // Any changes to the database format *must* include update-in-place code. // Original version: 2 @@ -216,6 +227,17 @@ public final class DBHelper { "; end"); } + static void createCredentialsTable(SQLiteDatabase db) { + String s = " (" + Credential.RECORD_ID + " integer primary key autoincrement, " + + Credential.PROVIDER_COLUMN + " text," + + Credential.ACCESS_TOKEN_COLUMN + " text," + + Credential.REFRESH_TOKEN_COLUMN + " text," + + Credential.EXPIRATION_COLUMN + " integer" + + ");"; + db.execSQL("create table " + Credential.TABLE_NAME + s); + db.execSQL(TRIGGER_HOST_AUTH_DELETE); + } + static void dropDeleteDuplicateMessagesTrigger(final SQLiteDatabase db) { db.execSQL("drop trigger message_delete_duplicates_on_insert"); } @@ -535,7 +557,8 @@ public final class DBHelper { + HostAuthColumns.DOMAIN + " text, " + HostAuthColumns.ACCOUNT_KEY + " integer," + HostAuthColumns.CLIENT_CERT_ALIAS + " text," - + HostAuthColumns.SERVER_CERT + " blob" + + HostAuthColumns.SERVER_CERT + " blob," + + HostAuthColumns.CREDENTIAL_KEY + " integer" + ");"; db.execSQL("create table " + HostAuth.TABLE_NAME + s); } @@ -733,6 +756,7 @@ public final class DBHelper { createMessageStateChangeTable(db); createPolicyTable(db); createQuickResponseTable(db); + createCredentialsTable(db); } @Override @@ -1317,6 +1341,15 @@ public final class DBHelper { } createDeleteDuplicateMessagesTrigger(mContext, db); } + + if (oldVersion <= 123) { + createCredentialsTable(db); + // Add the credentialKey column, and set it to -1 for all pre-existing hostAuths. + db.execSQL("alter table " + HostAuth.TABLE_NAME + + " add " + HostAuthColumns.CREDENTIAL_KEY + " integer"); + db.execSQL("update table " + HostAuth.TABLE_NAME + " set " + + HostAuthColumns.CREDENTIAL_KEY + "=-1"); + } } @Override diff --git a/src/com/android/email/provider/EmailProvider.java b/src/com/android/email/provider/EmailProvider.java index d1be0b2b3..af924a6d1 100644 --- a/src/com/android/email/provider/EmailProvider.java +++ b/src/com/android/email/provider/EmailProvider.java @@ -72,6 +72,7 @@ import com.android.email2.ui.MailActivityEmail; import com.android.emailcommon.Logging; import com.android.emailcommon.mail.Address; import com.android.emailcommon.provider.Account; +import com.android.emailcommon.provider.Credential; import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.EmailContent.AccountColumns; import com.android.emailcommon.provider.EmailContent.Attachment; @@ -261,6 +262,10 @@ public class EmailProvider extends ContentProvider { private static final int BODY = BODY_BASE; private static final int BODY_ID = BODY_BASE + 1; + private static final int CREDENTIAL_BASE = 0xB000; + private static final int CREDENTIAL = CREDENTIAL_BASE; + private static final int CREDENTIAL_ID = CREDENTIAL_BASE + 1; + private static final int BASE_SHIFT = 12; // 12 bits to the base type: 0, 0x1000, 0x2000, etc. private static final SparseArray TABLE_NAMES; @@ -277,6 +282,7 @@ public class EmailProvider extends ContentProvider { array.put(QUICK_RESPONSE_BASE >> BASE_SHIFT, QuickResponse.TABLE_NAME); array.put(UI_BASE >> BASE_SHIFT, null); array.put(BODY_BASE >> BASE_SHIFT, Body.TABLE_NAME); + array.put(CREDENTIAL_BASE >> BASE_SHIFT, Credential.TABLE_NAME); TABLE_NAMES = array; } @@ -645,6 +651,7 @@ public class EmailProvider extends ContentProvider { case HOSTAUTH_ID: case POLICY_ID: case QUICK_RESPONSE_ID: + case CREDENTIAL_ID: id = uri.getPathSegments().get(1); if (match == SYNCED_MESSAGE_ID) { // For synced messages, first copy the old message to the deleted table and @@ -822,6 +829,7 @@ public class EmailProvider extends ContentProvider { case MAILBOX: case ACCOUNT: case HOSTAUTH: + case CREDENTIAL: case POLICY: case QUICK_RESPONSE: longId = db.insert(TABLE_NAMES.valueAt(table), "foo", values); @@ -1041,6 +1049,11 @@ public class EmailProvider extends ContentProvider { // A specific hostauth sURIMatcher.addURI(EmailContent.AUTHORITY, "hostauth/*", HOSTAUTH_ID); + // All credential records + sURIMatcher.addURI(EmailContent.AUTHORITY, "credential", CREDENTIAL); + // A specific credential + sURIMatcher.addURI(EmailContent.AUTHORITY, "credential/*", CREDENTIAL_ID); + /** * THIS URI HAS SPECIAL SEMANTICS * ITS USE IS INTENDED FOR THE UI TO MARK CHANGES THAT NEED TO BE SYNCED BACK @@ -1878,6 +1891,7 @@ public class EmailProvider extends ContentProvider { case MAILBOX: case ACCOUNT: case HOSTAUTH: + case CREDENTIAL: case POLICY: if (match == ATTACHMENT) { if (values.containsKey(AttachmentColumns.LOCATION) &&