blob: da665906a70dff8b445b20535d6ac23646702e0a [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.server.accounts;
import android.accounts.Account;
import android.annotation.Nullable;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteStatement;
import android.os.FileUtils;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import android.util.Slog;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Persistence layer abstraction for accessing accounts_ce/accounts_de databases.
*
* <p>At first, CE database needs to be {@link #attachCeDatabase(File) attached to DE},
* in order for the tables to be available. All operations with CE database are done through the
* connection to the DE database, to which it is attached. This approach allows atomic
* transactions across two databases</p>
*/
class AccountsDb implements AutoCloseable {
private static final String TAG = "AccountsDb";
private static final String DATABASE_NAME = "accounts.db";
private static final int PRE_N_DATABASE_VERSION = 9;
private static final int CE_DATABASE_VERSION = 10;
private static final int DE_DATABASE_VERSION = 3; // Added visibility support in O
static final String TABLE_ACCOUNTS = "accounts";
private static final String ACCOUNTS_ID = "_id";
private static final String ACCOUNTS_NAME = "name";
private static final String ACCOUNTS_TYPE = "type";
private static final String ACCOUNTS_TYPE_COUNT = "count(type)";
private static final String ACCOUNTS_PASSWORD = "password";
private static final String ACCOUNTS_PREVIOUS_NAME = "previous_name";
private static final String ACCOUNTS_LAST_AUTHENTICATE_TIME_EPOCH_MILLIS =
"last_password_entry_time_millis_epoch";
private static final String TABLE_AUTHTOKENS = "authtokens";
private static final String AUTHTOKENS_ID = "_id";
private static final String AUTHTOKENS_ACCOUNTS_ID = "accounts_id";
private static final String AUTHTOKENS_TYPE = "type";
private static final String AUTHTOKENS_AUTHTOKEN = "authtoken";
private static final String TABLE_VISIBILITY = "visibility";
private static final String VISIBILITY_ACCOUNTS_ID = "accounts_id";
private static final String VISIBILITY_PACKAGE = "_package";
private static final String VISIBILITY_VALUE = "value";
private static final String TABLE_GRANTS = "grants";
private static final String GRANTS_ACCOUNTS_ID = "accounts_id";
private static final String GRANTS_AUTH_TOKEN_TYPE = "auth_token_type";
private static final String GRANTS_GRANTEE_UID = "uid";
private static final String TABLE_EXTRAS = "extras";
private static final String EXTRAS_ID = "_id";
private static final String EXTRAS_ACCOUNTS_ID = "accounts_id";
private static final String EXTRAS_KEY = "key";
private static final String EXTRAS_VALUE = "value";
private static final String TABLE_META = "meta";
private static final String META_KEY = "key";
private static final String META_VALUE = "value";
static final String TABLE_SHARED_ACCOUNTS = "shared_accounts";
private static final String SHARED_ACCOUNTS_ID = "_id";
private static String TABLE_DEBUG = "debug_table";
// Columns for debug_table table
private static String DEBUG_TABLE_ACTION_TYPE = "action_type";
private static String DEBUG_TABLE_TIMESTAMP = "time";
private static String DEBUG_TABLE_CALLER_UID = "caller_uid";
private static String DEBUG_TABLE_TABLE_NAME = "table_name";
private static String DEBUG_TABLE_KEY = "primary_key";
// These actions correspond to the occurrence of real actions. Since
// these are called by the authenticators, the uid associated will be
// of the authenticator.
static String DEBUG_ACTION_SET_PASSWORD = "action_set_password";
static String DEBUG_ACTION_CLEAR_PASSWORD = "action_clear_password";
static String DEBUG_ACTION_ACCOUNT_ADD = "action_account_add";
static String DEBUG_ACTION_ACCOUNT_REMOVE = "action_account_remove";
static String DEBUG_ACTION_ACCOUNT_REMOVE_DE = "action_account_remove_de";
static String DEBUG_ACTION_AUTHENTICATOR_REMOVE = "action_authenticator_remove";
static String DEBUG_ACTION_ACCOUNT_RENAME = "action_account_rename";
// These actions don't necessarily correspond to any action on
// accountDb taking place. As an example, there might be a request for
// addingAccount, which might not lead to addition of account on grounds
// of bad authentication. We will still be logging it to keep track of
// who called.
static String DEBUG_ACTION_CALLED_ACCOUNT_ADD = "action_called_account_add";
static String DEBUG_ACTION_CALLED_ACCOUNT_REMOVE = "action_called_account_remove";
static String DEBUG_ACTION_SYNC_DE_CE_ACCOUNTS = "action_sync_de_ce_accounts";
//This action doesn't add account to accountdb. Account is only
// added in finishSession which may be in a different user profile.
static String DEBUG_ACTION_CALLED_START_ACCOUNT_ADD = "action_called_start_account_add";
static String DEBUG_ACTION_CALLED_ACCOUNT_SESSION_FINISH =
"action_called_account_session_finish";
static final String CE_DATABASE_NAME = "accounts_ce.db";
static final String DE_DATABASE_NAME = "accounts_de.db";
private static final String CE_DB_PREFIX = "ceDb.";
private static final String CE_TABLE_ACCOUNTS = CE_DB_PREFIX + TABLE_ACCOUNTS;
private static final String CE_TABLE_AUTHTOKENS = CE_DB_PREFIX + TABLE_AUTHTOKENS;
private static final String CE_TABLE_EXTRAS = CE_DB_PREFIX + TABLE_EXTRAS;
static final int MAX_DEBUG_DB_SIZE = 64;
private static final String[] ACCOUNT_TYPE_COUNT_PROJECTION =
new String[] { ACCOUNTS_TYPE, ACCOUNTS_TYPE_COUNT};
private static final String COUNT_OF_MATCHING_GRANTS = ""
+ "SELECT COUNT(*) FROM " + TABLE_GRANTS + ", " + TABLE_ACCOUNTS
+ " WHERE " + GRANTS_ACCOUNTS_ID + "=" + ACCOUNTS_ID
+ " AND " + GRANTS_GRANTEE_UID + "=?"
+ " AND " + GRANTS_AUTH_TOKEN_TYPE + "=?"
+ " AND " + ACCOUNTS_NAME + "=?"
+ " AND " + ACCOUNTS_TYPE + "=?";
private static final String COUNT_OF_MATCHING_GRANTS_ANY_TOKEN = ""
+ "SELECT COUNT(*) FROM " + TABLE_GRANTS + ", " + TABLE_ACCOUNTS
+ " WHERE " + GRANTS_ACCOUNTS_ID + "=" + ACCOUNTS_ID
+ " AND " + GRANTS_GRANTEE_UID + "=?"
+ " AND " + ACCOUNTS_NAME + "=?"
+ " AND " + ACCOUNTS_TYPE + "=?";
private static final String SELECTION_ACCOUNTS_ID_BY_ACCOUNT =
"accounts_id=(select _id FROM accounts WHERE name=? AND type=?)";
private static final String[] COLUMNS_AUTHTOKENS_TYPE_AND_AUTHTOKEN =
{AUTHTOKENS_TYPE, AUTHTOKENS_AUTHTOKEN};
private static final String[] COLUMNS_EXTRAS_KEY_AND_VALUE = {EXTRAS_KEY, EXTRAS_VALUE};
private static final String ACCOUNT_ACCESS_GRANTS = ""
+ "SELECT " + AccountsDb.ACCOUNTS_NAME + ", "
+ AccountsDb.GRANTS_GRANTEE_UID
+ " FROM " + AccountsDb.TABLE_ACCOUNTS
+ ", " + AccountsDb.TABLE_GRANTS
+ " WHERE " + AccountsDb.GRANTS_ACCOUNTS_ID
+ "=" + AccountsDb.ACCOUNTS_ID;
private static final String META_KEY_FOR_AUTHENTICATOR_UID_FOR_TYPE_PREFIX =
"auth_uid_for_type:";
private static final String META_KEY_DELIMITER = ":";
private static final String SELECTION_META_BY_AUTHENTICATOR_TYPE = META_KEY + " LIKE ?";
private final DeDatabaseHelper mDeDatabase;
private final Context mContext;
private final File mPreNDatabaseFile;
final Object mDebugStatementLock = new Object();
private volatile long mDebugDbInsertionPoint = -1;
private volatile SQLiteStatement mDebugStatementForLogging; // not thread safe.
AccountsDb(DeDatabaseHelper deDatabase, Context context, File preNDatabaseFile) {
mDeDatabase = deDatabase;
mContext = context;
mPreNDatabaseFile = preNDatabaseFile;
}
private static class CeDatabaseHelper extends SQLiteOpenHelper {
CeDatabaseHelper(Context context, String ceDatabaseName) {
super(context, ceDatabaseName, null, CE_DATABASE_VERSION);
}
/**
* This call needs to be made while the mCacheLock is held.
* @param db The database.
*/
@Override
public void onCreate(SQLiteDatabase db) {
Log.i(TAG, "Creating CE database " + getDatabaseName());
db.execSQL("CREATE TABLE " + TABLE_ACCOUNTS + " ( "
+ ACCOUNTS_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ ACCOUNTS_NAME + " TEXT NOT NULL, "
+ ACCOUNTS_TYPE + " TEXT NOT NULL, "
+ ACCOUNTS_PASSWORD + " TEXT, "
+ "UNIQUE(" + ACCOUNTS_NAME + "," + ACCOUNTS_TYPE + "))");
db.execSQL("CREATE TABLE " + TABLE_AUTHTOKENS + " ( "
+ AUTHTOKENS_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ AUTHTOKENS_ACCOUNTS_ID + " INTEGER NOT NULL, "
+ AUTHTOKENS_TYPE + " TEXT NOT NULL, "
+ AUTHTOKENS_AUTHTOKEN + " TEXT, "
+ "UNIQUE (" + AUTHTOKENS_ACCOUNTS_ID + "," + AUTHTOKENS_TYPE + "))");
db.execSQL("CREATE TABLE " + TABLE_EXTRAS + " ( "
+ EXTRAS_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ EXTRAS_ACCOUNTS_ID + " INTEGER, "
+ EXTRAS_KEY + " TEXT NOT NULL, "
+ EXTRAS_VALUE + " TEXT, "
+ "UNIQUE(" + EXTRAS_ACCOUNTS_ID + "," + EXTRAS_KEY + "))");
createAccountsDeletionTrigger(db);
}
private void createAccountsDeletionTrigger(SQLiteDatabase db) {
db.execSQL(""
+ " CREATE TRIGGER " + TABLE_ACCOUNTS + "Delete DELETE ON " + TABLE_ACCOUNTS
+ " BEGIN"
+ " DELETE FROM " + TABLE_AUTHTOKENS
+ " WHERE " + AUTHTOKENS_ACCOUNTS_ID + "=OLD." + ACCOUNTS_ID + " ;"
+ " DELETE FROM " + TABLE_EXTRAS
+ " WHERE " + EXTRAS_ACCOUNTS_ID + "=OLD." + ACCOUNTS_ID + " ;"
+ " END");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.i(TAG, "Upgrade CE from version " + oldVersion + " to version " + newVersion);
if (oldVersion == 9) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "onUpgrade upgrading to v10");
}
db.execSQL("DROP TABLE IF EXISTS " + TABLE_META);
db.execSQL("DROP TABLE IF EXISTS " + TABLE_SHARED_ACCOUNTS);
// Recreate the trigger, since the old one references the table to be removed
db.execSQL("DROP TRIGGER IF EXISTS " + TABLE_ACCOUNTS + "Delete");
createAccountsDeletionTrigger(db);
db.execSQL("DROP TABLE IF EXISTS " + TABLE_GRANTS);
db.execSQL("DROP TABLE IF EXISTS " + TABLE_DEBUG);
oldVersion++;
}
if (oldVersion != newVersion) {
Log.e(TAG, "failed to upgrade version " + oldVersion + " to version " + newVersion);
}
}
@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.e(TAG, "onDowngrade: recreate accounts CE table");
resetDatabase(db);
onCreate(db);
}
@Override
public void onOpen(SQLiteDatabase db) {
if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "opened database " + CE_DATABASE_NAME);
}
/**
* Creates a new {@code CeDatabaseHelper}. If pre-N db file is present at the old location,
* it also performs migration to the new CE database.
*/
static CeDatabaseHelper create(
Context context,
File preNDatabaseFile,
File ceDatabaseFile) {
boolean newDbExists = ceDatabaseFile.exists();
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "CeDatabaseHelper.create ceDatabaseFile=" + ceDatabaseFile
+ " oldDbExists=" + preNDatabaseFile.exists()
+ " newDbExists=" + newDbExists);
}
boolean removeOldDb = false;
if (!newDbExists && preNDatabaseFile.exists()) {
removeOldDb = migratePreNDbToCe(preNDatabaseFile, ceDatabaseFile);
}
// Try to open and upgrade if necessary
CeDatabaseHelper ceHelper = new CeDatabaseHelper(context, ceDatabaseFile.getPath());
ceHelper.getWritableDatabase();
ceHelper.close();
if (removeOldDb) {
Slog.i(TAG, "Migration complete - removing pre-N db " + preNDatabaseFile);
if (!SQLiteDatabase.deleteDatabase(preNDatabaseFile)) {
Slog.e(TAG, "Cannot remove pre-N db " + preNDatabaseFile);
}
}
return ceHelper;
}
private static boolean migratePreNDbToCe(File oldDbFile, File ceDbFile) {
Slog.i(TAG, "Moving pre-N DB " + oldDbFile + " to CE " + ceDbFile);
try {
FileUtils.copyFileOrThrow(oldDbFile, ceDbFile);
} catch (IOException e) {
Slog.e(TAG, "Cannot copy file to " + ceDbFile + " from " + oldDbFile, e);
// Try to remove potentially damaged file if I/O error occurred
deleteDbFileWarnIfFailed(ceDbFile);
return false;
}
return true;
}
}
/**
* Returns information about auth tokens and their account for the specified query
* parameters.
* Output is in the format:
* <pre><code> | AUTHTOKEN_ID | ACCOUNT_NAME | AUTH_TOKEN_TYPE |</code></pre>
*/
Cursor findAuthtokenForAllAccounts(String accountType, String authToken) {
SQLiteDatabase db = mDeDatabase.getReadableDatabaseUserIsUnlocked();
return db.rawQuery(
"SELECT " + CE_TABLE_AUTHTOKENS + "." + AUTHTOKENS_ID
+ ", " + CE_TABLE_ACCOUNTS + "." + ACCOUNTS_NAME
+ ", " + CE_TABLE_AUTHTOKENS + "." + AUTHTOKENS_TYPE
+ " FROM " + CE_TABLE_ACCOUNTS
+ " JOIN " + CE_TABLE_AUTHTOKENS
+ " ON " + CE_TABLE_ACCOUNTS + "." + ACCOUNTS_ID
+ " = " + CE_TABLE_AUTHTOKENS + "." + AUTHTOKENS_ACCOUNTS_ID
+ " WHERE " + CE_TABLE_AUTHTOKENS + "." + AUTHTOKENS_AUTHTOKEN
+ " = ? AND " + CE_TABLE_ACCOUNTS + "." + ACCOUNTS_TYPE + " = ?",
new String[]{authToken, accountType});
}
Map<String, String> findAuthTokensByAccount(Account account) {
SQLiteDatabase db = mDeDatabase.getReadableDatabaseUserIsUnlocked();
HashMap<String, String> authTokensForAccount = new HashMap<>();
Cursor cursor = db.query(CE_TABLE_AUTHTOKENS,
COLUMNS_AUTHTOKENS_TYPE_AND_AUTHTOKEN,
SELECTION_ACCOUNTS_ID_BY_ACCOUNT,
new String[] {account.name, account.type},
null, null, null);
try {
while (cursor.moveToNext()) {
final String type = cursor.getString(0);
final String authToken = cursor.getString(1);
authTokensForAccount.put(type, authToken);
}
} finally {
cursor.close();
}
return authTokensForAccount;
}
boolean deleteAuthtokensByAccountIdAndType(long accountId, String authtokenType) {
SQLiteDatabase db = mDeDatabase.getWritableDatabaseUserIsUnlocked();
return db.delete(CE_TABLE_AUTHTOKENS,
AUTHTOKENS_ACCOUNTS_ID + "=?" + " AND " + AUTHTOKENS_TYPE + "=?",
new String[]{String.valueOf(accountId), authtokenType}) > 0;
}
boolean deleteAuthToken(String authTokenId) {
SQLiteDatabase db = mDeDatabase.getWritableDatabaseUserIsUnlocked();
return db.delete(
CE_TABLE_AUTHTOKENS, AUTHTOKENS_ID + "= ?",
new String[]{authTokenId}) > 0;
}
long insertAuthToken(long accountId, String authTokenType, String authToken) {
SQLiteDatabase db = mDeDatabase.getWritableDatabaseUserIsUnlocked();
ContentValues values = new ContentValues();
values.put(AUTHTOKENS_ACCOUNTS_ID, accountId);
values.put(AUTHTOKENS_TYPE, authTokenType);
values.put(AUTHTOKENS_AUTHTOKEN, authToken);
return db.insert(
CE_TABLE_AUTHTOKENS, AUTHTOKENS_AUTHTOKEN, values);
}
int updateCeAccountPassword(long accountId, String password) {
SQLiteDatabase db = mDeDatabase.getWritableDatabaseUserIsUnlocked();
final ContentValues values = new ContentValues();
values.put(ACCOUNTS_PASSWORD, password);
return db.update(
CE_TABLE_ACCOUNTS, values, ACCOUNTS_ID + "=?",
new String[] {String.valueOf(accountId)});
}
boolean renameCeAccount(long accountId, String newName) {
SQLiteDatabase db = mDeDatabase.getWritableDatabaseUserIsUnlocked();
final ContentValues values = new ContentValues();
values.put(ACCOUNTS_NAME, newName);
final String[] argsAccountId = {String.valueOf(accountId)};
return db.update(
CE_TABLE_ACCOUNTS, values, ACCOUNTS_ID + "=?", argsAccountId) > 0;
}
boolean deleteAuthTokensByAccountId(long accountId) {
SQLiteDatabase db = mDeDatabase.getWritableDatabaseUserIsUnlocked();
return db.delete(CE_TABLE_AUTHTOKENS, AUTHTOKENS_ACCOUNTS_ID + "=?",
new String[] {String.valueOf(accountId)}) > 0;
}
long findExtrasIdByAccountId(long accountId, String key) {
SQLiteDatabase db = mDeDatabase.getReadableDatabaseUserIsUnlocked();
Cursor cursor = db.query(
CE_TABLE_EXTRAS, new String[]{EXTRAS_ID},
EXTRAS_ACCOUNTS_ID + "=" + accountId + " AND " + EXTRAS_KEY + "=?",
new String[]{key}, null, null, null);
try {
if (cursor.moveToNext()) {
return cursor.getLong(0);
}
return -1;
} finally {
cursor.close();
}
}
boolean updateExtra(long extrasId, String value) {
SQLiteDatabase db = mDeDatabase.getWritableDatabaseUserIsUnlocked();
ContentValues values = new ContentValues();
values.put(EXTRAS_VALUE, value);
int rows = db.update(
TABLE_EXTRAS, values, EXTRAS_ID + "=?",
new String[]{String.valueOf(extrasId)});
return rows == 1;
}
long insertExtra(long accountId, String key, String value) {
SQLiteDatabase db = mDeDatabase.getWritableDatabaseUserIsUnlocked();
ContentValues values = new ContentValues();
values.put(EXTRAS_KEY, key);
values.put(EXTRAS_ACCOUNTS_ID, accountId);
values.put(EXTRAS_VALUE, value);
return db.insert(CE_TABLE_EXTRAS, EXTRAS_KEY, values);
}
Map<String, String> findUserExtrasForAccount(Account account) {
SQLiteDatabase db = mDeDatabase.getReadableDatabaseUserIsUnlocked();
Map<String, String> userExtrasForAccount = new HashMap<>();
String[] selectionArgs = {account.name, account.type};
try (Cursor cursor = db.query(CE_TABLE_EXTRAS,
COLUMNS_EXTRAS_KEY_AND_VALUE,
SELECTION_ACCOUNTS_ID_BY_ACCOUNT,
selectionArgs,
null, null, null)) {
while (cursor.moveToNext()) {
final String tmpkey = cursor.getString(0);
final String value = cursor.getString(1);
userExtrasForAccount.put(tmpkey, value);
}
}
return userExtrasForAccount;
}
long findCeAccountId(Account account) {
SQLiteDatabase db = mDeDatabase.getReadableDatabaseUserIsUnlocked();
String[] columns = { ACCOUNTS_ID };
String selection = "name=? AND type=?";
String[] selectionArgs = {account.name, account.type};
try (Cursor cursor = db.query(CE_TABLE_ACCOUNTS, columns, selection, selectionArgs,
null, null, null)) {
if (cursor.moveToNext()) {
return cursor.getLong(0);
}
return -1;
}
}
String findAccountPasswordByNameAndType(String name, String type) {
SQLiteDatabase db = mDeDatabase.getReadableDatabaseUserIsUnlocked();
String selection = ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE + "=?";
String[] selectionArgs = {name, type};
String[] columns = {ACCOUNTS_PASSWORD};
try (Cursor cursor = db.query(CE_TABLE_ACCOUNTS, columns, selection, selectionArgs,
null, null, null)) {
if (cursor.moveToNext()) {
return cursor.getString(0);
}
return null;
}
}
long insertCeAccount(Account account, String password) {
SQLiteDatabase db = mDeDatabase.getWritableDatabaseUserIsUnlocked();
ContentValues values = new ContentValues();
values.put(ACCOUNTS_NAME, account.name);
values.put(ACCOUNTS_TYPE, account.type);
values.put(ACCOUNTS_PASSWORD, password);
return db.insert(
CE_TABLE_ACCOUNTS, ACCOUNTS_NAME, values);
}
static class DeDatabaseHelper extends SQLiteOpenHelper {
private final int mUserId;
private volatile boolean mCeAttached;
private DeDatabaseHelper(Context context, int userId, String deDatabaseName) {
super(context, deDatabaseName, null, DE_DATABASE_VERSION);
mUserId = userId;
}
/**
* This call needs to be made while the mCacheLock is held. The way to
* ensure this is to get the lock any time a method is called ont the DatabaseHelper
* @param db The database.
*/
@Override
public void onCreate(SQLiteDatabase db) {
Log.i(TAG, "Creating DE database for user " + mUserId);
db.execSQL("CREATE TABLE " + TABLE_ACCOUNTS + " ( "
+ ACCOUNTS_ID + " INTEGER PRIMARY KEY, "
+ ACCOUNTS_NAME + " TEXT NOT NULL, "
+ ACCOUNTS_TYPE + " TEXT NOT NULL, "
+ ACCOUNTS_PREVIOUS_NAME + " TEXT, "
+ ACCOUNTS_LAST_AUTHENTICATE_TIME_EPOCH_MILLIS + " INTEGER DEFAULT 0, "
+ "UNIQUE(" + ACCOUNTS_NAME + "," + ACCOUNTS_TYPE + "))");
db.execSQL("CREATE TABLE " + TABLE_META + " ( "
+ META_KEY + " TEXT PRIMARY KEY NOT NULL, "
+ META_VALUE + " TEXT)");
createGrantsTable(db);
createSharedAccountsTable(db);
createAccountsDeletionTrigger(db);
createDebugTable(db);
createAccountsVisibilityTable(db);
createAccountsDeletionVisibilityCleanupTrigger(db);
}
private void createSharedAccountsTable(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + TABLE_SHARED_ACCOUNTS + " ( "
+ ACCOUNTS_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ ACCOUNTS_NAME + " TEXT NOT NULL, "
+ ACCOUNTS_TYPE + " TEXT NOT NULL, "
+ "UNIQUE(" + ACCOUNTS_NAME + "," + ACCOUNTS_TYPE + "))");
}
private void createAccountsDeletionTrigger(SQLiteDatabase db) {
db.execSQL(""
+ " CREATE TRIGGER " + TABLE_ACCOUNTS + "Delete DELETE ON " + TABLE_ACCOUNTS
+ " BEGIN"
+ " DELETE FROM " + TABLE_GRANTS
+ " WHERE " + GRANTS_ACCOUNTS_ID + "=OLD." + ACCOUNTS_ID + " ;"
+ " END");
}
private void createGrantsTable(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + TABLE_GRANTS + " ( "
+ GRANTS_ACCOUNTS_ID + " INTEGER NOT NULL, "
+ GRANTS_AUTH_TOKEN_TYPE + " STRING NOT NULL, "
+ GRANTS_GRANTEE_UID + " INTEGER NOT NULL, "
+ "UNIQUE (" + GRANTS_ACCOUNTS_ID + "," + GRANTS_AUTH_TOKEN_TYPE
+ "," + GRANTS_GRANTEE_UID + "))");
}
private void createAccountsVisibilityTable(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + TABLE_VISIBILITY + " ( "
+ VISIBILITY_ACCOUNTS_ID + " INTEGER NOT NULL, "
+ VISIBILITY_PACKAGE + " TEXT NOT NULL, "
+ VISIBILITY_VALUE + " INTEGER, "
+ "PRIMARY KEY(" + VISIBILITY_ACCOUNTS_ID + "," + VISIBILITY_PACKAGE + "))");
}
static void createDebugTable(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + TABLE_DEBUG + " ( "
+ ACCOUNTS_ID + " INTEGER,"
+ DEBUG_TABLE_ACTION_TYPE + " TEXT NOT NULL, "
+ DEBUG_TABLE_TIMESTAMP + " DATETIME,"
+ DEBUG_TABLE_CALLER_UID + " INTEGER NOT NULL,"
+ DEBUG_TABLE_TABLE_NAME + " TEXT NOT NULL,"
+ DEBUG_TABLE_KEY + " INTEGER PRIMARY KEY)");
db.execSQL("CREATE INDEX timestamp_index ON " + TABLE_DEBUG + " ("
+ DEBUG_TABLE_TIMESTAMP + ")");
}
private void createAccountsDeletionVisibilityCleanupTrigger(SQLiteDatabase db) {
db.execSQL(""
+ " CREATE TRIGGER "
+ TABLE_ACCOUNTS + "DeleteVisibility DELETE ON " + TABLE_ACCOUNTS
+ " BEGIN"
+ " DELETE FROM " + TABLE_VISIBILITY
+ " WHERE " + VISIBILITY_ACCOUNTS_ID + "=OLD." + ACCOUNTS_ID + " ;"
+ " END");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.i(TAG, "upgrade from version " + oldVersion + " to version " + newVersion);
if (oldVersion == 1) {
createAccountsVisibilityTable(db);
createAccountsDeletionVisibilityCleanupTrigger(db);
oldVersion = 3; // skip version 2 which had uid based table
}
if (oldVersion == 2) {
// Remove uid based table and replace it with packageName based
db.execSQL("DROP TRIGGER IF EXISTS " + TABLE_ACCOUNTS + "DeleteVisibility");
db.execSQL("DROP TABLE IF EXISTS " + TABLE_VISIBILITY);
createAccountsVisibilityTable(db);
createAccountsDeletionVisibilityCleanupTrigger(db);
oldVersion++;
}
if (oldVersion != newVersion) {
Log.e(TAG, "failed to upgrade version " + oldVersion + " to version " + newVersion);
}
}
@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.e(TAG, "onDowngrade: recreate accounts DE table");
resetDatabase(db);
onCreate(db);
}
public SQLiteDatabase getReadableDatabaseUserIsUnlocked() {
if(!mCeAttached) {
Log.wtf(TAG, "getReadableDatabaseUserIsUnlocked called while user " + mUserId
+ " is still locked. CE database is not yet available.", new Throwable());
}
return super.getReadableDatabase();
}
public SQLiteDatabase getWritableDatabaseUserIsUnlocked() {
if(!mCeAttached) {
Log.wtf(TAG, "getWritableDatabaseUserIsUnlocked called while user " + mUserId
+ " is still locked. CE database is not yet available.", new Throwable());
}
return super.getWritableDatabase();
}
@Override
public void onOpen(SQLiteDatabase db) {
if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "opened database " + DE_DATABASE_NAME);
}
private void migratePreNDbToDe(File preNDbFile) {
Log.i(TAG, "Migrate pre-N database to DE preNDbFile=" + preNDbFile);
SQLiteDatabase db = getWritableDatabase();
db.execSQL("ATTACH DATABASE '" + preNDbFile.getPath() + "' AS preNDb");
db.beginTransaction();
// Copy accounts fields
db.execSQL("INSERT INTO " + TABLE_ACCOUNTS
+ "(" + ACCOUNTS_ID + "," + ACCOUNTS_NAME + "," + ACCOUNTS_TYPE + ", "
+ ACCOUNTS_PREVIOUS_NAME + ", " + ACCOUNTS_LAST_AUTHENTICATE_TIME_EPOCH_MILLIS
+ ") "
+ "SELECT " + ACCOUNTS_ID + "," + ACCOUNTS_NAME + "," + ACCOUNTS_TYPE + ", "
+ ACCOUNTS_PREVIOUS_NAME + ", " + ACCOUNTS_LAST_AUTHENTICATE_TIME_EPOCH_MILLIS
+ " FROM preNDb." + TABLE_ACCOUNTS);
// Copy SHARED_ACCOUNTS
db.execSQL("INSERT INTO " + TABLE_SHARED_ACCOUNTS
+ "(" + SHARED_ACCOUNTS_ID + "," + ACCOUNTS_NAME + "," + ACCOUNTS_TYPE + ") " +
"SELECT " + SHARED_ACCOUNTS_ID + "," + ACCOUNTS_NAME + "," + ACCOUNTS_TYPE
+ " FROM preNDb." + TABLE_SHARED_ACCOUNTS);
// Copy DEBUG_TABLE
db.execSQL("INSERT INTO " + TABLE_DEBUG
+ "(" + ACCOUNTS_ID + "," + DEBUG_TABLE_ACTION_TYPE + ","
+ DEBUG_TABLE_TIMESTAMP + "," + DEBUG_TABLE_CALLER_UID + ","
+ DEBUG_TABLE_TABLE_NAME + "," + DEBUG_TABLE_KEY + ") " +
"SELECT " + ACCOUNTS_ID + "," + DEBUG_TABLE_ACTION_TYPE + ","
+ DEBUG_TABLE_TIMESTAMP + "," + DEBUG_TABLE_CALLER_UID + ","
+ DEBUG_TABLE_TABLE_NAME + "," + DEBUG_TABLE_KEY
+ " FROM preNDb." + TABLE_DEBUG);
// Copy GRANTS
db.execSQL("INSERT INTO " + TABLE_GRANTS
+ "(" + GRANTS_ACCOUNTS_ID + "," + GRANTS_AUTH_TOKEN_TYPE + ","
+ GRANTS_GRANTEE_UID + ") " +
"SELECT " + GRANTS_ACCOUNTS_ID + "," + GRANTS_AUTH_TOKEN_TYPE + ","
+ GRANTS_GRANTEE_UID + " FROM preNDb." + TABLE_GRANTS);
// Copy META
db.execSQL("INSERT INTO " + TABLE_META
+ "(" + META_KEY + "," + META_VALUE + ") "
+ "SELECT " + META_KEY + "," + META_VALUE + " FROM preNDb." + TABLE_META);
db.setTransactionSuccessful();
db.endTransaction();
db.execSQL("DETACH DATABASE preNDb");
}
}
boolean deleteDeAccount(long accountId) {
SQLiteDatabase db = mDeDatabase.getWritableDatabase();
return db.delete(TABLE_ACCOUNTS, ACCOUNTS_ID + "=" + accountId, null) > 0;
}
long insertSharedAccount(Account account) {
SQLiteDatabase db = mDeDatabase.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(ACCOUNTS_NAME, account.name);
values.put(ACCOUNTS_TYPE, account.type);
return db.insert(
TABLE_SHARED_ACCOUNTS, ACCOUNTS_NAME, values);
}
boolean deleteSharedAccount(Account account) {
SQLiteDatabase db = mDeDatabase.getWritableDatabase();
return db.delete(TABLE_SHARED_ACCOUNTS, ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE + "=?",
new String[]{account.name, account.type}) > 0;
}
int renameSharedAccount(Account account, String newName) {
SQLiteDatabase db = mDeDatabase.getWritableDatabase();
final ContentValues values = new ContentValues();
values.put(ACCOUNTS_NAME, newName);
return db.update(TABLE_SHARED_ACCOUNTS,
values,
ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE + "=?",
new String[] {account.name, account.type});
}
List<Account> getSharedAccounts() {
SQLiteDatabase db = mDeDatabase.getReadableDatabase();
ArrayList<Account> accountList = new ArrayList<>();
Cursor cursor = null;
try {
cursor = db.query(TABLE_SHARED_ACCOUNTS, new String[] {ACCOUNTS_NAME, ACCOUNTS_TYPE},
null, null, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
int nameIndex = cursor.getColumnIndex(ACCOUNTS_NAME);
int typeIndex = cursor.getColumnIndex(ACCOUNTS_TYPE);
do {
accountList.add(new Account(cursor.getString(nameIndex),
cursor.getString(typeIndex)));
} while (cursor.moveToNext());
}
} finally {
if (cursor != null) {
cursor.close();
}
}
return accountList;
}
long findSharedAccountId(Account account) {
SQLiteDatabase db = mDeDatabase.getReadableDatabase();
Cursor cursor = db.query(TABLE_SHARED_ACCOUNTS, new String[]{
ACCOUNTS_ID},
"name=? AND type=?", new String[]{account.name, account.type}, null, null,
null);
try {
if (cursor.moveToNext()) {
return cursor.getLong(0);
}
return -1;
} finally {
cursor.close();
}
}
long findAccountLastAuthenticatedTime(Account account) {
SQLiteDatabase db = mDeDatabase.getReadableDatabase();
return DatabaseUtils.longForQuery(db,
"SELECT " + AccountsDb.ACCOUNTS_LAST_AUTHENTICATE_TIME_EPOCH_MILLIS
+ " FROM " + TABLE_ACCOUNTS + " WHERE " + ACCOUNTS_NAME + "=? AND "
+ ACCOUNTS_TYPE + "=?",
new String[] {account.name, account.type});
}
boolean updateAccountLastAuthenticatedTime(Account account) {
SQLiteDatabase db = mDeDatabase.getWritableDatabase();
final ContentValues values = new ContentValues();
values.put(ACCOUNTS_LAST_AUTHENTICATE_TIME_EPOCH_MILLIS, System.currentTimeMillis());
int rowCount = db.update(TABLE_ACCOUNTS,
values,
ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE + "=?",
new String[] { account.name, account.type });
return rowCount > 0;
}
void dumpDeAccountsTable(PrintWriter pw) {
SQLiteDatabase db = mDeDatabase.getReadableDatabase();
Cursor cursor = db.query(
TABLE_ACCOUNTS, ACCOUNT_TYPE_COUNT_PROJECTION,
null, null, ACCOUNTS_TYPE, null, null);
try {
while (cursor.moveToNext()) {
// print type,count
pw.println(cursor.getString(0) + "," + cursor.getString(1));
}
} finally {
if (cursor != null) {
cursor.close();
}
}
}
long findDeAccountId(Account account) {
SQLiteDatabase db = mDeDatabase.getReadableDatabase();
String[] columns = {ACCOUNTS_ID};
String selection = "name=? AND type=?";
String[] selectionArgs = {account.name, account.type};
try (Cursor cursor = db.query(TABLE_ACCOUNTS, columns, selection, selectionArgs,
null, null, null)) {
if (cursor.moveToNext()) {
return cursor.getLong(0);
}
return -1;
}
}
Map<Long, Account> findAllDeAccounts() {
SQLiteDatabase db = mDeDatabase.getReadableDatabase();
LinkedHashMap<Long, Account> map = new LinkedHashMap<>();
String[] columns = {ACCOUNTS_ID, ACCOUNTS_TYPE, ACCOUNTS_NAME};
try (Cursor cursor = db.query(TABLE_ACCOUNTS, columns,
null, null, null, null, ACCOUNTS_ID)) {
while (cursor.moveToNext()) {
final long accountId = cursor.getLong(0);
final String accountType = cursor.getString(1);
final String accountName = cursor.getString(2);
final Account account = new Account(accountName, accountType);
map.put(accountId, account);
}
}
return map;
}
String findDeAccountPreviousName(Account account) {
SQLiteDatabase db = mDeDatabase.getReadableDatabase();
String[] columns = {ACCOUNTS_PREVIOUS_NAME};
String selection = ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE + "=?";
String[] selectionArgs = {account.name, account.type};
try (Cursor cursor = db.query(TABLE_ACCOUNTS, columns, selection, selectionArgs,
null, null, null)) {
if (cursor.moveToNext()) {
return cursor.getString(0);
}
}
return null;
}
long insertDeAccount(Account account, long accountId) {
SQLiteDatabase db = mDeDatabase.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(ACCOUNTS_ID, accountId);
values.put(ACCOUNTS_NAME, account.name);
values.put(ACCOUNTS_TYPE, account.type);
values.put(ACCOUNTS_LAST_AUTHENTICATE_TIME_EPOCH_MILLIS, System.currentTimeMillis());
return db.insert(TABLE_ACCOUNTS, ACCOUNTS_NAME, values);
}
boolean renameDeAccount(long accountId, String newName, String previousName) {
SQLiteDatabase db = mDeDatabase.getWritableDatabase();
final ContentValues values = new ContentValues();
values.put(ACCOUNTS_NAME, newName);
values.put(ACCOUNTS_PREVIOUS_NAME, previousName);
final String[] argsAccountId = {String.valueOf(accountId)};
return db.update(TABLE_ACCOUNTS, values, ACCOUNTS_ID + "=?", argsAccountId) > 0;
}
boolean deleteGrantsByAccountIdAuthTokenTypeAndUid(long accountId,
String authTokenType, long uid) {
SQLiteDatabase db = mDeDatabase.getWritableDatabase();
return db.delete(TABLE_GRANTS,
GRANTS_ACCOUNTS_ID + "=? AND " + GRANTS_AUTH_TOKEN_TYPE + "=? AND "
+ GRANTS_GRANTEE_UID + "=?",
new String[] {String.valueOf(accountId), authTokenType, String.valueOf(uid)}) > 0;
}
List<Integer> findAllUidGrants() {
SQLiteDatabase db = mDeDatabase.getReadableDatabase();
List<Integer> result = new ArrayList<>();
final Cursor cursor = db.query(TABLE_GRANTS,
new String[]{GRANTS_GRANTEE_UID},
null, null, GRANTS_GRANTEE_UID, null, null);
try {
while (cursor.moveToNext()) {
final int uid = cursor.getInt(0);
result.add(uid);
}
} finally {
cursor.close();
}
return result;
}
long findMatchingGrantsCount(int uid, String authTokenType, Account account) {
SQLiteDatabase db = mDeDatabase.getReadableDatabase();
String[] args = {String.valueOf(uid), authTokenType, account.name, account.type};
return DatabaseUtils.longForQuery(db, COUNT_OF_MATCHING_GRANTS, args);
}
long findMatchingGrantsCountAnyToken(int uid, Account account) {
SQLiteDatabase db = mDeDatabase.getReadableDatabase();
String[] args = {String.valueOf(uid), account.name, account.type};
return DatabaseUtils.longForQuery(db, COUNT_OF_MATCHING_GRANTS_ANY_TOKEN, args);
}
long insertGrant(long accountId, String authTokenType, int uid) {
SQLiteDatabase db = mDeDatabase.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(GRANTS_ACCOUNTS_ID, accountId);
values.put(GRANTS_AUTH_TOKEN_TYPE, authTokenType);
values.put(GRANTS_GRANTEE_UID, uid);
return db.insert(TABLE_GRANTS, GRANTS_ACCOUNTS_ID, values);
}
boolean deleteGrantsByUid(int uid) {
SQLiteDatabase db = mDeDatabase.getWritableDatabase();
return db.delete(TABLE_GRANTS, GRANTS_GRANTEE_UID + "=?",
new String[] {Integer.toString(uid)}) > 0;
}
boolean setAccountVisibility(long accountId, String packageName, int visibility) {
SQLiteDatabase db = mDeDatabase.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(VISIBILITY_ACCOUNTS_ID, String.valueOf(accountId));
values.put(VISIBILITY_PACKAGE, packageName);
values.put(VISIBILITY_VALUE, String.valueOf(visibility));
return (db.replace(TABLE_VISIBILITY, VISIBILITY_VALUE, values) != -1);
}
Integer findAccountVisibility(Account account, String packageName) {
SQLiteDatabase db = mDeDatabase.getReadableDatabase();
final Cursor cursor = db.query(TABLE_VISIBILITY, new String[] {VISIBILITY_VALUE},
SELECTION_ACCOUNTS_ID_BY_ACCOUNT + " AND " + VISIBILITY_PACKAGE + "=? ",
new String[] {account.name, account.type, packageName}, null, null, null);
try {
while (cursor.moveToNext()) {
return cursor.getInt(0);
}
} finally {
cursor.close();
}
return null;
}
Integer findAccountVisibility(long accountId, String packageName) {
SQLiteDatabase db = mDeDatabase.getReadableDatabase();
final Cursor cursor = db.query(TABLE_VISIBILITY, new String[] {VISIBILITY_VALUE},
VISIBILITY_ACCOUNTS_ID + "=? AND " + VISIBILITY_PACKAGE + "=? ",
new String[] {String.valueOf(accountId), packageName}, null, null, null);
try {
while (cursor.moveToNext()) {
return cursor.getInt(0);
}
} finally {
cursor.close();
}
return null;
}
Account findDeAccountByAccountId(long accountId) {
SQLiteDatabase db = mDeDatabase.getReadableDatabase();
final Cursor cursor = db.query(TABLE_ACCOUNTS, new String[] {ACCOUNTS_NAME, ACCOUNTS_TYPE},
ACCOUNTS_ID + "=? ", new String[] {String.valueOf(accountId)}, null, null, null);
try {
while (cursor.moveToNext()) {
return new Account(cursor.getString(0), cursor.getString(1));
}
} finally {
cursor.close();
}
return null;
}
/**
* Returns a map from packageNames to visibility.
*/
Map<String, Integer> findAllVisibilityValuesForAccount(Account account) {
SQLiteDatabase db = mDeDatabase.getReadableDatabase();
Map<String, Integer> result = new HashMap<>();
final Cursor cursor =
db.query(TABLE_VISIBILITY, new String[] {VISIBILITY_PACKAGE, VISIBILITY_VALUE},
SELECTION_ACCOUNTS_ID_BY_ACCOUNT, new String[] {account.name, account.type},
null, null, null);
try {
while (cursor.moveToNext()) {
result.put(cursor.getString(0), cursor.getInt(1));
}
} finally {
cursor.close();
}
return result;
}
/**
* Returns a map account -> (package -> visibility)
*/
Map <Account, Map<String, Integer>> findAllVisibilityValues() {
SQLiteDatabase db = mDeDatabase.getReadableDatabase();
Map<Account, Map<String, Integer>> result = new HashMap<>();
Cursor cursor = db.rawQuery(
"SELECT " + TABLE_VISIBILITY + "." + VISIBILITY_PACKAGE
+ ", " + TABLE_VISIBILITY + "." + VISIBILITY_VALUE
+ ", " + TABLE_ACCOUNTS + "." + ACCOUNTS_NAME
+ ", " + TABLE_ACCOUNTS + "." + ACCOUNTS_TYPE
+ " FROM " + TABLE_VISIBILITY
+ " JOIN " + TABLE_ACCOUNTS
+ " ON " + TABLE_ACCOUNTS + "." + ACCOUNTS_ID
+ " = " + TABLE_VISIBILITY + "." + VISIBILITY_ACCOUNTS_ID, null);
try {
while (cursor.moveToNext()) {
String packageName = cursor.getString(0);
Integer visibility = cursor.getInt(1);
String accountName = cursor.getString(2);
String accountType = cursor.getString(3);
Account account = new Account(accountName, accountType);
Map <String, Integer> accountVisibility = result.get(account);
if (accountVisibility == null) {
accountVisibility = new HashMap<>();
result.put(account, accountVisibility);
}
accountVisibility.put(packageName, visibility);
}
} finally {
cursor.close();
}
return result;
}
boolean deleteAccountVisibilityForPackage(String packageName) {
SQLiteDatabase db = mDeDatabase.getWritableDatabase();
return db.delete(TABLE_VISIBILITY, VISIBILITY_PACKAGE + "=? ",
new String[] {packageName}) > 0;
}
long insertOrReplaceMetaAuthTypeAndUid(String authenticatorType, int uid) {
SQLiteDatabase db = mDeDatabase.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(META_KEY,
META_KEY_FOR_AUTHENTICATOR_UID_FOR_TYPE_PREFIX + authenticatorType);
values.put(META_VALUE, uid);
return db.insertWithOnConflict(TABLE_META, null, values,
SQLiteDatabase.CONFLICT_REPLACE);
}
Map<String, Integer> findMetaAuthUid() {
SQLiteDatabase db = mDeDatabase.getReadableDatabase();
Cursor metaCursor = db.query(
TABLE_META,
new String[]{META_KEY, META_VALUE},
SELECTION_META_BY_AUTHENTICATOR_TYPE,
new String[]{META_KEY_FOR_AUTHENTICATOR_UID_FOR_TYPE_PREFIX + "%"},
null /* groupBy */,
null /* having */,
META_KEY);
Map<String, Integer> map = new LinkedHashMap<>();
try {
while (metaCursor.moveToNext()) {
String type = TextUtils
.split(metaCursor.getString(0), META_KEY_DELIMITER)[1];
String uidStr = metaCursor.getString(1);
if (TextUtils.isEmpty(type) || TextUtils.isEmpty(uidStr)) {
// Should never happen.
Slog.e(TAG, "Auth type empty: " + TextUtils.isEmpty(type)
+ ", uid empty: " + TextUtils.isEmpty(uidStr));
continue;
}
int uid = Integer.parseInt(metaCursor.getString(1));
map.put(type, uid);
}
} finally {
metaCursor.close();
}
return map;
}
boolean deleteMetaByAuthTypeAndUid(String type, int uid) {
SQLiteDatabase db = mDeDatabase.getWritableDatabase();
return db.delete(
TABLE_META,
META_KEY + "=? AND " + META_VALUE + "=?",
new String[]{
META_KEY_FOR_AUTHENTICATOR_UID_FOR_TYPE_PREFIX + type,
String.valueOf(uid)}
) > 0;
}
/**
* Returns list of all grants as {@link Pair pairs} of account name and UID.
*/
List<Pair<String, Integer>> findAllAccountGrants() {
SQLiteDatabase db = mDeDatabase.getReadableDatabase();
try (Cursor cursor = db.rawQuery(ACCOUNT_ACCESS_GRANTS, null)) {
if (cursor == null || !cursor.moveToFirst()) {
return Collections.emptyList();
}
List<Pair<String, Integer>> results = new ArrayList<>();
do {
final String accountName = cursor.getString(0);
final int uid = cursor.getInt(1);
results.add(Pair.create(accountName, uid));
} while (cursor.moveToNext());
return results;
}
}
private static class PreNDatabaseHelper extends SQLiteOpenHelper {
private final Context mContext;
private final int mUserId;
PreNDatabaseHelper(Context context, int userId, String preNDatabaseName) {
super(context, preNDatabaseName, null, PRE_N_DATABASE_VERSION);
mContext = context;
mUserId = userId;
}
@Override
public void onCreate(SQLiteDatabase db) {
// We use PreNDatabaseHelper only if pre-N db exists
throw new IllegalStateException("Legacy database cannot be created - only upgraded!");
}
private void createSharedAccountsTable(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + TABLE_SHARED_ACCOUNTS + " ( "
+ ACCOUNTS_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ ACCOUNTS_NAME + " TEXT NOT NULL, "
+ ACCOUNTS_TYPE + " TEXT NOT NULL, "
+ "UNIQUE(" + ACCOUNTS_NAME + "," + ACCOUNTS_TYPE + "))");
}
private void addLastSuccessfullAuthenticatedTimeColumn(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + TABLE_ACCOUNTS + " ADD COLUMN "
+ ACCOUNTS_LAST_AUTHENTICATE_TIME_EPOCH_MILLIS + " DEFAULT 0");
}
private void addOldAccountNameColumn(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + TABLE_ACCOUNTS + " ADD COLUMN " + ACCOUNTS_PREVIOUS_NAME);
}
private void addDebugTable(SQLiteDatabase db) {
DeDatabaseHelper.createDebugTable(db);
}
private void createAccountsDeletionTrigger(SQLiteDatabase db) {
db.execSQL(""
+ " CREATE TRIGGER " + TABLE_ACCOUNTS + "Delete DELETE ON " + TABLE_ACCOUNTS
+ " BEGIN"
+ " DELETE FROM " + TABLE_AUTHTOKENS
+ " WHERE " + AUTHTOKENS_ACCOUNTS_ID + "=OLD." + ACCOUNTS_ID + " ;"
+ " DELETE FROM " + TABLE_EXTRAS
+ " WHERE " + EXTRAS_ACCOUNTS_ID + "=OLD." + ACCOUNTS_ID + " ;"
+ " DELETE FROM " + TABLE_GRANTS
+ " WHERE " + GRANTS_ACCOUNTS_ID + "=OLD." + ACCOUNTS_ID + " ;"
+ " END");
}
private void createGrantsTable(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + TABLE_GRANTS + " ( "
+ GRANTS_ACCOUNTS_ID + " INTEGER NOT NULL, "
+ GRANTS_AUTH_TOKEN_TYPE + " STRING NOT NULL, "
+ GRANTS_GRANTEE_UID + " INTEGER NOT NULL, "
+ "UNIQUE (" + GRANTS_ACCOUNTS_ID + "," + GRANTS_AUTH_TOKEN_TYPE
+ "," + GRANTS_GRANTEE_UID + "))");
}
static long insertMetaAuthTypeAndUid(SQLiteDatabase db, String authenticatorType, int uid) {
ContentValues values = new ContentValues();
values.put(META_KEY,
META_KEY_FOR_AUTHENTICATOR_UID_FOR_TYPE_PREFIX + authenticatorType);
values.put(META_VALUE, uid);
return db.insert(TABLE_META, null, values);
}
private void populateMetaTableWithAuthTypeAndUID(SQLiteDatabase db,
Map<String, Integer> authTypeAndUIDMap) {
for (Map.Entry<String, Integer> entry : authTypeAndUIDMap.entrySet()) {
insertMetaAuthTypeAndUid(db, entry.getKey(), entry.getValue());
}
}
/**
* Pre-N database may need an upgrade before splitting
*/
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.e(TAG, "upgrade from version " + oldVersion + " to version " + newVersion);
if (oldVersion == 1) {
// no longer need to do anything since the work is done
// when upgrading from version 2
oldVersion++;
}
if (oldVersion == 2) {
createGrantsTable(db);
db.execSQL("DROP TRIGGER " + TABLE_ACCOUNTS + "Delete");
createAccountsDeletionTrigger(db);
oldVersion++;
}
if (oldVersion == 3) {
db.execSQL("UPDATE " + TABLE_ACCOUNTS + " SET " + ACCOUNTS_TYPE +
" = 'com.google' WHERE " + ACCOUNTS_TYPE + " == 'com.google.GAIA'");
oldVersion++;
}
if (oldVersion == 4) {
createSharedAccountsTable(db);
oldVersion++;
}
if (oldVersion == 5) {
addOldAccountNameColumn(db);
oldVersion++;
}
if (oldVersion == 6) {
addLastSuccessfullAuthenticatedTimeColumn(db);
oldVersion++;
}
if (oldVersion == 7) {
addDebugTable(db);
oldVersion++;
}
if (oldVersion == 8) {
populateMetaTableWithAuthTypeAndUID(
db,
AccountManagerService.getAuthenticatorTypeAndUIDForUser(mContext, mUserId));
oldVersion++;
}
if (oldVersion != newVersion) {
Log.e(TAG, "failed to upgrade version " + oldVersion + " to version " + newVersion);
}
}
@Override
public void onOpen(SQLiteDatabase db) {
if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "opened database " + DATABASE_NAME);
}
}
List<Account> findCeAccountsNotInDe() {
SQLiteDatabase db = mDeDatabase.getReadableDatabaseUserIsUnlocked();
// Select accounts from CE that do not exist in DE
Cursor cursor = db.rawQuery(
"SELECT " + ACCOUNTS_NAME + "," + ACCOUNTS_TYPE
+ " FROM " + CE_TABLE_ACCOUNTS
+ " WHERE NOT EXISTS "
+ " (SELECT " + ACCOUNTS_ID + " FROM " + TABLE_ACCOUNTS
+ " WHERE " + ACCOUNTS_ID + "=" + CE_TABLE_ACCOUNTS + "." + ACCOUNTS_ID
+ " )", null);
try {
List<Account> accounts = new ArrayList<>(cursor.getCount());
while (cursor.moveToNext()) {
String accountName = cursor.getString(0);
String accountType = cursor.getString(1);
accounts.add(new Account(accountName, accountType));
}
return accounts;
} finally {
cursor.close();
}
}
boolean deleteCeAccount(long accountId) {
SQLiteDatabase db = mDeDatabase.getWritableDatabaseUserIsUnlocked();
return db.delete(
CE_TABLE_ACCOUNTS, ACCOUNTS_ID + "=" + accountId, null) > 0;
}
boolean isCeDatabaseAttached() {
return mDeDatabase.mCeAttached;
}
void beginTransaction() {
mDeDatabase.getWritableDatabase().beginTransaction();
}
void setTransactionSuccessful() {
mDeDatabase.getWritableDatabase().setTransactionSuccessful();
}
void endTransaction() {
mDeDatabase.getWritableDatabase().endTransaction();
}
void attachCeDatabase(File ceDbFile) {
CeDatabaseHelper.create(mContext, mPreNDatabaseFile, ceDbFile);
SQLiteDatabase db = mDeDatabase.getWritableDatabase();
db.execSQL("ATTACH DATABASE '" + ceDbFile.getPath()+ "' AS ceDb");
mDeDatabase.mCeAttached = true;
}
/*
* Finds the row key where the next insertion should take place. Returns number of rows
* if it is less {@link #MAX_DEBUG_DB_SIZE}, otherwise finds the lowest number available.
*/
long calculateDebugTableInsertionPoint() {
try {
SQLiteDatabase db = mDeDatabase.getReadableDatabase();
String queryCountDebugDbRows = "SELECT COUNT(*) FROM " + TABLE_DEBUG;
int size = (int) DatabaseUtils.longForQuery(db, queryCountDebugDbRows, null);
if (size < MAX_DEBUG_DB_SIZE) {
return size;
}
// This query finds the smallest timestamp value (and if 2 records have
// same timestamp, the choose the lower id).
queryCountDebugDbRows =
"SELECT " + DEBUG_TABLE_KEY
+ " FROM " + TABLE_DEBUG
+ " ORDER BY " + DEBUG_TABLE_TIMESTAMP + ","
+ DEBUG_TABLE_KEY
+ " LIMIT 1";
return DatabaseUtils.longForQuery(db, queryCountDebugDbRows, null);
} catch (SQLiteException e) {
Log.e(TAG, "Failed to open debug table" + e);
return -1;
}
}
SQLiteStatement compileSqlStatementForLogging() {
SQLiteDatabase db = mDeDatabase.getWritableDatabase();
String sql = "INSERT OR REPLACE INTO " + AccountsDb.TABLE_DEBUG
+ " VALUES (?,?,?,?,?,?)";
return db.compileStatement(sql);
}
/**
* Returns statement for logging or {@code null} on database open failure.
* Returned value must be guarded by {link #debugStatementLock}
*/
@Nullable SQLiteStatement getStatementForLogging() {
if (mDebugStatementForLogging != null) {
return mDebugStatementForLogging;
}
try {
mDebugStatementForLogging = compileSqlStatementForLogging();
return mDebugStatementForLogging;
} catch (SQLiteException e) {
Log.e(TAG, "Failed to open debug table" + e);
return null;
}
}
void closeDebugStatement() {
synchronized (mDebugStatementLock) {
if (mDebugStatementForLogging != null) {
mDebugStatementForLogging.close();
mDebugStatementForLogging = null;
}
}
}
long reserveDebugDbInsertionPoint() {
if (mDebugDbInsertionPoint == -1) {
mDebugDbInsertionPoint = calculateDebugTableInsertionPoint();
return mDebugDbInsertionPoint;
}
mDebugDbInsertionPoint = (mDebugDbInsertionPoint + 1) % MAX_DEBUG_DB_SIZE;
return mDebugDbInsertionPoint;
}
void dumpDebugTable(PrintWriter pw) {
SQLiteDatabase db = mDeDatabase.getReadableDatabase();
Cursor cursor = db.query(TABLE_DEBUG, null,
null, null, null, null, DEBUG_TABLE_TIMESTAMP);
pw.println("AccountId, Action_Type, timestamp, UID, TableName, Key");
pw.println("Accounts History");
try {
while (cursor.moveToNext()) {
// print type,count
pw.println(cursor.getString(0) + "," + cursor.getString(1) + "," +
cursor.getString(2) + "," + cursor.getString(3) + ","
+ cursor.getString(4) + "," + cursor.getString(5));
}
} finally {
cursor.close();
}
}
@Override
public void close() {
mDeDatabase.close();
}
static void deleteDbFileWarnIfFailed(File dbFile) {
if (!SQLiteDatabase.deleteDatabase(dbFile)) {
Log.w(TAG, "Database at " + dbFile + " was not deleted successfully");
}
}
public static AccountsDb create(Context context, int userId, File preNDatabaseFile,
File deDatabaseFile) {
boolean newDbExists = deDatabaseFile.exists();
DeDatabaseHelper deDatabaseHelper = new DeDatabaseHelper(context, userId,
deDatabaseFile.getPath());
// If the db just created, and there is a legacy db, migrate it
if (!newDbExists && preNDatabaseFile.exists()) {
// Migrate legacy db to the latest version - PRE_N_DATABASE_VERSION
PreNDatabaseHelper
preNDatabaseHelper = new PreNDatabaseHelper(context, userId,
preNDatabaseFile.getPath());
// Open the database to force upgrade if required
preNDatabaseHelper.getWritableDatabase();
preNDatabaseHelper.close();
// Move data without SPII to DE
deDatabaseHelper.migratePreNDbToDe(preNDatabaseFile);
}
return new AccountsDb(deDatabaseHelper, context, preNDatabaseFile);
}
/**
* Removes all tables and triggers created by AccountManager.
*/
private static void resetDatabase(SQLiteDatabase db) {
try (Cursor c = db.rawQuery("SELECT name FROM sqlite_master WHERE type ='table'", null)) {
while (c.moveToNext()) {
String name = c.getString(0);
// Skip tables managed by SQLiteDatabase
if ("android_metadata".equals(name) || "sqlite_sequence".equals(name)) {
continue;
}
db.execSQL("DROP TABLE IF EXISTS " + name);
}
}
try (Cursor c = db.rawQuery("SELECT name FROM sqlite_master WHERE type ='trigger'", null)) {
while (c.moveToNext()) {
String name = c.getString(0);
db.execSQL("DROP TRIGGER IF EXISTS " + name);
}
}
}
}