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 */
16package com.android.messaging.datamodel;
17
18import android.content.ContentProvider;
19import android.content.ContentResolver;
20import android.content.ContentValues;
21import android.content.Context;
22import android.content.UriMatcher;
23import android.database.Cursor;
24import android.database.sqlite.SQLiteQueryBuilder;
25import android.net.Uri;
26import android.os.ParcelFileDescriptor;
27import android.text.TextUtils;
28
29import com.android.messaging.BugleApplication;
30import com.android.messaging.Factory;
31import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns;
32import com.android.messaging.datamodel.DatabaseHelper.ConversationParticipantsColumns;
33import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
34import com.android.messaging.datamodel.data.ConversationListItemData;
35import com.android.messaging.datamodel.data.ConversationMessageData;
36import com.android.messaging.datamodel.data.MessageData;
37import com.android.messaging.datamodel.data.ParticipantData;
38import com.android.messaging.util.Assert;
39import com.android.messaging.util.LogUtil;
40import com.android.messaging.util.OsUtil;
41import com.android.messaging.util.PhoneUtils;
42import com.android.messaging.widget.BugleWidgetProvider;
43import com.android.messaging.widget.WidgetConversationProvider;
44import com.google.common.annotations.VisibleForTesting;
45
46import java.io.FileDescriptor;
47import java.io.FileNotFoundException;
48import java.io.PrintWriter;
49
50/**
51 * A centralized provider for Uris exposed by Bugle.
52 *  */
53public 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