content: /*
   * 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);
              }
          }
      }
  }
  