/* * Copyright (c) 2014-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.stetho.inspector.database; import android.annotation.TargetApi; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteStatement; import com.facebook.stetho.common.Util; import com.facebook.stetho.inspector.protocol.module.Database; import com.facebook.stetho.inspector.protocol.module.DatabaseConstants; import com.facebook.stetho.inspector.protocol.module.DatabaseDescriptor; import com.facebook.stetho.inspector.protocol.module.DatabaseDriver2; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.annotation.concurrent.ThreadSafe; @ThreadSafe public class SqliteDatabaseDriver extends DatabaseDriver2<SqliteDatabaseDriver.SqliteDatabaseDescriptor> { private static final String[] UNINTERESTING_FILENAME_SUFFIXES = new String[]{ "-journal", "-shm", "-uid", "-wal" }; private final DatabaseFilesProvider mDatabaseFilesProvider; private final DatabaseConnectionProvider mDatabaseConnectionProvider; /** * Constructs the object with a {@link DatabaseFilesProvider} that supplies the database files * from {@link Context#databaseList()}. * * @param context the context * @deprecated use {@link SqliteDatabaseDriver#SqliteDatabaseDriver(Context, String, DatabaseFilesProvider, DatabaseConnectionProvider)} */ @Deprecated public SqliteDatabaseDriver(Context context) { this( context, new DefaultDatabaseFilesProvider(context), new DefaultDatabaseConnectionProvider()); } /** * @deprecated use {@link SqliteDatabaseDriver#SqliteDatabaseDriver(Context, String, DatabaseFilesProvider, DatabaseConnectionProvider)} */ @Deprecated public SqliteDatabaseDriver( Context context, DatabaseFilesProvider databaseFilesProvider) { this( context, databaseFilesProvider, new DefaultDatabaseConnectionProvider()); } /** * @param context the context * @param namespace label to apply to the driver when it appears in the UI * @param databaseFilesProvider a database file name provider * @param databaseConnectionProvider a database connection provider */ public SqliteDatabaseDriver( Context context, DatabaseFilesProvider databaseFilesProvider, DatabaseConnectionProvider databaseConnectionProvider) { super(context); mDatabaseFilesProvider = databaseFilesProvider; mDatabaseConnectionProvider = databaseConnectionProvider; } @Override public List<SqliteDatabaseDescriptor> getDatabaseNames() { ArrayList<SqliteDatabaseDescriptor> databases = new ArrayList<>(); List<File> potentialDatabaseFiles = mDatabaseFilesProvider.getDatabaseFiles(); Collections.sort(potentialDatabaseFiles); Iterable<File> tidiedList = tidyDatabaseList(potentialDatabaseFiles); for (File database : tidiedList) { databases.add(new SqliteDatabaseDescriptor(database)); } return databases; } /** * Attempt to smartly eliminate uninteresting shadow databases such as -journal and -uid. Note * that this only removes the database if it is true that it shadows another database lacking * the uninteresting suffix. * * @param databaseFiles Raw list of database files. * @return Tidied list with shadow databases removed. */ // @VisibleForTesting static List<File> tidyDatabaseList(List<File> databaseFiles) { Set<File> originalAsSet = new HashSet<File>(databaseFiles); List<File> tidiedList = new ArrayList<File>(); for (File databaseFile : databaseFiles) { String databaseFilename = databaseFile.getPath(); String sansSuffix = removeSuffix(databaseFilename, UNINTERESTING_FILENAME_SUFFIXES); if (sansSuffix.equals(databaseFilename) || !originalAsSet.contains(new File(sansSuffix))) { tidiedList.add(databaseFile); } } return tidiedList; } private static String removeSuffix(String str, String[] suffixesToRemove) { for (String suffix : suffixesToRemove) { if (str.endsWith(suffix)) { return str.substring(0, str.length() - suffix.length()); } } return str; } public List<String> getTableNames(SqliteDatabaseDescriptor databaseDesc) throws SQLiteException { SQLiteDatabase database = openDatabase(databaseDesc); try { Cursor cursor = database.rawQuery("SELECT name FROM sqlite_master WHERE type IN (?, ?)", new String[] { "table", "view" }); try { List<String> tableNames = new ArrayList<String>(); while (cursor.moveToNext()) { tableNames.add(cursor.getString(0)); } return tableNames; } finally { cursor.close(); } } finally { database.close(); } } public Database.ExecuteSQLResponse executeSQL( SqliteDatabaseDescriptor databaseDesc, String query, ExecuteResultHandler<Database.ExecuteSQLResponse> handler) throws SQLiteException { Util.throwIfNull(query); Util.throwIfNull(handler); SQLiteDatabase database = openDatabase(databaseDesc); try { String firstWordUpperCase = getFirstWord(query).toUpperCase(); switch (firstWordUpperCase) { case "UPDATE": case "DELETE": return executeUpdateDelete(database, query, handler); case "INSERT": return executeInsert(database, query, handler); case "SELECT": case "PRAGMA": case "EXPLAIN": return executeSelect(database, query, handler); default: return executeRawQuery(database, query, handler); } } finally { database.close(); } } private static String getFirstWord(String s) { s = s.trim(); int firstSpace = s.indexOf(' '); return firstSpace >= 0 ? s.substring(0, firstSpace) : s; } @TargetApi(DatabaseConstants.MIN_API_LEVEL) private <T> T executeUpdateDelete( SQLiteDatabase database, String query, ExecuteResultHandler<T> handler) { SQLiteStatement statement = database.compileStatement(query); int count = statement.executeUpdateDelete(); return handler.handleUpdateDelete(count); } private <T> T executeInsert( SQLiteDatabase database, String query, ExecuteResultHandler<T> handler) { SQLiteStatement statement = database.compileStatement(query); long count = statement.executeInsert(); return handler.handleInsert(count); } private <T> T executeSelect( SQLiteDatabase database, String query, ExecuteResultHandler<T> handler) { Cursor cursor = database.rawQuery(query, null); try { return handler.handleSelect(cursor); } finally { cursor.close(); } } private <T> T executeRawQuery( SQLiteDatabase database, String query, ExecuteResultHandler<T> handler) { database.execSQL(query); return handler.handleRawQuery(); } private SQLiteDatabase openDatabase( SqliteDatabaseDescriptor databaseDesc) throws SQLiteException { Util.throwIfNull(databaseDesc); return mDatabaseConnectionProvider.openDatabase(databaseDesc.file); } static class SqliteDatabaseDescriptor implements DatabaseDescriptor { public final File file; public SqliteDatabaseDescriptor(File file) { this.file = file; } @Override public String name() { return file.getName(); } } }