| /* |
| * Copyright (C) 2006 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 android.database; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.compat.annotation.UnsupportedAppUsage; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.content.OperationApplicationException; |
| import android.database.sqlite.Flags; |
| import android.database.sqlite.SQLiteAbortException; |
| import android.database.sqlite.SQLiteConstraintException; |
| import android.database.sqlite.SQLiteDatabase; |
| import android.database.sqlite.SQLiteDatabaseCorruptException; |
| import android.database.sqlite.SQLiteDiskIOException; |
| import android.database.sqlite.SQLiteException; |
| import android.database.sqlite.SQLiteFullException; |
| import android.database.sqlite.SQLiteProgram; |
| import android.database.sqlite.SQLiteStatement; |
| import android.os.Build; |
| import android.os.OperationCanceledException; |
| import android.os.Parcel; |
| import android.os.ParcelFileDescriptor; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| import com.android.internal.util.ArrayUtils; |
| |
| import java.io.FileNotFoundException; |
| import java.io.PrintStream; |
| import java.text.Collator; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * Static utility methods for dealing with databases and {@link Cursor}s. |
| */ |
| public class DatabaseUtils { |
| private static final String TAG = "DatabaseUtils"; |
| |
| private static final boolean DEBUG = false; |
| |
| /** One of the values returned by {@link #getSqlStatementType(String)}. */ |
| public static final int STATEMENT_SELECT = 1; |
| /** One of the values returned by {@link #getSqlStatementType(String)}. */ |
| public static final int STATEMENT_UPDATE = 2; |
| /** One of the values returned by {@link #getSqlStatementType(String)}. */ |
| public static final int STATEMENT_ATTACH = 3; |
| /** One of the values returned by {@link #getSqlStatementType(String)}. */ |
| public static final int STATEMENT_BEGIN = 4; |
| /** One of the values returned by {@link #getSqlStatementType(String)}. */ |
| public static final int STATEMENT_COMMIT = 5; |
| /** One of the values returned by {@link #getSqlStatementType(String)}. */ |
| public static final int STATEMENT_ABORT = 6; |
| /** One of the values returned by {@link #getSqlStatementType(String)}. */ |
| public static final int STATEMENT_PRAGMA = 7; |
| /** One of the values returned by {@link #getSqlStatementType(String)}. */ |
| public static final int STATEMENT_DDL = 8; |
| /** One of the values returned by {@link #getSqlStatementType(String)}. */ |
| public static final int STATEMENT_UNPREPARED = 9; |
| /** One of the values returned by {@link #getSqlStatementType(String)}. */ |
| public static final int STATEMENT_OTHER = 99; |
| |
| // The following statement types are "extended" and are for internal use only. These types |
| // are not public and are never returned by {@link #getSqlStatementType(String)}. |
| |
| /** An internal statement type @hide **/ |
| public static final int STATEMENT_WITH = 100; |
| /** An internal statement type @hide **/ |
| public static final int STATEMENT_CREATE = 101; |
| /** An internal statement type denoting a comment. @hide **/ |
| public static final int STATEMENT_COMMENT = 102; |
| |
| /** |
| * Special function for writing an exception result at the header of |
| * a parcel, to be used when returning an exception from a transaction. |
| * exception will be re-thrown by the function in another process |
| * @param reply Parcel to write to |
| * @param e The Exception to be written. |
| * @see Parcel#writeNoException |
| * @see Parcel#writeException |
| */ |
| public static final void writeExceptionToParcel(Parcel reply, Exception e) { |
| int code = 0; |
| boolean logException = true; |
| if (e instanceof FileNotFoundException) { |
| code = 1; |
| logException = false; |
| } else if (e instanceof IllegalArgumentException) { |
| code = 2; |
| } else if (e instanceof UnsupportedOperationException) { |
| code = 3; |
| } else if (e instanceof SQLiteAbortException) { |
| code = 4; |
| } else if (e instanceof SQLiteConstraintException) { |
| code = 5; |
| } else if (e instanceof SQLiteDatabaseCorruptException) { |
| code = 6; |
| } else if (e instanceof SQLiteFullException) { |
| code = 7; |
| } else if (e instanceof SQLiteDiskIOException) { |
| code = 8; |
| } else if (e instanceof SQLiteException) { |
| code = 9; |
| } else if (e instanceof OperationApplicationException) { |
| code = 10; |
| } else if (e instanceof OperationCanceledException) { |
| code = 11; |
| logException = false; |
| } else { |
| reply.writeException(e); |
| Log.e(TAG, "Writing exception to parcel", e); |
| return; |
| } |
| reply.writeInt(code); |
| reply.writeString(e.getMessage()); |
| |
| if (logException) { |
| Log.e(TAG, "Writing exception to parcel", e); |
| } |
| } |
| |
| /** |
| * Special function for reading an exception result from the header of |
| * a parcel, to be used after receiving the result of a transaction. This |
| * will throw the exception for you if it had been written to the Parcel, |
| * otherwise return and let you read the normal result data from the Parcel. |
| * @param reply Parcel to read from |
| * @see Parcel#writeNoException |
| * @see Parcel#readException |
| */ |
| public static final void readExceptionFromParcel(Parcel reply) { |
| int code = reply.readExceptionCode(); |
| if (code == 0) return; |
| String msg = reply.readString(); |
| DatabaseUtils.readExceptionFromParcel(reply, msg, code); |
| } |
| |
| public static void readExceptionWithFileNotFoundExceptionFromParcel( |
| Parcel reply) throws FileNotFoundException { |
| int code = reply.readExceptionCode(); |
| if (code == 0) return; |
| String msg = reply.readString(); |
| if (code == 1) { |
| throw new FileNotFoundException(msg); |
| } else { |
| DatabaseUtils.readExceptionFromParcel(reply, msg, code); |
| } |
| } |
| |
| public static void readExceptionWithOperationApplicationExceptionFromParcel( |
| Parcel reply) throws OperationApplicationException { |
| int code = reply.readExceptionCode(); |
| if (code == 0) return; |
| String msg = reply.readString(); |
| if (code == 10) { |
| throw new OperationApplicationException(msg); |
| } else { |
| DatabaseUtils.readExceptionFromParcel(reply, msg, code); |
| } |
| } |
| |
| private static final void readExceptionFromParcel(Parcel reply, String msg, int code) { |
| switch (code) { |
| case 2: |
| throw new IllegalArgumentException(msg); |
| case 3: |
| throw new UnsupportedOperationException(msg); |
| case 4: |
| throw new SQLiteAbortException(msg); |
| case 5: |
| throw new SQLiteConstraintException(msg); |
| case 6: |
| throw new SQLiteDatabaseCorruptException(msg); |
| case 7: |
| throw new SQLiteFullException(msg); |
| case 8: |
| throw new SQLiteDiskIOException(msg); |
| case 9: |
| throw new SQLiteException(msg); |
| case 11: |
| throw new OperationCanceledException(msg); |
| default: |
| reply.readException(code, msg); |
| } |
| } |
| |
| /** {@hide} */ |
| public static long executeInsert(@NonNull SQLiteDatabase db, @NonNull String sql, |
| @Nullable Object[] bindArgs) throws SQLException { |
| try (SQLiteStatement st = db.compileStatement(sql)) { |
| bindArgs(st, bindArgs); |
| return st.executeInsert(); |
| } |
| } |
| |
| /** {@hide} */ |
| public static int executeUpdateDelete(@NonNull SQLiteDatabase db, @NonNull String sql, |
| @Nullable Object[] bindArgs) throws SQLException { |
| try (SQLiteStatement st = db.compileStatement(sql)) { |
| bindArgs(st, bindArgs); |
| return st.executeUpdateDelete(); |
| } |
| } |
| |
| /** {@hide} */ |
| private static void bindArgs(@NonNull SQLiteStatement st, @Nullable Object[] bindArgs) { |
| if (bindArgs == null) return; |
| |
| for (int i = 0; i < bindArgs.length; i++) { |
| final Object bindArg = bindArgs[i]; |
| switch (getTypeOfObject(bindArg)) { |
| case Cursor.FIELD_TYPE_NULL: |
| st.bindNull(i + 1); |
| break; |
| case Cursor.FIELD_TYPE_INTEGER: |
| st.bindLong(i + 1, ((Number) bindArg).longValue()); |
| break; |
| case Cursor.FIELD_TYPE_FLOAT: |
| st.bindDouble(i + 1, ((Number) bindArg).doubleValue()); |
| break; |
| case Cursor.FIELD_TYPE_BLOB: |
| st.bindBlob(i + 1, (byte[]) bindArg); |
| break; |
| case Cursor.FIELD_TYPE_STRING: |
| default: |
| if (bindArg instanceof Boolean) { |
| // Provide compatibility with legacy |
| // applications which may pass Boolean values in |
| // bind args. |
| st.bindLong(i + 1, ((Boolean) bindArg).booleanValue() ? 1 : 0); |
| } else { |
| st.bindString(i + 1, bindArg.toString()); |
| } |
| break; |
| } |
| } |
| } |
| |
| /** |
| * Binds the given Object to the given SQLiteProgram using the proper |
| * typing. For example, bind numbers as longs/doubles, and everything else |
| * as a string by call toString() on it. |
| * |
| * @param prog the program to bind the object to |
| * @param index the 1-based index to bind at |
| * @param value the value to bind |
| */ |
| public static void bindObjectToProgram(SQLiteProgram prog, int index, |
| Object value) { |
| if (value == null) { |
| prog.bindNull(index); |
| } else if (value instanceof Double || value instanceof Float) { |
| prog.bindDouble(index, ((Number)value).doubleValue()); |
| } else if (value instanceof Number) { |
| prog.bindLong(index, ((Number)value).longValue()); |
| } else if (value instanceof Boolean) { |
| Boolean bool = (Boolean)value; |
| if (bool) { |
| prog.bindLong(index, 1); |
| } else { |
| prog.bindLong(index, 0); |
| } |
| } else if (value instanceof byte[]){ |
| prog.bindBlob(index, (byte[]) value); |
| } else { |
| prog.bindString(index, value.toString()); |
| } |
| } |
| |
| /** |
| * Bind the given selection with the given selection arguments. |
| * <p> |
| * Internally assumes that '?' is only ever used for arguments, and doesn't |
| * appear as a literal or escaped value. |
| * <p> |
| * This method is typically useful for trusted code that needs to cook up a |
| * fully-bound selection. |
| * |
| * @hide |
| */ |
| public static @Nullable String bindSelection(@Nullable String selection, |
| @Nullable Object... selectionArgs) { |
| if (selection == null) return null; |
| // If no arguments provided, so we can't bind anything |
| if (ArrayUtils.isEmpty(selectionArgs)) return selection; |
| // If no bindings requested, so we can shortcut |
| if (selection.indexOf('?') == -1) return selection; |
| |
| // Track the chars immediately before and after each bind request, to |
| // decide if it needs additional whitespace added |
| char before = ' '; |
| char after = ' '; |
| |
| int argIndex = 0; |
| final int len = selection.length(); |
| final StringBuilder res = new StringBuilder(len); |
| for (int i = 0; i < len; ) { |
| char c = selection.charAt(i++); |
| if (c == '?') { |
| // Assume this bind request is guarded until we find a specific |
| // trailing character below |
| after = ' '; |
| |
| // Sniff forward to see if the selection is requesting a |
| // specific argument index |
| int start = i; |
| for (; i < len; i++) { |
| c = selection.charAt(i); |
| if (c < '0' || c > '9') { |
| after = c; |
| break; |
| } |
| } |
| if (start != i) { |
| argIndex = Integer.parseInt(selection.substring(start, i)) - 1; |
| } |
| |
| // Manually bind the argument into the selection, adding |
| // whitespace when needed for clarity |
| final Object arg = selectionArgs[argIndex++]; |
| if (before != ' ' && before != '=') res.append(' '); |
| switch (DatabaseUtils.getTypeOfObject(arg)) { |
| case Cursor.FIELD_TYPE_NULL: |
| res.append("NULL"); |
| break; |
| case Cursor.FIELD_TYPE_INTEGER: |
| res.append(((Number) arg).longValue()); |
| break; |
| case Cursor.FIELD_TYPE_FLOAT: |
| res.append(((Number) arg).doubleValue()); |
| break; |
| case Cursor.FIELD_TYPE_BLOB: |
| throw new IllegalArgumentException("Blobs not supported"); |
| case Cursor.FIELD_TYPE_STRING: |
| default: |
| if (arg instanceof Boolean) { |
| // Provide compatibility with legacy applications which may pass |
| // Boolean values in bind args. |
| res.append(((Boolean) arg).booleanValue() ? 1 : 0); |
| } else { |
| res.append('\''); |
| res.append(arg.toString()); |
| res.append('\''); |
| } |
| break; |
| } |
| if (after != ' ') res.append(' '); |
| } else { |
| res.append(c); |
| before = c; |
| } |
| } |
| return res.toString(); |
| } |
| |
| /** |
| * Make a deep copy of the given argument list, ensuring that the returned |
| * value is completely isolated from any changes to the original arguments. |
| * |
| * @hide |
| */ |
| public static @Nullable Object[] deepCopyOf(@Nullable Object[] args) { |
| if (args == null) return null; |
| |
| final Object[] res = new Object[args.length]; |
| for (int i = 0; i < args.length; i++) { |
| final Object arg = args[i]; |
| |
| if ((arg == null) || (arg instanceof Number) || (arg instanceof String)) { |
| // When the argument is immutable, we can copy by reference |
| res[i] = arg; |
| } else if (arg instanceof byte[]) { |
| // Need to deep copy blobs |
| final byte[] castArg = (byte[]) arg; |
| res[i] = Arrays.copyOf(castArg, castArg.length); |
| } else { |
| // Convert everything else to string, making it immutable |
| res[i] = String.valueOf(arg); |
| } |
| } |
| return res; |
| } |
| |
| /** |
| * Returns data type of the given object's value. |
| *<p> |
| * Returned values are |
| * <ul> |
| * <li>{@link Cursor#FIELD_TYPE_NULL}</li> |
| * <li>{@link Cursor#FIELD_TYPE_INTEGER}</li> |
| * <li>{@link Cursor#FIELD_TYPE_FLOAT}</li> |
| * <li>{@link Cursor#FIELD_TYPE_STRING}</li> |
| * <li>{@link Cursor#FIELD_TYPE_BLOB}</li> |
| *</ul> |
| *</p> |
| * |
| * @param obj the object whose value type is to be returned |
| * @return object value type |
| * @hide |
| */ |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) |
| public static int getTypeOfObject(Object obj) { |
| if (obj == null) { |
| return Cursor.FIELD_TYPE_NULL; |
| } else if (obj instanceof byte[]) { |
| return Cursor.FIELD_TYPE_BLOB; |
| } else if (obj instanceof Float || obj instanceof Double) { |
| return Cursor.FIELD_TYPE_FLOAT; |
| } else if (obj instanceof Long || obj instanceof Integer |
| || obj instanceof Short || obj instanceof Byte) { |
| return Cursor.FIELD_TYPE_INTEGER; |
| } else { |
| return Cursor.FIELD_TYPE_STRING; |
| } |
| } |
| |
| /** |
| * Fills the specified cursor window by iterating over the contents of the cursor. |
| * The window is filled until the cursor is exhausted or the window runs out |
| * of space. |
| * |
| * The original position of the cursor is left unchanged by this operation. |
| * |
| * @param cursor The cursor that contains the data to put in the window. |
| * @param position The start position for filling the window. |
| * @param window The window to fill. |
| * @hide |
| */ |
| public static void cursorFillWindow(final Cursor cursor, |
| int position, final CursorWindow window) { |
| if (position < 0 || position >= cursor.getCount()) { |
| return; |
| } |
| final int oldPos = cursor.getPosition(); |
| final int numColumns = cursor.getColumnCount(); |
| window.clear(); |
| window.setStartPosition(position); |
| window.setNumColumns(numColumns); |
| if (cursor.moveToPosition(position)) { |
| rowloop: do { |
| if (!window.allocRow()) { |
| break; |
| } |
| for (int i = 0; i < numColumns; i++) { |
| final int type = cursor.getType(i); |
| final boolean success; |
| switch (type) { |
| case Cursor.FIELD_TYPE_NULL: |
| success = window.putNull(position, i); |
| break; |
| |
| case Cursor.FIELD_TYPE_INTEGER: |
| success = window.putLong(cursor.getLong(i), position, i); |
| break; |
| |
| case Cursor.FIELD_TYPE_FLOAT: |
| success = window.putDouble(cursor.getDouble(i), position, i); |
| break; |
| |
| case Cursor.FIELD_TYPE_BLOB: { |
| final byte[] value = cursor.getBlob(i); |
| success = value != null ? window.putBlob(value, position, i) |
| : window.putNull(position, i); |
| break; |
| } |
| |
| default: // assume value is convertible to String |
| case Cursor.FIELD_TYPE_STRING: { |
| final String value = cursor.getString(i); |
| success = value != null ? window.putString(value, position, i) |
| : window.putNull(position, i); |
| break; |
| } |
| } |
| if (!success) { |
| window.freeLastRow(); |
| break rowloop; |
| } |
| } |
| position += 1; |
| } while (cursor.moveToNext()); |
| } |
| cursor.moveToPosition(oldPos); |
| } |
| |
| /** |
| * Appends an SQL string to the given StringBuilder, including the opening |
| * and closing single quotes. Any single quotes internal to sqlString will |
| * be escaped. |
| * |
| * This method is deprecated because we want to encourage everyone |
| * to use the "?" binding form. However, when implementing a |
| * ContentProvider, one may want to add WHERE clauses that were |
| * not provided by the caller. Since "?" is a positional form, |
| * using it in this case could break the caller because the |
| * indexes would be shifted to accomodate the ContentProvider's |
| * internal bindings. In that case, it may be necessary to |
| * construct a WHERE clause manually. This method is useful for |
| * those cases. |
| * |
| * @param sb the StringBuilder that the SQL string will be appended to |
| * @param sqlString the raw string to be appended, which may contain single |
| * quotes |
| */ |
| public static void appendEscapedSQLString(StringBuilder sb, String sqlString) { |
| sb.append('\''); |
| int length = sqlString.length(); |
| for (int i = 0; i < length; i++) { |
| char c = sqlString.charAt(i); |
| if (Character.isHighSurrogate(c)) { |
| if (i == length - 1) { |
| continue; |
| } |
| if (Character.isLowSurrogate(sqlString.charAt(i + 1))) { |
| // add them both |
| sb.append(c); |
| sb.append(sqlString.charAt(i + 1)); |
| continue; |
| } else { |
| // this is a lone surrogate, skip it |
| continue; |
| } |
| } |
| if (Character.isLowSurrogate(c)) { |
| continue; |
| } |
| if (c == '\'') { |
| sb.append('\''); |
| } |
| sb.append(c); |
| } |
| sb.append('\''); |
| } |
| |
| /** |
| * SQL-escape a string. |
| */ |
| public static String sqlEscapeString(String value) { |
| StringBuilder escaper = new StringBuilder(); |
| |
| DatabaseUtils.appendEscapedSQLString(escaper, value); |
| |
| return escaper.toString(); |
| } |
| |
| /** |
| * Appends an Object to an SQL string with the proper escaping, etc. |
| */ |
| public static final void appendValueToSql(StringBuilder sql, Object value) { |
| if (value == null) { |
| sql.append("NULL"); |
| } else if (value instanceof Boolean) { |
| Boolean bool = (Boolean)value; |
| if (bool) { |
| sql.append('1'); |
| } else { |
| sql.append('0'); |
| } |
| } else { |
| appendEscapedSQLString(sql, value.toString()); |
| } |
| } |
| |
| /** |
| * Concatenates two SQL WHERE clauses, handling empty or null values. |
| */ |
| public static String concatenateWhere(String a, String b) { |
| if (TextUtils.isEmpty(a)) { |
| return b; |
| } |
| if (TextUtils.isEmpty(b)) { |
| return a; |
| } |
| |
| return "(" + a + ") AND (" + b + ")"; |
| } |
| |
| /** |
| * return the collation key |
| * @param name |
| * @return the collation key |
| */ |
| public static String getCollationKey(String name) { |
| byte [] arr = getCollationKeyInBytes(name); |
| try { |
| return new String(arr, 0, getKeyLen(arr), "ISO8859_1"); |
| } catch (Exception ex) { |
| return ""; |
| } |
| } |
| |
| /** |
| * return the collation key in hex format |
| * @param name |
| * @return the collation key in hex format |
| */ |
| public static String getHexCollationKey(String name) { |
| byte[] arr = getCollationKeyInBytes(name); |
| char[] keys = encodeHex(arr); |
| return new String(keys, 0, getKeyLen(arr) * 2); |
| } |
| |
| |
| /** |
| * Used building output as Hex |
| */ |
| private static final char[] DIGITS = { |
| '0', '1', '2', '3', '4', '5', '6', '7', |
| '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' |
| }; |
| |
| private static char[] encodeHex(byte[] input) { |
| int l = input.length; |
| char[] out = new char[l << 1]; |
| |
| // two characters form the hex value. |
| for (int i = 0, j = 0; i < l; i++) { |
| out[j++] = DIGITS[(0xF0 & input[i]) >>> 4 ]; |
| out[j++] = DIGITS[ 0x0F & input[i] ]; |
| } |
| |
| return out; |
| } |
| |
| private static int getKeyLen(byte[] arr) { |
| if (arr[arr.length - 1] != 0) { |
| return arr.length; |
| } else { |
| // remove zero "termination" |
| return arr.length-1; |
| } |
| } |
| |
| private static byte[] getCollationKeyInBytes(String name) { |
| if (mColl == null) { |
| mColl = Collator.getInstance(); |
| mColl.setStrength(Collator.PRIMARY); |
| } |
| return mColl.getCollationKey(name).toByteArray(); |
| } |
| |
| private static Collator mColl = null; |
| /** |
| * Prints the contents of a Cursor to System.out. The position is restored |
| * after printing. |
| * |
| * @param cursor the cursor to print |
| */ |
| public static void dumpCursor(Cursor cursor) { |
| dumpCursor(cursor, System.out); |
| } |
| |
| /** |
| * Prints the contents of a Cursor to a PrintSteam. The position is restored |
| * after printing. |
| * |
| * @param cursor the cursor to print |
| * @param stream the stream to print to |
| */ |
| public static void dumpCursor(Cursor cursor, PrintStream stream) { |
| stream.println(">>>>> Dumping cursor " + cursor); |
| if (cursor != null) { |
| int startPos = cursor.getPosition(); |
| |
| cursor.moveToPosition(-1); |
| while (cursor.moveToNext()) { |
| dumpCurrentRow(cursor, stream); |
| } |
| cursor.moveToPosition(startPos); |
| } |
| stream.println("<<<<<"); |
| } |
| |
| /** |
| * Prints the contents of a Cursor to a StringBuilder. The position |
| * is restored after printing. |
| * |
| * @param cursor the cursor to print |
| * @param sb the StringBuilder to print to |
| */ |
| public static void dumpCursor(Cursor cursor, StringBuilder sb) { |
| sb.append(">>>>> Dumping cursor ").append(cursor).append('\n'); |
| if (cursor != null) { |
| int startPos = cursor.getPosition(); |
| |
| cursor.moveToPosition(-1); |
| while (cursor.moveToNext()) { |
| dumpCurrentRow(cursor, sb); |
| } |
| cursor.moveToPosition(startPos); |
| } |
| sb.append("<<<<<\n"); |
| } |
| |
| /** |
| * Prints the contents of a Cursor to a String. The position is restored |
| * after printing. |
| * |
| * @param cursor the cursor to print |
| * @return a String that contains the dumped cursor |
| */ |
| public static String dumpCursorToString(Cursor cursor) { |
| StringBuilder sb = new StringBuilder(); |
| dumpCursor(cursor, sb); |
| return sb.toString(); |
| } |
| |
| /** |
| * Prints the contents of a Cursor's current row to System.out. |
| * |
| * @param cursor the cursor to print from |
| */ |
| public static void dumpCurrentRow(Cursor cursor) { |
| dumpCurrentRow(cursor, System.out); |
| } |
| |
| /** |
| * Prints the contents of a Cursor's current row to a PrintSteam. |
| * |
| * @param cursor the cursor to print |
| * @param stream the stream to print to |
| */ |
| public static void dumpCurrentRow(Cursor cursor, PrintStream stream) { |
| String[] cols = cursor.getColumnNames(); |
| stream.println("" + cursor.getPosition() + " {"); |
| int length = cols.length; |
| for (int i = 0; i< length; i++) { |
| String value; |
| try { |
| value = cursor.getString(i); |
| } catch (SQLiteException e) { |
| // assume that if the getString threw this exception then the column is not |
| // representable by a string, e.g. it is a BLOB. |
| value = "<unprintable>"; |
| } |
| stream.println(" " + cols[i] + '=' + value); |
| } |
| stream.println("}"); |
| } |
| |
| /** |
| * Prints the contents of a Cursor's current row to a StringBuilder. |
| * |
| * @param cursor the cursor to print |
| * @param sb the StringBuilder to print to |
| */ |
| public static void dumpCurrentRow(Cursor cursor, StringBuilder sb) { |
| String[] cols = cursor.getColumnNames(); |
| sb.append(cursor.getPosition()).append(" {\n"); |
| int length = cols.length; |
| for (int i = 0; i < length; i++) { |
| String value; |
| try { |
| value = cursor.getString(i); |
| } catch (SQLiteException e) { |
| // assume that if the getString threw this exception then the column is not |
| // representable by a string, e.g. it is a BLOB. |
| value = "<unprintable>"; |
| } |
| sb.append(" ").append(cols[i]).append('=').append(value).append('\n'); |
| } |
| sb.append("}\n"); |
| } |
| |
| /** |
| * Dump the contents of a Cursor's current row to a String. |
| * |
| * @param cursor the cursor to print |
| * @return a String that contains the dumped cursor row |
| */ |
| public static String dumpCurrentRowToString(Cursor cursor) { |
| StringBuilder sb = new StringBuilder(); |
| dumpCurrentRow(cursor, sb); |
| return sb.toString(); |
| } |
| |
| /** |
| * Reads a String out of a field in a Cursor and writes it to a Map. |
| * |
| * @param cursor The cursor to read from |
| * @param field The TEXT field to read |
| * @param values The {@link ContentValues} to put the value into, with the field as the key |
| */ |
| public static void cursorStringToContentValues(Cursor cursor, String field, |
| ContentValues values) { |
| cursorStringToContentValues(cursor, field, values, field); |
| } |
| |
| /** |
| * Reads a String out of a field in a Cursor and writes it to an InsertHelper. |
| * |
| * @param cursor The cursor to read from |
| * @param field The TEXT field to read |
| * @param inserter The InsertHelper to bind into |
| * @param index the index of the bind entry in the InsertHelper |
| */ |
| public static void cursorStringToInsertHelper(Cursor cursor, String field, |
| InsertHelper inserter, int index) { |
| inserter.bind(index, cursor.getString(cursor.getColumnIndexOrThrow(field))); |
| } |
| |
| /** |
| * Reads a String out of a field in a Cursor and writes it to a Map. |
| * |
| * @param cursor The cursor to read from |
| * @param field The TEXT field to read |
| * @param values The {@link ContentValues} to put the value into, with the field as the key |
| * @param key The key to store the value with in the map |
| */ |
| public static void cursorStringToContentValues(Cursor cursor, String field, |
| ContentValues values, String key) { |
| values.put(key, cursor.getString(cursor.getColumnIndexOrThrow(field))); |
| } |
| |
| /** |
| * Reads an Integer out of a field in a Cursor and writes it to a Map. |
| * |
| * @param cursor The cursor to read from |
| * @param field The INTEGER field to read |
| * @param values The {@link ContentValues} to put the value into, with the field as the key |
| */ |
| public static void cursorIntToContentValues(Cursor cursor, String field, ContentValues values) { |
| cursorIntToContentValues(cursor, field, values, field); |
| } |
| |
| /** |
| * Reads a Integer out of a field in a Cursor and writes it to a Map. |
| * |
| * @param cursor The cursor to read from |
| * @param field The INTEGER field to read |
| * @param values The {@link ContentValues} to put the value into, with the field as the key |
| * @param key The key to store the value with in the map |
| */ |
| public static void cursorIntToContentValues(Cursor cursor, String field, ContentValues values, |
| String key) { |
| int colIndex = cursor.getColumnIndex(field); |
| if (!cursor.isNull(colIndex)) { |
| values.put(key, cursor.getInt(colIndex)); |
| } else { |
| values.put(key, (Integer) null); |
| } |
| } |
| |
| /** |
| * Reads a Long out of a field in a Cursor and writes it to a Map. |
| * |
| * @param cursor The cursor to read from |
| * @param field The INTEGER field to read |
| * @param values The {@link ContentValues} to put the value into, with the field as the key |
| */ |
| public static void cursorLongToContentValues(Cursor cursor, String field, ContentValues values) |
| { |
| cursorLongToContentValues(cursor, field, values, field); |
| } |
| |
| /** |
| * Reads a Long out of a field in a Cursor and writes it to a Map. |
| * |
| * @param cursor The cursor to read from |
| * @param field The INTEGER field to read |
| * @param values The {@link ContentValues} to put the value into |
| * @param key The key to store the value with in the map |
| */ |
| public static void cursorLongToContentValues(Cursor cursor, String field, ContentValues values, |
| String key) { |
| int colIndex = cursor.getColumnIndex(field); |
| if (!cursor.isNull(colIndex)) { |
| Long value = Long.valueOf(cursor.getLong(colIndex)); |
| values.put(key, value); |
| } else { |
| values.put(key, (Long) null); |
| } |
| } |
| |
| /** |
| * Reads a Double out of a field in a Cursor and writes it to a Map. |
| * |
| * @param cursor The cursor to read from |
| * @param field The REAL field to read |
| * @param values The {@link ContentValues} to put the value into |
| */ |
| public static void cursorDoubleToCursorValues(Cursor cursor, String field, ContentValues values) |
| { |
| cursorDoubleToContentValues(cursor, field, values, field); |
| } |
| |
| /** |
| * Reads a Double out of a field in a Cursor and writes it to a Map. |
| * |
| * @param cursor The cursor to read from |
| * @param field The REAL field to read |
| * @param values The {@link ContentValues} to put the value into |
| * @param key The key to store the value with in the map |
| */ |
| public static void cursorDoubleToContentValues(Cursor cursor, String field, |
| ContentValues values, String key) { |
| int colIndex = cursor.getColumnIndex(field); |
| if (!cursor.isNull(colIndex)) { |
| values.put(key, cursor.getDouble(colIndex)); |
| } else { |
| values.put(key, (Double) null); |
| } |
| } |
| |
| /** |
| * Read the entire contents of a cursor row and store them in a ContentValues. |
| * |
| * @param cursor the cursor to read from. |
| * @param values the {@link ContentValues} to put the row into. |
| */ |
| public static void cursorRowToContentValues(Cursor cursor, ContentValues values) { |
| String[] columns = cursor.getColumnNames(); |
| int length = columns.length; |
| for (int i = 0; i < length; i++) { |
| if (cursor.getType(i) == Cursor.FIELD_TYPE_BLOB) { |
| values.put(columns[i], cursor.getBlob(i)); |
| } else { |
| values.put(columns[i], cursor.getString(i)); |
| } |
| } |
| } |
| |
| /** |
| * Picks a start position for {@link Cursor#fillWindow} such that the |
| * window will contain the requested row and a useful range of rows |
| * around it. |
| * |
| * When the data set is too large to fit in a cursor window, seeking the |
| * cursor can become a very expensive operation since we have to run the |
| * query again when we move outside the bounds of the current window. |
| * |
| * We try to choose a start position for the cursor window such that |
| * 1/3 of the window's capacity is used to hold rows before the requested |
| * position and 2/3 of the window's capacity is used to hold rows after the |
| * requested position. |
| * |
| * @param cursorPosition The row index of the row we want to get. |
| * @param cursorWindowCapacity The estimated number of rows that can fit in |
| * a cursor window, or 0 if unknown. |
| * @return The recommended start position, always less than or equal to |
| * the requested row. |
| * @hide |
| */ |
| @UnsupportedAppUsage |
| public static int cursorPickFillWindowStartPosition( |
| int cursorPosition, int cursorWindowCapacity) { |
| return Math.max(cursorPosition - cursorWindowCapacity / 3, 0); |
| } |
| |
| /** |
| * Query the table for the number of rows in the table. |
| * @param db the database the table is in |
| * @param table the name of the table to query |
| * @return the number of rows in the table |
| */ |
| public static long queryNumEntries(SQLiteDatabase db, String table) { |
| return queryNumEntries(db, table, null, null); |
| } |
| |
| /** |
| * Query the table for the number of rows in the table. |
| * @param db the database the table is in |
| * @param table the name of the table to query |
| * @param selection A filter declaring which rows to return, |
| * formatted as an SQL WHERE clause (excluding the WHERE itself). |
| * Passing null will count all rows for the given table |
| * @return the number of rows in the table filtered by the selection |
| */ |
| public static long queryNumEntries(SQLiteDatabase db, String table, String selection) { |
| return queryNumEntries(db, table, selection, null); |
| } |
| |
| /** |
| * Query the table for the number of rows in the table. |
| * @param db the database the table is in |
| * @param table the name of the table to query |
| * @param selection A filter declaring which rows to return, |
| * formatted as an SQL WHERE clause (excluding the WHERE itself). |
| * Passing null will count all rows for the given table |
| * @param selectionArgs You may include ?s in selection, |
| * which will be replaced by the values from selectionArgs, |
| * in order that they appear in the selection. |
| * The values will be bound as Strings. |
| * @return the number of rows in the table filtered by the selection |
| */ |
| public static long queryNumEntries(SQLiteDatabase db, String table, String selection, |
| String[] selectionArgs) { |
| String s = (!TextUtils.isEmpty(selection)) ? " where " + selection : ""; |
| return longForQuery(db, "select count(*) from " + table + s, |
| selectionArgs); |
| } |
| |
| /** |
| * Query the table to check whether a table is empty or not |
| * @param db the database the table is in |
| * @param table the name of the table to query |
| * @return True if the table is empty |
| * @hide |
| */ |
| public static boolean queryIsEmpty(SQLiteDatabase db, String table) { |
| long isEmpty = longForQuery(db, "select exists(select 1 from " + table + ")", null); |
| return isEmpty == 0; |
| } |
| |
| /** |
| * Utility method to run the query on the db and return the value in the |
| * first column of the first row. |
| */ |
| public static long longForQuery(SQLiteDatabase db, String query, String[] selectionArgs) { |
| SQLiteStatement prog = db.compileStatement(query); |
| try { |
| return longForQuery(prog, selectionArgs); |
| } finally { |
| prog.close(); |
| } |
| } |
| |
| /** |
| * Utility method to run the pre-compiled query and return the value in the |
| * first column of the first row. |
| */ |
| public static long longForQuery(SQLiteStatement prog, String[] selectionArgs) { |
| prog.bindAllArgsAsStrings(selectionArgs); |
| return prog.simpleQueryForLong(); |
| } |
| |
| /** |
| * Utility method to run the query on the db and return the value in the |
| * first column of the first row. |
| */ |
| public static String stringForQuery(SQLiteDatabase db, String query, String[] selectionArgs) { |
| SQLiteStatement prog = db.compileStatement(query); |
| try { |
| return stringForQuery(prog, selectionArgs); |
| } finally { |
| prog.close(); |
| } |
| } |
| |
| /** |
| * Utility method to run the pre-compiled query and return the value in the |
| * first column of the first row. |
| */ |
| public static String stringForQuery(SQLiteStatement prog, String[] selectionArgs) { |
| prog.bindAllArgsAsStrings(selectionArgs); |
| return prog.simpleQueryForString(); |
| } |
| |
| /** |
| * Utility method to run the query on the db and return the blob value in the |
| * first column of the first row. |
| * |
| * @return A read-only file descriptor for a copy of the blob value. |
| */ |
| public static ParcelFileDescriptor blobFileDescriptorForQuery(SQLiteDatabase db, |
| String query, String[] selectionArgs) { |
| SQLiteStatement prog = db.compileStatement(query); |
| try { |
| return blobFileDescriptorForQuery(prog, selectionArgs); |
| } finally { |
| prog.close(); |
| } |
| } |
| |
| /** |
| * Utility method to run the pre-compiled query and return the blob value in the |
| * first column of the first row. |
| * |
| * @return A read-only file descriptor for a copy of the blob value. |
| */ |
| public static ParcelFileDescriptor blobFileDescriptorForQuery(SQLiteStatement prog, |
| String[] selectionArgs) { |
| prog.bindAllArgsAsStrings(selectionArgs); |
| return prog.simpleQueryForBlobFileDescriptor(); |
| } |
| |
| /** |
| * Reads a String out of a column in a Cursor and writes it to a ContentValues. |
| * Adds nothing to the ContentValues if the column isn't present or if its value is null. |
| * |
| * @param cursor The cursor to read from |
| * @param column The column to read |
| * @param values The {@link ContentValues} to put the value into |
| */ |
| public static void cursorStringToContentValuesIfPresent(Cursor cursor, ContentValues values, |
| String column) { |
| final int index = cursor.getColumnIndex(column); |
| if (index != -1 && !cursor.isNull(index)) { |
| values.put(column, cursor.getString(index)); |
| } |
| } |
| |
| /** |
| * Reads a Long out of a column in a Cursor and writes it to a ContentValues. |
| * Adds nothing to the ContentValues if the column isn't present or if its value is null. |
| * |
| * @param cursor The cursor to read from |
| * @param column The column to read |
| * @param values The {@link ContentValues} to put the value into |
| */ |
| public static void cursorLongToContentValuesIfPresent(Cursor cursor, ContentValues values, |
| String column) { |
| final int index = cursor.getColumnIndex(column); |
| if (index != -1 && !cursor.isNull(index)) { |
| values.put(column, cursor.getLong(index)); |
| } |
| } |
| |
| /** |
| * Reads a Short out of a column in a Cursor and writes it to a ContentValues. |
| * Adds nothing to the ContentValues if the column isn't present or if its value is null. |
| * |
| * @param cursor The cursor to read from |
| * @param column The column to read |
| * @param values The {@link ContentValues} to put the value into |
| */ |
| public static void cursorShortToContentValuesIfPresent(Cursor cursor, ContentValues values, |
| String column) { |
| final int index = cursor.getColumnIndex(column); |
| if (index != -1 && !cursor.isNull(index)) { |
| values.put(column, cursor.getShort(index)); |
| } |
| } |
| |
| /** |
| * Reads a Integer out of a column in a Cursor and writes it to a ContentValues. |
| * Adds nothing to the ContentValues if the column isn't present or if its value is null. |
| * |
| * @param cursor The cursor to read from |
| * @param column The column to read |
| * @param values The {@link ContentValues} to put the value into |
| */ |
| public static void cursorIntToContentValuesIfPresent(Cursor cursor, ContentValues values, |
| String column) { |
| final int index = cursor.getColumnIndex(column); |
| if (index != -1 && !cursor.isNull(index)) { |
| values.put(column, cursor.getInt(index)); |
| } |
| } |
| |
| /** |
| * Reads a Float out of a column in a Cursor and writes it to a ContentValues. |
| * Adds nothing to the ContentValues if the column isn't present or if its value is null. |
| * |
| * @param cursor The cursor to read from |
| * @param column The column to read |
| * @param values The {@link ContentValues} to put the value into |
| */ |
| public static void cursorFloatToContentValuesIfPresent(Cursor cursor, ContentValues values, |
| String column) { |
| final int index = cursor.getColumnIndex(column); |
| if (index != -1 && !cursor.isNull(index)) { |
| values.put(column, cursor.getFloat(index)); |
| } |
| } |
| |
| /** |
| * Reads a Double out of a column in a Cursor and writes it to a ContentValues. |
| * Adds nothing to the ContentValues if the column isn't present or if its value is null. |
| * |
| * @param cursor The cursor to read from |
| * @param column The column to read |
| * @param values The {@link ContentValues} to put the value into |
| */ |
| public static void cursorDoubleToContentValuesIfPresent(Cursor cursor, ContentValues values, |
| String column) { |
| final int index = cursor.getColumnIndex(column); |
| if (index != -1 && !cursor.isNull(index)) { |
| values.put(column, cursor.getDouble(index)); |
| } |
| } |
| |
| /** |
| * This class allows users to do multiple inserts into a table using |
| * the same statement. |
| * <p> |
| * This class is not thread-safe. |
| * </p> |
| * |
| * @deprecated Use {@link SQLiteStatement} instead. |
| */ |
| @Deprecated |
| public static class InsertHelper { |
| private final SQLiteDatabase mDb; |
| private final String mTableName; |
| private HashMap<String, Integer> mColumns; |
| private String mInsertSQL = null; |
| private SQLiteStatement mInsertStatement = null; |
| private SQLiteStatement mReplaceStatement = null; |
| private SQLiteStatement mPreparedStatement = null; |
| |
| /** |
| * {@hide} |
| * |
| * These are the columns returned by sqlite's "PRAGMA |
| * table_info(...)" command that we depend on. |
| */ |
| public static final int TABLE_INFO_PRAGMA_COLUMNNAME_INDEX = 1; |
| |
| /** |
| * This field was accidentally exposed in earlier versions of the platform |
| * so we can hide it but we can't remove it. |
| * |
| * @hide |
| */ |
| public static final int TABLE_INFO_PRAGMA_DEFAULT_INDEX = 4; |
| |
| /** |
| * @param db the SQLiteDatabase to insert into |
| * @param tableName the name of the table to insert into |
| */ |
| public InsertHelper(SQLiteDatabase db, String tableName) { |
| mDb = db; |
| mTableName = tableName; |
| } |
| |
| private void buildSQL() throws SQLException { |
| StringBuilder sb = new StringBuilder(128); |
| sb.append("INSERT INTO "); |
| sb.append(mTableName); |
| sb.append(" ("); |
| |
| StringBuilder sbv = new StringBuilder(128); |
| sbv.append("VALUES ("); |
| |
| int i = 1; |
| Cursor cur = null; |
| try { |
| cur = mDb.rawQuery("PRAGMA table_info(" + mTableName + ")", null); |
| mColumns = new HashMap<String, Integer>(cur.getCount()); |
| while (cur.moveToNext()) { |
| String columnName = cur.getString(TABLE_INFO_PRAGMA_COLUMNNAME_INDEX); |
| String defaultValue = cur.getString(TABLE_INFO_PRAGMA_DEFAULT_INDEX); |
| |
| mColumns.put(columnName, i); |
| sb.append("'"); |
| sb.append(columnName); |
| sb.append("'"); |
| |
| if (defaultValue == null) { |
| sbv.append("?"); |
| } else { |
| sbv.append("COALESCE(?, "); |
| sbv.append(defaultValue); |
| sbv.append(")"); |
| } |
| |
| sb.append(i == cur.getCount() ? ") " : ", "); |
| sbv.append(i == cur.getCount() ? ");" : ", "); |
| ++i; |
| } |
| } finally { |
| if (cur != null) cur.close(); |
| } |
| |
| sb.append(sbv); |
| |
| mInsertSQL = sb.toString(); |
| if (DEBUG) Log.v(TAG, "insert statement is " + mInsertSQL); |
| } |
| |
| private SQLiteStatement getStatement(boolean allowReplace) throws SQLException { |
| if (allowReplace) { |
| if (mReplaceStatement == null) { |
| if (mInsertSQL == null) buildSQL(); |
| // chop "INSERT" off the front and prepend "INSERT OR REPLACE" instead. |
| String replaceSQL = "INSERT OR REPLACE" + mInsertSQL.substring(6); |
| mReplaceStatement = mDb.compileStatement(replaceSQL); |
| } |
| return mReplaceStatement; |
| } else { |
| if (mInsertStatement == null) { |
| if (mInsertSQL == null) buildSQL(); |
| mInsertStatement = mDb.compileStatement(mInsertSQL); |
| } |
| return mInsertStatement; |
| } |
| } |
| |
| /** |
| * Performs an insert, adding a new row with the given values. |
| * |
| * @param values the set of values with which to populate the |
| * new row |
| * @param allowReplace if true, the statement does "INSERT OR |
| * REPLACE" instead of "INSERT", silently deleting any |
| * previously existing rows that would cause a conflict |
| * |
| * @return the row ID of the newly inserted row, or -1 if an |
| * error occurred |
| */ |
| private long insertInternal(ContentValues values, boolean allowReplace) { |
| // Start a transaction even though we don't really need one. |
| // This is to help maintain compatibility with applications that |
| // access InsertHelper from multiple threads even though they never should have. |
| // The original code used to lock the InsertHelper itself which was prone |
| // to deadlocks. Starting a transaction achieves the same mutual exclusion |
| // effect as grabbing a lock but without the potential for deadlocks. |
| mDb.beginTransactionNonExclusive(); |
| try { |
| SQLiteStatement stmt = getStatement(allowReplace); |
| stmt.clearBindings(); |
| if (DEBUG) Log.v(TAG, "--- inserting in table " + mTableName); |
| for (Map.Entry<String, Object> e: values.valueSet()) { |
| final String key = e.getKey(); |
| int i = getColumnIndex(key); |
| DatabaseUtils.bindObjectToProgram(stmt, i, e.getValue()); |
| if (DEBUG) { |
| Log.v(TAG, "binding " + e.getValue() + " to column " + |
| i + " (" + key + ")"); |
| } |
| } |
| long result = stmt.executeInsert(); |
| mDb.setTransactionSuccessful(); |
| return result; |
| } catch (SQLException e) { |
| Log.e(TAG, "Error inserting " + values + " into table " + mTableName, e); |
| return -1; |
| } finally { |
| mDb.endTransaction(); |
| } |
| } |
| |
| /** |
| * Returns the index of the specified column. This is index is suitagble for use |
| * in calls to bind(). |
| * @param key the column name |
| * @return the index of the column |
| */ |
| public int getColumnIndex(String key) { |
| getStatement(false); |
| final Integer index = mColumns.get(key); |
| if (index == null) { |
| throw new IllegalArgumentException("column '" + key + "' is invalid"); |
| } |
| return index; |
| } |
| |
| /** |
| * Bind the value to an index. A prepareForInsert() or prepareForReplace() |
| * without a matching execute() must have already have been called. |
| * @param index the index of the slot to which to bind |
| * @param value the value to bind |
| */ |
| public void bind(int index, double value) { |
| mPreparedStatement.bindDouble(index, value); |
| } |
| |
| /** |
| * Bind the value to an index. A prepareForInsert() or prepareForReplace() |
| * without a matching execute() must have already have been called. |
| * @param index the index of the slot to which to bind |
| * @param value the value to bind |
| */ |
| public void bind(int index, float value) { |
| mPreparedStatement.bindDouble(index, value); |
| } |
| |
| /** |
| * Bind the value to an index. A prepareForInsert() or prepareForReplace() |
| * without a matching execute() must have already have been called. |
| * @param index the index of the slot to which to bind |
| * @param value the value to bind |
| */ |
| public void bind(int index, long value) { |
| mPreparedStatement.bindLong(index, value); |
| } |
| |
| /** |
| * Bind the value to an index. A prepareForInsert() or prepareForReplace() |
| * without a matching execute() must have already have been called. |
| * @param index the index of the slot to which to bind |
| * @param value the value to bind |
| */ |
| public void bind(int index, int value) { |
| mPreparedStatement.bindLong(index, value); |
| } |
| |
| /** |
| * Bind the value to an index. A prepareForInsert() or prepareForReplace() |
| * without a matching execute() must have already have been called. |
| * @param index the index of the slot to which to bind |
| * @param value the value to bind |
| */ |
| public void bind(int index, boolean value) { |
| mPreparedStatement.bindLong(index, value ? 1 : 0); |
| } |
| |
| /** |
| * Bind null to an index. A prepareForInsert() or prepareForReplace() |
| * without a matching execute() must have already have been called. |
| * @param index the index of the slot to which to bind |
| */ |
| public void bindNull(int index) { |
| mPreparedStatement.bindNull(index); |
| } |
| |
| /** |
| * Bind the value to an index. A prepareForInsert() or prepareForReplace() |
| * without a matching execute() must have already have been called. |
| * @param index the index of the slot to which to bind |
| * @param value the value to bind |
| */ |
| public void bind(int index, byte[] value) { |
| if (value == null) { |
| mPreparedStatement.bindNull(index); |
| } else { |
| mPreparedStatement.bindBlob(index, value); |
| } |
| } |
| |
| /** |
| * Bind the value to an index. A prepareForInsert() or prepareForReplace() |
| * without a matching execute() must have already have been called. |
| * @param index the index of the slot to which to bind |
| * @param value the value to bind |
| */ |
| public void bind(int index, String value) { |
| if (value == null) { |
| mPreparedStatement.bindNull(index); |
| } else { |
| mPreparedStatement.bindString(index, value); |
| } |
| } |
| |
| /** |
| * Performs an insert, adding a new row with the given values. |
| * If the table contains conflicting rows, an error is |
| * returned. |
| * |
| * @param values the set of values with which to populate the |
| * new row |
| * |
| * @return the row ID of the newly inserted row, or -1 if an |
| * error occurred |
| */ |
| public long insert(ContentValues values) { |
| return insertInternal(values, false); |
| } |
| |
| /** |
| * Execute the previously prepared insert or replace using the bound values |
| * since the last call to prepareForInsert or prepareForReplace. |
| * |
| * <p>Note that calling bind() and then execute() is not thread-safe. The only thread-safe |
| * way to use this class is to call insert() or replace(). |
| * |
| * @return the row ID of the newly inserted row, or -1 if an |
| * error occurred |
| */ |
| public long execute() { |
| if (mPreparedStatement == null) { |
| throw new IllegalStateException("you must prepare this inserter before calling " |
| + "execute"); |
| } |
| try { |
| if (DEBUG) Log.v(TAG, "--- doing insert or replace in table " + mTableName); |
| return mPreparedStatement.executeInsert(); |
| } catch (SQLException e) { |
| Log.e(TAG, "Error executing InsertHelper with table " + mTableName, e); |
| return -1; |
| } finally { |
| // you can only call this once per prepare |
| mPreparedStatement = null; |
| } |
| } |
| |
| /** |
| * Prepare the InsertHelper for an insert. The pattern for this is: |
| * <ul> |
| * <li>prepareForInsert() |
| * <li>bind(index, value); |
| * <li>bind(index, value); |
| * <li>... |
| * <li>bind(index, value); |
| * <li>execute(); |
| * </ul> |
| */ |
| public void prepareForInsert() { |
| mPreparedStatement = getStatement(false); |
| mPreparedStatement.clearBindings(); |
| } |
| |
| /** |
| * Prepare the InsertHelper for a replace. The pattern for this is: |
| * <ul> |
| * <li>prepareForReplace() |
| * <li>bind(index, value); |
| * <li>bind(index, value); |
| * <li>... |
| * <li>bind(index, value); |
| * <li>execute(); |
| * </ul> |
| */ |
| public void prepareForReplace() { |
| mPreparedStatement = getStatement(true); |
| mPreparedStatement.clearBindings(); |
| } |
| |
| /** |
| * Performs an insert, adding a new row with the given values. |
| * If the table contains conflicting rows, they are deleted |
| * and replaced with the new row. |
| * |
| * @param values the set of values with which to populate the |
| * new row |
| * |
| * @return the row ID of the newly inserted row, or -1 if an |
| * error occurred |
| */ |
| public long replace(ContentValues values) { |
| return insertInternal(values, true); |
| } |
| |
| /** |
| * Close this object and release any resources associated with |
| * it. The behavior of calling <code>insert()</code> after |
| * calling this method is undefined. |
| */ |
| public void close() { |
| if (mInsertStatement != null) { |
| mInsertStatement.close(); |
| mInsertStatement = null; |
| } |
| if (mReplaceStatement != null) { |
| mReplaceStatement.close(); |
| mReplaceStatement = null; |
| } |
| mInsertSQL = null; |
| mColumns = null; |
| } |
| } |
| |
| /** |
| * Creates a db and populates it with the sql statements in sqlStatements. |
| * |
| * @param context the context to use to create the db |
| * @param dbName the name of the db to create |
| * @param dbVersion the version to set on the db |
| * @param sqlStatements the statements to use to populate the db. This should be a single string |
| * of the form returned by sqlite3's <tt>.dump</tt> command (statements separated by |
| * semicolons) |
| */ |
| static public void createDbFromSqlStatements( |
| Context context, String dbName, int dbVersion, String sqlStatements) { |
| SQLiteDatabase db = context.openOrCreateDatabase(dbName, 0, null); |
| // TODO: this is not quite safe since it assumes that all semicolons at the end of a line |
| // terminate statements. It is possible that a text field contains ;\n. We will have to fix |
| // this if that turns out to be a problem. |
| String[] statements = TextUtils.split(sqlStatements, ";\n"); |
| for (String statement : statements) { |
| if (TextUtils.isEmpty(statement)) continue; |
| db.execSQL(statement); |
| } |
| db.setVersion(dbVersion); |
| db.close(); |
| } |
| |
| /** |
| * The legacy prefix matcher. |
| */ |
| private static String getSqlStatementPrefixSimple(@NonNull String sql) { |
| sql = sql.trim(); |
| if (sql.length() < 3) { |
| return null; |
| } |
| return sql.substring(0, 3).toUpperCase(Locale.ROOT); |
| } |
| |
| /** |
| * A regular expression that matches the first three characters in a SQL statement, after |
| * skipping past comments and whitespace. PREFIX_GROUP_NUM is the regex group that contains |
| * the matching prefix string. If PREFIX_REGEX is changed, PREFIX_GROUP_NUM may require an |
| * update too. |
| */ |
| private static final String PREFIX_REGEX = |
| "(" // Zero-or more... |
| + "\\s+" // Leading space |
| + "|" |
| + "--.*?\n" // Line comment |
| + "|" |
| + "/\\*[\\w\\W]*?\\*/" // Block comment |
| + ")*" |
| + "(\\w\\w\\w)"; // Three word-characters |
| private static final int PREFIX_GROUP_NUM = 2; |
| private static final Pattern sPrefixPattern = Pattern.compile(PREFIX_REGEX); |
| |
| /** |
| * Return the three-letter prefix of a SQL statement, skipping past whitespace and comments. |
| * Comments either start with "--" and run to the end of the line or are C-style block |
| * comments. The function returns null if a prefix could not be found. |
| */ |
| private static String getSqlStatementPrefixExtendedRegex(String sql) { |
| Matcher m = sPrefixPattern.matcher(sql); |
| if (m.lookingAt()) { |
| return m.group(PREFIX_GROUP_NUM).toUpperCase(Locale.ROOT); |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * Return the index of the first character past comments and whitespace. -1 is returned if |
| * a comment is malformed. |
| */ |
| private static int getSqlStatementPrefixOffset(String s) { |
| final int limit = s.length() - 2; |
| if (limit < 0) return -1; |
| int i = 0; |
| while (i < limit) { |
| final char c = s.charAt(i); |
| if (c <= ' ') { |
| // This behavior conforms to String.trim(), which is used by the legacy Android |
| // SQL prefix logic. This test is not unicode-aware. Notice that it accepts the |
| // null character as whitespace even though the null character will terminate the |
| // SQL string in native code. |
| i++; |
| } else if (c == '-') { |
| if (s.charAt(i+1) != '-') return i; |
| i = s.indexOf('\n', i+2); |
| if (i < 0) return -1; |
| i++; |
| } else if (c == '/') { |
| if (s.charAt(i+1) != '*') return i; |
| i++; |
| do { |
| i = s.indexOf('*', i+1); |
| if (i < 0) return -1; |
| i++; |
| } while (s.charAt(i) != '/'); |
| i++; |
| } else { |
| return i; |
| } |
| } |
| return -1; |
| } |
| |
| /** |
| * Scan past leading comments without using the Java regex routines. |
| */ |
| private static String getSqlStatementPrefixExtendedNoRegex(String sql) { |
| int n = getSqlStatementPrefixOffset(sql); |
| if (n < 0) { |
| // Bad comment syntax. |
| return null; |
| } |
| final int end = sql.length(); |
| if (n > end) { |
| // Bad scanning. This indicates a programming error. |
| return null; |
| } |
| final int eos = Math.min(n+3, end); |
| return sql.substring(n, eos).toUpperCase(Locale.ROOT); |
| } |
| |
| /** |
| * Return the extended statement type for the SQL statement. This is not a public API and it |
| * can return values that are not publicly visible. |
| * @hide |
| */ |
| private static int categorizeStatement(@NonNull String prefix, @NonNull String sql) { |
| if (prefix == null) return STATEMENT_OTHER; |
| |
| switch (prefix) { |
| case "SEL": return STATEMENT_SELECT; |
| case "INS": |
| case "UPD": |
| case "REP": |
| case "DEL": return STATEMENT_UPDATE; |
| case "ATT": return STATEMENT_ATTACH; |
| case "COM": |
| case "END": return STATEMENT_COMMIT; |
| case "ROL": |
| if (sql.toUpperCase(Locale.ROOT).contains(" TO ")) { |
| // Rollback to savepoint. |
| return STATEMENT_OTHER; |
| } |
| return STATEMENT_ABORT; |
| case "BEG": return STATEMENT_BEGIN; |
| case "PRA": return STATEMENT_PRAGMA; |
| case "CRE": return STATEMENT_CREATE; |
| case "DRO": |
| case "ALT": return STATEMENT_DDL; |
| case "ANA": |
| case "DET": return STATEMENT_UNPREPARED; |
| case "WIT": return STATEMENT_WITH; |
| default: |
| if (prefix.startsWith("--") || prefix.startsWith("/*")) { |
| return STATEMENT_COMMENT; |
| } |
| return STATEMENT_OTHER; |
| } |
| } |
| |
| /** |
| * Return the extended statement type for the SQL statement. This is not a public API and it |
| * can return values that are not publicly visible. |
| * @hide |
| */ |
| public static int getSqlStatementTypeExtended(@NonNull String sql) { |
| if (Flags.simpleSqlCommentScanner()) { |
| return categorizeStatement(getSqlStatementPrefixExtendedNoRegex(sql), sql); |
| } else { |
| int type = categorizeStatement(getSqlStatementPrefixSimple(sql), sql); |
| if (type == STATEMENT_COMMENT) { |
| type = categorizeStatement(getSqlStatementPrefixExtendedRegex(sql), sql); |
| } |
| return type; |
| } |
| } |
| |
| /** |
| * Convert an extended statement type to a public SQL statement type value. |
| * @hide |
| */ |
| public static int getSqlStatementType(int extended) { |
| switch (extended) { |
| case STATEMENT_CREATE: return STATEMENT_DDL; |
| case STATEMENT_WITH: return STATEMENT_OTHER; |
| case STATEMENT_COMMENT: return STATEMENT_OTHER; |
| } |
| return extended; |
| } |
| |
| /** |
| * Returns one of the following which represent the type of the given SQL statement. |
| * <ol> |
| * <li>{@link #STATEMENT_SELECT}</li> |
| * <li>{@link #STATEMENT_UPDATE}</li> |
| * <li>{@link #STATEMENT_ATTACH}</li> |
| * <li>{@link #STATEMENT_BEGIN}</li> |
| * <li>{@link #STATEMENT_COMMIT}</li> |
| * <li>{@link #STATEMENT_ABORT}</li> |
| * <li>{@link #STATEMENT_PRAGMA}</li> |
| * <li>{@link #STATEMENT_DDL}</li> |
| * <li>{@link #STATEMENT_UNPREPARED}</li> |
| * <li>{@link #STATEMENT_OTHER}</li> |
| * </ol> |
| * @param sql the SQL statement whose type is returned by this method |
| * @return one of the values listed above |
| */ |
| public static int getSqlStatementType(String sql) { |
| return getSqlStatementType(getSqlStatementTypeExtended(sql)); |
| } |
| |
| /** |
| * Appends one set of selection args to another. This is useful when adding a selection |
| * argument to a user provided set. |
| */ |
| public static String[] appendSelectionArgs(String[] originalValues, String[] newValues) { |
| if (originalValues == null || originalValues.length == 0) { |
| return newValues; |
| } |
| String[] result = new String[originalValues.length + newValues.length ]; |
| System.arraycopy(originalValues, 0, result, 0, originalValues.length); |
| System.arraycopy(newValues, 0, result, originalValues.length, newValues.length); |
| return result; |
| } |
| |
| /** |
| * Returns column index of "_id" column, or -1 if not found. |
| * @hide |
| */ |
| public static int findRowIdColumnIndex(String[] columnNames) { |
| int length = columnNames.length; |
| for (int i = 0; i < length; i++) { |
| if (columnNames[i].equals("_id")) { |
| return i; |
| } |
| } |
| return -1; |
| } |
| |
| /** |
| * Escape the given argument for use in a {@code LIKE} statement. |
| * @hide |
| */ |
| public static String escapeForLike(@NonNull String arg) { |
| // Shamelessly borrowed from com.android.providers.media.util.DatabaseUtils |
| final StringBuilder sb = new StringBuilder(); |
| for (int i = 0; i < arg.length(); i++) { |
| final char c = arg.charAt(i); |
| switch (c) { |
| case '%': sb.append('\\'); |
| break; |
| case '_': sb.append('\\'); |
| break; |
| } |
| sb.append(c); |
| } |
| return sb.toString(); |
| } |
| } |