Allow database to hold oauth credentials
Change-Id: I127297fd78c7676995f1dcfa59fbbcafe4e72e8e
diff --git a/emailcommon/src/com/android/emailcommon/provider/Account.java b/emailcommon/src/com/android/emailcommon/provider/Account.java
index 1f152cf..18e37b2 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.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 @@
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 @@
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<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
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 0000000..dbb5932
--- /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<Credential> CREATOR
+ = new Parcelable.Creator<Credential>() {
+ @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 0abaa79..6e13f04 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.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 @@
Mailbox.initMailbox();
QuickResponse.initQuickResponse();
HostAuth.initHostAuth();
+ Credential.initCredential();
Policy.initPolicy();
Message.initMessage();
MessageMove.init();
@@ -169,6 +172,14 @@
}
}
+
+ 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 static <T extends EmailContent> T restoreContentWithId(Context context,
Class<T> 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 @@
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 5e17a51..8c822a3 100644
--- a/emailcommon/src/com/android/emailcommon/provider/HostAuth.java
+++ b/emailcommon/src/com/android/emailcommon/provider/HostAuth.java
@@ -52,8 +52,9 @@
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 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 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
&& 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 3a29c4a..d066bd6 100644
--- a/src/com/android/email/provider/DBHelper.java
+++ b/src/com/android/email/provider/DBHelper.java
@@ -32,6 +32,7 @@
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 @@
" 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 @@
// 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 @@
"; 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 @@
+ 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 @@
createMessageStateChangeTable(db);
createPolicyTable(db);
createQuickResponseTable(db);
+ createCredentialsTable(db);
}
@Override
@@ -1317,6 +1341,15 @@
}
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 d1be0b2..af924a6 100644
--- a/src/com/android/email/provider/EmailProvider.java
+++ b/src/com/android/email/provider/EmailProvider.java
@@ -72,6 +72,7 @@
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 @@
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<String> TABLE_NAMES;
@@ -277,6 +282,7 @@
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 @@
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 @@
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 @@
// 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 @@
case MAILBOX:
case ACCOUNT:
case HOSTAUTH:
+ case CREDENTIAL:
case POLICY:
if (match == ATTACHMENT) {
if (values.containsKey(AttachmentColumns.LOCATION) &&