blob: 58a2a05b892e5d158142231c89c8a5a0c9781d3b [file] [log] [blame]
/*
* Copyright (C) 2009 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 com.android.providers.contacts;
import com.android.providers.contacts.ContactsDatabaseHelper.ActivitiesColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.PackagesColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.provider.BaseColumns;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.RawContacts;
import android.provider.SocialContract;
import android.provider.SocialContract.Activities;
import android.net.Uri;
import java.util.ArrayList;
import java.util.HashMap;
/**
* Social activity content provider. The contract between this provider and
* applications is defined in {@link SocialContract}.
*/
public class SocialProvider extends ContentProvider {
// TODO: clean up debug tag
private static final String TAG = "SocialProvider ~~~~";
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private static final int ACTIVITIES = 1000;
private static final int ACTIVITIES_ID = 1001;
private static final int ACTIVITIES_AUTHORED_BY = 1002;
private static final int CONTACT_STATUS_ID = 3000;
private static final String DEFAULT_SORT_ORDER = Activities.THREAD_PUBLISHED + " DESC, "
+ Activities.PUBLISHED + " ASC";
/** Contains just the contacts columns */
private static final HashMap<String, String> sContactsProjectionMap;
/** Contains just the contacts columns */
private static final HashMap<String, String> sRawContactsProjectionMap;
/** Contains just the activities columns */
private static final HashMap<String, String> sActivitiesProjectionMap;
/** Contains the activities, raw contacts, and contacts columns, for joined tables */
private static final HashMap<String, String> sActivitiesContactsProjectionMap;
static {
// Contacts URI matching table
final UriMatcher matcher = sUriMatcher;
matcher.addURI(SocialContract.AUTHORITY, "activities", ACTIVITIES);
matcher.addURI(SocialContract.AUTHORITY, "activities/#", ACTIVITIES_ID);
matcher.addURI(SocialContract.AUTHORITY, "activities/authored_by/#", ACTIVITIES_AUTHORED_BY);
matcher.addURI(SocialContract.AUTHORITY, "contact_status/#", CONTACT_STATUS_ID);
HashMap<String, String> columns;
// Contacts projection map
columns = new HashMap<String, String>();
// TODO: fix display name reference (in fact, use the contacts view instead of the table)
columns.put(Contacts.DISPLAY_NAME, "contact." + Contacts.DISPLAY_NAME + " AS "
+ Contacts.DISPLAY_NAME);
sContactsProjectionMap = columns;
// Contacts projection map
columns = new HashMap<String, String>();
columns.put(RawContacts._ID, Tables.RAW_CONTACTS + "." + RawContacts._ID + " AS _id");
columns.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
sRawContactsProjectionMap = columns;
// Activities projection map
columns = new HashMap<String, String>();
columns.put(Activities._ID, "activities._id AS _id");
columns.put(Activities.RES_PACKAGE, PackagesColumns.PACKAGE + " AS "
+ Activities.RES_PACKAGE);
columns.put(Activities.MIMETYPE, Activities.MIMETYPE);
columns.put(Activities.RAW_ID, Activities.RAW_ID);
columns.put(Activities.IN_REPLY_TO, Activities.IN_REPLY_TO);
columns.put(Activities.AUTHOR_CONTACT_ID, Activities.AUTHOR_CONTACT_ID);
columns.put(Activities.TARGET_CONTACT_ID, Activities.TARGET_CONTACT_ID);
columns.put(Activities.PUBLISHED, Activities.PUBLISHED);
columns.put(Activities.THREAD_PUBLISHED, Activities.THREAD_PUBLISHED);
columns.put(Activities.TITLE, Activities.TITLE);
columns.put(Activities.SUMMARY, Activities.SUMMARY);
columns.put(Activities.LINK, Activities.LINK);
columns.put(Activities.THUMBNAIL, Activities.THUMBNAIL);
sActivitiesProjectionMap = columns;
// Activities, raw contacts, and contacts projection map for joins
columns = new HashMap<String, String>();
columns.putAll(sContactsProjectionMap);
columns.putAll(sRawContactsProjectionMap);
columns.putAll(sActivitiesProjectionMap); // Final _id will be from Activities
sActivitiesContactsProjectionMap = columns;
}
private ContactsDatabaseHelper mDbHelper;
/** {@inheritDoc} */
@Override
public boolean onCreate() {
final Context context = getContext();
mDbHelper = ContactsDatabaseHelper.getInstance(context);
return true;
}
/**
* Called when a change has been made.
*
* @param uri the uri that the change was made to
*/
private void onChange(Uri uri) {
getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null);
}
/** {@inheritDoc} */
@Override
public boolean isTemporary() {
return false;
}
/** {@inheritDoc} */
@Override
public Uri insert(Uri uri, ContentValues values) {
final int match = sUriMatcher.match(uri);
long id = 0;
switch (match) {
case ACTIVITIES: {
id = insertActivity(values);
break;
}
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
final Uri result = ContentUris.withAppendedId(Activities.CONTENT_URI, id);
onChange(result);
return result;
}
/**
* Inserts an item into the {@link Tables#ACTIVITIES} table.
*
* @param values the values for the new row
* @return the row ID of the newly created row
*/
private long insertActivity(ContentValues values) {
// TODO verify that IN_REPLY_TO != RAW_ID
final SQLiteDatabase db = mDbHelper.getWritableDatabase();
long id = 0;
db.beginTransaction();
try {
// TODO: Consider enforcing Binder.getCallingUid() for package name
// requested by this insert.
// Replace package name and mime-type with internal mappings
final String packageName = values.getAsString(Activities.RES_PACKAGE);
if (packageName != null) {
values.put(ActivitiesColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
}
values.remove(Activities.RES_PACKAGE);
final String mimeType = values.getAsString(Activities.MIMETYPE);
values.put(ActivitiesColumns.MIMETYPE_ID, mDbHelper.getMimeTypeId(mimeType));
values.remove(Activities.MIMETYPE);
long published = values.getAsLong(Activities.PUBLISHED);
long threadPublished = published;
String inReplyTo = values.getAsString(Activities.IN_REPLY_TO);
if (inReplyTo != null) {
threadPublished = getThreadPublished(db, inReplyTo, published);
}
values.put(Activities.THREAD_PUBLISHED, threadPublished);
// Insert the data row itself
id = db.insert(Tables.ACTIVITIES, Activities.RAW_ID, values);
// Adjust thread timestamps on replies that have already been inserted
if (values.containsKey(Activities.RAW_ID)) {
adjustReplyTimestamps(db, values.getAsString(Activities.RAW_ID), published);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return id;
}
/**
* Finds the timestamp of the original message in the thread. If not found, returns
* {@code defaultValue}.
*/
private long getThreadPublished(SQLiteDatabase db, String rawId, long defaultValue) {
String inReplyTo = null;
long threadPublished = defaultValue;
final Cursor c = db.query(Tables.ACTIVITIES,
new String[]{Activities.IN_REPLY_TO, Activities.PUBLISHED},
Activities.RAW_ID + " = ?", new String[]{rawId}, null, null, null);
try {
if (c.moveToFirst()) {
inReplyTo = c.getString(0);
threadPublished = c.getLong(1);
}
} finally {
c.close();
}
if (inReplyTo != null) {
// Call recursively to obtain the original timestamp of the entire thread
return getThreadPublished(db, inReplyTo, threadPublished);
}
return threadPublished;
}
/**
* In case the original message of a thread arrives after its reply messages, we need
* to check if there are any replies in the database and if so adjust their thread_published.
*/
private void adjustReplyTimestamps(SQLiteDatabase db, String inReplyTo, long threadPublished) {
ContentValues values = new ContentValues();
values.put(Activities.THREAD_PUBLISHED, threadPublished);
/*
* Issuing an exploratory update. If it updates nothing, we are done. Otherwise,
* we will run a query to find the updated records again and repeat recursively.
*/
int replies = db.update(Tables.ACTIVITIES, values,
Activities.IN_REPLY_TO + "= ?", new String[] {inReplyTo});
if (replies == 0) {
return;
}
/*
* Presumably this code will be executed very infrequently since messages tend to arrive
* in the order they get sent.
*/
ArrayList<String> rawIds = new ArrayList<String>(replies);
final Cursor c = db.query(Tables.ACTIVITIES,
new String[]{Activities.RAW_ID},
Activities.IN_REPLY_TO + " = ?", new String[] {inReplyTo}, null, null, null);
try {
while (c.moveToNext()) {
rawIds.add(c.getString(0));
}
} finally {
c.close();
}
for (String rawId : rawIds) {
adjustReplyTimestamps(db, rawId, threadPublished);
}
}
/** {@inheritDoc} */
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
final SQLiteDatabase db = mDbHelper.getWritableDatabase();
final int match = sUriMatcher.match(uri);
switch (match) {
case ACTIVITIES_ID: {
final long activityId = ContentUris.parseId(uri);
return db.delete(Tables.ACTIVITIES, Activities._ID + "=" + activityId, null);
}
case ACTIVITIES_AUTHORED_BY: {
final long contactId = ContentUris.parseId(uri);
return db.delete(Tables.ACTIVITIES, Activities.AUTHOR_CONTACT_ID + "=" + contactId, null);
}
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
}
/** {@inheritDoc} */
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException();
}
/** {@inheritDoc} */
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
final SQLiteDatabase db = mDbHelper.getReadableDatabase();
final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
String limit = null;
final int match = sUriMatcher.match(uri);
switch (match) {
case ACTIVITIES: {
qb.setTables(Tables.ACTIVITIES_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS);
qb.setProjectionMap(sActivitiesContactsProjectionMap);
break;
}
case ACTIVITIES_ID: {
// TODO: enforce that caller has read access to this data
long activityId = ContentUris.parseId(uri);
qb.setTables(Tables.ACTIVITIES_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS);
qb.setProjectionMap(sActivitiesContactsProjectionMap);
qb.appendWhere(Activities._ID + "=" + activityId);
break;
}
case ACTIVITIES_AUTHORED_BY: {
long contactId = ContentUris.parseId(uri);
qb.setTables(Tables.ACTIVITIES_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS);
qb.setProjectionMap(sActivitiesContactsProjectionMap);
qb.appendWhere(Activities.AUTHOR_CONTACT_ID + "=" + contactId);
break;
}
case CONTACT_STATUS_ID: {
long aggId = ContentUris.parseId(uri);
qb.setTables(Tables.ACTIVITIES_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS);
qb.setProjectionMap(sActivitiesContactsProjectionMap);
// Latest status of a contact is any top-level status
// authored by one of its children contacts.
qb.appendWhere(Activities.IN_REPLY_TO + " IS NULL AND ");
qb.appendWhere(Activities.AUTHOR_CONTACT_ID + " IN (SELECT " + BaseColumns._ID
+ " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts.CONTACT_ID + "="
+ aggId + ")");
sortOrder = Activities.PUBLISHED + " DESC";
limit = "1";
break;
}
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
// Default to reverse-chronological sort if nothing requested
if (sortOrder == null) {
sortOrder = DEFAULT_SORT_ORDER;
}
// Perform the query and set the notification uri
final Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder, limit);
if (c != null) {
c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI);
}
return c;
}
@Override
public String getType(Uri uri) {
final int match = sUriMatcher.match(uri);
switch (match) {
case ACTIVITIES:
case ACTIVITIES_AUTHORED_BY:
return Activities.CONTENT_TYPE;
case ACTIVITIES_ID:
final SQLiteDatabase db = mDbHelper.getReadableDatabase();
long activityId = ContentUris.parseId(uri);
return mDbHelper.getActivityMimeType(activityId);
case CONTACT_STATUS_ID:
return Contacts.CONTENT_ITEM_TYPE;
}
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
}