• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
matcher.addURI(SocialContract.AUTHORITY, "activities", ACTIVITIES)77         matcher.addURI(SocialContract.AUTHORITY, "activities", ACTIVITIES);
matcher.addURI(SocialContract.AUTHORITY, "activities/#", ACTIVITIES_ID)78         matcher.addURI(SocialContract.AUTHORITY, "activities/#", ACTIVITIES_ID);
matcher.addURI(SocialContract.AUTHORITY, "activities/authored_by/#", ACTIVITIES_AUTHORED_BY)79         matcher.addURI(SocialContract.AUTHORITY, "activities/authored_by/#", ACTIVITIES_AUTHORED_BY);
80 
matcher.addURI(SocialContract.AUTHORITY, "contact_status/#", CONTACT_STATUS_ID)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>();
columns.put(Contacts.DISPLAY_NAME, ContactsColumns.CONCRETE_DISPLAY_NAME + " AS " + Contacts.DISPLAY_NAME)87         columns.put(Contacts.DISPLAY_NAME, ContactsColumns.CONCRETE_DISPLAY_NAME + " AS "
88                 + Contacts.DISPLAY_NAME);
89         sContactsProjectionMap = columns;
90 
91         // Contacts projection map
92         columns = new HashMap<String, String>();
columns.put(RawContacts._ID, Tables.RAW_CONTACTS + "." + RawContacts._ID + " AS _id")93         columns.put(RawContacts._ID, Tables.RAW_CONTACTS + "." + RawContacts._ID + " AS _id");
columns.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID)94         columns.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
95         sRawContactsProjectionMap = columns;
96 
97         // Activities projection map
98         columns = new HashMap<String, String>();
columns.put(Activities._ID, "activities._id AS _id")99         columns.put(Activities._ID, "activities._id AS _id");
columns.put(Activities.RES_PACKAGE, PackagesColumns.PACKAGE + " AS " + Activities.RES_PACKAGE)100         columns.put(Activities.RES_PACKAGE, PackagesColumns.PACKAGE + " AS "
101                 + Activities.RES_PACKAGE);
columns.put(Activities.MIMETYPE, Activities.MIMETYPE)102         columns.put(Activities.MIMETYPE, Activities.MIMETYPE);
columns.put(Activities.RAW_ID, Activities.RAW_ID)103         columns.put(Activities.RAW_ID, Activities.RAW_ID);
columns.put(Activities.IN_REPLY_TO, Activities.IN_REPLY_TO)104         columns.put(Activities.IN_REPLY_TO, Activities.IN_REPLY_TO);
columns.put(Activities.AUTHOR_CONTACT_ID, Activities.AUTHOR_CONTACT_ID)105         columns.put(Activities.AUTHOR_CONTACT_ID, Activities.AUTHOR_CONTACT_ID);
columns.put(Activities.TARGET_CONTACT_ID, Activities.TARGET_CONTACT_ID)106         columns.put(Activities.TARGET_CONTACT_ID, Activities.TARGET_CONTACT_ID);
columns.put(Activities.PUBLISHED, Activities.PUBLISHED)107         columns.put(Activities.PUBLISHED, Activities.PUBLISHED);
columns.put(Activities.THREAD_PUBLISHED, Activities.THREAD_PUBLISHED)108         columns.put(Activities.THREAD_PUBLISHED, Activities.THREAD_PUBLISHED);
columns.put(Activities.TITLE, Activities.TITLE)109         columns.put(Activities.TITLE, Activities.TITLE);
columns.put(Activities.SUMMARY, Activities.SUMMARY)110         columns.put(Activities.SUMMARY, Activities.SUMMARY);
columns.put(Activities.LINK, Activities.LINK)111         columns.put(Activities.LINK, Activities.LINK);
columns.put(Activities.THUMBNAIL, Activities.THUMBNAIL)112         columns.put(Activities.THUMBNAIL, Activities.THUMBNAIL);
113         sActivitiesProjectionMap = columns;
114 
115         // Activities, raw contacts, and contacts projection map for joins
116         columns = new HashMap<String, String>();
117         columns.putAll(sContactsProjectionMap);
118         columns.putAll(sRawContactsProjectionMap);
119         columns.putAll(sActivitiesProjectionMap); // Final _id will be from Activities
120         sActivitiesContactsProjectionMap = columns;
121 
122     }
123 
124     private ContactsDatabaseHelper mDbHelper;
125 
126     /** {@inheritDoc} */
127     @Override
onCreate()128     public boolean onCreate() {
129         final Context context = getContext();
130         mDbHelper = ContactsDatabaseHelper.getInstance(context);
131 
132         // TODO remove this, it's here to force opening the database on boot for testing
133         mDbHelper.getReadableDatabase();
134 
135         return true;
136     }
137 
138     /**
139      * Called when a change has been made.
140      *
141      * @param uri the uri that the change was made to
142      */
onChange(Uri uri)143     private void onChange(Uri uri) {
144         getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null);
145     }
146 
147     /** {@inheritDoc} */
148     @Override
isTemporary()149     public boolean isTemporary() {
150         return false;
151     }
152 
153     /** {@inheritDoc} */
154     @Override
insert(Uri uri, ContentValues values)155     public Uri insert(Uri uri, ContentValues values) {
156         final int match = sUriMatcher.match(uri);
157         long id = 0;
158         switch (match) {
159             case ACTIVITIES: {
160                 id = insertActivity(values);
161                 break;
162             }
163 
164             default:
165                 throw new UnsupportedOperationException("Unknown uri: " + uri);
166         }
167 
168         final Uri result = ContentUris.withAppendedId(Activities.CONTENT_URI, id);
169         onChange(result);
170         return result;
171     }
172 
173     /**
174      * Inserts an item into the {@link Tables#ACTIVITIES} table.
175      *
176      * @param values the values for the new row
177      * @return the row ID of the newly created row
178      */
insertActivity(ContentValues values)179     private long insertActivity(ContentValues values) {
180 
181         // TODO verify that IN_REPLY_TO != RAW_ID
182 
183         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
184         long id = 0;
185         db.beginTransaction();
186         try {
187             // TODO: Consider enforcing Binder.getCallingUid() for package name
188             // requested by this insert.
189 
190             // Replace package name and mime-type with internal mappings
191             final String packageName = values.getAsString(Activities.RES_PACKAGE);
192             if (packageName != null) {
193                 values.put(ActivitiesColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
194             }
195             values.remove(Activities.RES_PACKAGE);
196 
197             final String mimeType = values.getAsString(Activities.MIMETYPE);
198             values.put(ActivitiesColumns.MIMETYPE_ID, mDbHelper.getMimeTypeId(mimeType));
199             values.remove(Activities.MIMETYPE);
200 
201             long published = values.getAsLong(Activities.PUBLISHED);
202             long threadPublished = published;
203 
204             String inReplyTo = values.getAsString(Activities.IN_REPLY_TO);
205             if (inReplyTo != null) {
206                 threadPublished = getThreadPublished(db, inReplyTo, published);
207             }
208 
209             values.put(Activities.THREAD_PUBLISHED, threadPublished);
210 
211             // Insert the data row itself
212             id = db.insert(Tables.ACTIVITIES, Activities.RAW_ID, values);
213 
214             // Adjust thread timestamps on replies that have already been inserted
215             if (values.containsKey(Activities.RAW_ID)) {
216                 adjustReplyTimestamps(db, values.getAsString(Activities.RAW_ID), published);
217             }
218 
219             db.setTransactionSuccessful();
220         } finally {
221             db.endTransaction();
222         }
223         return id;
224     }
225 
226     /**
227      * Finds the timestamp of the original message in the thread. If not found, returns
228      * {@code defaultValue}.
229      */
getThreadPublished(SQLiteDatabase db, String rawId, long defaultValue)230     private long getThreadPublished(SQLiteDatabase db, String rawId, long defaultValue) {
231         String inReplyTo = null;
232         long threadPublished = defaultValue;
233 
234         final Cursor c = db.query(Tables.ACTIVITIES,
235                 new String[]{Activities.IN_REPLY_TO, Activities.PUBLISHED},
236                 Activities.RAW_ID + " = ?", new String[]{rawId}, null, null, null);
237         try {
238             if (c.moveToFirst()) {
239                 inReplyTo = c.getString(0);
240                 threadPublished = c.getLong(1);
241             }
242         } finally {
243             c.close();
244         }
245 
246         if (inReplyTo != null) {
247 
248             // Call recursively to obtain the original timestamp of the entire thread
249             return getThreadPublished(db, inReplyTo, threadPublished);
250         }
251 
252         return threadPublished;
253     }
254 
255     /**
256      * In case the original message of a thread arrives after its reply messages, we need
257      * to check if there are any replies in the database and if so adjust their thread_published.
258      */
adjustReplyTimestamps(SQLiteDatabase db, String inReplyTo, long threadPublished)259     private void adjustReplyTimestamps(SQLiteDatabase db, String inReplyTo, long threadPublished) {
260 
261         ContentValues values = new ContentValues();
262         values.put(Activities.THREAD_PUBLISHED, threadPublished);
263 
264         /*
265          * Issuing an exploratory update. If it updates nothing, we are done.  Otherwise,
266          * we will run a query to find the updated records again and repeat recursively.
267          */
268         int replies = db.update(Tables.ACTIVITIES, values,
269                 Activities.IN_REPLY_TO + "= ?", new String[] {inReplyTo});
270 
271         if (replies == 0) {
272             return;
273         }
274 
275         /*
276          * Presumably this code will be executed very infrequently since messages tend to arrive
277          * in the order they get sent.
278          */
279         ArrayList<String> rawIds = new ArrayList<String>(replies);
280         final Cursor c = db.query(Tables.ACTIVITIES,
281                 new String[]{Activities.RAW_ID},
282                 Activities.IN_REPLY_TO + " = ?", new String[] {inReplyTo}, null, null, null);
283         try {
284             while (c.moveToNext()) {
285                 rawIds.add(c.getString(0));
286             }
287         } finally {
288             c.close();
289         }
290 
291         for (String rawId : rawIds) {
292             adjustReplyTimestamps(db, rawId, threadPublished);
293         }
294     }
295 
296     /** {@inheritDoc} */
297     @Override
delete(Uri uri, String selection, String[] selectionArgs)298     public int delete(Uri uri, String selection, String[] selectionArgs) {
299         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
300 
301         final int match = sUriMatcher.match(uri);
302         switch (match) {
303             case ACTIVITIES_ID: {
304                 final long activityId = ContentUris.parseId(uri);
305                 return db.delete(Tables.ACTIVITIES, Activities._ID + "=" + activityId, null);
306             }
307 
308             case ACTIVITIES_AUTHORED_BY: {
309                 final long contactId = ContentUris.parseId(uri);
310                 return db.delete(Tables.ACTIVITIES, Activities.AUTHOR_CONTACT_ID + "=" + contactId, null);
311             }
312 
313             default:
314                 throw new UnsupportedOperationException("Unknown uri: " + uri);
315         }
316     }
317 
318     /** {@inheritDoc} */
319     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)320     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
321         throw new UnsupportedOperationException();
322     }
323 
324     /** {@inheritDoc} */
325     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)326     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
327             String sortOrder) {
328         final SQLiteDatabase db = mDbHelper.getReadableDatabase();
329         final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
330         String limit = null;
331 
332         final int match = sUriMatcher.match(uri);
333         switch (match) {
334             case ACTIVITIES: {
335                 qb.setTables(Tables.ACTIVITIES_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS);
336                 qb.setProjectionMap(sActivitiesContactsProjectionMap);
337                 break;
338             }
339 
340             case ACTIVITIES_ID: {
341                 // TODO: enforce that caller has read access to this data
342                 long activityId = ContentUris.parseId(uri);
343                 qb.setTables(Tables.ACTIVITIES_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS);
344                 qb.setProjectionMap(sActivitiesContactsProjectionMap);
345                 qb.appendWhere(Activities._ID + "=" + activityId);
346                 break;
347             }
348 
349             case ACTIVITIES_AUTHORED_BY: {
350                 long contactId = ContentUris.parseId(uri);
351                 qb.setTables(Tables.ACTIVITIES_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS);
352                 qb.setProjectionMap(sActivitiesContactsProjectionMap);
353                 qb.appendWhere(Activities.AUTHOR_CONTACT_ID + "=" + contactId);
354                 break;
355             }
356 
357             case CONTACT_STATUS_ID: {
358                 long aggId = ContentUris.parseId(uri);
359                 qb.setTables(Tables.ACTIVITIES_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS);
360                 qb.setProjectionMap(sActivitiesContactsProjectionMap);
361 
362                 // Latest status of a contact is any top-level status
363                 // authored by one of its children contacts.
364                 qb.appendWhere(Activities.IN_REPLY_TO + " IS NULL AND ");
365                 qb.appendWhere(Activities.AUTHOR_CONTACT_ID + " IN (SELECT " + BaseColumns._ID
366                         + " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts.CONTACT_ID + "="
367                         + aggId + ")");
368                 sortOrder = Activities.PUBLISHED + " DESC";
369                 limit = "1";
370                 break;
371             }
372 
373             default:
374                 throw new UnsupportedOperationException("Unknown uri: " + uri);
375         }
376 
377         // Default to reverse-chronological sort if nothing requested
378         if (sortOrder == null) {
379             sortOrder = DEFAULT_SORT_ORDER;
380         }
381 
382         // Perform the query and set the notification uri
383         final Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder, limit);
384         if (c != null) {
385             c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI);
386         }
387         return c;
388     }
389 
390     @Override
getType(Uri uri)391     public String getType(Uri uri) {
392         final int match = sUriMatcher.match(uri);
393         switch (match) {
394             case ACTIVITIES:
395             case ACTIVITIES_AUTHORED_BY:
396                 return Activities.CONTENT_TYPE;
397             case ACTIVITIES_ID:
398                 final SQLiteDatabase db = mDbHelper.getReadableDatabase();
399                 long activityId = ContentUris.parseId(uri);
400                 return mDbHelper.getActivityMimeType(activityId);
401             case CONTACT_STATUS_ID:
402                 return Contacts.CONTENT_ITEM_TYPE;
403         }
404         throw new UnsupportedOperationException("Unknown uri: " + uri);
405     }
406 }
407