Home | History | Annotate | Download | only in contacts
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 
     17 package com.android.providers.contacts;
     18 
     19 import com.android.providers.contacts.ContactsDatabaseHelper.ActivitiesColumns;
     20 import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
     21 import com.android.providers.contacts.ContactsDatabaseHelper.PackagesColumns;
     22 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
     23 
     24 import android.content.ContentProvider;
     25 import android.content.ContentUris;
     26 import android.content.ContentValues;
     27 import android.content.Context;
     28 import android.content.UriMatcher;
     29 import android.database.Cursor;
     30 import android.database.sqlite.SQLiteDatabase;
     31 import android.database.sqlite.SQLiteQueryBuilder;
     32 import android.provider.BaseColumns;
     33 import android.provider.ContactsContract;
     34 import android.provider.ContactsContract.Contacts;
     35 import android.provider.ContactsContract.RawContacts;
     36 import android.provider.SocialContract;
     37 import android.provider.SocialContract.Activities;
     38 
     39 import android.net.Uri;
     40 
     41 import java.util.ArrayList;
     42 import java.util.HashMap;
     43 
     44 /**
     45  * Social activity content provider. The contract between this provider and
     46  * applications is defined in {@link SocialContract}.
     47  */
     48 public class SocialProvider extends ContentProvider {
     49     // TODO: clean up debug tag
     50     private static final String TAG = "SocialProvider ~~~~";
     51 
     52     private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
     53 
     54     private static final int ACTIVITIES = 1000;
     55     private static final int ACTIVITIES_ID = 1001;
     56     private static final int ACTIVITIES_AUTHORED_BY = 1002;
     57 
     58     private static final int CONTACT_STATUS_ID = 3000;
     59 
     60     private static final String DEFAULT_SORT_ORDER = Activities.THREAD_PUBLISHED + " DESC, "
     61             + Activities.PUBLISHED + " ASC";
     62 
     63     /** Contains just the contacts columns */
     64     private static final HashMap<String, String> sContactsProjectionMap;
     65     /** Contains just the contacts columns */
     66     private static final HashMap<String, String> sRawContactsProjectionMap;
     67     /** Contains just the activities columns */
     68     private static final HashMap<String, String> sActivitiesProjectionMap;
     69 
     70     /** Contains the activities, raw contacts, and contacts columns, for joined tables */
     71     private static final HashMap<String, String> sActivitiesContactsProjectionMap;
     72 
     73     static {
     74         // Contacts URI matching table
     75         final UriMatcher matcher = sUriMatcher;
     76 
     77         matcher.addURI(SocialContract.AUTHORITY, "activities", ACTIVITIES);
     78         matcher.addURI(SocialContract.AUTHORITY, "activities/#", ACTIVITIES_ID);
     79         matcher.addURI(SocialContract.AUTHORITY, "activities/authored_by/#", ACTIVITIES_AUTHORED_BY);
     80 
     81         matcher.addURI(SocialContract.AUTHORITY, "contact_status/#", CONTACT_STATUS_ID);
     82 
     83         HashMap<String, String> columns;
     84 
     85         // Contacts projection map
     86         columns = new HashMap<String, String>();
     87         // TODO: fix display name reference (in fact, use the contacts view instead of the table)
     88         columns.put(Contacts.DISPLAY_NAME, "contact." + Contacts.DISPLAY_NAME + " AS "
     89                 + Contacts.DISPLAY_NAME);
     90         sContactsProjectionMap = columns;
     91 
     92         // Contacts projection map
     93         columns = new HashMap<String, String>();
     94         columns.put(RawContacts._ID, Tables.RAW_CONTACTS + "." + RawContacts._ID + " AS _id");
     95         columns.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
     96         sRawContactsProjectionMap = columns;
     97 
     98         // Activities projection map
     99         columns = new HashMap<String, String>();
    100         columns.put(Activities._ID, "activities._id AS _id");
    101         columns.put(Activities.RES_PACKAGE, PackagesColumns.PACKAGE + " AS "
    102                 + Activities.RES_PACKAGE);
    103         columns.put(Activities.MIMETYPE, Activities.MIMETYPE);
    104         columns.put(Activities.RAW_ID, Activities.RAW_ID);
    105         columns.put(Activities.IN_REPLY_TO, Activities.IN_REPLY_TO);
    106         columns.put(Activities.AUTHOR_CONTACT_ID, Activities.AUTHOR_CONTACT_ID);
    107         columns.put(Activities.TARGET_CONTACT_ID, Activities.TARGET_CONTACT_ID);
    108         columns.put(Activities.PUBLISHED, Activities.PUBLISHED);
    109         columns.put(Activities.THREAD_PUBLISHED, Activities.THREAD_PUBLISHED);
    110         columns.put(Activities.TITLE, Activities.TITLE);
    111         columns.put(Activities.SUMMARY, Activities.SUMMARY);
    112         columns.put(Activities.LINK, Activities.LINK);
    113         columns.put(Activities.THUMBNAIL, Activities.THUMBNAIL);
    114         sActivitiesProjectionMap = columns;
    115 
    116         // Activities, raw contacts, and contacts projection map for joins
    117         columns = new HashMap<String, String>();
    118         columns.putAll(sContactsProjectionMap);
    119         columns.putAll(sRawContactsProjectionMap);
    120         columns.putAll(sActivitiesProjectionMap); // Final _id will be from Activities
    121         sActivitiesContactsProjectionMap = columns;
    122 
    123     }
    124 
    125     private ContactsDatabaseHelper mDbHelper;
    126 
    127     /** {@inheritDoc} */
    128     @Override
    129     public boolean onCreate() {
    130         final Context context = getContext();
    131         mDbHelper = ContactsDatabaseHelper.getInstance(context);
    132         return true;
    133     }
    134 
    135     /**
    136      * Called when a change has been made.
    137      *
    138      * @param uri the uri that the change was made to
    139      */
    140     private void onChange(Uri uri) {
    141         getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null);
    142     }
    143 
    144     /** {@inheritDoc} */
    145     @Override
    146     public boolean isTemporary() {
    147         return false;
    148     }
    149 
    150     /** {@inheritDoc} */
    151     @Override
    152     public Uri insert(Uri uri, ContentValues values) {
    153         final int match = sUriMatcher.match(uri);
    154         long id = 0;
    155         switch (match) {
    156             case ACTIVITIES: {
    157                 id = insertActivity(values);
    158                 break;
    159             }
    160 
    161             default:
    162                 throw new UnsupportedOperationException("Unknown uri: " + uri);
    163         }
    164 
    165         final Uri result = ContentUris.withAppendedId(Activities.CONTENT_URI, id);
    166         onChange(result);
    167         return result;
    168     }
    169 
    170     /**
    171      * Inserts an item into the {@link Tables#ACTIVITIES} table.
    172      *
    173      * @param values the values for the new row
    174      * @return the row ID of the newly created row
    175      */
    176     private long insertActivity(ContentValues values) {
    177 
    178         // TODO verify that IN_REPLY_TO != RAW_ID
    179 
    180         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
    181         long id = 0;
    182         db.beginTransaction();
    183         try {
    184             // TODO: Consider enforcing Binder.getCallingUid() for package name
    185             // requested by this insert.
    186 
    187             // Replace package name and mime-type with internal mappings
    188             final String packageName = values.getAsString(Activities.RES_PACKAGE);
    189             if (packageName != null) {
    190                 values.put(ActivitiesColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
    191             }
    192             values.remove(Activities.RES_PACKAGE);
    193 
    194             final String mimeType = values.getAsString(Activities.MIMETYPE);
    195             values.put(ActivitiesColumns.MIMETYPE_ID, mDbHelper.getMimeTypeId(mimeType));
    196             values.remove(Activities.MIMETYPE);
    197 
    198             long published = values.getAsLong(Activities.PUBLISHED);
    199             long threadPublished = published;
    200 
    201             String inReplyTo = values.getAsString(Activities.IN_REPLY_TO);
    202             if (inReplyTo != null) {
    203                 threadPublished = getThreadPublished(db, inReplyTo, published);
    204             }
    205 
    206             values.put(Activities.THREAD_PUBLISHED, threadPublished);
    207 
    208             // Insert the data row itself
    209             id = db.insert(Tables.ACTIVITIES, Activities.RAW_ID, values);
    210 
    211             // Adjust thread timestamps on replies that have already been inserted
    212             if (values.containsKey(Activities.RAW_ID)) {
    213                 adjustReplyTimestamps(db, values.getAsString(Activities.RAW_ID), published);
    214             }
    215 
    216             db.setTransactionSuccessful();
    217         } finally {
    218             db.endTransaction();
    219         }
    220         return id;
    221     }
    222 
    223     /**
    224      * Finds the timestamp of the original message in the thread. If not found, returns
    225      * {@code defaultValue}.
    226      */
    227     private long getThreadPublished(SQLiteDatabase db, String rawId, long defaultValue) {
    228         String inReplyTo = null;
    229         long threadPublished = defaultValue;
    230 
    231         final Cursor c = db.query(Tables.ACTIVITIES,
    232                 new String[]{Activities.IN_REPLY_TO, Activities.PUBLISHED},
    233                 Activities.RAW_ID + " = ?", new String[]{rawId}, null, null, null);
    234         try {
    235             if (c.moveToFirst()) {
    236                 inReplyTo = c.getString(0);
    237                 threadPublished = c.getLong(1);
    238             }
    239         } finally {
    240             c.close();
    241         }
    242 
    243         if (inReplyTo != null) {
    244 
    245             // Call recursively to obtain the original timestamp of the entire thread
    246             return getThreadPublished(db, inReplyTo, threadPublished);
    247         }
    248 
    249         return threadPublished;
    250     }
    251 
    252     /**
    253      * In case the original message of a thread arrives after its reply messages, we need
    254      * to check if there are any replies in the database and if so adjust their thread_published.
    255      */
    256     private void adjustReplyTimestamps(SQLiteDatabase db, String inReplyTo, long threadPublished) {
    257 
    258         ContentValues values = new ContentValues();
    259         values.put(Activities.THREAD_PUBLISHED, threadPublished);
    260 
    261         /*
    262          * Issuing an exploratory update. If it updates nothing, we are done.  Otherwise,
    263          * we will run a query to find the updated records again and repeat recursively.
    264          */
    265         int replies = db.update(Tables.ACTIVITIES, values,
    266                 Activities.IN_REPLY_TO + "= ?", new String[] {inReplyTo});
    267 
    268         if (replies == 0) {
    269             return;
    270         }
    271 
    272         /*
    273          * Presumably this code will be executed very infrequently since messages tend to arrive
    274          * in the order they get sent.
    275          */
    276         ArrayList<String> rawIds = new ArrayList<String>(replies);
    277         final Cursor c = db.query(Tables.ACTIVITIES,
    278                 new String[]{Activities.RAW_ID},
    279                 Activities.IN_REPLY_TO + " = ?", new String[] {inReplyTo}, null, null, null);
    280         try {
    281             while (c.moveToNext()) {
    282                 rawIds.add(c.getString(0));
    283             }
    284         } finally {
    285             c.close();
    286         }
    287 
    288         for (String rawId : rawIds) {
    289             adjustReplyTimestamps(db, rawId, threadPublished);
    290         }
    291     }
    292 
    293     /** {@inheritDoc} */
    294     @Override
    295     public int delete(Uri uri, String selection, String[] selectionArgs) {
    296         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
    297 
    298         final int match = sUriMatcher.match(uri);
    299         switch (match) {
    300             case ACTIVITIES_ID: {
    301                 final long activityId = ContentUris.parseId(uri);
    302                 return db.delete(Tables.ACTIVITIES, Activities._ID + "=" + activityId, null);
    303             }
    304 
    305             case ACTIVITIES_AUTHORED_BY: {
    306                 final long contactId = ContentUris.parseId(uri);
    307                 return db.delete(Tables.ACTIVITIES, Activities.AUTHOR_CONTACT_ID + "=" + contactId, null);
    308             }
    309 
    310             default:
    311                 throw new UnsupportedOperationException("Unknown uri: " + uri);
    312         }
    313     }
    314 
    315     /** {@inheritDoc} */
    316     @Override
    317     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    318         throw new UnsupportedOperationException();
    319     }
    320 
    321     /** {@inheritDoc} */
    322     @Override
    323     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
    324             String sortOrder) {
    325         final SQLiteDatabase db = mDbHelper.getReadableDatabase();
    326         final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
    327         String limit = null;
    328 
    329         final int match = sUriMatcher.match(uri);
    330         switch (match) {
    331             case ACTIVITIES: {
    332                 qb.setTables(Tables.ACTIVITIES_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS);
    333                 qb.setProjectionMap(sActivitiesContactsProjectionMap);
    334                 break;
    335             }
    336 
    337             case ACTIVITIES_ID: {
    338                 // TODO: enforce that caller has read access to this data
    339                 long activityId = ContentUris.parseId(uri);
    340                 qb.setTables(Tables.ACTIVITIES_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS);
    341                 qb.setProjectionMap(sActivitiesContactsProjectionMap);
    342                 qb.appendWhere(Activities._ID + "=" + activityId);
    343                 break;
    344             }
    345 
    346             case ACTIVITIES_AUTHORED_BY: {
    347                 long contactId = ContentUris.parseId(uri);
    348                 qb.setTables(Tables.ACTIVITIES_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS);
    349                 qb.setProjectionMap(sActivitiesContactsProjectionMap);
    350                 qb.appendWhere(Activities.AUTHOR_CONTACT_ID + "=" + contactId);
    351                 break;
    352             }
    353 
    354             case CONTACT_STATUS_ID: {
    355                 long aggId = ContentUris.parseId(uri);
    356                 qb.setTables(Tables.ACTIVITIES_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS);
    357                 qb.setProjectionMap(sActivitiesContactsProjectionMap);
    358 
    359                 // Latest status of a contact is any top-level status
    360                 // authored by one of its children contacts.
    361                 qb.appendWhere(Activities.IN_REPLY_TO + " IS NULL AND ");
    362                 qb.appendWhere(Activities.AUTHOR_CONTACT_ID + " IN (SELECT " + BaseColumns._ID
    363                         + " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts.CONTACT_ID + "="
    364                         + aggId + ")");
    365                 sortOrder = Activities.PUBLISHED + " DESC";
    366                 limit = "1";
    367                 break;
    368             }
    369 
    370             default:
    371                 throw new UnsupportedOperationException("Unknown uri: " + uri);
    372         }
    373 
    374         // Default to reverse-chronological sort if nothing requested
    375         if (sortOrder == null) {
    376             sortOrder = DEFAULT_SORT_ORDER;
    377         }
    378 
    379         // Perform the query and set the notification uri
    380         final Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder, limit);
    381         if (c != null) {
    382             c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI);
    383         }
    384         return c;
    385     }
    386 
    387     @Override
    388     public String getType(Uri uri) {
    389         final int match = sUriMatcher.match(uri);
    390         switch (match) {
    391             case ACTIVITIES:
    392             case ACTIVITIES_AUTHORED_BY:
    393                 return Activities.CONTENT_TYPE;
    394             case ACTIVITIES_ID:
    395                 final SQLiteDatabase db = mDbHelper.getReadableDatabase();
    396                 long activityId = ContentUris.parseId(uri);
    397                 return mDbHelper.getActivityMimeType(activityId);
    398             case CONTACT_STATUS_ID:
    399                 return Contacts.CONTENT_ITEM_TYPE;
    400         }
    401         throw new UnsupportedOperationException("Unknown uri: " + uri);
    402     }
    403 }
    404