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