Giter Club home page Giter Club logo

contentpal's Introduction

Build Status codecov

ContentPal

A friend to help with Android ContentProvider operations.

Rationale

Android uses the concept of "ContentProviders" to share data among apps quite successfully. While the concept behind these ContentProviders is quite powerful it can be a real hassle to work with. Reading and writing data can be a a challenging task. The resulting code is easy to get wrong and often quite verbose and hard to read. Using the efficient batch operations is non trivial as well, especially when working with "related" rows.

This library aims to add an abstraction layer to ContentProviders to handle operations in an object oriented manner. With ContentPal, ContentProvider operations are written in a declarative way before being enqueued for aggregated execution.
The resulting code is easier to write and read.

Note that this library is not meant to be an ORM and uses database terminology all over the place.

Note well

The interfaces of this library are not considered 100% stable at this time. Design and names are still subject to change.

Goals

This library has been created with specific goals in mind.

  • reduce boilerplate when working with content providers
  • provide a certain level of type safety, i.e. reduce the risk of writing data to the wrong table
  • declarative way to describe operations
  • utilize IPC limit as good as possible in order to make big transactions efficient
  • automatically resolve references when inserting related rows in a single transaction

ContactsPal

ContactsPal provides ContentProvider specific classes including some of the most important Table and RowData implementations like RawContacts and PhoneData.

CalendarPal

CalendarPal provides CalendarProvider specific classes including some of the most important Table and RowData implementations like Events and ReminderData.

Example

Insert a Contact

Without ContactsPal

The following example is taken from Android's ContactsProvider Documentation. It shows the basic steps to create a new contact with display name, phone and email.

ArrayList<ContentProviderOperation> ops =
    new ArrayList<ContentProviderOperation>();

ContentProviderOperation.Builder op =
    ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
    .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, mSelectedAccount.getType())
    .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, mSelectedAccount.getName());
ops.add(op.build());

op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
    .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
    .withValue(ContactsContract.Data.MIMETYPE,
        ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
    .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name);
ops.add(op.build());

op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
    .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
    .withValue(ContactsContract.Data.MIMETYPE,
        ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
    .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phone)
    .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, phoneType);
ops.add(op.build());

op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
    .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
    .withValue(ContactsContract.Data.MIMETYPE,
        ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
    .withValue(ContactsContract.CommonDataKinds.Email.ADDRESS, email)
    .withValue(ContactsContract.CommonDataKinds.Email.TYPE, emailType);
op.withYieldAllowed(true);
ops.add(op.build());

getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);

With ContactsPal

The following code snippets show the same operation with ContentPal/ContactsPal:

Step 1 - setup

Before you can perform any operations you need to create a few required objects like the tables to work with. Usually this has to be done only once.

ContentProviderClient client =
  getContentResolver().acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
OperationsQueue operationsQueue = new BasicOperationsQueue(client);
Step 2 - execute

Create an OperationsBatch which contains the operations to insert the RawContact and the data rows and enqueue them for execution.

operationsQueue.enqueue(
  new Yieldable( // optional, not required when inserting only one contact
    new InsertRawContactBatch(
      account,
      // list the data to be inserted
      new DisplayNameData(name),
      new Typed(phoneType, new PhoneData(phone)),
      new Typed(emailType, new EmailData(email))))));

At this point you can enqueue more operations. All operations will be executed automatically when the transaction size grows too large or when flush() is called.

References between the inserted rows will be resolved automatically.

Also note that all relevant types are generic having the contract they implement as the generic type. That makes it much harder to get confused. E.g. you can not accidentally try to insert an email address into RawContacts.

Step 3 - shut down

Once everything is done, just make sure all pending operations get committed and close the ContentProviderClient

operationsQueue.flush();
client.release();

Reading Contacts

To read contacts you build a view onto the Table to read and iterate over a RowSet that contains the rows you need.

// a sync-adapter view onto RawContacts, scoped to a specific account
View<ContactsContract.RawContacts> rawContacts =
  new Synced<>(new AccountScoped<>(new RawContacts(client), account));
    
// iterate over all "dirty" contacts 
for (RowSnapshot<ContactsContract.RawContacts> rowSnapshot:new Dirty<>(rawContacts))
{
  // work with the row snapshot
   
  // for instance, set the dirty flag to 0 like so
  operationsQueue.enqueue(
    new SingletonOperationsBatch(
      new new Put<>(rowSnapshot, new CleanData())));
}

The same can also be achieved in a declarative way like this:

operationsQueue.enqueue(
  new MappedRowSetBatch<>(
    // map all dirty rows of a specific account
    new Dirty<>(new Synced<>(new AccountScoped<>(new RawContacts(client), account))),
    new Function<ContactsContract.RawContacts, Operation<>>()
    {
      public Operation<> apply(RowSnapshot<ContactsContract.RawContacts> rowSnapwhot)
      {
        // work with the row and return an Operation, can be a NoOp or this:
        return new Put<>(rowSnapshot, new CleanData());
      }
    }
  )
);

License

Copyright dmfs GmbH 2017, licensed under Apache2.

contentpal's People

Contributors

dmfs avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

morristech

contentpal's Issues

Allow passing a TransactionContext to a query

In order to reference virtual rows in a query predicate the query needs access to the TransactionContext. This will allow to query tables scoped to "virtual rows" which have just been committed earlier.

CalendarScoped Predicate decorator

The code below is repeated at multiple places, we can create a Predicate delegating class for it:

new AllOf(predicate, new EqArg(CalendarContract.Events.CALENDAR_ID, mCalendarRow.values().charData(CalendarContract.Calendars._ID).value("-1")))

`contentpal-testing` module with test utilities

#76 Adds a couple of useful utilities to test ContentPal and implementations based on ContentPal like ContactsPal and CalendarPal. In order to make these utilities available to these implementations we need a separate contentpal-testing module which can be imported as a test dependency.
In general there are two obvious ways:

  • create a contentpal-testing module which depends on contentpal and add it as a testCompile dependency to contentpal. Though this creates a circular reference which needs to be resolved (possibly be limiting the dependency on a specific source set or something like this). The Android Gradle plugin doesn't seem to be able to resolve this automatically (unlike the Java plugin).

  • outsource the contentpal interfaces to a module like contentpal-interfaces and create a contentpal-testing module which depends on this module. This works well as long as the test utilities don't use any contentpal classes (which they probably shouldn't do anyway).

OpenTasksPal

In order to access the OpenTasks ContentProvider, as a developer I want an OpenTasksPal which provides abstractions for the OpenTasksContract.

Bulk read rawcontact data

As a developer I want to be able to read the data rows of multiple RawContacts in a convenient manner, e.g. by declaring an Iterable like so:

Iterable<RowSet<Data>> rawContacts = new BulkRawContactDataRows(dataView, new AccountEq(account));

Where rawContact would iterate RowSets of all RawContacts in the given account.

RowData to Predicate adapter

For testing purposes it can be useful to have a Predicate which takes RowData and matches all rows which contain the given RowData.

Usage:

new QueryRowSet(dataTable, new RowDataPredicate(rowData));

This would return all rows which contain the given rowData.

The question is, is that even possible at all? RowData is added to a ContentProviderOperation.Builder and requires a TransactionContext to do so. We could certainly subclass the Builder so it builds the actual predicate instead and provide an EmptyTransactionContext but that means you can not select by ids of virtual rows (because you need the TransactionContext). In most cases that's not an issue but in some edge cases it is.

Add default projections for Views in CalendarPal

It's known that some devices cause problems when trying to get all columns of a calendar table by passing null as the projection. We need to provide defaults for regular apps and for sync-adapters.

DelegatingPredicate

Abstract base class for Predicates which only need to compose a Predicate in the constructor.

Read single rawcontact data

As a developer I want to read the data rows of a RawContact in a convenient manner, e.g. by declaring a RowSet like so

RowSet<Data> dataRows = new RawContactDataRows(dataView, new RowUriReference<RawContacts>(rawContactUri));

which represents the set of all data rows of the given RawContact.

Simple OperationsQueue

Sometimes the use case doesn't justify the boilerplate code required to create and close a ContentProviderClient in order to use a BasicOperationsQueue. This is especially true if you just want to execute a single OperationsBatch.

What we need is another simple OperationsQueue which just executes every OperationsBatch instantly using ContentResolver.applyBatch(…). This is also useful when using assert operations, because the lazyness of BasicOperationsQueue makes it unsuitable for assert operations. You wouldn't know which OperationsBatch actually caused an error and some operations might have been executed, while everything after the erroneous batch would be "lost".

java.lang.IllegalArgumentException: Unable to resolve virtual RowReference

Caused by: java.lang.IllegalArgumentException: Unable to resolve virtual RowReference org.dmfs.android.contentpal.references.VirtualRowReference@ae1645f
 at org.dmfs.android.contentpal.transactions.contexts.EmptyTransactionContext.resolved(EmptyTransactionContext.java:44)
 at org.dmfs.android.contentpal.transactions.contexts.Quick.resolved(Quick.java:50)
 at org.dmfs.android.contentpal.operations.Put.contentOperationBuilder(Put.java:68)
 at org.dmfs.android.contentpal.transactions.BaseTransaction.with(BaseTransaction.java:105)
 at org.dmfs.android.contentpal.queues.BasicOperationsQueue.enqueue(BasicOperationsQueue.java:57)

CalendarPal

In order to access the CalendarProvider, as a developer I want a "CalendaPal" which provides abstractions for calendar and event tables.

Special `BatchOperation` to insert raw contacts.

As a developer I want to be able to insert new contacts even easier like so

new RawContactInsertBatch<>(
 AccountScoped(account, new RawContacts()),
 new DisplayNameData("John Doe"),
 new Primary(new Typed(ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE, new PhoneData("123"))),
 new Custom("personal", new EmailData("[email protected]")))));
 new NoteData("A note"),
 new WorkPostal(
    new StreetData("Some street"),
    new PostcodeData("0000"),
    new CityData("Some City"),
    new CountryData("Some country")));

The only thing which needs to be provided to insert a new RawContact is the table and the data, hence we can reduce this to a single batch.

`AssertAndPut` OperationsBatch

Sometimes it might make sense to have a "Compare & Swap"-like operation like so:

public final class AssertAndPut<T> extends DelegatingOperationsBatch
{
    public AssertAndPut(RowSnapshot<T> row, RowData<T> assertion, RowData<T> values)
    {
       super(
             new Assert(row, assertion),
             new Put(row, values)
       );
    }
}

Add Table to write Groups

In order to create custom contact Groups, as a developer I need a Table and specialized RowData` to create entries.

upgrade to jems 1.8

upgrade iterators and optional dependencies to 1.8 (introduce a shared Gradle property).
also replace all Collections.<>emptyList() with EmptyIterables which had been added recently.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.