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