1 /* 2 * Copyright (C) 2015 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 package com.android.messaging.datamodel; 17 18 import android.content.ContentProvider; 19 import android.content.ContentResolver; 20 import android.content.ContentValues; 21 import android.content.Context; 22 import android.content.UriMatcher; 23 import android.database.Cursor; 24 import android.database.sqlite.SQLiteQueryBuilder; 25 import android.net.Uri; 26 import android.os.ParcelFileDescriptor; 27 import android.text.TextUtils; 28 29 import com.android.messaging.BugleApplication; 30 import com.android.messaging.Factory; 31 import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns; 32 import com.android.messaging.datamodel.DatabaseHelper.ConversationParticipantsColumns; 33 import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns; 34 import com.android.messaging.datamodel.data.ConversationListItemData; 35 import com.android.messaging.datamodel.data.ConversationMessageData; 36 import com.android.messaging.datamodel.data.MessageData; 37 import com.android.messaging.datamodel.data.ParticipantData; 38 import com.android.messaging.util.Assert; 39 import com.android.messaging.util.LogUtil; 40 import com.android.messaging.util.OsUtil; 41 import com.android.messaging.util.PhoneUtils; 42 import com.android.messaging.widget.BugleWidgetProvider; 43 import com.android.messaging.widget.WidgetConversationProvider; 44 import com.google.common.annotations.VisibleForTesting; 45 46 import java.io.FileDescriptor; 47 import java.io.FileNotFoundException; 48 import java.io.PrintWriter; 49 50 /** 51 * A centralized provider for Uris exposed by Bugle. 52 * */ 53 public class MessagingContentProvider extends ContentProvider { 54 private static final String TAG = LogUtil.BUGLE_TAG; 55 56 @VisibleForTesting 57 public static final String AUTHORITY = 58 "com.android.messaging.datamodel.MessagingContentProvider"; 59 private static final String CONTENT_AUTHORITY = "content://" + AUTHORITY + '/'; 60 61 // Conversations query 62 private static final String CONVERSATIONS_QUERY = "conversations"; 63 64 public static final Uri CONVERSATIONS_URI = Uri.parse(CONTENT_AUTHORITY + CONVERSATIONS_QUERY); 65 static final Uri PARTS_URI = Uri.parse(CONTENT_AUTHORITY + DatabaseHelper.PARTS_TABLE); 66 67 // Messages query 68 private static final String MESSAGES_QUERY = "messages"; 69 70 static final Uri MESSAGES_URI = Uri.parse(CONTENT_AUTHORITY + MESSAGES_QUERY); 71 72 public static final Uri CONVERSATION_MESSAGES_URI = Uri.parse(CONTENT_AUTHORITY + 73 MESSAGES_QUERY + "/conversation"); 74 75 // Conversation participants query 76 private static final String PARTICIPANTS_QUERY = "participants"; 77 78 static class ConversationParticipantsQueryColumns extends ParticipantColumns { 79 static final String CONVERSATION_ID = ConversationParticipantsColumns.CONVERSATION_ID; 80 } 81 82 static final Uri CONVERSATION_PARTICIPANTS_URI = Uri.parse(CONTENT_AUTHORITY + 83 PARTICIPANTS_QUERY + "/conversation"); 84 85 public static final Uri PARTICIPANTS_URI = Uri.parse(CONTENT_AUTHORITY + PARTICIPANTS_QUERY); 86 87 // Conversation images query 88 private static final String CONVERSATION_IMAGES_QUERY = "conversation_images"; 89 90 public static final Uri CONVERSATION_IMAGES_URI = Uri.parse(CONTENT_AUTHORITY + 91 CONVERSATION_IMAGES_QUERY); 92 93 private static final String DRAFT_IMAGES_QUERY = "draft_images"; 94 95 public static final Uri DRAFT_IMAGES_URI = Uri.parse(CONTENT_AUTHORITY + 96 DRAFT_IMAGES_QUERY); 97 98 /** 99 * Notifies that <i>all</i> data exposed by the provider needs to be refreshed. 100 * <p> 101 * <b>IMPORTANT!</b> You probably shouldn't be calling this. Prefer to notify more specific 102 * uri's instead. Currently only sync uses this, because sync can potentially update many 103 * different tables at once. 104 */ notifyEverythingChanged()105 public static void notifyEverythingChanged() { 106 final Uri uri = Uri.parse(CONTENT_AUTHORITY); 107 final Context context = Factory.get().getApplicationContext(); 108 final ContentResolver cr = context.getContentResolver(); 109 cr.notifyChange(uri, null); 110 111 // Notify any conversations widgets the conversation list has changed. 112 BugleWidgetProvider.notifyConversationListChanged(context); 113 114 // Notify all conversation widgets to update. 115 WidgetConversationProvider.notifyMessagesChanged(context, null /*conversationId*/); 116 } 117 118 /** 119 * Build a participant uri from the conversation id. 120 */ buildConversationParticipantsUri(final String conversationId)121 public static Uri buildConversationParticipantsUri(final String conversationId) { 122 final Uri.Builder builder = CONVERSATION_PARTICIPANTS_URI.buildUpon(); 123 builder.appendPath(conversationId); 124 return builder.build(); 125 } 126 notifyParticipantsChanged(final String conversationId)127 public static void notifyParticipantsChanged(final String conversationId) { 128 final Uri uri = buildConversationParticipantsUri(conversationId); 129 final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver(); 130 cr.notifyChange(uri, null); 131 } 132 notifyAllMessagesChanged()133 public static void notifyAllMessagesChanged() { 134 final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver(); 135 cr.notifyChange(CONVERSATION_MESSAGES_URI, null); 136 } 137 notifyAllParticipantsChanged()138 public static void notifyAllParticipantsChanged() { 139 final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver(); 140 cr.notifyChange(CONVERSATION_PARTICIPANTS_URI, null); 141 } 142 143 // Default value for unknown dimension of image 144 public static final int UNSPECIFIED_SIZE = -1; 145 146 // Internal 147 private static final int CONVERSATIONS_QUERY_CODE = 10; 148 149 private static final int CONVERSATION_QUERY_CODE = 20; 150 private static final int CONVERSATION_MESSAGES_QUERY_CODE = 30; 151 private static final int CONVERSATION_PARTICIPANTS_QUERY_CODE = 40; 152 private static final int CONVERSATION_IMAGES_QUERY_CODE = 50; 153 private static final int DRAFT_IMAGES_QUERY_CODE = 60; 154 private static final int PARTICIPANTS_QUERY_CODE = 70; 155 156 // TODO: Move to a better structured URI namespace. 157 private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); 158 static { sURIMatcher.addURI(AUTHORITY, CONVERSATIONS_QUERY, CONVERSATIONS_QUERY_CODE)159 sURIMatcher.addURI(AUTHORITY, CONVERSATIONS_QUERY, CONVERSATIONS_QUERY_CODE); sURIMatcher.addURI(AUTHORITY, CONVERSATIONS_QUERY + "/*", CONVERSATION_QUERY_CODE)160 sURIMatcher.addURI(AUTHORITY, CONVERSATIONS_QUERY + "/*", CONVERSATION_QUERY_CODE); sURIMatcher.addURI(AUTHORITY, MESSAGES_QUERY + "/conversation/*", CONVERSATION_MESSAGES_QUERY_CODE)161 sURIMatcher.addURI(AUTHORITY, MESSAGES_QUERY + "/conversation/*", 162 CONVERSATION_MESSAGES_QUERY_CODE); sURIMatcher.addURI(AUTHORITY, PARTICIPANTS_QUERY + "/conversation/*", CONVERSATION_PARTICIPANTS_QUERY_CODE)163 sURIMatcher.addURI(AUTHORITY, PARTICIPANTS_QUERY + "/conversation/*", 164 CONVERSATION_PARTICIPANTS_QUERY_CODE); sURIMatcher.addURI(AUTHORITY, PARTICIPANTS_QUERY, PARTICIPANTS_QUERY_CODE)165 sURIMatcher.addURI(AUTHORITY, PARTICIPANTS_QUERY, PARTICIPANTS_QUERY_CODE); sURIMatcher.addURI(AUTHORITY, CONVERSATION_IMAGES_QUERY + "/*", CONVERSATION_IMAGES_QUERY_CODE)166 sURIMatcher.addURI(AUTHORITY, CONVERSATION_IMAGES_QUERY + "/*", 167 CONVERSATION_IMAGES_QUERY_CODE); sURIMatcher.addURI(AUTHORITY, DRAFT_IMAGES_QUERY + "/*", DRAFT_IMAGES_QUERY_CODE)168 sURIMatcher.addURI(AUTHORITY, DRAFT_IMAGES_QUERY + "/*", 169 DRAFT_IMAGES_QUERY_CODE); 170 } 171 172 /** 173 * Build a messages uri from the conversation id. 174 */ buildConversationMessagesUri(final String conversationId)175 public static Uri buildConversationMessagesUri(final String conversationId) { 176 final Uri.Builder builder = CONVERSATION_MESSAGES_URI.buildUpon(); 177 builder.appendPath(conversationId); 178 return builder.build(); 179 } 180 notifyMessagesChanged(final String conversationId)181 public static void notifyMessagesChanged(final String conversationId) { 182 final Uri uri = buildConversationMessagesUri(conversationId); 183 final Context context = Factory.get().getApplicationContext(); 184 final ContentResolver cr = context.getContentResolver(); 185 cr.notifyChange(uri, null); 186 notifyConversationListChanged(); 187 188 // Notify the widget the messages changed 189 WidgetConversationProvider.notifyMessagesChanged(context, conversationId); 190 } 191 192 /** 193 * Build a conversation metadata uri from a conversation id. 194 */ buildConversationMetadataUri(final String conversationId)195 public static Uri buildConversationMetadataUri(final String conversationId) { 196 final Uri.Builder builder = CONVERSATIONS_URI.buildUpon(); 197 builder.appendPath(conversationId); 198 return builder.build(); 199 } 200 notifyConversationMetadataChanged(final String conversationId)201 public static void notifyConversationMetadataChanged(final String conversationId) { 202 final Uri uri = buildConversationMetadataUri(conversationId); 203 final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver(); 204 cr.notifyChange(uri, null); 205 notifyConversationListChanged(); 206 } 207 notifyPartsChanged()208 public static void notifyPartsChanged() { 209 final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver(); 210 cr.notifyChange(PARTS_URI, null); 211 } 212 notifyConversationListChanged()213 public static void notifyConversationListChanged() { 214 final Context context = Factory.get().getApplicationContext(); 215 final ContentResolver cr = context.getContentResolver(); 216 cr.notifyChange(CONVERSATIONS_URI, null); 217 218 // Notify the widget the conversation list changed 219 BugleWidgetProvider.notifyConversationListChanged(context); 220 } 221 222 /** 223 * Build a conversation images uri from a conversation id. 224 */ buildConversationImagesUri(final String conversationId)225 public static Uri buildConversationImagesUri(final String conversationId) { 226 final Uri.Builder builder = CONVERSATION_IMAGES_URI.buildUpon(); 227 builder.appendPath(conversationId); 228 return builder.build(); 229 } 230 231 /** 232 * Build a draft images uri from a conversation id. 233 */ buildDraftImagesUri(final String conversationId)234 public static Uri buildDraftImagesUri(final String conversationId) { 235 final Uri.Builder builder = DRAFT_IMAGES_URI.buildUpon(); 236 builder.appendPath(conversationId); 237 return builder.build(); 238 } 239 240 private DatabaseHelper mDatabaseHelper; 241 private DatabaseWrapper mDatabaseWrapper; 242 MessagingContentProvider()243 public MessagingContentProvider() { 244 super(); 245 } 246 247 @VisibleForTesting setDatabaseForTest(final DatabaseWrapper db)248 public void setDatabaseForTest(final DatabaseWrapper db) { 249 Assert.isTrue(BugleApplication.isRunningTests()); 250 mDatabaseWrapper = db; 251 } 252 getDatabaseWrapper()253 private DatabaseWrapper getDatabaseWrapper() { 254 if (mDatabaseWrapper == null) { 255 mDatabaseWrapper = mDatabaseHelper.getDatabase(); 256 } 257 return mDatabaseWrapper; 258 } 259 260 @Override query(final Uri uri, final String[] projection, String selection, final String[] selectionArgs, String sortOrder)261 public Cursor query(final Uri uri, final String[] projection, String selection, 262 final String[] selectionArgs, String sortOrder) { 263 264 // Processes other than self are allowed to temporarily access the media 265 // scratch space; we grant uri read access on a case-by-case basis. Dialer app and 266 // contacts app would doQuery() on the vCard uri before trying to open the inputStream. 267 // There's nothing that we need to return for this uri so just No-Op. 268 //if (isMediaScratchSpaceUri(uri)) { 269 // return null; 270 //} 271 272 final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); 273 274 String[] queryArgs = selectionArgs; 275 final int match = sURIMatcher.match(uri); 276 String groupBy = null; 277 String limit = null; 278 switch (match) { 279 case CONVERSATIONS_QUERY_CODE: 280 queryBuilder.setTables(ConversationListItemData.getConversationListView()); 281 // Hide empty conversations (ones with 0 sort_timestamp) 282 queryBuilder.appendWhere(ConversationColumns.SORT_TIMESTAMP + " > 0 "); 283 break; 284 case CONVERSATION_QUERY_CODE: 285 queryBuilder.setTables(ConversationListItemData.getConversationListView()); 286 if (uri.getPathSegments().size() == 2) { 287 queryBuilder.appendWhere(ConversationColumns._ID + "=?"); 288 // Get the conversation id from the uri 289 queryArgs = prependArgs(queryArgs, uri.getPathSegments().get(1)); 290 } else { 291 throw new IllegalArgumentException("Malformed URI " + uri); 292 } 293 break; 294 case CONVERSATION_PARTICIPANTS_QUERY_CODE: 295 queryBuilder.setTables(DatabaseHelper.PARTICIPANTS_TABLE); 296 if (uri.getPathSegments().size() == 3 && 297 TextUtils.equals(uri.getPathSegments().get(1), "conversation")) { 298 queryBuilder.appendWhere(ParticipantColumns._ID + " IN ( " + "SELECT " 299 + ConversationParticipantsColumns.PARTICIPANT_ID + " AS " 300 + ParticipantColumns._ID 301 + " FROM " + DatabaseHelper.CONVERSATION_PARTICIPANTS_TABLE 302 + " WHERE " + ConversationParticipantsColumns.CONVERSATION_ID 303 + " =? UNION SELECT " + ParticipantColumns._ID + " FROM " 304 + DatabaseHelper.PARTICIPANTS_TABLE + " WHERE " 305 + ParticipantColumns.SUB_ID + " != " 306 + ParticipantData.OTHER_THAN_SELF_SUB_ID + " )"); 307 // Get the conversation id from the uri 308 queryArgs = prependArgs(queryArgs, uri.getPathSegments().get(2)); 309 } else { 310 throw new IllegalArgumentException("Malformed URI " + uri); 311 } 312 break; 313 case PARTICIPANTS_QUERY_CODE: 314 queryBuilder.setTables(DatabaseHelper.PARTICIPANTS_TABLE); 315 if (uri.getPathSegments().size() != 1) { 316 throw new IllegalArgumentException("Malformed URI " + uri); 317 } 318 break; 319 case CONVERSATION_MESSAGES_QUERY_CODE: 320 if (uri.getPathSegments().size() == 3 && 321 TextUtils.equals(uri.getPathSegments().get(1), "conversation")) { 322 // Get the conversation id from the uri 323 final String conversationId = uri.getPathSegments().get(2); 324 325 // We need to handle this query differently, instead of falling through to the 326 // generic query call at the bottom. For performance reasons, the conversation 327 // messages query is executed as a raw query. It is invalid to specify 328 // selection/sorting for this query. 329 330 if (selection == null && selectionArgs == null && sortOrder == null) { 331 return queryConversationMessages(conversationId, uri); 332 } else { 333 throw new IllegalArgumentException( 334 "Cannot set selection or sort order with this query"); 335 } 336 } else { 337 throw new IllegalArgumentException("Malformed URI " + uri); 338 } 339 case CONVERSATION_IMAGES_QUERY_CODE: 340 queryBuilder.setTables(ConversationImagePartsView.getViewName()); 341 if (uri.getPathSegments().size() == 2) { 342 // Exclude draft. 343 queryBuilder.appendWhere( 344 ConversationImagePartsView.Columns.CONVERSATION_ID + " =? AND " + 345 ConversationImagePartsView.Columns.STATUS + "<>" + 346 MessageData.BUGLE_STATUS_OUTGOING_DRAFT); 347 // Get the conversation id from the uri 348 queryArgs = prependArgs(queryArgs, uri.getPathSegments().get(1)); 349 } else { 350 throw new IllegalArgumentException("Malformed URI " + uri); 351 } 352 break; 353 case DRAFT_IMAGES_QUERY_CODE: 354 queryBuilder.setTables(ConversationImagePartsView.getViewName()); 355 if (uri.getPathSegments().size() == 2) { 356 // Draft only. 357 queryBuilder.appendWhere( 358 ConversationImagePartsView.Columns.CONVERSATION_ID + " =? AND " + 359 ConversationImagePartsView.Columns.STATUS + "=" + 360 MessageData.BUGLE_STATUS_OUTGOING_DRAFT); 361 // Get the conversation id from the uri 362 queryArgs = prependArgs(queryArgs, uri.getPathSegments().get(1)); 363 } else { 364 throw new IllegalArgumentException("Malformed URI " + uri); 365 } 366 break; 367 default: { 368 throw new IllegalArgumentException("Unknown URI " + uri); 369 } 370 } 371 372 final Cursor cursor = getDatabaseWrapper().query(queryBuilder, projection, selection, 373 queryArgs, groupBy, null, sortOrder, limit); 374 cursor.setNotificationUri(getContext().getContentResolver(), uri); 375 return cursor; 376 } 377 queryConversationMessages(final String conversationId, final Uri notifyUri)378 private Cursor queryConversationMessages(final String conversationId, final Uri notifyUri) { 379 final String[] queryArgs = { conversationId }; 380 final Cursor cursor = getDatabaseWrapper().rawQuery( 381 ConversationMessageData.getConversationMessagesQuerySql(), queryArgs); 382 cursor.setNotificationUri(getContext().getContentResolver(), notifyUri); 383 return cursor; 384 } 385 386 @Override getType(final Uri uri)387 public String getType(final Uri uri) { 388 final StringBuilder sb = new 389 StringBuilder("vnd.android.cursor.dir/vnd.android.messaging."); 390 391 switch (sURIMatcher.match(uri)) { 392 case CONVERSATIONS_QUERY_CODE: { 393 sb.append(CONVERSATIONS_QUERY); 394 break; 395 } 396 default: { 397 throw new IllegalArgumentException("Unknown URI: " + uri); 398 } 399 } 400 return sb.toString(); 401 } 402 getDatabase()403 protected DatabaseHelper getDatabase() { 404 return DatabaseHelper.getInstance(getContext()); 405 } 406 407 @Override openFile(final Uri uri, final String fileMode)408 public ParcelFileDescriptor openFile(final Uri uri, final String fileMode) 409 throws FileNotFoundException { 410 throw new IllegalArgumentException("openFile not supported: " + uri); 411 } 412 413 @Override insert(final Uri uri, final ContentValues values)414 public Uri insert(final Uri uri, final ContentValues values) { 415 throw new IllegalStateException("Insert not supported " + uri); 416 } 417 418 @Override delete(final Uri uri, final String selection, final String[] selectionArgs)419 public int delete(final Uri uri, final String selection, final String[] selectionArgs) { 420 throw new IllegalArgumentException("Delete not supported: " + uri); 421 } 422 423 @Override update(final Uri uri, final ContentValues values, final String selection, final String[] selectionArgs)424 public int update(final Uri uri, final ContentValues values, final String selection, 425 final String[] selectionArgs) { 426 throw new IllegalArgumentException("Update not supported: " + uri); 427 } 428 429 /** 430 * Prepends new arguments to the existing argument list. 431 * 432 * @param oldArgList The current list of arguments. May be {@code null} 433 * @param args The new arguments to prepend 434 * @return A new argument list with the given arguments prepended 435 */ prependArgs(final String[] oldArgList, final String... args)436 private String[] prependArgs(final String[] oldArgList, final String... args) { 437 if (args == null || args.length == 0) { 438 return oldArgList; 439 } 440 final int oldArgCount = (oldArgList == null ? 0 : oldArgList.length); 441 final int newArgCount = args.length; 442 443 final String[] newArgs = new String[oldArgCount + newArgCount]; 444 System.arraycopy(args, 0, newArgs, 0, newArgCount); 445 if (oldArgCount > 0) { 446 System.arraycopy(oldArgList, 0, newArgs, newArgCount, oldArgCount); 447 } 448 return newArgs; 449 } 450 /** 451 * {@inheritDoc} 452 */ 453 @Override dump(final FileDescriptor fd, final PrintWriter writer, final String[] args)454 public void dump(final FileDescriptor fd, final PrintWriter writer, final String[] args) { 455 // First dump out the default SMS app package name 456 String defaultSmsApp = PhoneUtils.getDefault().getDefaultSmsApp(); 457 if (TextUtils.isEmpty(defaultSmsApp)) { 458 if (OsUtil.isAtLeastKLP()) { 459 defaultSmsApp = "None"; 460 } else { 461 defaultSmsApp = "None (pre-Kitkat)"; 462 } 463 } 464 writer.println("Default SMS app: " + defaultSmsApp); 465 // Now dump logs 466 LogUtil.dump(writer); 467 } 468 469 @Override onCreate()470 public boolean onCreate() { 471 // This is going to wind up calling into createDatabase() below. 472 mDatabaseHelper = (DatabaseHelper) getDatabase(); 473 // We cannot initialize mDatabaseWrapper yet as the Factory may not be initialized 474 return true; 475 } 476 } 477