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