diff options
Diffstat (limited to 'core')
22 files changed, 1090 insertions, 182 deletions
diff --git a/core/java/android/content/AsyncTaskLoader.java b/core/java/android/content/AsyncTaskLoader.java index 0b54396..944ca6b 100644 --- a/core/java/android/content/AsyncTaskLoader.java +++ b/core/java/android/content/AsyncTaskLoader.java @@ -173,6 +173,7 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { if (DEBUG) Slog.v(TAG, "cancelLoad: cancelled=" + cancelled); if (cancelled) { mCancellingTask = mTask; + onCancelLoadInBackground(); } mTask = null; return cancelled; @@ -256,6 +257,25 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { } /** + * Override this method to try to abort the computation currently taking + * place on a background thread. + * + * Note that when this method is called, it is possible that {@link #loadInBackground} + * has not started yet or has already completed. + */ + protected void onCancelLoadInBackground() { + } + + /** + * Returns true if the current execution of {@link #loadInBackground()} is being canceled. + * + * @return True if the current execution of {@link #loadInBackground()} is being canceled. + */ + protected boolean isLoadInBackgroundCanceled() { + return mCancellingTask != null; + } + + /** * Locks the current thread until the loader completes the current load * operation. Returns immediately if there is no load operation running. * Should not be called from the UI thread: calling it from the UI diff --git a/core/java/android/content/CancelationSignal.java b/core/java/android/content/CancelationSignal.java new file mode 100644 index 0000000..58cf59d --- /dev/null +++ b/core/java/android/content/CancelationSignal.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2012 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.content; + +import android.os.RemoteException; + +/** + * Provides the ability to cancel an operation in progress. + */ +public final class CancelationSignal { + private boolean mIsCanceled; + private OnCancelListener mOnCancelListener; + private ICancelationSignal mRemote; + + /** + * Creates a cancelation signal, initially not canceled. + */ + public CancelationSignal() { + } + + /** + * Returns true if the operation has been canceled. + * + * @return True if the operation has been canceled. + */ + public boolean isCanceled() { + synchronized (this) { + return mIsCanceled; + } + } + + /** + * Throws {@link OperationCanceledException} if the operation has been canceled. + * + * @throws OperationCanceledException if the operation has been canceled. + */ + public void throwIfCanceled() { + if (isCanceled()) { + throw new OperationCanceledException(); + } + } + + /** + * Cancels the operation and signals the cancelation listener. + * If the operation has not yet started, then it will be canceled as soon as it does. + */ + public void cancel() { + synchronized (this) { + if (!mIsCanceled) { + mIsCanceled = true; + if (mOnCancelListener != null) { + mOnCancelListener.onCancel(); + } + if (mRemote != null) { + try { + mRemote.cancel(); + } catch (RemoteException ex) { + } + } + } + } + } + + /** + * Sets the cancelation listener to be called when canceled. + * If {@link CancelationSignal#cancel} has already been called, then the provided + * listener is invoked immediately. + * + * The listener is called while holding the cancelation signal's lock which is + * also held while registering or unregistering the listener. Because of the lock, + * it is not possible for the listener to run after it has been unregistered. + * This design choice makes it easier for clients of {@link CancelationSignal} to + * prevent race conditions related to listener registration and unregistration. + * + * @param listener The cancelation listener, or null to remove the current listener. + */ + public void setOnCancelListener(OnCancelListener listener) { + synchronized (this) { + mOnCancelListener = listener; + if (mIsCanceled && listener != null) { + listener.onCancel(); + } + } + } + + /** + * Sets the remote transport. + * + * @param remote The remote transport, or null to remove. + * + * @hide + */ + public void setRemote(ICancelationSignal remote) { + synchronized (this) { + mRemote = remote; + if (mIsCanceled && remote != null) { + try { + remote.cancel(); + } catch (RemoteException ex) { + } + } + } + } + + /** + * Creates a transport that can be returned back to the caller of + * a Binder function and subsequently used to dispatch a cancelation signal. + * + * @return The new cancelation signal transport. + * + * @hide + */ + public static ICancelationSignal createTransport() { + return new Transport(); + } + + /** + * Given a locally created transport, returns its associated cancelation signal. + * + * @param transport The locally created transport, or null if none. + * @return The associated cancelation signal, or null if none. + * + * @hide + */ + public static CancelationSignal fromTransport(ICancelationSignal transport) { + if (transport instanceof Transport) { + return ((Transport)transport).mCancelationSignal; + } + return null; + } + + /** + * Listens for cancelation. + */ + public interface OnCancelListener { + /** + * Called when {@link CancelationSignal#cancel} is invoked. + */ + void onCancel(); + } + + private static final class Transport extends ICancelationSignal.Stub { + final CancelationSignal mCancelationSignal = new CancelationSignal(); + + @Override + public void cancel() throws RemoteException { + mCancelationSignal.cancel(); + } + } +} diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java index 116ca48..adbeb6a 100644 --- a/core/java/android/content/ContentProvider.java +++ b/core/java/android/content/ContentProvider.java @@ -29,6 +29,7 @@ import android.os.Binder; import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.os.Process; +import android.os.RemoteException; import android.util.Log; import java.io.File; @@ -174,28 +175,33 @@ public abstract class ContentProvider implements ComponentCallbacks2 { return getContentProvider().getClass().getName(); } + @Override public Cursor query(Uri uri, String[] projection, - String selection, String[] selectionArgs, String sortOrder) { + String selection, String[] selectionArgs, String sortOrder, + ICancelationSignal cancelationSignal) { enforceReadPermission(uri); - return ContentProvider.this.query(uri, projection, selection, - selectionArgs, sortOrder); + return ContentProvider.this.query(uri, projection, selection, selectionArgs, sortOrder, + CancelationSignal.fromTransport(cancelationSignal)); } + @Override public String getType(Uri uri) { return ContentProvider.this.getType(uri); } - + @Override public Uri insert(Uri uri, ContentValues initialValues) { enforceWritePermission(uri); return ContentProvider.this.insert(uri, initialValues); } + @Override public int bulkInsert(Uri uri, ContentValues[] initialValues) { enforceWritePermission(uri); return ContentProvider.this.bulkInsert(uri, initialValues); } + @Override public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) throws OperationApplicationException { for (ContentProviderOperation operation : operations) { @@ -210,17 +216,20 @@ public abstract class ContentProvider implements ComponentCallbacks2 { return ContentProvider.this.applyBatch(operations); } + @Override public int delete(Uri uri, String selection, String[] selectionArgs) { enforceWritePermission(uri); return ContentProvider.this.delete(uri, selection, selectionArgs); } + @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { enforceWritePermission(uri); return ContentProvider.this.update(uri, values, selection, selectionArgs); } + @Override public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { if (mode != null && mode.startsWith("rw")) enforceWritePermission(uri); @@ -228,6 +237,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 { return ContentProvider.this.openFile(uri, mode); } + @Override public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException { if (mode != null && mode.startsWith("rw")) enforceWritePermission(uri); @@ -235,6 +245,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 { return ContentProvider.this.openAssetFile(uri, mode); } + @Override public Bundle call(String method, String arg, Bundle extras) { return ContentProvider.this.call(method, arg, extras); } @@ -251,6 +262,11 @@ public abstract class ContentProvider implements ComponentCallbacks2 { return ContentProvider.this.openTypedAssetFile(uri, mimeType, opts); } + @Override + public ICancelationSignal createCancelationSignal() throws RemoteException { + return CancelationSignal.createTransport(); + } + private void enforceReadPermission(Uri uri) { final int uid = Binder.getCallingUid(); if (uid == mMyUid) { @@ -541,6 +557,75 @@ public abstract class ContentProvider implements ComponentCallbacks2 { String selection, String[] selectionArgs, String sortOrder); /** + * Implement this to handle query requests from clients with support for cancelation. + * This method can be called from multiple threads, as described in + * <a href="{@docRoot}guide/topics/fundamentals/processes-and-threads.html#Threads">Processes + * and Threads</a>. + * <p> + * Example client call:<p> + * <pre>// Request a specific record. + * Cursor managedCursor = managedQuery( + ContentUris.withAppendedId(Contacts.People.CONTENT_URI, 2), + projection, // Which columns to return. + null, // WHERE clause. + null, // WHERE clause value substitution + People.NAME + " ASC"); // Sort order.</pre> + * Example implementation:<p> + * <pre>// SQLiteQueryBuilder is a helper class that creates the + // proper SQL syntax for us. + SQLiteQueryBuilder qBuilder = new SQLiteQueryBuilder(); + + // Set the table we're querying. + qBuilder.setTables(DATABASE_TABLE_NAME); + + // If the query ends in a specific record number, we're + // being asked for a specific record, so set the + // WHERE clause in our query. + if((URI_MATCHER.match(uri)) == SPECIFIC_MESSAGE){ + qBuilder.appendWhere("_id=" + uri.getPathLeafId()); + } + + // Make the query. + Cursor c = qBuilder.query(mDb, + projection, + selection, + selectionArgs, + groupBy, + having, + sortOrder); + c.setNotificationUri(getContext().getContentResolver(), uri); + return c;</pre> + * <p> + * If you implement this method then you must also implement the version of + * {@link #query(Uri, String[], String, String[], String)} that does not take a cancelation + * provider to ensure correct operation on older versions of the Android Framework in + * which the cancelation signal overload was not available. + * + * @param uri The URI to query. This will be the full URI sent by the client; + * if the client is requesting a specific record, the URI will end in a record number + * that the implementation should parse and add to a WHERE or HAVING clause, specifying + * that _id value. + * @param projection The list of columns to put into the cursor. If + * null all columns are included. + * @param selection A selection criteria to apply when filtering rows. + * If null then all rows are included. + * @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. + * @param sortOrder How the rows in the cursor should be sorted. + * If null then the provider is free to define the sort order. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * If the operation is canceled, then {@link OperationCanceledException} will be thrown + * when the query is executed. + * @return a Cursor or null. + */ + public Cursor query(Uri uri, String[] projection, + String selection, String[] selectionArgs, String sortOrder, + CancelationSignal cancelationSignal) { + return query(uri, projection, selection, selectionArgs, sortOrder); + } + + /** * Implement this to handle requests for the MIME type of the data at the * given URI. The returned MIME type should start with * <code>vnd.android.cursor.item</code> for a single record, diff --git a/core/java/android/content/ContentProviderClient.java b/core/java/android/content/ContentProviderClient.java index 0540109..9a1fa65 100644 --- a/core/java/android/content/ContentProviderClient.java +++ b/core/java/android/content/ContentProviderClient.java @@ -47,7 +47,20 @@ public class ContentProviderClient { /** See {@link ContentProvider#query ContentProvider.query} */ public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, String sortOrder) throws RemoteException { - return mContentProvider.query(url, projection, selection, selectionArgs, sortOrder); + return query(url, projection, selection, selectionArgs, sortOrder, null); + } + + /** See {@link ContentProvider#query ContentProvider.query} */ + public Cursor query(Uri url, String[] projection, String selection, + String[] selectionArgs, String sortOrder, CancelationSignal cancelationSignal) + throws RemoteException { + ICancelationSignal remoteCancelationSignal = null; + if (cancelationSignal != null) { + remoteCancelationSignal = mContentProvider.createCancelationSignal(); + cancelationSignal.setRemote(remoteCancelationSignal); + } + return mContentProvider.query(url, projection, selection, selectionArgs, sortOrder, + remoteCancelationSignal); } /** See {@link ContentProvider#getType ContentProvider.getType} */ diff --git a/core/java/android/content/ContentProviderNative.java b/core/java/android/content/ContentProviderNative.java index b089bf2..e0e277a 100644 --- a/core/java/android/content/ContentProviderNative.java +++ b/core/java/android/content/ContentProviderNative.java @@ -21,7 +21,6 @@ import android.database.BulkCursorNative; import android.database.BulkCursorToCursorAdaptor; import android.database.Cursor; import android.database.CursorToBulkCursorAdaptor; -import android.database.CursorWindow; import android.database.DatabaseUtils; import android.database.IBulkCursor; import android.database.IContentObserver; @@ -41,8 +40,6 @@ import java.util.ArrayList; * {@hide} */ abstract public class ContentProviderNative extends Binder implements IContentProvider { - private static final String TAG = "ContentProvider"; - public ContentProviderNative() { attachInterface(this, descriptor); @@ -108,8 +105,11 @@ abstract public class ContentProviderNative extends Binder implements IContentPr String sortOrder = data.readString(); IContentObserver observer = IContentObserver.Stub.asInterface( data.readStrongBinder()); + ICancelationSignal cancelationSignal = ICancelationSignal.Stub.asInterface( + data.readStrongBinder()); - Cursor cursor = query(url, projection, selection, selectionArgs, sortOrder); + Cursor cursor = query(url, projection, selection, selectionArgs, sortOrder, + cancelationSignal); if (cursor != null) { CursorToBulkCursorAdaptor adaptor = new CursorToBulkCursorAdaptor( cursor, observer, getProviderName()); @@ -295,6 +295,16 @@ abstract public class ContentProviderNative extends Binder implements IContentPr } return true; } + + case CREATE_CANCELATION_SIGNAL_TRANSACTION: + { + data.enforceInterface(IContentProvider.descriptor); + + ICancelationSignal cancelationSignal = createCancelationSignal(); + reply.writeNoException(); + reply.writeStrongBinder(cancelationSignal.asBinder()); + return true; + } } } catch (Exception e) { DatabaseUtils.writeExceptionToParcel(reply, e); @@ -324,7 +334,8 @@ final class ContentProviderProxy implements IContentProvider } public Cursor query(Uri url, String[] projection, String selection, - String[] selectionArgs, String sortOrder) throws RemoteException { + String[] selectionArgs, String sortOrder, ICancelationSignal cancelationSignal) + throws RemoteException { BulkCursorToCursorAdaptor adaptor = new BulkCursorToCursorAdaptor(); Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); @@ -352,6 +363,7 @@ final class ContentProviderProxy implements IContentProvider } data.writeString(sortOrder); data.writeStrongBinder(adaptor.getObserver().asBinder()); + data.writeStrongBinder(cancelationSignal != null ? cancelationSignal.asBinder() : null); mRemote.transact(IContentProvider.QUERY_TRANSACTION, data, reply, 0); @@ -620,5 +632,24 @@ final class ContentProviderProxy implements IContentProvider } } + public ICancelationSignal createCancelationSignal() throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + try { + data.writeInterfaceToken(IContentProvider.descriptor); + + mRemote.transact(IContentProvider.CREATE_CANCELATION_SIGNAL_TRANSACTION, + data, reply, 0); + + DatabaseUtils.readExceptionFromParcel(reply); + ICancelationSignal cancelationSignal = ICancelationSignal.Stub.asInterface( + reply.readStrongBinder()); + return cancelationSignal; + } finally { + data.recycle(); + reply.recycle(); + } + } + private IBinder mRemote; } diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index 0debb84..e79475a 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -22,6 +22,7 @@ import android.accounts.Account; import android.app.ActivityManagerNative; import android.app.ActivityThread; import android.app.AppGlobals; +import android.content.ContentProvider.Transport; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.AssetFileDescriptor; import android.content.res.Resources; @@ -302,13 +303,62 @@ public abstract class ContentResolver { */ public final Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + return query(uri, projection, selection, selectionArgs, sortOrder, null); + } + + /** + * <p> + * Query the given URI, returning a {@link Cursor} over the result set. + * </p> + * <p> + * For best performance, the caller should follow these guidelines: + * <ul> + * <li>Provide an explicit projection, to prevent + * reading data from storage that aren't going to be used.</li> + * <li>Use question mark parameter markers such as 'phone=?' instead of + * explicit values in the {@code selection} parameter, so that queries + * that differ only by those values will be recognized as the same + * for caching purposes.</li> + * </ul> + * </p> + * + * @param uri The URI, using the content:// scheme, for the content to + * retrieve. + * @param projection A list of which columns to return. Passing null will + * return all columns, which is inefficient. + * @param selection A filter declaring which rows to return, formatted as an + * SQL WHERE clause (excluding the WHERE itself). Passing null will + * return all rows for the given URI. + * @param selectionArgs You may include ?s in selection, which will be + * replaced by the values from selectionArgs, in the order that they + * appear in the selection. The values will be bound as Strings. + * @param sortOrder How to order the rows, formatted as an SQL ORDER BY + * clause (excluding the ORDER BY itself). Passing null will use the + * default sort order, which may be unordered. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * If the operation is canceled, then {@link OperationCanceledException} will be thrown + * when the query is executed. + * @return A Cursor object, which is positioned before the first entry, or null + * @see Cursor + */ + public final Cursor query(final Uri uri, String[] projection, + String selection, String[] selectionArgs, String sortOrder, + CancelationSignal cancelationSignal) { IContentProvider provider = acquireProvider(uri); if (provider == null) { return null; } try { long startTime = SystemClock.uptimeMillis(); - Cursor qCursor = provider.query(uri, projection, selection, selectionArgs, sortOrder); + + ICancelationSignal remoteCancelationSignal = null; + if (cancelationSignal != null) { + cancelationSignal.throwIfCanceled(); + remoteCancelationSignal = provider.createCancelationSignal(); + cancelationSignal.setRemote(remoteCancelationSignal); + } + Cursor qCursor = provider.query(uri, projection, + selection, selectionArgs, sortOrder, remoteCancelationSignal); if (qCursor == null) { releaseProvider(provider); return null; diff --git a/core/java/android/content/CursorLoader.java b/core/java/android/content/CursorLoader.java index 7af535b..6e4aca8 100644 --- a/core/java/android/content/CursorLoader.java +++ b/core/java/android/content/CursorLoader.java @@ -19,7 +19,6 @@ package android.content; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; -import android.os.AsyncTask; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -49,18 +48,42 @@ public class CursorLoader extends AsyncTaskLoader<Cursor> { String mSortOrder; Cursor mCursor; + CancelationSignal mCancelationSignal; /* Runs on a worker thread */ @Override public Cursor loadInBackground() { - Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection, - mSelectionArgs, mSortOrder); - if (cursor != null) { - // Ensure the cursor window is filled - cursor.getCount(); - registerContentObserver(cursor, mObserver); + synchronized (this) { + if (isLoadInBackgroundCanceled()) { + throw new OperationCanceledException(); + } + mCancelationSignal = new CancelationSignal(); + } + try { + Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection, + mSelectionArgs, mSortOrder, mCancelationSignal); + if (cursor != null) { + // Ensure the cursor window is filled + cursor.getCount(); + registerContentObserver(cursor, mObserver); + } + return cursor; + } finally { + synchronized (this) { + mCancelationSignal = null; + } + } + } + + @Override + protected void onCancelLoadInBackground() { + super.onCancelLoadInBackground(); + + synchronized (this) { + if (mCancelationSignal != null) { + mCancelationSignal.cancel(); + } } - return cursor; } /** diff --git a/core/java/android/content/ICancelationSignal.aidl b/core/java/android/content/ICancelationSignal.aidl new file mode 100644 index 0000000..3f5a24d --- /dev/null +++ b/core/java/android/content/ICancelationSignal.aidl @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2012 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.content; + +/** + * @hide + */ +interface ICancelationSignal { + oneway void cancel(); +} diff --git a/core/java/android/content/IContentProvider.java b/core/java/android/content/IContentProvider.java index 2a67ff8..f52157f 100644 --- a/core/java/android/content/IContentProvider.java +++ b/core/java/android/content/IContentProvider.java @@ -34,7 +34,8 @@ import java.util.ArrayList; */ public interface IContentProvider extends IInterface { public Cursor query(Uri url, String[] projection, String selection, - String[] selectionArgs, String sortOrder) throws RemoteException; + String[] selectionArgs, String sortOrder, ICancelationSignal cancelationSignal) + throws RemoteException; public String getType(Uri url) throws RemoteException; public Uri insert(Uri url, ContentValues initialValues) throws RemoteException; @@ -50,6 +51,7 @@ public interface IContentProvider extends IInterface { public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) throws RemoteException, OperationApplicationException; public Bundle call(String method, String arg, Bundle extras) throws RemoteException; + public ICancelationSignal createCancelationSignal() throws RemoteException; // Data interchange. public String[] getStreamTypes(Uri url, String mimeTypeFilter) throws RemoteException; @@ -71,4 +73,5 @@ public interface IContentProvider extends IInterface { static final int CALL_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 20; static final int GET_STREAM_TYPES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 21; static final int OPEN_TYPED_ASSET_FILE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 22; + static final int CREATE_CANCELATION_SIGNAL_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 23; } diff --git a/core/java/android/content/OperationCanceledException.java b/core/java/android/content/OperationCanceledException.java new file mode 100644 index 0000000..24afcfa --- /dev/null +++ b/core/java/android/content/OperationCanceledException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2012 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.content; + +/** + * An exception type that is thrown when an operation in progress is canceled. + * + * @see CancelationSignal + */ +public class OperationCanceledException extends RuntimeException { + public OperationCanceledException() { + this(null); + } + + public OperationCanceledException(String message) { + super(message != null ? message : "The operation has been canceled."); + } +} diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java index 72f62fd..710bd53 100644 --- a/core/java/android/database/sqlite/SQLiteConnection.java +++ b/core/java/android/database/sqlite/SQLiteConnection.java @@ -19,6 +19,8 @@ package android.database.sqlite; import dalvik.system.BlockGuard; import dalvik.system.CloseGuard; +import android.content.CancelationSignal; +import android.content.OperationCanceledException; import android.database.Cursor; import android.database.CursorWindow; import android.database.DatabaseUtils; @@ -82,7 +84,7 @@ import java.util.regex.Pattern; * * @hide */ -public final class SQLiteConnection { +public final class SQLiteConnection implements CancelationSignal.OnCancelListener { private static final String TAG = "SQLiteConnection"; private static final boolean DEBUG = false; @@ -108,6 +110,12 @@ public final class SQLiteConnection { private boolean mOnlyAllowReadOnlyOperations; + // The number of times attachCancelationSignal has been called. + // Because SQLite statement execution can be re-entrant, we keep track of how many + // times we have attempted to attach a cancelation signal to the connection so that + // we can ensure that we detach the signal at the right time. + private int mCancelationSignalAttachCount; + private static native int nativeOpen(String path, int openFlags, String label, boolean enableTrace, boolean enableProfile); private static native void nativeClose(int connectionPtr); @@ -145,6 +153,8 @@ public final class SQLiteConnection { int connectionPtr, int statementPtr, int windowPtr, int startPos, int requiredPos, boolean countAllRows); private static native int nativeGetDbLookaside(int connectionPtr); + private static native void nativeCancel(int connectionPtr); + private static native void nativeResetCancel(int connectionPtr, boolean cancelable); private SQLiteConnection(SQLiteConnectionPool pool, SQLiteDatabaseConfiguration configuration, @@ -345,11 +355,14 @@ public final class SQLiteConnection { * * @param sql The SQL statement to execute. * @param bindArgs The arguments to bind, or null if none. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. * * @throws SQLiteException if an error occurs, such as a syntax error * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. */ - public void execute(String sql, Object[] bindArgs) { + public void execute(String sql, Object[] bindArgs, + CancelationSignal cancelationSignal) { if (sql == null) { throw new IllegalArgumentException("sql must not be null."); } @@ -361,7 +374,12 @@ public final class SQLiteConnection { throwIfStatementForbidden(statement); bindArguments(statement, bindArgs); applyBlockGuardPolicy(statement); - nativeExecute(mConnectionPtr, statement.mStatementPtr); + attachCancelationSignal(cancelationSignal); + try { + nativeExecute(mConnectionPtr, statement.mStatementPtr); + } finally { + detachCancelationSignal(cancelationSignal); + } } finally { releasePreparedStatement(statement); } @@ -378,13 +396,16 @@ public final class SQLiteConnection { * * @param sql The SQL statement to execute. * @param bindArgs The arguments to bind, or null if none. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. * @return The value of the first column in the first row of the result set * as a <code>long</code>, or zero if none. * * @throws SQLiteException if an error occurs, such as a syntax error * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. */ - public long executeForLong(String sql, Object[] bindArgs) { + public long executeForLong(String sql, Object[] bindArgs, + CancelationSignal cancelationSignal) { if (sql == null) { throw new IllegalArgumentException("sql must not be null."); } @@ -396,7 +417,12 @@ public final class SQLiteConnection { throwIfStatementForbidden(statement); bindArguments(statement, bindArgs); applyBlockGuardPolicy(statement); - return nativeExecuteForLong(mConnectionPtr, statement.mStatementPtr); + attachCancelationSignal(cancelationSignal); + try { + return nativeExecuteForLong(mConnectionPtr, statement.mStatementPtr); + } finally { + detachCancelationSignal(cancelationSignal); + } } finally { releasePreparedStatement(statement); } @@ -413,13 +439,16 @@ public final class SQLiteConnection { * * @param sql The SQL statement to execute. * @param bindArgs The arguments to bind, or null if none. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. * @return The value of the first column in the first row of the result set * as a <code>String</code>, or null if none. * * @throws SQLiteException if an error occurs, such as a syntax error * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. */ - public String executeForString(String sql, Object[] bindArgs) { + public String executeForString(String sql, Object[] bindArgs, + CancelationSignal cancelationSignal) { if (sql == null) { throw new IllegalArgumentException("sql must not be null."); } @@ -431,7 +460,12 @@ public final class SQLiteConnection { throwIfStatementForbidden(statement); bindArguments(statement, bindArgs); applyBlockGuardPolicy(statement); - return nativeExecuteForString(mConnectionPtr, statement.mStatementPtr); + attachCancelationSignal(cancelationSignal); + try { + return nativeExecuteForString(mConnectionPtr, statement.mStatementPtr); + } finally { + detachCancelationSignal(cancelationSignal); + } } finally { releasePreparedStatement(statement); } @@ -449,14 +483,17 @@ public final class SQLiteConnection { * * @param sql The SQL statement to execute. * @param bindArgs The arguments to bind, or null if none. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. * @return The file descriptor for a shared memory region that contains * the value of the first column in the first row of the result set as a BLOB, * or null if none. * * @throws SQLiteException if an error occurs, such as a syntax error * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. */ - public ParcelFileDescriptor executeForBlobFileDescriptor(String sql, Object[] bindArgs) { + public ParcelFileDescriptor executeForBlobFileDescriptor(String sql, Object[] bindArgs, + CancelationSignal cancelationSignal) { if (sql == null) { throw new IllegalArgumentException("sql must not be null."); } @@ -469,9 +506,14 @@ public final class SQLiteConnection { throwIfStatementForbidden(statement); bindArguments(statement, bindArgs); applyBlockGuardPolicy(statement); - int fd = nativeExecuteForBlobFileDescriptor( - mConnectionPtr, statement.mStatementPtr); - return fd >= 0 ? ParcelFileDescriptor.adoptFd(fd) : null; + attachCancelationSignal(cancelationSignal); + try { + int fd = nativeExecuteForBlobFileDescriptor( + mConnectionPtr, statement.mStatementPtr); + return fd >= 0 ? ParcelFileDescriptor.adoptFd(fd) : null; + } finally { + detachCancelationSignal(cancelationSignal); + } } finally { releasePreparedStatement(statement); } @@ -489,12 +531,15 @@ public final class SQLiteConnection { * * @param sql The SQL statement to execute. * @param bindArgs The arguments to bind, or null if none. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. * @return The number of rows that were changed. * * @throws SQLiteException if an error occurs, such as a syntax error * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. */ - public int executeForChangedRowCount(String sql, Object[] bindArgs) { + public int executeForChangedRowCount(String sql, Object[] bindArgs, + CancelationSignal cancelationSignal) { if (sql == null) { throw new IllegalArgumentException("sql must not be null."); } @@ -507,8 +552,13 @@ public final class SQLiteConnection { throwIfStatementForbidden(statement); bindArguments(statement, bindArgs); applyBlockGuardPolicy(statement); - return nativeExecuteForChangedRowCount( - mConnectionPtr, statement.mStatementPtr); + attachCancelationSignal(cancelationSignal); + try { + return nativeExecuteForChangedRowCount( + mConnectionPtr, statement.mStatementPtr); + } finally { + detachCancelationSignal(cancelationSignal); + } } finally { releasePreparedStatement(statement); } @@ -526,12 +576,15 @@ public final class SQLiteConnection { * * @param sql The SQL statement to execute. * @param bindArgs The arguments to bind, or null if none. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. * @return The row id of the last row that was inserted, or 0 if none. * * @throws SQLiteException if an error occurs, such as a syntax error * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. */ - public long executeForLastInsertedRowId(String sql, Object[] bindArgs) { + public long executeForLastInsertedRowId(String sql, Object[] bindArgs, + CancelationSignal cancelationSignal) { if (sql == null) { throw new IllegalArgumentException("sql must not be null."); } @@ -544,8 +597,13 @@ public final class SQLiteConnection { throwIfStatementForbidden(statement); bindArguments(statement, bindArgs); applyBlockGuardPolicy(statement); - return nativeExecuteForLastInsertedRowId( - mConnectionPtr, statement.mStatementPtr); + attachCancelationSignal(cancelationSignal); + try { + return nativeExecuteForLastInsertedRowId( + mConnectionPtr, statement.mStatementPtr); + } finally { + detachCancelationSignal(cancelationSignal); + } } finally { releasePreparedStatement(statement); } @@ -571,14 +629,17 @@ public final class SQLiteConnection { * so that it does. Must be greater than or equal to <code>startPos</code>. * @param countAllRows True to count all rows that the query would return * regagless of whether they fit in the window. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. * @return The number of rows that were counted during query execution. Might * not be all rows in the result set unless <code>countAllRows</code> is true. * * @throws SQLiteException if an error occurs, such as a syntax error * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. */ public int executeForCursorWindow(String sql, Object[] bindArgs, - CursorWindow window, int startPos, int requiredPos, boolean countAllRows) { + CursorWindow window, int startPos, int requiredPos, boolean countAllRows, + CancelationSignal cancelationSignal) { if (sql == null) { throw new IllegalArgumentException("sql must not be null."); } @@ -597,14 +658,19 @@ public final class SQLiteConnection { throwIfStatementForbidden(statement); bindArguments(statement, bindArgs); applyBlockGuardPolicy(statement); - final long result = nativeExecuteForCursorWindow( - mConnectionPtr, statement.mStatementPtr, window.mWindowPtr, - startPos, requiredPos, countAllRows); - actualPos = (int)(result >> 32); - countedRows = (int)result; - filledRows = window.getNumRows(); - window.setStartPosition(actualPos); - return countedRows; + attachCancelationSignal(cancelationSignal); + try { + final long result = nativeExecuteForCursorWindow( + mConnectionPtr, statement.mStatementPtr, window.mWindowPtr, + startPos, requiredPos, countAllRows); + actualPos = (int)(result >> 32); + countedRows = (int)result; + filledRows = window.getNumRows(); + window.setStartPosition(actualPos); + return countedRows; + } finally { + detachCancelationSignal(cancelationSignal); + } } finally { releasePreparedStatement(statement); } @@ -685,6 +751,46 @@ public final class SQLiteConnection { recyclePreparedStatement(statement); } + private void attachCancelationSignal(CancelationSignal cancelationSignal) { + if (cancelationSignal != null) { + cancelationSignal.throwIfCanceled(); + + mCancelationSignalAttachCount += 1; + if (mCancelationSignalAttachCount == 1) { + // Reset cancelation flag before executing the statement. + nativeResetCancel(mConnectionPtr, true /*cancelable*/); + + // After this point, onCancel() may be called concurrently. + cancelationSignal.setOnCancelListener(this); + } + } + } + + private void detachCancelationSignal(CancelationSignal cancelationSignal) { + if (cancelationSignal != null) { + assert mCancelationSignalAttachCount > 0; + + mCancelationSignalAttachCount -= 1; + if (mCancelationSignalAttachCount == 0) { + // After this point, onCancel() cannot be called concurrently. + cancelationSignal.setOnCancelListener(null); + + // Reset cancelation flag after executing the statement. + nativeResetCancel(mConnectionPtr, false /*cancelable*/); + } + } + } + + // CancelationSignal.OnCancelationListener callback. + // This method may be called on a different thread than the executing statement. + // However, it will only be called between calls to attachCancelationSignal and + // detachCancelationSignal, while a statement is executing. We can safely assume + // that the SQLite connection is still alive. + @Override + public void onCancel() { + nativeCancel(mConnectionPtr); + } + private void bindArguments(PreparedStatement statement, Object[] bindArgs) { final int count = bindArgs != null ? bindArgs.length : 0; if (count != statement.mNumParameters) { @@ -822,8 +928,8 @@ public final class SQLiteConnection { long pageCount = 0; long pageSize = 0; try { - pageCount = executeForLong("PRAGMA page_count;", null); - pageSize = executeForLong("PRAGMA page_size;", null); + pageCount = executeForLong("PRAGMA page_count;", null, null); + pageSize = executeForLong("PRAGMA page_size;", null, null); } catch (SQLiteException ex) { // Ignore. } @@ -834,15 +940,15 @@ public final class SQLiteConnection { // the main database which we have already described. CursorWindow window = new CursorWindow("collectDbStats"); try { - executeForCursorWindow("PRAGMA database_list;", null, window, 0, 0, false); + executeForCursorWindow("PRAGMA database_list;", null, window, 0, 0, false, null); for (int i = 1; i < window.getNumRows(); i++) { String name = window.getString(i, 1); String path = window.getString(i, 2); pageCount = 0; pageSize = 0; try { - pageCount = executeForLong("PRAGMA " + name + ".page_count;", null); - pageSize = executeForLong("PRAGMA " + name + ".page_size;", null); + pageCount = executeForLong("PRAGMA " + name + ".page_count;", null, null); + pageSize = executeForLong("PRAGMA " + name + ".page_size;", null, null); } catch (SQLiteException ex) { // Ignore. } diff --git a/core/java/android/database/sqlite/SQLiteConnectionPool.java b/core/java/android/database/sqlite/SQLiteConnectionPool.java index 5469213..d335738 100644 --- a/core/java/android/database/sqlite/SQLiteConnectionPool.java +++ b/core/java/android/database/sqlite/SQLiteConnectionPool.java @@ -18,6 +18,8 @@ package android.database.sqlite; import dalvik.system.CloseGuard; +import android.content.CancelationSignal; +import android.content.OperationCanceledException; import android.database.sqlite.SQLiteDebug.DbStats; import android.os.SystemClock; import android.util.Log; @@ -282,13 +284,16 @@ public final class SQLiteConnectionPool implements Closeable { * @param sql If not null, try to find a connection that already has * the specified SQL statement in its prepared statement cache. * @param connectionFlags The connection request flags. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. * @return The connection that was acquired, never null. * * @throws IllegalStateException if the pool has been closed. * @throws SQLiteException if a database error occurs. + * @throws OperationCanceledException if the operation was canceled. */ - public SQLiteConnection acquireConnection(String sql, int connectionFlags) { - return waitForConnection(sql, connectionFlags); + public SQLiteConnection acquireConnection(String sql, int connectionFlags, + CancelationSignal cancelationSignal) { + return waitForConnection(sql, connectionFlags, cancelationSignal); } /** @@ -497,7 +502,8 @@ public final class SQLiteConnectionPool implements Closeable { } // Might throw. - private SQLiteConnection waitForConnection(String sql, int connectionFlags) { + private SQLiteConnection waitForConnection(String sql, int connectionFlags, + CancelationSignal cancelationSignal) { final boolean wantPrimaryConnection = (connectionFlags & CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY) != 0; @@ -505,6 +511,11 @@ public final class SQLiteConnectionPool implements Closeable { synchronized (mLock) { throwIfClosedLocked(); + // Abort if canceled. + if (cancelationSignal != null) { + cancelationSignal.throwIfCanceled(); + } + // Try to acquire a connection. SQLiteConnection connection = null; if (!wantPrimaryConnection) { @@ -538,6 +549,18 @@ public final class SQLiteConnectionPool implements Closeable { } else { mConnectionWaiterQueue = waiter; } + + if (cancelationSignal != null) { + final int nonce = waiter.mNonce; + cancelationSignal.setOnCancelListener(new CancelationSignal.OnCancelListener() { + @Override + public void onCancel() { + synchronized (mLock) { + cancelConnectionWaiterLocked(waiter, nonce); + } + } + }); + } } // Park the thread until a connection is assigned or the pool is closed. @@ -547,7 +570,9 @@ public final class SQLiteConnectionPool implements Closeable { for (;;) { // Detect and recover from connection leaks. if (mConnectionLeaked.compareAndSet(true, false)) { - wakeConnectionWaitersLocked(); + synchronized (mLock) { + wakeConnectionWaitersLocked(); + } } // Wait to be unparked (may already have happened), a timeout, or interruption. @@ -560,15 +585,16 @@ public final class SQLiteConnectionPool implements Closeable { synchronized (mLock) { throwIfClosedLocked(); - SQLiteConnection connection = waiter.mAssignedConnection; - if (connection != null) { - recycleConnectionWaiterLocked(waiter); - return connection; - } - - RuntimeException ex = waiter.mException; - if (ex != null) { + final SQLiteConnection connection = waiter.mAssignedConnection; + final RuntimeException ex = waiter.mException; + if (connection != null || ex != null) { + if (cancelationSignal != null) { + cancelationSignal.setOnCancelListener(null); + } recycleConnectionWaiterLocked(waiter); + if (connection != null) { + return connection; + } throw ex; // rethrow! } @@ -585,6 +611,40 @@ public final class SQLiteConnectionPool implements Closeable { } // Can't throw. + private void cancelConnectionWaiterLocked(ConnectionWaiter waiter, int nonce) { + if (waiter.mNonce != nonce) { + // Waiter already removed and recycled. + return; + } + + if (waiter.mAssignedConnection != null || waiter.mException != null) { + // Waiter is done waiting but has not woken up yet. + return; + } + + // Waiter must still be waiting. Dequeue it. + ConnectionWaiter predecessor = null; + ConnectionWaiter current = mConnectionWaiterQueue; + while (current != waiter) { + assert current != null; + predecessor = current; + current = current.mNext; + } + if (predecessor != null) { + predecessor.mNext = waiter.mNext; + } else { + mConnectionWaiterQueue = waiter.mNext; + } + + // Send the waiter an exception and unpark it. + waiter.mException = new OperationCanceledException(); + LockSupport.unpark(waiter.mThread); + + // Check whether removing this waiter will enable other waiters to make progress. + wakeConnectionWaitersLocked(); + } + + // Can't throw. private void logConnectionPoolBusyLocked(long waitMillis, int connectionFlags) { final Thread thread = Thread.currentThread(); StringBuilder msg = new StringBuilder(); @@ -826,6 +886,7 @@ public final class SQLiteConnectionPool implements Closeable { waiter.mSql = null; waiter.mAssignedConnection = null; waiter.mException = null; + waiter.mNonce += 1; mConnectionWaiterPool = waiter; } @@ -904,5 +965,6 @@ public final class SQLiteConnectionPool implements Closeable { public int mConnectionFlags; public SQLiteConnection mAssignedConnection; public RuntimeException mException; + public int mNonce; } } diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index 9cb6480..7db7bfb 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -16,7 +16,9 @@ package android.database.sqlite; +import android.content.CancelationSignal; import android.content.ContentValues; +import android.content.OperationCanceledException; import android.content.res.Resources; import android.database.Cursor; import android.database.DatabaseErrorHandler; @@ -492,7 +494,7 @@ public class SQLiteDatabase extends SQLiteClosable { boolean exclusive) { getThreadSession().beginTransaction(exclusive ? SQLiteSession.TRANSACTION_MODE_EXCLUSIVE : SQLiteSession.TRANSACTION_MODE_IMMEDIATE, transactionListener, - getThreadDefaultConnectionFlags(false /*readOnly*/)); + getThreadDefaultConnectionFlags(false /*readOnly*/), null); } /** @@ -500,7 +502,7 @@ public class SQLiteDatabase extends SQLiteClosable { * are committed and rolled back. */ public void endTransaction() { - getThreadSession().endTransaction(); + getThreadSession().endTransaction(null); } /** @@ -597,7 +599,7 @@ public class SQLiteDatabase extends SQLiteClosable { } private boolean yieldIfContendedHelper(boolean throwIfUnsafe, long sleepAfterYieldDelay) { - return getThreadSession().yieldTransaction(sleepAfterYieldDelay, throwIfUnsafe); + return getThreadSession().yieldTransaction(sleepAfterYieldDelay, throwIfUnsafe, null); } /** @@ -935,7 +937,48 @@ public class SQLiteDatabase extends SQLiteClosable { String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) { return queryWithFactory(null, distinct, table, columns, selection, selectionArgs, - groupBy, having, orderBy, limit); + groupBy, having, orderBy, limit, null); + } + + /** + * Query the given URL, returning a {@link Cursor} over the result set. + * + * @param distinct true if you want each row to be unique, false otherwise. + * @param table The table name to compile the query against. + * @param columns A list of which columns to return. Passing null will + * return all columns, which is discouraged to prevent reading + * data from storage that isn't going to be used. + * @param selection A filter declaring which rows to return, formatted as an + * SQL WHERE clause (excluding the WHERE itself). Passing null + * will return 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. + * @param groupBy A filter declaring how to group rows, formatted as an SQL + * GROUP BY clause (excluding the GROUP BY itself). Passing null + * will cause the rows to not be grouped. + * @param having A filter declare which row groups to include in the cursor, + * if row grouping is being used, formatted as an SQL HAVING + * clause (excluding the HAVING itself). Passing null will cause + * all row groups to be included, and is required when row + * grouping is not being used. + * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause + * (excluding the ORDER BY itself). Passing null will use the + * default sort order, which may be unordered. + * @param limit Limits the number of rows returned by the query, + * formatted as LIMIT clause. Passing null denotes no LIMIT clause. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * If the operation is canceled, then {@link OperationCanceledException} will be thrown + * when the query is executed. + * @return A {@link Cursor} object, which is positioned before the first entry. Note that + * {@link Cursor}s are not synchronized, see the documentation for more details. + * @see Cursor + */ + public Cursor query(boolean distinct, String table, String[] columns, + String selection, String[] selectionArgs, String groupBy, + String having, String orderBy, String limit, CancelationSignal cancelationSignal) { + return queryWithFactory(null, distinct, table, columns, selection, selectionArgs, + groupBy, having, orderBy, limit, cancelationSignal); } /** @@ -974,12 +1017,55 @@ public class SQLiteDatabase extends SQLiteClosable { boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) { + return queryWithFactory(cursorFactory, distinct, table, columns, selection, + selectionArgs, groupBy, having, orderBy, limit, null); + } + + /** + * Query the given URL, returning a {@link Cursor} over the result set. + * + * @param cursorFactory the cursor factory to use, or null for the default factory + * @param distinct true if you want each row to be unique, false otherwise. + * @param table The table name to compile the query against. + * @param columns A list of which columns to return. Passing null will + * return all columns, which is discouraged to prevent reading + * data from storage that isn't going to be used. + * @param selection A filter declaring which rows to return, formatted as an + * SQL WHERE clause (excluding the WHERE itself). Passing null + * will return 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. + * @param groupBy A filter declaring how to group rows, formatted as an SQL + * GROUP BY clause (excluding the GROUP BY itself). Passing null + * will cause the rows to not be grouped. + * @param having A filter declare which row groups to include in the cursor, + * if row grouping is being used, formatted as an SQL HAVING + * clause (excluding the HAVING itself). Passing null will cause + * all row groups to be included, and is required when row + * grouping is not being used. + * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause + * (excluding the ORDER BY itself). Passing null will use the + * default sort order, which may be unordered. + * @param limit Limits the number of rows returned by the query, + * formatted as LIMIT clause. Passing null denotes no LIMIT clause. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * If the operation is canceled, then {@link OperationCanceledException} will be thrown + * when the query is executed. + * @return A {@link Cursor} object, which is positioned before the first entry. Note that + * {@link Cursor}s are not synchronized, see the documentation for more details. + * @see Cursor + */ + public Cursor queryWithFactory(CursorFactory cursorFactory, + boolean distinct, String table, String[] columns, + String selection, String[] selectionArgs, String groupBy, + String having, String orderBy, String limit, CancelationSignal cancelationSignal) { throwIfNotOpen(); // fail fast String sql = SQLiteQueryBuilder.buildQueryString( distinct, table, columns, selection, groupBy, having, orderBy, limit); - return rawQueryWithFactory( - cursorFactory, sql, selectionArgs, findEditTable(table)); + return rawQueryWithFactory(cursorFactory, sql, selectionArgs, + findEditTable(table), cancelationSignal); } /** @@ -1067,7 +1153,25 @@ public class SQLiteDatabase extends SQLiteClosable { * {@link Cursor}s are not synchronized, see the documentation for more details. */ public Cursor rawQuery(String sql, String[] selectionArgs) { - return rawQueryWithFactory(null, sql, selectionArgs, null); + return rawQueryWithFactory(null, sql, selectionArgs, null, null); + } + + /** + * Runs the provided SQL and returns a {@link Cursor} over the result set. + * + * @param sql the SQL query. The SQL string must not be ; terminated + * @param selectionArgs You may include ?s in where clause in the query, + * which will be replaced by the values from selectionArgs. The + * values will be bound as Strings. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * If the operation is canceled, then {@link OperationCanceledException} will be thrown + * when the query is executed. + * @return A {@link Cursor} object, which is positioned before the first entry. Note that + * {@link Cursor}s are not synchronized, see the documentation for more details. + */ + public Cursor rawQuery(String sql, String[] selectionArgs, + CancelationSignal cancelationSignal) { + return rawQueryWithFactory(null, sql, selectionArgs, null, cancelationSignal); } /** @@ -1085,9 +1189,31 @@ public class SQLiteDatabase extends SQLiteClosable { public Cursor rawQueryWithFactory( CursorFactory cursorFactory, String sql, String[] selectionArgs, String editTable) { + return rawQueryWithFactory(cursorFactory, sql, selectionArgs, editTable, null); + } + + /** + * Runs the provided SQL and returns a cursor over the result set. + * + * @param cursorFactory the cursor factory to use, or null for the default factory + * @param sql the SQL query. The SQL string must not be ; terminated + * @param selectionArgs You may include ?s in where clause in the query, + * which will be replaced by the values from selectionArgs. The + * values will be bound as Strings. + * @param editTable the name of the first table, which is editable + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * If the operation is canceled, then {@link OperationCanceledException} will be thrown + * when the query is executed. + * @return A {@link Cursor} object, which is positioned before the first entry. Note that + * {@link Cursor}s are not synchronized, see the documentation for more details. + */ + public Cursor rawQueryWithFactory( + CursorFactory cursorFactory, String sql, String[] selectionArgs, + String editTable, CancelationSignal cancelationSignal) { throwIfNotOpen(); // fail fast - SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(this, sql, editTable); + SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(this, sql, editTable, + cancelationSignal); return driver.query(cursorFactory != null ? cursorFactory : mCursorFactory, selectionArgs); } @@ -1786,7 +1912,7 @@ public class SQLiteDatabase extends SQLiteClosable { */ void lockPrimaryConnection() { getThreadSession().beginTransaction(SQLiteSession.TRANSACTION_MODE_DEFERRED, - null, SQLiteConnectionPool.CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY); + null, SQLiteConnectionPool.CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY, null); } /** @@ -1795,7 +1921,7 @@ public class SQLiteDatabase extends SQLiteClosable { * @see #lockPrimaryConnection() */ void unlockPrimaryConnection() { - getThreadSession().endTransaction(); + getThreadSession().endTransaction(null); } @Override diff --git a/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java b/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java index 52fd1d2..c490dc6 100644 --- a/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java +++ b/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java @@ -16,6 +16,7 @@ package android.database.sqlite; +import android.content.CancelationSignal; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase.CursorFactory; @@ -28,16 +29,19 @@ public class SQLiteDirectCursorDriver implements SQLiteCursorDriver { private final SQLiteDatabase mDatabase; private final String mEditTable; private final String mSql; + private final CancelationSignal mCancelationSignal; private SQLiteQuery mQuery; - public SQLiteDirectCursorDriver(SQLiteDatabase db, String sql, String editTable) { + public SQLiteDirectCursorDriver(SQLiteDatabase db, String sql, String editTable, + CancelationSignal cancelationSignal) { mDatabase = db; mEditTable = editTable; mSql = sql; + mCancelationSignal = cancelationSignal; } public Cursor query(CursorFactory factory, String[] selectionArgs) { - final SQLiteQuery query = new SQLiteQuery(mDatabase, mSql); + final SQLiteQuery query = new SQLiteQuery(mDatabase, mSql, mCancelationSignal); final Cursor cursor; try { query.bindAllArgsAsStrings(selectionArgs); diff --git a/core/java/android/database/sqlite/SQLiteProgram.java b/core/java/android/database/sqlite/SQLiteProgram.java index 8194458..f3da2a6 100644 --- a/core/java/android/database/sqlite/SQLiteProgram.java +++ b/core/java/android/database/sqlite/SQLiteProgram.java @@ -16,6 +16,7 @@ package android.database.sqlite; +import android.content.CancelationSignal; import android.database.DatabaseUtils; import java.util.Arrays; @@ -36,7 +37,8 @@ public abstract class SQLiteProgram extends SQLiteClosable { private final int mNumParameters; private final Object[] mBindArgs; - SQLiteProgram(SQLiteDatabase db, String sql, Object[] bindArgs) { + SQLiteProgram(SQLiteDatabase db, String sql, Object[] bindArgs, + CancelationSignal cancelationSignalForPrepare) { mDatabase = db; mSql = sql.trim(); @@ -54,7 +56,8 @@ public abstract class SQLiteProgram extends SQLiteClosable { boolean assumeReadOnly = (n == DatabaseUtils.STATEMENT_SELECT); SQLiteStatementInfo info = new SQLiteStatementInfo(); db.getThreadSession().prepare(mSql, - db.getThreadDefaultConnectionFlags(assumeReadOnly), info); + db.getThreadDefaultConnectionFlags(assumeReadOnly), + cancelationSignalForPrepare, info); mReadOnly = info.readOnly; mColumnNames = info.columnNames; mNumParameters = info.numParameters; diff --git a/core/java/android/database/sqlite/SQLiteQuery.java b/core/java/android/database/sqlite/SQLiteQuery.java index 17aa886..df2e260 100644 --- a/core/java/android/database/sqlite/SQLiteQuery.java +++ b/core/java/android/database/sqlite/SQLiteQuery.java @@ -16,6 +16,8 @@ package android.database.sqlite; +import android.content.CancelationSignal; +import android.content.OperationCanceledException; import android.database.CursorWindow; import android.util.Log; @@ -29,8 +31,12 @@ import android.util.Log; public final class SQLiteQuery extends SQLiteProgram { private static final String TAG = "SQLiteQuery"; - SQLiteQuery(SQLiteDatabase db, String query) { - super(db, query, null); + private final CancelationSignal mCancelationSignal; + + SQLiteQuery(SQLiteDatabase db, String query, CancelationSignal cancelationSignal) { + super(db, query, null, cancelationSignal); + + mCancelationSignal = cancelationSignal; } /** @@ -44,6 +50,9 @@ public final class SQLiteQuery extends SQLiteProgram { * return regardless of whether they fit in the window. * @return Number of rows that were enumerated. Might not be all rows * unless countAllRows is true. + * + * @throws SQLiteException if an error occurs. + * @throws OperationCanceledException if the operation was canceled. */ int fillWindow(CursorWindow window, int startPos, int requiredPos, boolean countAllRows) { acquireReference(); @@ -51,7 +60,8 @@ public final class SQLiteQuery extends SQLiteProgram { window.acquireReference(); try { int numRows = getSession().executeForCursorWindow(getSql(), getBindArgs(), - window, startPos, requiredPos, countAllRows, getConnectionFlags()); + window, startPos, requiredPos, countAllRows, getConnectionFlags(), + mCancelationSignal); return numRows; } catch (SQLiteDatabaseCorruptException ex) { onCorruption(); diff --git a/core/java/android/database/sqlite/SQLiteQueryBuilder.java b/core/java/android/database/sqlite/SQLiteQueryBuilder.java index 1b7b398..89469cb 100644 --- a/core/java/android/database/sqlite/SQLiteQueryBuilder.java +++ b/core/java/android/database/sqlite/SQLiteQueryBuilder.java @@ -16,6 +16,8 @@ package android.database.sqlite; +import android.content.CancelationSignal; +import android.content.OperationCanceledException; import android.database.Cursor; import android.database.DatabaseUtils; import android.provider.BaseColumns; @@ -137,8 +139,9 @@ public class SQLiteQueryBuilder /** * Sets the cursor factory to be used for the query. You can use * one factory for all queries on a database but it is normally - * easier to specify the factory when doing this query. @param - * factory the factor to use + * easier to specify the factory when doing this query. + * + * @param factory the factory to use. */ public void setCursorFactory(SQLiteDatabase.CursorFactory factory) { mFactory = factory; @@ -289,7 +292,7 @@ public class SQLiteQueryBuilder String selection, String[] selectionArgs, String groupBy, String having, String sortOrder) { return query(db, projectionIn, selection, selectionArgs, groupBy, having, sortOrder, - null /* limit */); + null /* limit */, null /* cancelationSignal */); } /** @@ -327,6 +330,48 @@ public class SQLiteQueryBuilder public Cursor query(SQLiteDatabase db, String[] projectionIn, String selection, String[] selectionArgs, String groupBy, String having, String sortOrder, String limit) { + return query(db, projectionIn, selection, selectionArgs, + groupBy, having, sortOrder, limit, null); + } + + /** + * Perform a query by combining all current settings and the + * information passed into this method. + * + * @param db the database to query on + * @param projectionIn A list of which columns to return. Passing + * null will return all columns, which is discouraged to prevent + * reading data from storage that isn't going to be used. + * @param selection A filter declaring which rows to return, + * formatted as an SQL WHERE clause (excluding the WHERE + * itself). Passing null will return all rows for the given URL. + * @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. + * @param groupBy A filter declaring how to group rows, formatted + * as an SQL GROUP BY clause (excluding the GROUP BY + * itself). Passing null will cause the rows to not be grouped. + * @param having A filter declare which row groups to include in + * the cursor, if row grouping is being used, formatted as an + * SQL HAVING clause (excluding the HAVING itself). Passing + * null will cause all row groups to be included, and is + * required when row grouping is not being used. + * @param sortOrder How to order the rows, formatted as an SQL + * ORDER BY clause (excluding the ORDER BY itself). Passing null + * will use the default sort order, which may be unordered. + * @param limit Limits the number of rows returned by the query, + * formatted as LIMIT clause. Passing null denotes no LIMIT clause. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * If the operation is canceled, then {@link OperationCanceledException} will be thrown + * when the query is executed. + * @return a cursor over the result set + * @see android.content.ContentResolver#query(android.net.Uri, String[], + * String, String[], String) + */ + public Cursor query(SQLiteDatabase db, String[] projectionIn, + String selection, String[] selectionArgs, String groupBy, + String having, String sortOrder, String limit, CancelationSignal cancelationSignal) { if (mTables == null) { return null; } @@ -341,7 +386,8 @@ public class SQLiteQueryBuilder // in both the wrapped and original forms. String sqlForValidation = buildQuery(projectionIn, "(" + selection + ")", groupBy, having, sortOrder, limit); - validateQuerySql(db, sqlForValidation); // will throw if query is invalid + validateQuerySql(db, sqlForValidation, + cancelationSignal); // will throw if query is invalid } String sql = buildQuery( @@ -353,16 +399,18 @@ public class SQLiteQueryBuilder } return db.rawQueryWithFactory( mFactory, sql, selectionArgs, - SQLiteDatabase.findEditTable(mTables)); // will throw if query is invalid + SQLiteDatabase.findEditTable(mTables), + cancelationSignal); // will throw if query is invalid } /** * Verifies that a SQL SELECT statement is valid by compiling it. * If the SQL statement is not valid, this method will throw a {@link SQLiteException}. */ - private void validateQuerySql(SQLiteDatabase db, String sql) { + private void validateQuerySql(SQLiteDatabase db, String sql, + CancelationSignal cancelationSignal) { db.getThreadSession().prepare(sql, - db.getThreadDefaultConnectionFlags(true /*readOnly*/), null); + db.getThreadDefaultConnectionFlags(true /*readOnly*/), cancelationSignal, null); } /** diff --git a/core/java/android/database/sqlite/SQLiteSession.java b/core/java/android/database/sqlite/SQLiteSession.java index a933051..b5a3e31 100644 --- a/core/java/android/database/sqlite/SQLiteSession.java +++ b/core/java/android/database/sqlite/SQLiteSession.java @@ -16,6 +16,8 @@ package android.database.sqlite; +import android.content.CancelationSignal; +import android.content.OperationCanceledException; import android.database.CursorWindow; import android.database.DatabaseUtils; import android.os.ParcelFileDescriptor; @@ -156,8 +158,6 @@ import android.os.ParcelFileDescriptor; * triggers may call custom SQLite functions that perform additional queries. * </p> * - * TODO: Support timeouts on all possibly blocking operations. - * * @hide */ public final class SQLiteSession { @@ -280,24 +280,34 @@ public final class SQLiteSession { * @param transactionListener The transaction listener, or null if none. * @param connectionFlags The connection flags to use if a connection must be * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. * * @throws IllegalStateException if {@link #setTransactionSuccessful} has already been * called for the current transaction. + * @throws SQLiteException if an error occurs. + * @throws OperationCanceledException if the operation was canceled. * * @see #setTransactionSuccessful * @see #yieldTransaction * @see #endTransaction */ public void beginTransaction(int transactionMode, - SQLiteTransactionListener transactionListener, int connectionFlags) { + SQLiteTransactionListener transactionListener, int connectionFlags, + CancelationSignal cancelationSignal) { throwIfTransactionMarkedSuccessful(); - beginTransactionUnchecked(transactionMode, transactionListener, connectionFlags); + beginTransactionUnchecked(transactionMode, transactionListener, connectionFlags, + cancelationSignal); } private void beginTransactionUnchecked(int transactionMode, - SQLiteTransactionListener transactionListener, int connectionFlags) { + SQLiteTransactionListener transactionListener, int connectionFlags, + CancelationSignal cancelationSignal) { + if (cancelationSignal != null) { + cancelationSignal.throwIfCanceled(); + } + if (mTransactionStack == null) { - acquireConnection(null, connectionFlags); // might throw + acquireConnection(null, connectionFlags, cancelationSignal); // might throw } try { // Set up the transaction such that we can back out safely @@ -306,13 +316,15 @@ public final class SQLiteSession { // Execute SQL might throw a runtime exception. switch (transactionMode) { case TRANSACTION_MODE_IMMEDIATE: - mConnection.execute("BEGIN IMMEDIATE;", null); // might throw + mConnection.execute("BEGIN IMMEDIATE;", null, + cancelationSignal); // might throw break; case TRANSACTION_MODE_EXCLUSIVE: - mConnection.execute("BEGIN EXCLUSIVE;", null); // might throw + mConnection.execute("BEGIN EXCLUSIVE;", null, + cancelationSignal); // might throw break; default: - mConnection.execute("BEGIN;", null); // might throw + mConnection.execute("BEGIN;", null, cancelationSignal); // might throw break; } } @@ -323,7 +335,7 @@ public final class SQLiteSession { transactionListener.onBegin(); // might throw } catch (RuntimeException ex) { if (mTransactionStack == null) { - mConnection.execute("ROLLBACK;", null); // might throw + mConnection.execute("ROLLBACK;", null, cancelationSignal); // might throw } throw ex; } @@ -372,20 +384,28 @@ public final class SQLiteSession { * This method must be called exactly once for each call to {@link #beginTransaction}. * </p> * + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. + * * @throws IllegalStateException if there is no current transaction. + * @throws SQLiteException if an error occurs. + * @throws OperationCanceledException if the operation was canceled. * * @see #beginTransaction * @see #setTransactionSuccessful * @see #yieldTransaction */ - public void endTransaction() { + public void endTransaction(CancelationSignal cancelationSignal) { throwIfNoTransaction(); assert mConnection != null; - endTransactionUnchecked(); + endTransactionUnchecked(cancelationSignal); } - private void endTransactionUnchecked() { + private void endTransactionUnchecked(CancelationSignal cancelationSignal) { + if (cancelationSignal != null) { + cancelationSignal.throwIfCanceled(); + } + final Transaction top = mTransactionStack; boolean successful = top.mMarkedSuccessful && !top.mChildFailed; @@ -414,9 +434,9 @@ public final class SQLiteSession { } else { try { if (successful) { - mConnection.execute("COMMIT;", null); // might throw + mConnection.execute("COMMIT;", null, cancelationSignal); // might throw } else { - mConnection.execute("ROLLBACK;", null); // might throw + mConnection.execute("ROLLBACK;", null, cancelationSignal); // might throw } } finally { releaseConnection(); // might throw @@ -467,16 +487,20 @@ public final class SQLiteSession { * @param throwIfUnsafe If true, then instead of returning false when no * transaction is in progress, a nested transaction is in progress, or when * the transaction has already been marked successful, throws {@link IllegalStateException}. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. * @return True if the transaction was actually yielded. * * @throws IllegalStateException if <code>throwIfNested</code> is true and * there is no current transaction, there is a nested transaction in progress or * if {@link #setTransactionSuccessful} has already been called for the current transaction. + * @throws SQLiteException if an error occurs. + * @throws OperationCanceledException if the operation was canceled. * * @see #beginTransaction * @see #endTransaction */ - public boolean yieldTransaction(long sleepAfterYieldDelayMillis, boolean throwIfUnsafe) { + public boolean yieldTransaction(long sleepAfterYieldDelayMillis, boolean throwIfUnsafe, + CancelationSignal cancelationSignal) { if (throwIfUnsafe) { throwIfNoTransaction(); throwIfTransactionMarkedSuccessful(); @@ -493,10 +517,16 @@ public final class SQLiteSession { return false; } - return yieldTransactionUnchecked(sleepAfterYieldDelayMillis); // might throw + return yieldTransactionUnchecked(sleepAfterYieldDelayMillis, + cancelationSignal); // might throw } - private boolean yieldTransactionUnchecked(long sleepAfterYieldDelayMillis) { + private boolean yieldTransactionUnchecked(long sleepAfterYieldDelayMillis, + CancelationSignal cancelationSignal) { + if (cancelationSignal != null) { + cancelationSignal.throwIfCanceled(); + } + if (!mConnectionPool.shouldYieldConnection(mConnection, mConnectionFlags)) { return false; } @@ -504,7 +534,7 @@ public final class SQLiteSession { final int transactionMode = mTransactionStack.mMode; final SQLiteTransactionListener listener = mTransactionStack.mListener; final int connectionFlags = mConnectionFlags; - endTransactionUnchecked(); // might throw + endTransactionUnchecked(cancelationSignal); // might throw if (sleepAfterYieldDelayMillis > 0) { try { @@ -514,7 +544,8 @@ public final class SQLiteSession { } } - beginTransactionUnchecked(transactionMode, listener, connectionFlags); // might throw + beginTransactionUnchecked(transactionMode, listener, connectionFlags, + cancelationSignal); // might throw return true; } @@ -535,17 +566,24 @@ public final class SQLiteSession { * @param sql The SQL statement to prepare. * @param connectionFlags The connection flags to use if a connection must be * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. * @param outStatementInfo The {@link SQLiteStatementInfo} object to populate * with information about the statement, or null if none. * * @throws SQLiteException if an error occurs, such as a syntax error. + * @throws OperationCanceledException if the operation was canceled. */ - public void prepare(String sql, int connectionFlags, SQLiteStatementInfo outStatementInfo) { + public void prepare(String sql, int connectionFlags, CancelationSignal cancelationSignal, + SQLiteStatementInfo outStatementInfo) { if (sql == null) { throw new IllegalArgumentException("sql must not be null."); } - acquireConnection(sql, connectionFlags); // might throw + if (cancelationSignal != null) { + cancelationSignal.throwIfCanceled(); + } + + acquireConnection(sql, connectionFlags, cancelationSignal); // might throw try { mConnection.prepare(sql, outStatementInfo); // might throw } finally { @@ -560,22 +598,25 @@ public final class SQLiteSession { * @param bindArgs The arguments to bind, or null if none. * @param connectionFlags The connection flags to use if a connection must be * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. * * @throws SQLiteException if an error occurs, such as a syntax error * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. */ - public void execute(String sql, Object[] bindArgs, int connectionFlags) { + public void execute(String sql, Object[] bindArgs, int connectionFlags, + CancelationSignal cancelationSignal) { if (sql == null) { throw new IllegalArgumentException("sql must not be null."); } - if (executeSpecial(sql, bindArgs, connectionFlags)) { + if (executeSpecial(sql, bindArgs, connectionFlags, cancelationSignal)) { return; } - acquireConnection(sql, connectionFlags); // might throw + acquireConnection(sql, connectionFlags, cancelationSignal); // might throw try { - mConnection.execute(sql, bindArgs); // might throw + mConnection.execute(sql, bindArgs, cancelationSignal); // might throw } finally { releaseConnection(); // might throw } @@ -588,24 +629,27 @@ public final class SQLiteSession { * @param bindArgs The arguments to bind, or null if none. * @param connectionFlags The connection flags to use if a connection must be * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. * @return The value of the first column in the first row of the result set * as a <code>long</code>, or zero if none. * * @throws SQLiteException if an error occurs, such as a syntax error * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. */ - public long executeForLong(String sql, Object[] bindArgs, int connectionFlags) { + public long executeForLong(String sql, Object[] bindArgs, int connectionFlags, + CancelationSignal cancelationSignal) { if (sql == null) { throw new IllegalArgumentException("sql must not be null."); } - if (executeSpecial(sql, bindArgs, connectionFlags)) { + if (executeSpecial(sql, bindArgs, connectionFlags, cancelationSignal)) { return 0; } - acquireConnection(sql, connectionFlags); // might throw + acquireConnection(sql, connectionFlags, cancelationSignal); // might throw try { - return mConnection.executeForLong(sql, bindArgs); // might throw + return mConnection.executeForLong(sql, bindArgs, cancelationSignal); // might throw } finally { releaseConnection(); // might throw } @@ -618,24 +662,27 @@ public final class SQLiteSession { * @param bindArgs The arguments to bind, or null if none. * @param connectionFlags The connection flags to use if a connection must be * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. * @return The value of the first column in the first row of the result set * as a <code>String</code>, or null if none. * * @throws SQLiteException if an error occurs, such as a syntax error * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. */ - public String executeForString(String sql, Object[] bindArgs, int connectionFlags) { + public String executeForString(String sql, Object[] bindArgs, int connectionFlags, + CancelationSignal cancelationSignal) { if (sql == null) { throw new IllegalArgumentException("sql must not be null."); } - if (executeSpecial(sql, bindArgs, connectionFlags)) { + if (executeSpecial(sql, bindArgs, connectionFlags, cancelationSignal)) { return null; } - acquireConnection(sql, connectionFlags); // might throw + acquireConnection(sql, connectionFlags, cancelationSignal); // might throw try { - return mConnection.executeForString(sql, bindArgs); // might throw + return mConnection.executeForString(sql, bindArgs, cancelationSignal); // might throw } finally { releaseConnection(); // might throw } @@ -649,26 +696,29 @@ public final class SQLiteSession { * @param bindArgs The arguments to bind, or null if none. * @param connectionFlags The connection flags to use if a connection must be * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. * @return The file descriptor for a shared memory region that contains * the value of the first column in the first row of the result set as a BLOB, * or null if none. * * @throws SQLiteException if an error occurs, such as a syntax error * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. */ public ParcelFileDescriptor executeForBlobFileDescriptor(String sql, Object[] bindArgs, - int connectionFlags) { + int connectionFlags, CancelationSignal cancelationSignal) { if (sql == null) { throw new IllegalArgumentException("sql must not be null."); } - if (executeSpecial(sql, bindArgs, connectionFlags)) { + if (executeSpecial(sql, bindArgs, connectionFlags, cancelationSignal)) { return null; } - acquireConnection(sql, connectionFlags); // might throw + acquireConnection(sql, connectionFlags, cancelationSignal); // might throw try { - return mConnection.executeForBlobFileDescriptor(sql, bindArgs); // might throw + return mConnection.executeForBlobFileDescriptor(sql, bindArgs, + cancelationSignal); // might throw } finally { releaseConnection(); // might throw } @@ -682,23 +732,27 @@ public final class SQLiteSession { * @param bindArgs The arguments to bind, or null if none. * @param connectionFlags The connection flags to use if a connection must be * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. * @return The number of rows that were changed. * * @throws SQLiteException if an error occurs, such as a syntax error * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. */ - public int executeForChangedRowCount(String sql, Object[] bindArgs, int connectionFlags) { + public int executeForChangedRowCount(String sql, Object[] bindArgs, int connectionFlags, + CancelationSignal cancelationSignal) { if (sql == null) { throw new IllegalArgumentException("sql must not be null."); } - if (executeSpecial(sql, bindArgs, connectionFlags)) { + if (executeSpecial(sql, bindArgs, connectionFlags, cancelationSignal)) { return 0; } - acquireConnection(sql, connectionFlags); // might throw + acquireConnection(sql, connectionFlags, cancelationSignal); // might throw try { - return mConnection.executeForChangedRowCount(sql, bindArgs); // might throw + return mConnection.executeForChangedRowCount(sql, bindArgs, + cancelationSignal); // might throw } finally { releaseConnection(); // might throw } @@ -712,23 +766,27 @@ public final class SQLiteSession { * @param bindArgs The arguments to bind, or null if none. * @param connectionFlags The connection flags to use if a connection must be * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. * @return The row id of the last row that was inserted, or 0 if none. * * @throws SQLiteException if an error occurs, such as a syntax error * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. */ - public long executeForLastInsertedRowId(String sql, Object[] bindArgs, int connectionFlags) { + public long executeForLastInsertedRowId(String sql, Object[] bindArgs, int connectionFlags, + CancelationSignal cancelationSignal) { if (sql == null) { throw new IllegalArgumentException("sql must not be null."); } - if (executeSpecial(sql, bindArgs, connectionFlags)) { + if (executeSpecial(sql, bindArgs, connectionFlags, cancelationSignal)) { return 0; } - acquireConnection(sql, connectionFlags); // might throw + acquireConnection(sql, connectionFlags, cancelationSignal); // might throw try { - return mConnection.executeForLastInsertedRowId(sql, bindArgs); // might throw + return mConnection.executeForLastInsertedRowId(sql, bindArgs, + cancelationSignal); // might throw } finally { releaseConnection(); // might throw } @@ -750,15 +808,17 @@ public final class SQLiteSession { * regagless of whether they fit in the window. * @param connectionFlags The connection flags to use if a connection must be * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. * @return The number of rows that were counted during query execution. Might * not be all rows in the result set unless <code>countAllRows</code> is true. * * @throws SQLiteException if an error occurs, such as a syntax error * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. */ public int executeForCursorWindow(String sql, Object[] bindArgs, CursorWindow window, int startPos, int requiredPos, boolean countAllRows, - int connectionFlags) { + int connectionFlags, CancelationSignal cancelationSignal) { if (sql == null) { throw new IllegalArgumentException("sql must not be null."); } @@ -766,15 +826,16 @@ public final class SQLiteSession { throw new IllegalArgumentException("window must not be null."); } - if (executeSpecial(sql, bindArgs, connectionFlags)) { + if (executeSpecial(sql, bindArgs, connectionFlags, cancelationSignal)) { window.clear(); return 0; } - acquireConnection(sql, connectionFlags); // might throw + acquireConnection(sql, connectionFlags, cancelationSignal); // might throw try { return mConnection.executeForCursorWindow(sql, bindArgs, - window, startPos, requiredPos, countAllRows); // might throw + window, startPos, requiredPos, countAllRows, + cancelationSignal); // might throw } finally { releaseConnection(); // might throw } @@ -793,35 +854,45 @@ public final class SQLiteSession { * @param bindArgs The arguments to bind, or null if none. * @param connectionFlags The connection flags to use if a connection must be * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancelationSignal A signal to cancel the operation in progress, or null if none. * @return True if the statement was of a special form that was handled here, * false otherwise. * * @throws SQLiteException if an error occurs, such as a syntax error * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. */ - private boolean executeSpecial(String sql, Object[] bindArgs, int connectionFlags) { + private boolean executeSpecial(String sql, Object[] bindArgs, int connectionFlags, + CancelationSignal cancelationSignal) { + if (cancelationSignal != null) { + cancelationSignal.throwIfCanceled(); + } + final int type = DatabaseUtils.getSqlStatementType(sql); switch (type) { case DatabaseUtils.STATEMENT_BEGIN: - beginTransaction(TRANSACTION_MODE_EXCLUSIVE, null, connectionFlags); + beginTransaction(TRANSACTION_MODE_EXCLUSIVE, null, connectionFlags, + cancelationSignal); return true; case DatabaseUtils.STATEMENT_COMMIT: setTransactionSuccessful(); - endTransaction(); + endTransaction(cancelationSignal); return true; case DatabaseUtils.STATEMENT_ABORT: - endTransaction(); + endTransaction(cancelationSignal); return true; } return false; } - private void acquireConnection(String sql, int connectionFlags) { + private void acquireConnection(String sql, int connectionFlags, + CancelationSignal cancelationSignal) { if (mConnection == null) { assert mConnectionUseCount == 0; - mConnection = mConnectionPool.acquireConnection(sql, connectionFlags); // might throw + mConnection = mConnectionPool.acquireConnection(sql, connectionFlags, + cancelationSignal); // might throw mConnectionFlags = connectionFlags; } mConnectionUseCount += 1; diff --git a/core/java/android/database/sqlite/SQLiteStatement.java b/core/java/android/database/sqlite/SQLiteStatement.java index 4e20da0..b1092d7 100644 --- a/core/java/android/database/sqlite/SQLiteStatement.java +++ b/core/java/android/database/sqlite/SQLiteStatement.java @@ -28,7 +28,7 @@ import android.os.ParcelFileDescriptor; */ public final class SQLiteStatement extends SQLiteProgram { SQLiteStatement(SQLiteDatabase db, String sql, Object[] bindArgs) { - super(db, sql, bindArgs); + super(db, sql, bindArgs, null); } /** @@ -41,7 +41,7 @@ public final class SQLiteStatement extends SQLiteProgram { public void execute() { acquireReference(); try { - getSession().execute(getSql(), getBindArgs(), getConnectionFlags()); + getSession().execute(getSql(), getBindArgs(), getConnectionFlags(), null); } catch (SQLiteDatabaseCorruptException ex) { onCorruption(); throw ex; @@ -62,7 +62,7 @@ public final class SQLiteStatement extends SQLiteProgram { acquireReference(); try { return getSession().executeForChangedRowCount( - getSql(), getBindArgs(), getConnectionFlags()); + getSql(), getBindArgs(), getConnectionFlags(), null); } catch (SQLiteDatabaseCorruptException ex) { onCorruption(); throw ex; @@ -84,7 +84,7 @@ public final class SQLiteStatement extends SQLiteProgram { acquireReference(); try { return getSession().executeForLastInsertedRowId( - getSql(), getBindArgs(), getConnectionFlags()); + getSql(), getBindArgs(), getConnectionFlags(), null); } catch (SQLiteDatabaseCorruptException ex) { onCorruption(); throw ex; @@ -105,7 +105,7 @@ public final class SQLiteStatement extends SQLiteProgram { acquireReference(); try { return getSession().executeForLong( - getSql(), getBindArgs(), getConnectionFlags()); + getSql(), getBindArgs(), getConnectionFlags(), null); } catch (SQLiteDatabaseCorruptException ex) { onCorruption(); throw ex; @@ -126,7 +126,7 @@ public final class SQLiteStatement extends SQLiteProgram { acquireReference(); try { return getSession().executeForString( - getSql(), getBindArgs(), getConnectionFlags()); + getSql(), getBindArgs(), getConnectionFlags(), null); } catch (SQLiteDatabaseCorruptException ex) { onCorruption(); throw ex; @@ -147,7 +147,7 @@ public final class SQLiteStatement extends SQLiteProgram { acquireReference(); try { return getSession().executeForBlobFileDescriptor( - getSql(), getBindArgs(), getConnectionFlags()); + getSql(), getBindArgs(), getConnectionFlags(), null); } catch (SQLiteDatabaseCorruptException ex) { onCorruption(); throw ex; diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index ba9046c..9d96c0d 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -747,7 +747,7 @@ public final class Settings { Cursor c = null; try { c = cp.query(mUri, SELECT_VALUE, NAME_EQ_PLACEHOLDER, - new String[]{name}, null); + new String[]{name}, null, null); if (c == null) { Log.w(TAG, "Can't get key " + name + " from " + mUri); return null; diff --git a/core/jni/android_database_SQLiteCommon.cpp b/core/jni/android_database_SQLiteCommon.cpp index a94b9d2..3484467 100644 --- a/core/jni/android_database_SQLiteCommon.cpp +++ b/core/jni/android_database_SQLiteCommon.cpp @@ -68,50 +68,53 @@ void throw_sqlite3_exception(JNIEnv* env, int errcode, exceptionClass = "android/database/sqlite/SQLiteDatabaseCorruptException"; break; case SQLITE_CONSTRAINT: - exceptionClass = "android/database/sqlite/SQLiteConstraintException"; - break; + exceptionClass = "android/database/sqlite/SQLiteConstraintException"; + break; case SQLITE_ABORT: - exceptionClass = "android/database/sqlite/SQLiteAbortException"; - break; + exceptionClass = "android/database/sqlite/SQLiteAbortException"; + break; case SQLITE_DONE: - exceptionClass = "android/database/sqlite/SQLiteDoneException"; - break; + exceptionClass = "android/database/sqlite/SQLiteDoneException"; + break; case SQLITE_FULL: - exceptionClass = "android/database/sqlite/SQLiteFullException"; - break; + exceptionClass = "android/database/sqlite/SQLiteFullException"; + break; case SQLITE_MISUSE: - exceptionClass = "android/database/sqlite/SQLiteMisuseException"; - break; + exceptionClass = "android/database/sqlite/SQLiteMisuseException"; + break; case SQLITE_PERM: - exceptionClass = "android/database/sqlite/SQLiteAccessPermException"; - break; + exceptionClass = "android/database/sqlite/SQLiteAccessPermException"; + break; case SQLITE_BUSY: - exceptionClass = "android/database/sqlite/SQLiteDatabaseLockedException"; - break; + exceptionClass = "android/database/sqlite/SQLiteDatabaseLockedException"; + break; case SQLITE_LOCKED: - exceptionClass = "android/database/sqlite/SQLiteTableLockedException"; - break; + exceptionClass = "android/database/sqlite/SQLiteTableLockedException"; + break; case SQLITE_READONLY: - exceptionClass = "android/database/sqlite/SQLiteReadOnlyDatabaseException"; - break; + exceptionClass = "android/database/sqlite/SQLiteReadOnlyDatabaseException"; + break; case SQLITE_CANTOPEN: - exceptionClass = "android/database/sqlite/SQLiteCantOpenDatabaseException"; - break; + exceptionClass = "android/database/sqlite/SQLiteCantOpenDatabaseException"; + break; case SQLITE_TOOBIG: - exceptionClass = "android/database/sqlite/SQLiteBlobTooBigException"; - break; + exceptionClass = "android/database/sqlite/SQLiteBlobTooBigException"; + break; case SQLITE_RANGE: - exceptionClass = "android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException"; - break; + exceptionClass = "android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException"; + break; case SQLITE_NOMEM: - exceptionClass = "android/database/sqlite/SQLiteOutOfMemoryException"; - break; + exceptionClass = "android/database/sqlite/SQLiteOutOfMemoryException"; + break; case SQLITE_MISMATCH: - exceptionClass = "android/database/sqlite/SQLiteDatatypeMismatchException"; - break; + exceptionClass = "android/database/sqlite/SQLiteDatatypeMismatchException"; + break; + case SQLITE_INTERRUPT: + exceptionClass = "android/content/OperationCanceledException"; + break; default: - exceptionClass = "android/database/sqlite/SQLiteException"; - break; + exceptionClass = "android/database/sqlite/SQLiteException"; + break; } if (sqlite3Message != NULL && message != NULL) { diff --git a/core/jni/android_database_SQLiteConnection.cpp b/core/jni/android_database_SQLiteConnection.cpp index d0d53f6..e061ac3 100644 --- a/core/jni/android_database_SQLiteConnection.cpp +++ b/core/jni/android_database_SQLiteConnection.cpp @@ -67,8 +67,10 @@ struct SQLiteConnection { const String8 path; const String8 label; + volatile bool canceled; + SQLiteConnection(sqlite3* db, int openFlags, const String8& path, const String8& label) : - db(db), openFlags(openFlags), path(path), label(label) { } + db(db), openFlags(openFlags), path(path), label(label), canceled(false) { } }; // Called each time a statement begins execution, when tracing is enabled. @@ -85,6 +87,12 @@ static void sqliteProfileCallback(void *data, const char *sql, sqlite3_uint64 tm connection->label.string(), sql, tm * 0.000001f); } +// Called after each SQLite VM instruction when cancelation is enabled. +static int sqliteProgressHandlerCallback(void* data) { + SQLiteConnection* connection = static_cast<SQLiteConnection*>(data); + return connection->canceled; +} + static jint nativeOpen(JNIEnv* env, jclass clazz, jstring pathStr, jint openFlags, jstring labelStr, jboolean enableTrace, jboolean enableProfile) { @@ -871,6 +879,24 @@ static jint nativeGetDbLookaside(JNIEnv* env, jobject clazz, jint connectionPtr) return cur; } +static void nativeCancel(JNIEnv* env, jobject clazz, jint connectionPtr) { + SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr); + connection->canceled = true; +} + +static void nativeResetCancel(JNIEnv* env, jobject clazz, jint connectionPtr, + jboolean cancelable) { + SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr); + connection->canceled = false; + + if (cancelable) { + sqlite3_progress_handler(connection->db, 4, sqliteProgressHandlerCallback, + connection); + } else { + sqlite3_progress_handler(connection->db, 0, NULL, NULL); + } +} + static JNINativeMethod sMethods[] = { @@ -923,6 +949,10 @@ static JNINativeMethod sMethods[] = (void*)nativeExecuteForCursorWindow }, { "nativeGetDbLookaside", "(I)I", (void*)nativeGetDbLookaside }, + { "nativeCancel", "(I)V", + (void*)nativeCancel }, + { "nativeResetCancel", "(IZ)V", + (void*)nativeResetCancel }, }; #define FIND_CLASS(var, className) \ |