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 17package com.android.providers.contacts; 18 19import com.android.providers.contacts.ContactsDatabaseHelper.ActivitiesColumns; 20import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns; 21import com.android.providers.contacts.ContactsDatabaseHelper.PackagesColumns; 22import com.android.providers.contacts.ContactsDatabaseHelper.Tables; 23 24import android.content.ContentProvider; 25import android.content.ContentUris; 26import android.content.ContentValues; 27import android.content.Context; 28import android.content.UriMatcher; 29import android.database.Cursor; 30import android.database.sqlite.SQLiteDatabase; 31import android.database.sqlite.SQLiteQueryBuilder; 32import android.provider.BaseColumns; 33import android.provider.ContactsContract; 34import android.provider.ContactsContract.Contacts; 35import android.provider.ContactsContract.RawContacts; 36import android.provider.SocialContract; 37import android.provider.SocialContract.Activities; 38 39import android.net.Uri; 40 41import java.util.ArrayList; 42import java.util.HashMap; 43 44/** 45 * Social activity content provider. The contract between this provider and 46 * applications is defined in {@link SocialContract}. 47 */ 48public 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