summaryrefslogtreecommitdiffstats
path: root/src/com/android/providers/contacts/SocialProvider.java
blob: 349e1fc5abcf5bd6b6baf0a4d7d505f5116d9eed (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
/*
 * Copyright (C) 2009 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.providers.contacts;

import com.android.providers.contacts.ContactsDatabaseHelper.ActivitiesColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.PackagesColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.Tables;

import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.provider.BaseColumns;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.RawContacts;
import android.provider.SocialContract;
import android.provider.SocialContract.Activities;

import android.net.Uri;

import java.util.ArrayList;
import java.util.HashMap;

/**
 * Social activity content provider. The contract between this provider and
 * applications is defined in {@link SocialContract}.
 */
public class SocialProvider extends ContentProvider {
    // TODO: clean up debug tag
    private static final String TAG = "SocialProvider ~~~~";

    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    private static final int ACTIVITIES = 1000;
    private static final int ACTIVITIES_ID = 1001;
    private static final int ACTIVITIES_AUTHORED_BY = 1002;

    private static final int CONTACT_STATUS_ID = 3000;

    private static final String DEFAULT_SORT_ORDER = Activities.THREAD_PUBLISHED + " DESC, "
            + Activities.PUBLISHED + " ASC";

    /** Contains just the contacts columns */
    private static final HashMap<String, String> sContactsProjectionMap;
    /** Contains just the contacts columns */
    private static final HashMap<String, String> sRawContactsProjectionMap;
    /** Contains just the activities columns */
    private static final HashMap<String, String> sActivitiesProjectionMap;

    /** Contains the activities, raw contacts, and contacts columns, for joined tables */
    private static final HashMap<String, String> sActivitiesContactsProjectionMap;

    static {
        // Contacts URI matching table
        final UriMatcher matcher = sUriMatcher;

        matcher.addURI(SocialContract.AUTHORITY, "activities", ACTIVITIES);
        matcher.addURI(SocialContract.AUTHORITY, "activities/#", ACTIVITIES_ID);
        matcher.addURI(SocialContract.AUTHORITY, "activities/authored_by/#", ACTIVITIES_AUTHORED_BY);

        matcher.addURI(SocialContract.AUTHORITY, "contact_status/#", CONTACT_STATUS_ID);

        HashMap<String, String> columns;

        // Contacts projection map
        columns = new HashMap<String, String>();
        columns.put(Contacts.DISPLAY_NAME, ContactsColumns.CONCRETE_DISPLAY_NAME + " AS "
                + Contacts.DISPLAY_NAME);
        sContactsProjectionMap = columns;

        // Contacts projection map
        columns = new HashMap<String, String>();
        columns.put(RawContacts._ID, Tables.RAW_CONTACTS + "." + RawContacts._ID + " AS _id");
        columns.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
        sRawContactsProjectionMap = columns;

        // Activities projection map
        columns = new HashMap<String, String>();
        columns.put(Activities._ID, "activities._id AS _id");
        columns.put(Activities.RES_PACKAGE, PackagesColumns.PACKAGE + " AS "
                + Activities.RES_PACKAGE);
        columns.put(Activities.MIMETYPE, Activities.MIMETYPE);
        columns.put(Activities.RAW_ID, Activities.RAW_ID);
        columns.put(Activities.IN_REPLY_TO, Activities.IN_REPLY_TO);
        columns.put(Activities.AUTHOR_CONTACT_ID, Activities.AUTHOR_CONTACT_ID);
        columns.put(Activities.TARGET_CONTACT_ID, Activities.TARGET_CONTACT_ID);
        columns.put(Activities.PUBLISHED, Activities.PUBLISHED);
        columns.put(Activities.THREAD_PUBLISHED, Activities.THREAD_PUBLISHED);
        columns.put(Activities.TITLE, Activities.TITLE);
        columns.put(Activities.SUMMARY, Activities.SUMMARY);
        columns.put(Activities.LINK, Activities.LINK);
        columns.put(Activities.THUMBNAIL, Activities.THUMBNAIL);
        sActivitiesProjectionMap = columns;

        // Activities, raw contacts, and contacts projection map for joins
        columns = new HashMap<String, String>();
        columns.putAll(sContactsProjectionMap);
        columns.putAll(sRawContactsProjectionMap);
        columns.putAll(sActivitiesProjectionMap); // Final _id will be from Activities
        sActivitiesContactsProjectionMap = columns;

    }

    private ContactsDatabaseHelper mDbHelper;

    /** {@inheritDoc} */
    @Override
    public boolean onCreate() {
        final Context context = getContext();
        mDbHelper = ContactsDatabaseHelper.getInstance(context);

        // TODO remove this, it's here to force opening the database on boot for testing
        mDbHelper.getReadableDatabase();

        return true;
    }

    /**
     * Called when a change has been made.
     *
     * @param uri the uri that the change was made to
     */
    private void onChange(Uri uri) {
        getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null);
    }

    /** {@inheritDoc} */
    @Override
    public boolean isTemporary() {
        return false;
    }

    /** {@inheritDoc} */
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        final int match = sUriMatcher.match(uri);
        long id = 0;
        switch (match) {
            case ACTIVITIES: {
                id = insertActivity(values);
                break;
            }

            default:
                throw new UnsupportedOperationException("Unknown uri: " + uri);
        }

        final Uri result = ContentUris.withAppendedId(Activities.CONTENT_URI, id);
        onChange(result);
        return result;
    }

    /**
     * Inserts an item into the {@link Tables#ACTIVITIES} table.
     *
     * @param values the values for the new row
     * @return the row ID of the newly created row
     */
    private long insertActivity(ContentValues values) {

        // TODO verify that IN_REPLY_TO != RAW_ID

        final SQLiteDatabase db = mDbHelper.getWritableDatabase();
        long id = 0;
        db.beginTransaction();
        try {
            // TODO: Consider enforcing Binder.getCallingUid() for package name
            // requested by this insert.

            // Replace package name and mime-type with internal mappings
            final String packageName = values.getAsString(Activities.RES_PACKAGE);
            if (packageName != null) {
                values.put(ActivitiesColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
            }
            values.remove(Activities.RES_PACKAGE);

            final String mimeType = values.getAsString(Activities.MIMETYPE);
            values.put(ActivitiesColumns.MIMETYPE_ID, mDbHelper.getMimeTypeId(mimeType));
            values.remove(Activities.MIMETYPE);

            long published = values.getAsLong(Activities.PUBLISHED);
            long threadPublished = published;

            String inReplyTo = values.getAsString(Activities.IN_REPLY_TO);
            if (inReplyTo != null) {
                threadPublished = getThreadPublished(db, inReplyTo, published);
            }

            values.put(Activities.THREAD_PUBLISHED, threadPublished);

            // Insert the data row itself
            id = db.insert(Tables.ACTIVITIES, Activities.RAW_ID, values);

            // Adjust thread timestamps on replies that have already been inserted
            if (values.containsKey(Activities.RAW_ID)) {
                adjustReplyTimestamps(db, values.getAsString(Activities.RAW_ID), published);
            }

            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }
        return id;
    }

    /**
     * Finds the timestamp of the original message in the thread. If not found, returns
     * {@code defaultValue}.
     */
    private long getThreadPublished(SQLiteDatabase db, String rawId, long defaultValue) {
        String inReplyTo = null;
        long threadPublished = defaultValue;

        final Cursor c = db.query(Tables.ACTIVITIES,
                new String[]{Activities.IN_REPLY_TO, Activities.PUBLISHED},
                Activities.RAW_ID + " = ?", new String[]{rawId}, null, null, null);
        try {
            if (c.moveToFirst()) {
                inReplyTo = c.getString(0);
                threadPublished = c.getLong(1);
            }
        } finally {
            c.close();
        }

        if (inReplyTo != null) {

            // Call recursively to obtain the original timestamp of the entire thread
            return getThreadPublished(db, inReplyTo, threadPublished);
        }

        return threadPublished;
    }

    /**
     * In case the original message of a thread arrives after its reply messages, we need
     * to check if there are any replies in the database and if so adjust their thread_published.
     */
    private void adjustReplyTimestamps(SQLiteDatabase db, String inReplyTo, long threadPublished) {

        ContentValues values = new ContentValues();
        values.put(Activities.THREAD_PUBLISHED, threadPublished);

        /*
         * Issuing an exploratory update. If it updates nothing, we are done.  Otherwise,
         * we will run a query to find the updated records again and repeat recursively.
         */
        int replies = db.update(Tables.ACTIVITIES, values,
                Activities.IN_REPLY_TO + "= ?", new String[] {inReplyTo});

        if (replies == 0) {
            return;
        }

        /*
         * Presumably this code will be executed very infrequently since messages tend to arrive
         * in the order they get sent.
         */
        ArrayList<String> rawIds = new ArrayList<String>(replies);
        final Cursor c = db.query(Tables.ACTIVITIES,
                new String[]{Activities.RAW_ID},
                Activities.IN_REPLY_TO + " = ?", new String[] {inReplyTo}, null, null, null);
        try {
            while (c.moveToNext()) {
                rawIds.add(c.getString(0));
            }
        } finally {
            c.close();
        }

        for (String rawId : rawIds) {
            adjustReplyTimestamps(db, rawId, threadPublished);
        }
    }

    /** {@inheritDoc} */
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        final SQLiteDatabase db = mDbHelper.getWritableDatabase();

        final int match = sUriMatcher.match(uri);
        switch (match) {
            case ACTIVITIES_ID: {
                final long activityId = ContentUris.parseId(uri);
                return db.delete(Tables.ACTIVITIES, Activities._ID + "=" + activityId, null);
            }

            case ACTIVITIES_AUTHORED_BY: {
                final long contactId = ContentUris.parseId(uri);
                return db.delete(Tables.ACTIVITIES, Activities.AUTHOR_CONTACT_ID + "=" + contactId, null);
            }

            default:
                throw new UnsupportedOperationException("Unknown uri: " + uri);
        }
    }

    /** {@inheritDoc} */
    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        throw new UnsupportedOperationException();
    }

    /** {@inheritDoc} */
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
            String sortOrder) {
        final SQLiteDatabase db = mDbHelper.getReadableDatabase();
        final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
        String limit = null;

        final int match = sUriMatcher.match(uri);
        switch (match) {
            case ACTIVITIES: {
                qb.setTables(Tables.ACTIVITIES_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS);
                qb.setProjectionMap(sActivitiesContactsProjectionMap);
                break;
            }

            case ACTIVITIES_ID: {
                // TODO: enforce that caller has read access to this data
                long activityId = ContentUris.parseId(uri);
                qb.setTables(Tables.ACTIVITIES_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS);
                qb.setProjectionMap(sActivitiesContactsProjectionMap);
                qb.appendWhere(Activities._ID + "=" + activityId);
                break;
            }

            case ACTIVITIES_AUTHORED_BY: {
                long contactId = ContentUris.parseId(uri);
                qb.setTables(Tables.ACTIVITIES_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS);
                qb.setProjectionMap(sActivitiesContactsProjectionMap);
                qb.appendWhere(Activities.AUTHOR_CONTACT_ID + "=" + contactId);
                break;
            }

            case CONTACT_STATUS_ID: {
                long aggId = ContentUris.parseId(uri);
                qb.setTables(Tables.ACTIVITIES_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS);
                qb.setProjectionMap(sActivitiesContactsProjectionMap);

                // Latest status of a contact is any top-level status
                // authored by one of its children contacts.
                qb.appendWhere(Activities.IN_REPLY_TO + " IS NULL AND ");
                qb.appendWhere(Activities.AUTHOR_CONTACT_ID + " IN (SELECT " + BaseColumns._ID
                        + " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts.CONTACT_ID + "="
                        + aggId + ")");
                sortOrder = Activities.PUBLISHED + " DESC";
                limit = "1";
                break;
            }

            default:
                throw new UnsupportedOperationException("Unknown uri: " + uri);
        }

        // Default to reverse-chronological sort if nothing requested
        if (sortOrder == null) {
            sortOrder = DEFAULT_SORT_ORDER;
        }

        // Perform the query and set the notification uri
        final Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder, limit);
        if (c != null) {
            c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI);
        }
        return c;
    }

    @Override
    public String getType(Uri uri) {
        final int match = sUriMatcher.match(uri);
        switch (match) {
            case ACTIVITIES:
            case ACTIVITIES_AUTHORED_BY:
                return Activities.CONTENT_TYPE;
            case ACTIVITIES_ID:
                final SQLiteDatabase db = mDbHelper.getReadableDatabase();
                long activityId = ContentUris.parseId(uri);
                return mDbHelper.getActivityMimeType(activityId);
            case CONTACT_STATUS_ID:
                return Contacts.CONTENT_ITEM_TYPE;
        }
        throw new UnsupportedOperationException("Unknown uri: " + uri);
    }
}