/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.messaging.datamodel; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.database.Cursor; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.os.ParcelFileDescriptor; import android.text.TextUtils; import com.android.messaging.BugleApplication; import com.android.messaging.Factory; import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns; import com.android.messaging.datamodel.DatabaseHelper.ConversationParticipantsColumns; import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns; import com.android.messaging.datamodel.data.ConversationListItemData; import com.android.messaging.datamodel.data.ConversationMessageData; import com.android.messaging.datamodel.data.MessageData; import com.android.messaging.datamodel.data.ParticipantData; import com.android.messaging.util.Assert; import com.android.messaging.util.LogUtil; import com.android.messaging.util.OsUtil; import com.android.messaging.util.PhoneUtils; import com.android.messaging.widget.BugleWidgetProvider; import com.android.messaging.widget.WidgetConversationProvider; import com.google.common.annotations.VisibleForTesting; import java.io.FileDescriptor; import java.io.FileNotFoundException; import java.io.PrintWriter; /** * A centralized provider for Uris exposed by Bugle. * */ public class MessagingContentProvider extends ContentProvider { private static final String TAG = LogUtil.BUGLE_TAG; @VisibleForTesting public static final String AUTHORITY = "com.android.messaging.datamodel.MessagingContentProvider"; private static final String CONTENT_AUTHORITY = "content://" + AUTHORITY + '/'; // Conversations query private static final String CONVERSATIONS_QUERY = "conversations"; public static final Uri CONVERSATIONS_URI = Uri.parse(CONTENT_AUTHORITY + CONVERSATIONS_QUERY); static final Uri PARTS_URI = Uri.parse(CONTENT_AUTHORITY + DatabaseHelper.PARTS_TABLE); // Messages query private static final String MESSAGES_QUERY = "messages"; static final Uri MESSAGES_URI = Uri.parse(CONTENT_AUTHORITY + MESSAGES_QUERY); public static final Uri CONVERSATION_MESSAGES_URI = Uri.parse(CONTENT_AUTHORITY + MESSAGES_QUERY + "/conversation"); // Conversation participants query private static final String PARTICIPANTS_QUERY = "participants"; static class ConversationParticipantsQueryColumns extends ParticipantColumns { static final String CONVERSATION_ID = ConversationParticipantsColumns.CONVERSATION_ID; } static final Uri CONVERSATION_PARTICIPANTS_URI = Uri.parse(CONTENT_AUTHORITY + PARTICIPANTS_QUERY + "/conversation"); public static final Uri PARTICIPANTS_URI = Uri.parse(CONTENT_AUTHORITY + PARTICIPANTS_QUERY); // Conversation images query private static final String CONVERSATION_IMAGES_QUERY = "conversation_images"; public static final Uri CONVERSATION_IMAGES_URI = Uri.parse(CONTENT_AUTHORITY + CONVERSATION_IMAGES_QUERY); private static final String DRAFT_IMAGES_QUERY = "draft_images"; public static final Uri DRAFT_IMAGES_URI = Uri.parse(CONTENT_AUTHORITY + DRAFT_IMAGES_QUERY); /** * Notifies that all data exposed by the provider needs to be refreshed. *

* IMPORTANT! You probably shouldn't be calling this. Prefer to notify more specific * uri's instead. Currently only sync uses this, because sync can potentially update many * different tables at once. */ public static void notifyEverythingChanged() { final Uri uri = Uri.parse(CONTENT_AUTHORITY); final Context context = Factory.get().getApplicationContext(); final ContentResolver cr = context.getContentResolver(); cr.notifyChange(uri, null); // Notify any conversations widgets the conversation list has changed. BugleWidgetProvider.notifyConversationListChanged(context); // Notify all conversation widgets to update. WidgetConversationProvider.notifyMessagesChanged(context, null /*conversationId*/); } /** * Build a participant uri from the conversation id. */ public static Uri buildConversationParticipantsUri(final String conversationId) { final Uri.Builder builder = CONVERSATION_PARTICIPANTS_URI.buildUpon(); builder.appendPath(conversationId); return builder.build(); } public static void notifyParticipantsChanged(final String conversationId) { final Uri uri = buildConversationParticipantsUri(conversationId); final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver(); cr.notifyChange(uri, null); } public static void notifyAllMessagesChanged() { final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver(); cr.notifyChange(CONVERSATION_MESSAGES_URI, null); } public static void notifyAllParticipantsChanged() { final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver(); cr.notifyChange(CONVERSATION_PARTICIPANTS_URI, null); } // Default value for unknown dimension of image public static final int UNSPECIFIED_SIZE = -1; // Internal private static final int CONVERSATIONS_QUERY_CODE = 10; private static final int CONVERSATION_QUERY_CODE = 20; private static final int CONVERSATION_MESSAGES_QUERY_CODE = 30; private static final int CONVERSATION_PARTICIPANTS_QUERY_CODE = 40; private static final int CONVERSATION_IMAGES_QUERY_CODE = 50; private static final int DRAFT_IMAGES_QUERY_CODE = 60; private static final int PARTICIPANTS_QUERY_CODE = 70; // TODO: Move to a better structured URI namespace. private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); static { sURIMatcher.addURI(AUTHORITY, CONVERSATIONS_QUERY, CONVERSATIONS_QUERY_CODE); sURIMatcher.addURI(AUTHORITY, CONVERSATIONS_QUERY + "/*", CONVERSATION_QUERY_CODE); sURIMatcher.addURI(AUTHORITY, MESSAGES_QUERY + "/conversation/*", CONVERSATION_MESSAGES_QUERY_CODE); sURIMatcher.addURI(AUTHORITY, PARTICIPANTS_QUERY + "/conversation/*", CONVERSATION_PARTICIPANTS_QUERY_CODE); sURIMatcher.addURI(AUTHORITY, PARTICIPANTS_QUERY, PARTICIPANTS_QUERY_CODE); sURIMatcher.addURI(AUTHORITY, CONVERSATION_IMAGES_QUERY + "/*", CONVERSATION_IMAGES_QUERY_CODE); sURIMatcher.addURI(AUTHORITY, DRAFT_IMAGES_QUERY + "/*", DRAFT_IMAGES_QUERY_CODE); } /** * Build a messages uri from the conversation id. */ public static Uri buildConversationMessagesUri(final String conversationId) { final Uri.Builder builder = CONVERSATION_MESSAGES_URI.buildUpon(); builder.appendPath(conversationId); return builder.build(); } public static void notifyMessagesChanged(final String conversationId) { final Uri uri = buildConversationMessagesUri(conversationId); final Context context = Factory.get().getApplicationContext(); final ContentResolver cr = context.getContentResolver(); cr.notifyChange(uri, null); notifyConversationListChanged(); // Notify the widget the messages changed WidgetConversationProvider.notifyMessagesChanged(context, conversationId); } /** * Build a conversation metadata uri from a conversation id. */ public static Uri buildConversationMetadataUri(final String conversationId) { final Uri.Builder builder = CONVERSATIONS_URI.buildUpon(); builder.appendPath(conversationId); return builder.build(); } public static void notifyConversationMetadataChanged(final String conversationId) { final Uri uri = buildConversationMetadataUri(conversationId); final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver(); cr.notifyChange(uri, null); notifyConversationListChanged(); } public static void notifyPartsChanged() { final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver(); cr.notifyChange(PARTS_URI, null); } public static void notifyConversationListChanged() { final Context context = Factory.get().getApplicationContext(); final ContentResolver cr = context.getContentResolver(); cr.notifyChange(CONVERSATIONS_URI, null); // Notify the widget the conversation list changed BugleWidgetProvider.notifyConversationListChanged(context); } /** * Build a conversation images uri from a conversation id. */ public static Uri buildConversationImagesUri(final String conversationId) { final Uri.Builder builder = CONVERSATION_IMAGES_URI.buildUpon(); builder.appendPath(conversationId); return builder.build(); } /** * Build a draft images uri from a conversation id. */ public static Uri buildDraftImagesUri(final String conversationId) { final Uri.Builder builder = DRAFT_IMAGES_URI.buildUpon(); builder.appendPath(conversationId); return builder.build(); } private DatabaseHelper mDatabaseHelper; private DatabaseWrapper mDatabaseWrapper; public MessagingContentProvider() { super(); } @VisibleForTesting public void setDatabaseForTest(final DatabaseWrapper db) { Assert.isTrue(BugleApplication.isRunningTests()); mDatabaseWrapper = db; } private DatabaseWrapper getDatabaseWrapper() { if (mDatabaseWrapper == null) { mDatabaseWrapper = mDatabaseHelper.getDatabase(); } return mDatabaseWrapper; } @Override public Cursor query(final Uri uri, final String[] projection, String selection, final String[] selectionArgs, String sortOrder) { // Processes other than self are allowed to temporarily access the media // scratch space; we grant uri read access on a case-by-case basis. Dialer app and // contacts app would doQuery() on the vCard uri before trying to open the inputStream. // There's nothing that we need to return for this uri so just No-Op. //if (isMediaScratchSpaceUri(uri)) { // return null; //} final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); String[] queryArgs = selectionArgs; final int match = sURIMatcher.match(uri); String groupBy = null; String limit = null; switch (match) { case CONVERSATIONS_QUERY_CODE: queryBuilder.setTables(ConversationListItemData.getConversationListView()); // Hide empty conversations (ones with 0 sort_timestamp) queryBuilder.appendWhere(ConversationColumns.SORT_TIMESTAMP + " > 0 "); break; case CONVERSATION_QUERY_CODE: queryBuilder.setTables(ConversationListItemData.getConversationListView()); if (uri.getPathSegments().size() == 2) { queryBuilder.appendWhere(ConversationColumns._ID + "=?"); // Get the conversation id from the uri queryArgs = prependArgs(queryArgs, uri.getPathSegments().get(1)); } else { throw new IllegalArgumentException("Malformed URI " + uri); } break; case CONVERSATION_PARTICIPANTS_QUERY_CODE: queryBuilder.setTables(DatabaseHelper.PARTICIPANTS_TABLE); if (uri.getPathSegments().size() == 3 && TextUtils.equals(uri.getPathSegments().get(1), "conversation")) { queryBuilder.appendWhere(ParticipantColumns._ID + " IN ( " + "SELECT " + ConversationParticipantsColumns.PARTICIPANT_ID + " AS " + ParticipantColumns._ID + " FROM " + DatabaseHelper.CONVERSATION_PARTICIPANTS_TABLE + " WHERE " + ConversationParticipantsColumns.CONVERSATION_ID + " =? UNION SELECT " + ParticipantColumns._ID + " FROM " + DatabaseHelper.PARTICIPANTS_TABLE + " WHERE " + ParticipantColumns.SUB_ID + " != " + ParticipantData.OTHER_THAN_SELF_SUB_ID + " )"); // Get the conversation id from the uri queryArgs = prependArgs(queryArgs, uri.getPathSegments().get(2)); } else { throw new IllegalArgumentException("Malformed URI " + uri); } break; case PARTICIPANTS_QUERY_CODE: queryBuilder.setTables(DatabaseHelper.PARTICIPANTS_TABLE); if (uri.getPathSegments().size() != 1) { throw new IllegalArgumentException("Malformed URI " + uri); } break; case CONVERSATION_MESSAGES_QUERY_CODE: if (uri.getPathSegments().size() == 3 && TextUtils.equals(uri.getPathSegments().get(1), "conversation")) { // Get the conversation id from the uri final String conversationId = uri.getPathSegments().get(2); // We need to handle this query differently, instead of falling through to the // generic query call at the bottom. For performance reasons, the conversation // messages query is executed as a raw query. It is invalid to specify // selection/sorting for this query. if (selection == null && selectionArgs == null && sortOrder == null) { return queryConversationMessages(conversationId, uri); } else { throw new IllegalArgumentException( "Cannot set selection or sort order with this query"); } } else { throw new IllegalArgumentException("Malformed URI " + uri); } case CONVERSATION_IMAGES_QUERY_CODE: queryBuilder.setTables(ConversationImagePartsView.getViewName()); if (uri.getPathSegments().size() == 2) { // Exclude draft. queryBuilder.appendWhere( ConversationImagePartsView.Columns.CONVERSATION_ID + " =? AND " + ConversationImagePartsView.Columns.STATUS + "<>" + MessageData.BUGLE_STATUS_OUTGOING_DRAFT); // Get the conversation id from the uri queryArgs = prependArgs(queryArgs, uri.getPathSegments().get(1)); } else { throw new IllegalArgumentException("Malformed URI " + uri); } break; case DRAFT_IMAGES_QUERY_CODE: queryBuilder.setTables(ConversationImagePartsView.getViewName()); if (uri.getPathSegments().size() == 2) { // Draft only. queryBuilder.appendWhere( ConversationImagePartsView.Columns.CONVERSATION_ID + " =? AND " + ConversationImagePartsView.Columns.STATUS + "=" + MessageData.BUGLE_STATUS_OUTGOING_DRAFT); // Get the conversation id from the uri queryArgs = prependArgs(queryArgs, uri.getPathSegments().get(1)); } else { throw new IllegalArgumentException("Malformed URI " + uri); } break; default: { throw new IllegalArgumentException("Unknown URI " + uri); } } final Cursor cursor = getDatabaseWrapper().query(queryBuilder, projection, selection, queryArgs, groupBy, null, sortOrder, limit); cursor.setNotificationUri(getContext().getContentResolver(), uri); return cursor; } private Cursor queryConversationMessages(final String conversationId, final Uri notifyUri) { final String[] queryArgs = { conversationId }; final Cursor cursor = getDatabaseWrapper().rawQuery( ConversationMessageData.getConversationMessagesQuerySql(), queryArgs); cursor.setNotificationUri(getContext().getContentResolver(), notifyUri); return cursor; } @Override public String getType(final Uri uri) { final StringBuilder sb = new StringBuilder("vnd.android.cursor.dir/vnd.android.messaging."); switch (sURIMatcher.match(uri)) { case CONVERSATIONS_QUERY_CODE: { sb.append(CONVERSATIONS_QUERY); break; } default: { throw new IllegalArgumentException("Unknown URI: " + uri); } } return sb.toString(); } protected DatabaseHelper getDatabase() { return DatabaseHelper.getInstance(getContext()); } @Override public ParcelFileDescriptor openFile(final Uri uri, final String fileMode) throws FileNotFoundException { throw new IllegalArgumentException("openFile not supported: " + uri); } @Override public Uri insert(final Uri uri, final ContentValues values) { throw new IllegalStateException("Insert not supported " + uri); } @Override public int delete(final Uri uri, final String selection, final String[] selectionArgs) { throw new IllegalArgumentException("Delete not supported: " + uri); } @Override public int update(final Uri uri, final ContentValues values, final String selection, final String[] selectionArgs) { throw new IllegalArgumentException("Update not supported: " + uri); } /** * Prepends new arguments to the existing argument list. * * @param oldArgList The current list of arguments. May be {@code null} * @param args The new arguments to prepend * @return A new argument list with the given arguments prepended */ private String[] prependArgs(final String[] oldArgList, final String... args) { if (args == null || args.length == 0) { return oldArgList; } final int oldArgCount = (oldArgList == null ? 0 : oldArgList.length); final int newArgCount = args.length; final String[] newArgs = new String[oldArgCount + newArgCount]; System.arraycopy(args, 0, newArgs, 0, newArgCount); if (oldArgCount > 0) { System.arraycopy(oldArgList, 0, newArgs, newArgCount, oldArgCount); } return newArgs; } /** * {@inheritDoc} */ @Override public void dump(final FileDescriptor fd, final PrintWriter writer, final String[] args) { // First dump out the default SMS app package name String defaultSmsApp = PhoneUtils.getDefault().getDefaultSmsApp(); if (TextUtils.isEmpty(defaultSmsApp)) { if (OsUtil.isAtLeastKLP()) { defaultSmsApp = "None"; } else { defaultSmsApp = "None (pre-Kitkat)"; } } writer.println("Default SMS app: " + defaultSmsApp); // Now dump logs LogUtil.dump(writer); } @Override public boolean onCreate() { // This is going to wind up calling into createDatabase() below. mDatabaseHelper = (DatabaseHelper) getDatabase(); // We cannot initialize mDatabaseWrapper yet as the Factory may not be initialized return true; } }