Home | History | Annotate | Download | only in datamodel
      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      */
    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      */
    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 
    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 
    133     public static void notifyAllMessagesChanged() {
    134         final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver();
    135         cr.notifyChange(CONVERSATION_MESSAGES_URI, null);
    136     }
    137 
    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 {
    159         sURIMatcher.addURI(AUTHORITY, CONVERSATIONS_QUERY, CONVERSATIONS_QUERY_CODE);
    160         sURIMatcher.addURI(AUTHORITY, CONVERSATIONS_QUERY + "/*", CONVERSATION_QUERY_CODE);
    161         sURIMatcher.addURI(AUTHORITY, MESSAGES_QUERY + "/conversation/*",
    162                 CONVERSATION_MESSAGES_QUERY_CODE);
    163         sURIMatcher.addURI(AUTHORITY, PARTICIPANTS_QUERY + "/conversation/*",
    164                 CONVERSATION_PARTICIPANTS_QUERY_CODE);
    165         sURIMatcher.addURI(AUTHORITY, PARTICIPANTS_QUERY, PARTICIPANTS_QUERY_CODE);
    166         sURIMatcher.addURI(AUTHORITY, CONVERSATION_IMAGES_QUERY + "/*",
    167                 CONVERSATION_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      */
    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 
    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      */
    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 
    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 
    208     public static void notifyPartsChanged() {
    209         final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver();
    210         cr.notifyChange(PARTS_URI, null);
    211     }
    212 
    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      */
    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      */
    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 
    243     public MessagingContentProvider() {
    244         super();
    245     }
    246 
    247     @VisibleForTesting
    248     public void setDatabaseForTest(final DatabaseWrapper db) {
    249         Assert.isTrue(BugleApplication.isRunningTests());
    250         mDatabaseWrapper = db;
    251     }
    252 
    253     private DatabaseWrapper getDatabaseWrapper() {
    254         if (mDatabaseWrapper == null) {
    255             mDatabaseWrapper = mDatabaseHelper.getDatabase();
    256         }
    257         return mDatabaseWrapper;
    258     }
    259 
    260     @Override
    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 
    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
    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 
    403     protected DatabaseHelper getDatabase() {
    404         return DatabaseHelper.getInstance(getContext());
    405     }
    406 
    407     @Override
    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
    414     public Uri insert(final Uri uri, final ContentValues values) {
    415         throw new IllegalStateException("Insert not supported " + uri);
    416     }
    417 
    418     @Override
    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
    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      */
    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
    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
    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