Merge "Allow database to hold oauth credentials" into ub-mail-master

This commit is contained in:
Martin Hibdon 2013-12-10 00:27:31 +00:00 committed by Android (Google) Code Review
commit 44a6fc31a2
6 changed files with 319 additions and 20 deletions

View File

@ -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<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

View File

@ -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;
}
}

View File

@ -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 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 @@ 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 {

View File

@ -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
}

View File

@ -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

View File

@ -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;
@ -264,6 +265,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<String> TABLE_NAMES;
@ -280,6 +285,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;
}
@ -652,6 +658,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
@ -829,6 +836,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);
@ -1048,6 +1056,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
@ -1885,6 +1898,7 @@ public class EmailProvider extends ContentProvider {
case MAILBOX:
case ACCOUNT:
case HOSTAUTH:
case CREDENTIAL:
case POLICY:
if (match == ATTACHMENT) {
if (values.containsKey(AttachmentColumns.LOCATION) &&