Android Open Source - android-memento Abstract Sqlite Content Provider






From Project

Back to project page android-memento.

License

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.

Java Source Code

/*
 * 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();
}




Java Source Code List

com.twofortyfouram.memento.debug.provider.SqliteContentProviderImpl.java
com.twofortyfouram.memento.debug.provider.SqliteOpenHelperImpl.java
com.twofortyfouram.memento.debug.provider.SqliteUriMatcherImpl.java
com.twofortyfouram.memento.debug.provider.TableOneContract.java
com.twofortyfouram.memento.provider.ContentChangeNotificationQueueTest.java
com.twofortyfouram.memento.provider.ContentChangeNotificationQueue.java
com.twofortyfouram.memento.provider.ContentProviderOperationServiceTest.java
com.twofortyfouram.memento.provider.ContentProviderOperationService.java
com.twofortyfouram.memento.provider.ContentProviderUtilTest.java
com.twofortyfouram.memento.provider.ContentProviderUtil.java
com.twofortyfouram.memento.provider.ImmutableUriMatcherTest.java
com.twofortyfouram.memento.provider.ImmutableUriMatcher.java
com.twofortyfouram.memento.provider.sqlite.AbstractSqliteContentProviderIntegrationTest.java
com.twofortyfouram.memento.provider.sqlite.AbstractSqliteContentProviderTest.java
com.twofortyfouram.memento.provider.sqlite.AbstractSqliteContentProvider.java
com.twofortyfouram.memento.provider.sqlite.SqliteColumnBuilderTest.java
com.twofortyfouram.memento.provider.sqlite.SqliteColumnBuilder.java
com.twofortyfouram.memento.provider.sqlite.SqliteIndexBuilderTest.java
com.twofortyfouram.memento.provider.sqlite.SqliteIndexBuilder.java
com.twofortyfouram.memento.provider.sqlite.SqliteOpenHelperCompat.java
com.twofortyfouram.memento.provider.sqlite.SqliteStorageClass.java
com.twofortyfouram.memento.provider.sqlite.SqliteTableBuilderTest.java
com.twofortyfouram.memento.provider.sqlite.SqliteTableBuilder.java
com.twofortyfouram.memento.provider.sqlite.SqliteUriMatchTest.java
com.twofortyfouram.memento.provider.sqlite.SqliteUriMatch.java
com.twofortyfouram.memento.provider.sqlite.SqliteUriMatcher.java