diff options
author | Jeff Brown <jeffbrown@google.com> | 2012-01-25 19:37:13 -0800 |
---|---|---|
committer | Jeff Brown <jeffbrown@google.com> | 2012-01-27 17:33:21 -0800 |
commit | 75ea64fc54f328d37b115cfb1ded1e45c30380ed (patch) | |
tree | 4254a5d2d0662de8b606b38fea6987da17c130e3 | |
parent | ebc016c01ea9d5707287cfc19ccc59b21a486c00 (diff) | |
download | frameworks_base-75ea64fc54f328d37b115cfb1ded1e45c30380ed.zip frameworks_base-75ea64fc54f328d37b115cfb1ded1e45c30380ed.tar.gz frameworks_base-75ea64fc54f328d37b115cfb1ded1e45c30380ed.tar.bz2 |
Implement a cancelation mechanism for queries.
Added new API to enable cancelation of SQLite and content provider
queries by means of a CancelationSignal object. The application
creates a CancelationSignal object and passes it as an argument
to the query. The cancelation signal can then be used to cancel
the query while it is executing.
If the cancelation signal is raised before the query is executed,
then it is immediately terminated.
Change-Id: If2c76e9a7e56ea5e98768b6d4f225f0a1ca61c61
30 files changed, 1156 insertions, 202 deletions
@@ -94,6 +94,7 @@ LOCAL_SRC_FILES += \ core/java/android/bluetooth/IBluetoothHealthCallback.aidl \ core/java/android/bluetooth/IBluetoothPbap.aidl \ core/java/android/bluetooth/IBluetoothStateChangeCallback.aidl \ + core/java/android/content/ICancelationSignal.aidl \ core/java/android/content/IClipboard.aidl \ core/java/android/content/IContentService.aidl \ core/java/android/content/IIntentReceiver.aidl \ diff --git a/api/current.txt b/api/current.txt index 8d6a135..5a6d065 100644 --- a/api/current.txt +++ b/api/current.txt @@ -4659,7 +4659,9 @@ package android.content { public abstract class AsyncTaskLoader extends android.content.Loader { ctor public AsyncTaskLoader(android.content.Context); method public boolean cancelLoad(); + method protected boolean isLoadInBackgroundCanceled(); method public abstract D loadInBackground(); + method protected void onCancelLoadInBackground(); method public void onCanceled(D); method protected D onLoadInBackground(); method public void setUpdateThrottle(long); @@ -4701,6 +4703,18 @@ package android.content { method public final void setResultExtras(android.os.Bundle); } + public final class CancelationSignal { + ctor public CancelationSignal(); + method public void cancel(); + method public boolean isCanceled(); + method public void setOnCancelListener(android.content.CancelationSignal.OnCancelListener); + method public void throwIfCanceled(); + } + + public static abstract interface CancelationSignal.OnCancelListener { + method public abstract void onCancel(); + } + public class ClipData implements android.os.Parcelable { ctor public ClipData(java.lang.CharSequence, java.lang.String[], android.content.ClipData.Item); ctor public ClipData(android.content.ClipDescription, android.content.ClipData.Item); @@ -4820,6 +4834,7 @@ package android.content { method public android.os.ParcelFileDescriptor openPipeHelper(android.net.Uri, java.lang.String, android.os.Bundle, T, android.content.ContentProvider.PipeDataWriter<T>) throws java.io.FileNotFoundException; method public android.content.res.AssetFileDescriptor openTypedAssetFile(android.net.Uri, java.lang.String, android.os.Bundle) throws java.io.FileNotFoundException; method public abstract android.database.Cursor query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String); + method public android.database.Cursor query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, android.content.CancelationSignal); method protected final void setPathPermissions(android.content.pm.PathPermission[]); method protected final void setReadPermission(java.lang.String); method protected final void setWritePermission(java.lang.String); @@ -4843,6 +4858,7 @@ package android.content { method public android.os.ParcelFileDescriptor openFile(android.net.Uri, java.lang.String) throws java.io.FileNotFoundException, android.os.RemoteException; method public final android.content.res.AssetFileDescriptor openTypedAssetFileDescriptor(android.net.Uri, java.lang.String, android.os.Bundle) throws java.io.FileNotFoundException, android.os.RemoteException; method public android.database.Cursor query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String) throws android.os.RemoteException; + method public android.database.Cursor query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, android.content.CancelationSignal) throws android.os.RemoteException; method public boolean release(); method public int update(android.net.Uri, android.content.ContentValues, java.lang.String, java.lang.String[]) throws android.os.RemoteException; } @@ -4929,6 +4945,7 @@ package android.content { method public final java.io.OutputStream openOutputStream(android.net.Uri, java.lang.String) throws java.io.FileNotFoundException; method public final android.content.res.AssetFileDescriptor openTypedAssetFileDescriptor(android.net.Uri, java.lang.String, android.os.Bundle) throws java.io.FileNotFoundException; method public final android.database.Cursor query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String); + method public final android.database.Cursor query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, android.content.CancelationSignal); method public final void registerContentObserver(android.net.Uri, boolean, android.database.ContentObserver); method public static void removePeriodicSync(android.accounts.Account, java.lang.String, android.os.Bundle); method public static void removeStatusChangeListener(java.lang.Object); @@ -5777,6 +5794,11 @@ package android.content { method public int getNumSuccessfulYieldPoints(); } + public class OperationCanceledException extends java.lang.RuntimeException { + ctor public OperationCanceledException(); + ctor public OperationCanceledException(java.lang.String); + } + public class PeriodicSync implements android.os.Parcelable { ctor public PeriodicSync(android.accounts.Account, java.lang.String, android.os.Bundle, long); method public int describeContents(); @@ -7221,11 +7243,15 @@ package android.database.sqlite { method public static android.database.sqlite.SQLiteDatabase openOrCreateDatabase(java.lang.String, android.database.sqlite.SQLiteDatabase.CursorFactory); method public static android.database.sqlite.SQLiteDatabase openOrCreateDatabase(java.lang.String, android.database.sqlite.SQLiteDatabase.CursorFactory, android.database.DatabaseErrorHandler); method public android.database.Cursor query(boolean, java.lang.String, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String); + method public android.database.Cursor query(boolean, java.lang.String, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String, android.content.CancelationSignal); method public android.database.Cursor query(java.lang.String, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String); method public android.database.Cursor query(java.lang.String, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String); method public android.database.Cursor queryWithFactory(android.database.sqlite.SQLiteDatabase.CursorFactory, boolean, java.lang.String, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String); + method public android.database.Cursor queryWithFactory(android.database.sqlite.SQLiteDatabase.CursorFactory, boolean, java.lang.String, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String, android.content.CancelationSignal); method public android.database.Cursor rawQuery(java.lang.String, java.lang.String[]); + method public android.database.Cursor rawQuery(java.lang.String, java.lang.String[], android.content.CancelationSignal); method public android.database.Cursor rawQueryWithFactory(android.database.sqlite.SQLiteDatabase.CursorFactory, java.lang.String, java.lang.String[], java.lang.String); + method public android.database.Cursor rawQueryWithFactory(android.database.sqlite.SQLiteDatabase.CursorFactory, java.lang.String, java.lang.String[], java.lang.String, android.content.CancelationSignal); method public static int releaseMemory(); method public long replace(java.lang.String, java.lang.String, android.content.ContentValues); method public long replaceOrThrow(java.lang.String, java.lang.String, android.content.ContentValues) throws android.database.SQLException; @@ -7347,6 +7373,7 @@ package android.database.sqlite { method public java.lang.String getTables(); method public android.database.Cursor query(android.database.sqlite.SQLiteDatabase, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String); method public android.database.Cursor query(android.database.sqlite.SQLiteDatabase, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String); + method public android.database.Cursor query(android.database.sqlite.SQLiteDatabase, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String, android.content.CancelationSignal); method public void setCursorFactory(android.database.sqlite.SQLiteDatabase.CursorFactory); method public void setDistinct(boolean); method public void setProjectionMap(java.util.Map<java.lang.String, java.lang.String>); 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) \ diff --git a/media/java/android/media/MediaScanner.java b/media/java/android/media/MediaScanner.java index b06ef95..5cc93ea 100644 --- a/media/java/android/media/MediaScanner.java +++ b/media/java/android/media/MediaScanner.java @@ -1035,7 +1035,7 @@ public class MediaScanner // First read existing files from the files table c = mMediaProvider.query(mFilesUri, FILES_PRESCAN_PROJECTION, - where, selectionArgs, null); + where, selectionArgs, null, null); if (c != null) { mWasEmptyPriorToScan = c.getCount() == 0; @@ -1072,7 +1072,7 @@ public class MediaScanner // compute original size of images mOriginalCount = 0; - c = mMediaProvider.query(mImagesUri, ID_PROJECTION, null, null, null); + c = mMediaProvider.query(mImagesUri, ID_PROJECTION, null, null, null, null); if (c != null) { mOriginalCount = c.getCount(); c.close(); @@ -1107,7 +1107,7 @@ public class MediaScanner new String [] { "_data" }, null, null, - null); + null, null); Log.v(TAG, "pruneDeadThumbnailFiles... " + c); if (c != null && c.moveToFirst()) { do { @@ -1472,7 +1472,7 @@ public class MediaScanner if (bestMatch.mRowId == 0) { Cursor c = mMediaProvider.query(mAudioUri, ID_PROJECTION, MediaStore.Files.FileColumns.DATA + "=?", - new String[] { bestMatch.mPath }, null); + new String[] { bestMatch.mPath }, null, null); if (c != null) { if (c.moveToNext()) { bestMatch.mRowId = c.getLong(0); diff --git a/media/java/android/mtp/MtpDatabase.java b/media/java/android/mtp/MtpDatabase.java index 19db1c0..268c9fc 100755 --- a/media/java/android/mtp/MtpDatabase.java +++ b/media/java/android/mtp/MtpDatabase.java @@ -266,7 +266,7 @@ public class MtpDatabase { Cursor c = null; try { c = mMediaProvider.query(mObjectsUri, ID_PROJECTION, PATH_WHERE, - new String[] { path }, null); + new String[] { path }, null, null); if (c != null && c.getCount() > 0) { Log.w(TAG, "file already exists in beginSendObject: " + path); return -1; @@ -433,7 +433,7 @@ public class MtpDatabase { } } - return mMediaProvider.query(mObjectsUri, ID_PROJECTION, where, whereArgs, null); + return mMediaProvider.query(mObjectsUri, ID_PROJECTION, where, whereArgs, null, null); } private int[] getObjectList(int storageID, int format, int parent) { @@ -699,7 +699,7 @@ public class MtpDatabase { String path = null; String[] whereArgs = new String[] { Integer.toString(handle) }; try { - c = mMediaProvider.query(mObjectsUri, PATH_PROJECTION, ID_WHERE, whereArgs, null); + c = mMediaProvider.query(mObjectsUri, PATH_PROJECTION, ID_WHERE, whereArgs, null, null); if (c != null && c.moveToNext()) { path = c.getString(1); } @@ -815,7 +815,7 @@ public class MtpDatabase { Cursor c = null; try { c = mMediaProvider.query(mObjectsUri, OBJECT_INFO_PROJECTION, - ID_WHERE, new String[] { Integer.toString(handle) }, null); + ID_WHERE, new String[] { Integer.toString(handle) }, null, null); if (c != null && c.moveToNext()) { outStorageFormatParent[0] = c.getInt(1); outStorageFormatParent[1] = c.getInt(2); @@ -858,7 +858,7 @@ public class MtpDatabase { Cursor c = null; try { c = mMediaProvider.query(mObjectsUri, PATH_SIZE_FORMAT_PROJECTION, - ID_WHERE, new String[] { Integer.toString(handle) }, null); + ID_WHERE, new String[] { Integer.toString(handle) }, null, null); if (c != null && c.moveToNext()) { String path = c.getString(1); path.getChars(0, path.length(), outFilePath, 0); @@ -887,7 +887,7 @@ public class MtpDatabase { Cursor c = null; try { c = mMediaProvider.query(mObjectsUri, PATH_SIZE_FORMAT_PROJECTION, - ID_WHERE, new String[] { Integer.toString(handle) }, null); + ID_WHERE, new String[] { Integer.toString(handle) }, null, null); if (c != null && c.moveToNext()) { // don't convert to media path here, since we will be matching // against paths in the database matching /data/media @@ -933,7 +933,7 @@ public class MtpDatabase { Uri uri = Files.getMtpReferencesUri(mVolumeName, handle); Cursor c = null; try { - c = mMediaProvider.query(uri, ID_PROJECTION, null, null, null); + c = mMediaProvider.query(uri, ID_PROJECTION, null, null, null, null); if (c == null) { return null; } diff --git a/media/java/android/mtp/MtpPropertyGroup.java b/media/java/android/mtp/MtpPropertyGroup.java index 76c8569..dab5454 100644 --- a/media/java/android/mtp/MtpPropertyGroup.java +++ b/media/java/android/mtp/MtpPropertyGroup.java @@ -191,7 +191,7 @@ class MtpPropertyGroup { // for now we are only reading properties from the "objects" table c = mProvider.query(mUri, new String [] { Files.FileColumns._ID, column }, - ID_WHERE, new String[] { Integer.toString(id) }, null); + ID_WHERE, new String[] { Integer.toString(id) }, null, null); if (c != null && c.moveToNext()) { return c.getString(1); } else { @@ -211,7 +211,7 @@ class MtpPropertyGroup { try { c = mProvider.query(Audio.Media.getContentUri(mVolumeName), new String [] { Files.FileColumns._ID, column }, - ID_WHERE, new String[] { Integer.toString(id) }, null); + ID_WHERE, new String[] { Integer.toString(id) }, null, null); if (c != null && c.moveToNext()) { return c.getString(1); } else { @@ -232,7 +232,7 @@ class MtpPropertyGroup { Uri uri = Audio.Genres.getContentUriForAudioId(mVolumeName, id); c = mProvider.query(uri, new String [] { Files.FileColumns._ID, Audio.GenresColumns.NAME }, - null, null, null); + null, null, null, null); if (c != null && c.moveToNext()) { return c.getString(1); } else { @@ -254,7 +254,7 @@ class MtpPropertyGroup { // for now we are only reading properties from the "objects" table c = mProvider.query(mUri, new String [] { Files.FileColumns._ID, column }, - ID_WHERE, new String[] { Integer.toString(id) }, null); + ID_WHERE, new String[] { Integer.toString(id) }, null, null); if (c != null && c.moveToNext()) { return new Long(c.getLong(1)); } @@ -323,7 +323,7 @@ class MtpPropertyGroup { try { // don't query if not necessary if (depth > 0 || handle == 0xFFFFFFFF || mColumns.length > 1) { - c = mProvider.query(mUri, mColumns, where, whereArgs, null); + c = mProvider.query(mUri, mColumns, where, whereArgs, null, null); if (c == null) { return new MtpPropertyList(0, MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE); } diff --git a/test-runner/src/android/test/mock/MockContentProvider.java b/test-runner/src/android/test/mock/MockContentProvider.java index e0ce322..4ff943e 100644 --- a/test-runner/src/android/test/mock/MockContentProvider.java +++ b/test-runner/src/android/test/mock/MockContentProvider.java @@ -21,6 +21,7 @@ import android.content.ContentProviderOperation; import android.content.ContentProviderResult; import android.content.ContentValues; import android.content.Context; +import android.content.ICancelationSignal; import android.content.IContentProvider; import android.content.OperationApplicationException; import android.content.pm.PathPermission; @@ -92,7 +93,7 @@ public class MockContentProvider extends ContentProvider { @Override public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, - String sortOrder) throws RemoteException { + String sortOrder, ICancelationSignal cancelationSignal) throws RemoteException { return MockContentProvider.this.query(url, projection, selection, selectionArgs, sortOrder); } @@ -124,6 +125,11 @@ public class MockContentProvider extends ContentProvider { throws RemoteException, FileNotFoundException { return MockContentProvider.this.openTypedAssetFile(url, mimeType, opts); } + + @Override + public ICancelationSignal createCancelationSignal() throws RemoteException { + return null; + } } private final InversionIContentProvider mIContentProvider = new InversionIContentProvider(); diff --git a/test-runner/src/android/test/mock/MockIContentProvider.java b/test-runner/src/android/test/mock/MockIContentProvider.java index b7733a4..41bc27d 100644 --- a/test-runner/src/android/test/mock/MockIContentProvider.java +++ b/test-runner/src/android/test/mock/MockIContentProvider.java @@ -21,6 +21,7 @@ import android.content.ContentProviderResult; import android.content.ContentValues; import android.content.EntityIterator; import android.content.IContentProvider; +import android.content.ICancelationSignal; import android.content.res.AssetFileDescriptor; import android.database.Cursor; import android.net.Uri; @@ -72,7 +73,7 @@ public class MockIContentProvider implements IContentProvider { } public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, - String sortOrder) { + String sortOrder, ICancelationSignal cancelationSignal) { throw new UnsupportedOperationException("unimplemented mock method"); } @@ -103,4 +104,9 @@ public class MockIContentProvider implements IContentProvider { throws RemoteException, FileNotFoundException { throw new UnsupportedOperationException("unimplemented mock method"); } + + @Override + public ICancelationSignal createCancelationSignal() throws RemoteException { + throw new UnsupportedOperationException("unimplemented mock method"); + } } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContentProvider.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContentProvider.java index c91a3bf..c64ab65 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContentProvider.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContentProvider.java @@ -19,6 +19,7 @@ package com.android.layoutlib.bridge.android; import android.content.ContentProviderOperation; import android.content.ContentProviderResult; import android.content.ContentValues; +import android.content.ICancelationSignal; import android.content.IContentProvider; import android.content.OperationApplicationException; import android.content.res.AssetFileDescriptor; @@ -90,8 +91,8 @@ public final class BridgeContentProvider implements IContentProvider { } @Override - public Cursor query(Uri arg0, String[] arg1, String arg2, String[] arg3, String arg4) - throws RemoteException { + public Cursor query(Uri arg0, String[] arg1, String arg2, String[] arg3, String arg4, + ICancelationSignal arg5) throws RemoteException { // TODO Auto-generated method stub return null; } @@ -122,4 +123,9 @@ public final class BridgeContentProvider implements IContentProvider { return null; } + @Override + public ICancelationSignal createCancelationSignal() throws RemoteException { + // TODO Auto-generated method stub + return null; + } } |