Back to project page android-memento.
The source code is released under:
Apache License
If you think the Android project android-memento listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.
/* * android-memento-lib https://github.com/twofortyfouram/android-memento * Copyright 2014 two forty four a.m. LLC */*from w ww. j a v a 2 s . c om*/ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ package com.twofortyfouram.memento.provider.sqlite; import com.twofortyfouram.annotation.Slow; import com.twofortyfouram.annotation.Slow.Speed; import com.twofortyfouram.annotation.VisibleForTesting; import com.twofortyfouram.annotation.VisibleForTesting.Visibility; import com.twofortyfouram.assertion.Assertions; import com.twofortyfouram.log.Lumberjack; import com.twofortyfouram.memento.provider.ContentChangeNotificationQueue; import com.twofortyfouram.spackle.util.AndroidSdkVersion; import com.twofortyfouram.spackle.util.ThreadUtil; import net.jcip.annotations.ThreadSafe; import android.annotation.TargetApi; import android.app.SearchManager; import android.content.ContentProvider; import android.content.ContentProviderClient; import android.content.ContentProviderOperation; import android.content.ContentProviderResult; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.OperationApplicationException; import android.content.UriMatcher; import android.content.pm.ProviderInfo; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.os.Build; import android.os.SystemClock; import android.provider.BaseColumns; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import android.text.format.DateUtils; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Locale; import java.util.Map; /** * Assists in creating a first-class SQLite-backed ContentProvider that is DRY, * thread-safe, and bug-free. Typical Android ContentProviders require building a {@link * UriMatcher} * and then repeating the same switch statement within each of query(), insert(), update(), and * delete(). This class takes a different approach: Subclasses return an instance of * {@link SqliteUriMatcher} via {@link #newSqliteUriMatcher()}, which handles all of the * Uri-matching * logic. Not only is this significantly more DRY, but it also separates the responsibilities of * the * ContentProvider from the Uri-matching. Unit testing then becomes easy, because developers only * really need to worry about testing their {@link SqliteUriMatcher} implementation. * <p> * Additional features of this class include: * <ul> * <li>Atomic transactions for {@link #applyBatch(ArrayList)} and * {@link #bulkInsert(Uri, ContentValues[])}</li> * <li>Automatic content change notifications via the {@link android.content.ContentResolver} as * well as via {@link android.content.Intent#ACTION_PROVIDER_CHANGED}. Note that {@link * android.content.Intent#ACTION_PROVIDER_CHANGED} will be secured if the ContentProvider has a * read * permission as per {@link android.content.ContentProvider#getReadPermission()} (path permissions * as per {@link android.content.ContentProvider#getPathPermissions()} are not currently supported * for securing {@link * android.content.Intent#ACTION_PROVIDER_CHANGED} Intents). * If the * provider is not exported, the Intent will be package-only for API 14 and above. Content change * notifications are only sent after a transaction succeeds, so for example no notifications will * be * sent if a call to {@link #applyBatch(java.util.ArrayList)} fails. Also note that content change * notifications will be for the base URI of the item that changed and will not contain /#ID as the * last path segment. Finally note that if the same URI changes more than once * in a batch operation, the multiple duplicate notifications are coalesced.</li> * <li>Support for the query parameter {@link SearchManager#SUGGEST_PARAMETER_LIMIT}</li> * <li>Support for {@link BaseColumns#_COUNT} queries</li> * <li>Enhanced security by using projection maps for queries</li> * </ul> * <p>An example implementation can be found in the debug build target of the library.</p> */ @ThreadSafe public abstract class AbstractSqliteContentProvider extends ContentProvider { /** * Debug flag to slow down ContentProvider methods. The primary purpose is to make it easier to * test asynchronous operations, such as Loaders in a UI. * <p> * This is a debug feature only and MUST be disabled in release builds. * * @see #SLOW_ACCESS_DELAY_MILLISECONDS */ @VisibleForTesting(Visibility.PRIVATE) /* package */ static final boolean IS_SLOW_ACCESS_ENABLED = false; /** * Time in milliseconds to delay access. * * @see #IS_SLOW_ACCESS_ENABLED */ private static final long SLOW_ACCESS_DELAY_MILLISECONDS = 2 * DateUtils.SECOND_IN_MILLIS; /** * Projection map for {@link BaseColumns#_COUNT}. */ @NonNull private static final Map<String, String> COUNT_PROJECTION_MAP = Collections .unmodifiableMap(getCountProjectionMap()); /** * @return Projection map for {@link BaseColumns#_COUNT}. */ @NonNull private static HashMap<String, String> getCountProjectionMap() { final HashMap<String, String> temp = new HashMap<String, String>(); temp.put(BaseColumns._COUNT, String.format(Locale.US, "COUNT(*) AS %s", BaseColumns._COUNT)); //$NON-NLS-1$ return temp; } /** * Helper to open the database. * <p> * This field will be initialized in {@link #onCreate()}. */ @Nullable private volatile SQLiteOpenHelper mSqliteOpenHelper = null; /** * Matcher for {@code Uri}s. * <p> * This field will be initialized in {@link #onCreate()}. */ @Nullable private volatile SqliteUriMatcher mSqliteUriMatcher = null; /** * Thread-specific container for operation results. */ @NonNull private final ThreadLocal<ContentChangeNotificationQueue> mThreadLocalContentChangeNotificationQueue = new ThreadLocal<ContentChangeNotificationQueue>(); /** * Flag indicating whether the ContentProvider is exported. */ private volatile boolean mIsExported = false; /** * Read permission for the provider. May be null if there is no read permission. */ @Nullable private volatile String mReadPermission = null; @Override public boolean onCreate() { /* * Initialize logging here because the ContentProvider is created before the Application * object. It is OK for the Application object to also call Lumberjack.init(Context). * * Only do this when running on the main thread. Although this should always be called on * the main thread by the Android framework, that won't be the case when run in a unit test. */ if (ThreadUtil.isMainThread()) { Lumberjack.init(getContext()); } /* * These fields are volatile, which therefore makes their initialization here in onCreate() * thread-safe. No additional synchronization is required because onCreate() is guaranteed * to be called by the Android framework before any of the other methods that need these fields. */ mSqliteOpenHelper = newSqliteOpenHelper(); mSqliteUriMatcher = newSqliteUriMatcher(); return true; } @Override public void attachInfo(final Context context, final ProviderInfo info) { super.attachInfo(context, info); // info should only be null during unit tests if (null != info) { /* * These fields are volatile, which therefore makes their initialization here in attachInfo() * thread-safe. No additional synchronization is required because attachInfo() is guaranteed * to be called by the Android framework before any of the other methods that need these fields. */ mIsExported = info.exported; mReadPermission = info.readPermission; } } @Override @TargetApi(Build.VERSION_CODES.HONEYCOMB) public void shutdown() { mSqliteOpenHelper.close(); if (AndroidSdkVersion.isAtLeastSdk(Build.VERSION_CODES.HONEYCOMB)) { super.shutdown(); } } @Override public void onLowMemory() { super.onLowMemory(); final int bytesReleased = SQLiteDatabase.releaseMemory(); Lumberjack.v("Released %d bytes of memory", bytesReleased); //$NON-NLS-1$ } @Override public String getType(final Uri uri) { try { final SqliteUriMatch match = mSqliteUriMatcher.match(uri); return match.getMimeType(); } catch (IllegalArgumentException e) { return null; } } @Override @Slow(Speed.MILLISECONDS) public int delete(final Uri uri, final String selection, final String[] selectionArgs) { assertNotNull(uri, "uri"); //$NON-NLS-1$ checkSlowAccess(); Lumberjack.v("URI: %s, selection: %s, selectionArgs: %s", uri, selection, //$NON-NLS-1$ selectionArgs); final SQLiteDatabase database = mSqliteOpenHelper.getWritableDatabase(); int count = 0; final SqliteUriMatch match = mSqliteUriMatcher.match(uri); if (match.isIdUri()) { final String tableName = match.getTableName(); final String segment = uri.getLastPathSegment(); final String idSelectionArg = newAndIdSelection(selection); final String[] idSelectionArgs = newAndIdSelectionArgs(segment, selectionArgs); count = database.delete(tableName, idSelectionArg, idSelectionArgs); } else { if (null == selection) { count = database.delete(match.getTableName(), "1", null); //$NON-NLS-1$ } else { count = database.delete(match.getTableName(), selection, selectionArgs); } } Lumberjack.v("%s rows actually deleted", count); //$NON-NLS-1$ if (0 < count) { getContentChangeNotificationQueue().onContentChanged(match.getNotifyUris()); } return count; } @Override @Slow(Speed.MILLISECONDS) public Uri insert(final Uri uri, final ContentValues values) { assertNotNull(uri, "uri"); //$NON-NLS-1$ checkSlowAccess(); Lumberjack.v("URI: %s, values: %s", uri, values); //$NON-NLS-1$ final SQLiteDatabase database = mSqliteOpenHelper.getWritableDatabase(); final SqliteUriMatch match = mSqliteUriMatcher.match(uri); final ContentValues valuesToInsert; if (match.isIdUri()) { // Make a copy to avoid mutating the input parameter valuesToInsert = new ContentValues(values.size() + 1); valuesToInsert.putAll(values); valuesToInsert.put(BaseColumns._ID, uri.getLastPathSegment()); } else { valuesToInsert = values; } Uri resultUri = null; final long rowID = database.insertOrThrow(match.getTableName(), null, valuesToInsert); if (-1 != rowID) { resultUri = ContentUris.withAppendedId(match.getBaseUri(), rowID); } if (null != resultUri) { getContentChangeNotificationQueue().onContentChanged(match.getNotifyUris()); } return resultUri; } /** * {@inheritDoc} * <p> * This implementation guarantees that bulk inserts are fully atomic. */ @Override @Slow(Speed.MILLISECONDS) public int bulkInsert(@NonNull final Uri uri, @NonNull final ContentValues[] values) { assertNotNull(uri, "uri"); //$NON-NLS-1$ assertNotNull(values, "values"); //$NON-NLS-1$ Lumberjack.v("URI: %s, values: %s", uri, values); //$NON-NLS-1$ final SQLiteDatabase database = mSqliteOpenHelper.getWritableDatabase(); final ContentChangeNotificationQueue contentChangeNotificationQueue = getContentChangeNotificationQueue(); int count = 0; if (contentChangeNotificationQueue.isBatch()) { count = super.bulkInsert(uri, values); } else { boolean isSuccess = false; contentChangeNotificationQueue.startBatch(); database.beginTransaction(); try { count = super.bulkInsert(uri, values); database.setTransactionSuccessful(); isSuccess = true; } finally { database.endTransaction(); contentChangeNotificationQueue.endBatch(isSuccess); } } return count; } /** * {@inheritDoc} * <p> * This method supports {@link SearchManager#SUGGEST_PARAMETER_LIMIT} and queries with a * projection consisting only of {@link android.provider.BaseColumns#_COUNT}.</p> * <p> * Note: The order of columns in the returned cursor are not guaranteed to be consistent or * guaranteed to match the order of the columns in {@code projection}. * </p> */ @Override @Slow(Speed.MILLISECONDS) public Cursor query(final Uri uri, final String[] projection, final String selection, final String[] selectionArgs, final String sortOrder) { assertNotNull(uri, "uri"); //$NON-NLS-1$ checkSlowAccess(); Lumberjack .v("URI: %s, projection: %s, selection: %s, selectionArgs: %s, sortOrder: %s", uri, //$NON-NLS-1$ projection, selection, selectionArgs, sortOrder); final SQLiteDatabase database = mSqliteOpenHelper.getWritableDatabase(); Cursor result = null; final SqliteUriMatch match = mSqliteUriMatcher.match(uri); final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); if (AndroidSdkVersion.isAtLeastSdk(Build.VERSION_CODES.ICE_CREAM_SANDWICH)) { setStrictIcs(qb); } qb.setProjectionMap(match.getProjectionMap()); if (null != projection && 1 == projection.length && BaseColumns._COUNT.equals(projection[0])) { qb.setProjectionMap(COUNT_PROJECTION_MAP); } qb.setTables(match.getTableName()); final String idSelectionArg; final String[] idSelectionArgs; if (match.isIdUri()) { final String segment = uri.getLastPathSegment(); idSelectionArg = newAndIdSelection(selection); idSelectionArgs = newAndIdSelectionArgs(segment, selectionArgs); } else { idSelectionArg = selection; idSelectionArgs = selectionArgs; } result = qb.query(database, projection, idSelectionArg, idSelectionArgs, null, null, sortOrder, uri.getQueryParameter(SearchManager.SUGGEST_PARAMETER_LIMIT)); result.setNotificationUri(getContext().getContentResolver(), uri); return result; } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) private static void setStrictIcs(@NonNull final SQLiteQueryBuilder builder) { Assertions.assertNotNull(builder, "builder"); //$NON-NLS-1$ builder.setStrict(true); } @Override @Slow(Speed.MILLISECONDS) public int update(final Uri uri, final ContentValues values, @Nullable final String selection, @NonNull final String[] selectionArgs) { assertNotNull(uri, "uri"); //$NON-NLS-1$ checkSlowAccess(); Lumberjack .v("URI: %s, values: %s, selection: %s, selectionArgs: %s", uri, values, selection, //$NON-NLS-1$ selectionArgs); final SQLiteDatabase database = mSqliteOpenHelper.getWritableDatabase(); int count = 0; final SqliteUriMatch match = mSqliteUriMatcher.match(uri); final String idSelectionArg; final String[] idSelectionArgs; if (match.isIdUri()) { final String segment = uri.getLastPathSegment(); idSelectionArg = newAndIdSelection(selection); idSelectionArgs = newAndIdSelectionArgs(segment, selectionArgs); } else { idSelectionArg = selection; idSelectionArgs = selectionArgs; } count = database.update(match.getTableName(), values, idSelectionArg, idSelectionArgs); Lumberjack.v("%s rows updated", count); //$NON-NLS-1$ if (0 < count) { getContentChangeNotificationQueue().onContentChanged(match.getNotifyUris()); } return count; } /** * {@inheritDoc} * <p> * This implementation guarantees that batch operations are fully atomic. */ @Override @Slow(Speed.MILLISECONDS) public ContentProviderResult[] applyBatch( @NonNull final ArrayList<ContentProviderOperation> operations) throws OperationApplicationException { final SQLiteDatabase database = mSqliteOpenHelper.getWritableDatabase(); final ContentChangeNotificationQueue contentChangeNotificationQueue = getContentChangeNotificationQueue(); ContentProviderResult[] result = null; if (contentChangeNotificationQueue.isBatch()) { result = super.applyBatch(operations); } else { boolean isSuccessful = false; contentChangeNotificationQueue.startBatch(); database.beginTransaction(); try { result = super.applyBatch(operations); database.setTransactionSuccessful(); isSuccessful = true; } finally { database.endTransaction(); contentChangeNotificationQueue.endBatch(isSuccessful); } } return result; } /** * When an ID is appended to the base URI, this method is used to format a new where clause for * {@code _id = ? AND (selection)}. This new where clause is intended to be used in * conjunction * with the selection arguments generated by {@link #newAndIdSelectionArgs(String, String[])}. * * @param selection Where clause to select. * @return Formatted where clause with the ID selection and the where cause combined. Be sure * to use {@link #newAndIdSelectionArgs(String, String[])} for the args. * @see #newAndIdSelectionArgs(String, String[]) */ @NonNull @VisibleForTesting(Visibility.PRIVATE) /*package*/ static String newAndIdSelection(@Nullable final String selection) { if (!TextUtils.isEmpty(selection)) { return String.format(Locale.US, "%s = ? AND (%s)", BaseColumns._ID, //$NON-NLS-1$ selection); } return String.format(Locale.US, "%s = ?", BaseColumns._ID); //$NON-NLS-1$ } /** * @param id ID of the element to select. * @param args Selection arguments. * @return New selection arguments including the {@code id}. */ @NonNull @VisibleForTesting(Visibility.PRIVATE) /*package*/ static String[] newAndIdSelectionArgs(@NonNull final String id, @Nullable final String[] args) { Assertions.assertNotEmpty(id, "id"); //$NON-NLS-1$ if (null == args || 0 == args.length) { return new String[]{id}; } final String[] result = new String[args.length + 1]; result[0] = id; System.arraycopy(args, 0, result, 1, args.length); return result; } /** * A helper method to simplify the usage of {@link #runInTransaction(Runnable)}. * * @param context Application context. * @param contentAuthority Authority of the Content Provider. * @param runnable Runnable to execute. * @throws java.lang.IllegalArgumentException If provider for {@code contentAuthority} is * not found. * @throws java.lang.UnsupportedOperationException If provider for {@code contentAuthority} is * not an * instance of {@link com.twofortyfouram.memento.provider.sqlite.AbstractSqliteContentProvider}. * @see #runInTransaction(Runnable) */ public static void runInTransaction(@NonNull final Context context, @NonNull final String contentAuthority, @NonNull final Runnable runnable) { Assertions.assertNotNull(context, "context"); //$NON-NLS-1$ Assertions.assertNotNull(contentAuthority, "contentAuthority"); //$NON-NLS-1$ Assertions.assertNotNull(runnable, "runnable"); //$NON-NLS-1$ ContentProviderClient client = null; try { client = context.getContentResolver().acquireContentProviderClient(contentAuthority); final ContentProvider provider = client.getLocalContentProvider(); if (null == provider) { throw new IllegalArgumentException( Lumberjack.formatMessage("No provider found for authority %s", //$NON-NLS-1$ contentAuthority) ); } if (!(provider instanceof AbstractSqliteContentProvider)) { throw new UnsupportedOperationException(Lumberjack .formatMessage("Provider with authority %s is not an instance of %s", //$NON-NLS-1$ contentAuthority, AbstractSqliteContentProvider.class.getName())); } final AbstractSqliteContentProvider abstractSqliteContentProvider = (AbstractSqliteContentProvider) client.getLocalContentProvider(); abstractSqliteContentProvider.runInTransaction(runnable); } finally { if (null != client) { client.release(); client = null; } } } /** * Runs a block of code inside a single atomic transaction. For the most part, multiple * operations should be performed using {@link #applyBatch(ArrayList)}. In certain cases where * that isn't possible (for example, queries), this method can be used. * <p> * The work done inside {@code runnable} should only be database operations to minimize the * amount of time the database is blocked. * * @param runnable to execute inside the database transaction. */ public void runInTransaction(@NonNull final Runnable runnable) { Assertions.assertNotNull(runnable, "runnable"); //$NON-NLS-1$ final SQLiteDatabase database = mSqliteOpenHelper.getWritableDatabase(); final ContentChangeNotificationQueue contentChangeNotificationQueue = getContentChangeNotificationQueue(); if (contentChangeNotificationQueue.isBatch()) { runnable.run(); } else { boolean isSuccessful = false; contentChangeNotificationQueue.startBatch(); database.beginTransaction(); try { runnable.run(); database.setTransactionSuccessful(); isSuccessful = true; } finally { database.endTransaction(); contentChangeNotificationQueue.endBatch(isSuccessful); } } } /** * @return Gets a {@link ContentChangeNotificationQueue} for the current thread. */ @NonNull private ContentChangeNotificationQueue getContentChangeNotificationQueue() { ContentChangeNotificationQueue queue = mThreadLocalContentChangeNotificationQueue.get(); if (null == queue) { queue = new ContentChangeNotificationQueue(getContext(), mIsExported, mReadPermission); mThreadLocalContentChangeNotificationQueue.set(queue); } return queue; } private static void checkSlowAccess() { if (IS_SLOW_ACCESS_ENABLED) { SystemClock.sleep(SLOW_ACCESS_DELAY_MILLISECONDS); } } /** * Unlike {@link Assertions#assertNotNull(Object, String)}, this method throws * {@link NullPointerException}. This is necessary, because ContentProviders are allowed to * throw {@link NullPointerException} across process boundaries as <a href= * "http://developer.android.com/guide/topics/providers/content-provider-creating.html#Query" * >documented</a>. * * @param object Object to check for being null. * @param name Name of the object for human-readable exceptions. * @throws NullPointerException If {@code object} is null. */ private static void assertNotNull(@Nullable final Object object, @Nullable final String name) { if (null == object) { throw new NullPointerException( Lumberjack.formatMessage("%s cannot be null", name)); //$NON-NLS-1$ } } /** * @return An {@link SqliteUriMatcher} appropriate for the current ContentProvider. The object * returned by this method must be thread-safe. */ @NonNull protected abstract SqliteUriMatcher newSqliteUriMatcher(); /** * @return A {@link SQLiteOpenHelper} appropriate for the current ContentProvider. The object * returned by this method must be thread-safe. */ @NonNull protected abstract SQLiteOpenHelper newSqliteOpenHelper(); }