1/*
2 * Copyright (C) 2009 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
17package com.android.providers.contacts;
18
19import static com.android.providers.contacts.util.DbQueryUtils.checkForSupportedColumns;
20import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause;
21import static com.android.providers.contacts.util.DbQueryUtils.getInequalityClause;
22
23import android.app.AppOpsManager;
24import android.content.ContentProvider;
25import android.content.ContentUris;
26import android.content.ContentValues;
27import android.content.Context;
28import android.content.UriMatcher;
29import android.database.Cursor;
30import android.database.DatabaseUtils;
31import android.database.sqlite.SQLiteDatabase;
32import android.database.sqlite.SQLiteQueryBuilder;
33import android.net.Uri;
34import android.provider.CallLog;
35import android.provider.CallLog.Calls;
36import android.text.TextUtils;
37import android.util.Log;
38
39import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
40import com.android.providers.contacts.util.SelectionBuilder;
41import com.google.common.annotations.VisibleForTesting;
42
43import java.util.HashMap;
44import java.util.List;
45
46/**
47 * Call log content provider.
48 */
49public class CallLogProvider extends ContentProvider {
50    /** Selection clause to use to exclude voicemail records.  */
51    private static final String EXCLUDE_VOICEMAIL_SELECTION = getInequalityClause(
52            Calls.TYPE, Calls.VOICEMAIL_TYPE);
53
54    private static final int CALLS = 1;
55
56    private static final int CALLS_ID = 2;
57
58    private static final int CALLS_FILTER = 3;
59
60    private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
61    static {
62        sURIMatcher.addURI(CallLog.AUTHORITY, "calls", CALLS);
63        sURIMatcher.addURI(CallLog.AUTHORITY, "calls/#", CALLS_ID);
64        sURIMatcher.addURI(CallLog.AUTHORITY, "calls/filter/*", CALLS_FILTER);
65    }
66
67    private static final HashMap<String, String> sCallsProjectionMap;
68    static {
69
70        // Calls projection map
71        sCallsProjectionMap = new HashMap<String, String>();
72        sCallsProjectionMap.put(Calls._ID, Calls._ID);
73        sCallsProjectionMap.put(Calls.NUMBER, Calls.NUMBER);
74        sCallsProjectionMap.put(Calls.NUMBER_PRESENTATION, Calls.NUMBER_PRESENTATION);
75        sCallsProjectionMap.put(Calls.DATE, Calls.DATE);
76        sCallsProjectionMap.put(Calls.DURATION, Calls.DURATION);
77        sCallsProjectionMap.put(Calls.TYPE, Calls.TYPE);
78        sCallsProjectionMap.put(Calls.NEW, Calls.NEW);
79        sCallsProjectionMap.put(Calls.VOICEMAIL_URI, Calls.VOICEMAIL_URI);
80        sCallsProjectionMap.put(Calls.IS_READ, Calls.IS_READ);
81        sCallsProjectionMap.put(Calls.CACHED_NAME, Calls.CACHED_NAME);
82        sCallsProjectionMap.put(Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_TYPE);
83        sCallsProjectionMap.put(Calls.CACHED_NUMBER_LABEL, Calls.CACHED_NUMBER_LABEL);
84        sCallsProjectionMap.put(Calls.COUNTRY_ISO, Calls.COUNTRY_ISO);
85        sCallsProjectionMap.put(Calls.GEOCODED_LOCATION, Calls.GEOCODED_LOCATION);
86        sCallsProjectionMap.put(Calls.CACHED_LOOKUP_URI, Calls.CACHED_LOOKUP_URI);
87        sCallsProjectionMap.put(Calls.CACHED_MATCHED_NUMBER, Calls.CACHED_MATCHED_NUMBER);
88        sCallsProjectionMap.put(Calls.CACHED_NORMALIZED_NUMBER, Calls.CACHED_NORMALIZED_NUMBER);
89        sCallsProjectionMap.put(Calls.CACHED_PHOTO_ID, Calls.CACHED_PHOTO_ID);
90        sCallsProjectionMap.put(Calls.CACHED_FORMATTED_NUMBER, Calls.CACHED_FORMATTED_NUMBER);
91    }
92
93    private ContactsDatabaseHelper mDbHelper;
94    private DatabaseUtils.InsertHelper mCallsInserter;
95    private boolean mUseStrictPhoneNumberComparation;
96    private VoicemailPermissions mVoicemailPermissions;
97    private CallLogInsertionHelper mCallLogInsertionHelper;
98
99    @Override
100    public boolean onCreate() {
101        setAppOps(AppOpsManager.OP_READ_CALL_LOG, AppOpsManager.OP_WRITE_CALL_LOG);
102        if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
103            Log.d(Constants.PERFORMANCE_TAG, "CallLogProvider.onCreate start");
104        }
105        final Context context = getContext();
106        mDbHelper = getDatabaseHelper(context);
107        mUseStrictPhoneNumberComparation =
108            context.getResources().getBoolean(
109                    com.android.internal.R.bool.config_use_strict_phone_number_comparation);
110        mVoicemailPermissions = new VoicemailPermissions(context);
111        mCallLogInsertionHelper = createCallLogInsertionHelper(context);
112        if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
113            Log.d(Constants.PERFORMANCE_TAG, "CallLogProvider.onCreate finish");
114        }
115        return true;
116    }
117
118    @VisibleForTesting
119    protected CallLogInsertionHelper createCallLogInsertionHelper(final Context context) {
120        return DefaultCallLogInsertionHelper.getInstance(context);
121    }
122
123    @VisibleForTesting
124    protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
125        return ContactsDatabaseHelper.getInstance(context);
126    }
127
128    @Override
129    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
130            String sortOrder) {
131        final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
132        qb.setTables(Tables.CALLS);
133        qb.setProjectionMap(sCallsProjectionMap);
134        qb.setStrict(true);
135
136        final SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
137        checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder);
138
139        final int match = sURIMatcher.match(uri);
140        switch (match) {
141            case CALLS:
142                break;
143
144            case CALLS_ID: {
145                selectionBuilder.addClause(getEqualityClause(Calls._ID,
146                        parseCallIdFromUri(uri)));
147                break;
148            }
149
150            case CALLS_FILTER: {
151                List<String> pathSegments = uri.getPathSegments();
152                String phoneNumber = pathSegments.size() >= 2 ? pathSegments.get(2) : null;
153                if (!TextUtils.isEmpty(phoneNumber)) {
154                    qb.appendWhere("PHONE_NUMBERS_EQUAL(number, ");
155                    qb.appendWhereEscapeString(phoneNumber);
156                    qb.appendWhere(mUseStrictPhoneNumberComparation ? ", 1)" : ", 0)");
157                } else {
158                    qb.appendWhere(Calls.NUMBER_PRESENTATION + "!="
159                            + Calls.PRESENTATION_ALLOWED);
160                }
161                break;
162            }
163
164            default:
165                throw new IllegalArgumentException("Unknown URL " + uri);
166        }
167
168        final int limit = getIntParam(uri, Calls.LIMIT_PARAM_KEY, 0);
169        final int offset = getIntParam(uri, Calls.OFFSET_PARAM_KEY, 0);
170        String limitClause = null;
171        if (limit > 0) {
172            limitClause = offset + "," + limit;
173        }
174
175        final SQLiteDatabase db = mDbHelper.getReadableDatabase();
176        final Cursor c = qb.query(db, projection, selectionBuilder.build(), selectionArgs, null,
177                null, sortOrder, limitClause);
178        if (c != null) {
179            c.setNotificationUri(getContext().getContentResolver(), CallLog.CONTENT_URI);
180        }
181        return c;
182    }
183
184    /**
185     * Gets an integer query parameter from a given uri.
186     *
187     * @param uri The uri to extract the query parameter from.
188     * @param key The query parameter key.
189     * @param defaultValue A default value to return if the query parameter does not exist.
190     * @return The value from the query parameter in the Uri.  Or the default value if the parameter
191     * does not exist in the uri.
192     * @throws IllegalArgumentException when the value in the query parameter is not an integer.
193     */
194    private int getIntParam(Uri uri, String key, int defaultValue) {
195        String valueString = uri.getQueryParameter(key);
196        if (valueString == null) {
197            return defaultValue;
198        }
199
200        try {
201            return Integer.parseInt(valueString);
202        } catch (NumberFormatException e) {
203            String msg = "Integer required for " + key + " parameter but value '" + valueString +
204                    "' was found instead.";
205            throw new IllegalArgumentException(msg, e);
206        }
207    }
208
209    @Override
210    public String getType(Uri uri) {
211        int match = sURIMatcher.match(uri);
212        switch (match) {
213            case CALLS:
214                return Calls.CONTENT_TYPE;
215            case CALLS_ID:
216                return Calls.CONTENT_ITEM_TYPE;
217            case CALLS_FILTER:
218                return Calls.CONTENT_TYPE;
219            default:
220                throw new IllegalArgumentException("Unknown URI: " + uri);
221        }
222    }
223
224    @Override
225    public Uri insert(Uri uri, ContentValues values) {
226        checkForSupportedColumns(sCallsProjectionMap, values);
227        // Inserting a voicemail record through call_log requires the voicemail
228        // permission and also requires the additional voicemail param set.
229        if (hasVoicemailValue(values)) {
230            checkIsAllowVoicemailRequest(uri);
231            mVoicemailPermissions.checkCallerHasFullAccess();
232        }
233        if (mCallsInserter == null) {
234            SQLiteDatabase db = mDbHelper.getWritableDatabase();
235            mCallsInserter = new DatabaseUtils.InsertHelper(db, Tables.CALLS);
236        }
237
238        ContentValues copiedValues = new ContentValues(values);
239
240        // Add the computed fields to the copied values.
241        mCallLogInsertionHelper.addComputedValues(copiedValues);
242
243        long rowId = getDatabaseModifier(mCallsInserter).insert(copiedValues);
244        if (rowId > 0) {
245            return ContentUris.withAppendedId(uri, rowId);
246        }
247        return null;
248    }
249
250    @Override
251    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
252        checkForSupportedColumns(sCallsProjectionMap, values);
253        // Request that involves changing record type to voicemail requires the
254        // voicemail param set in the uri.
255        if (hasVoicemailValue(values)) {
256            checkIsAllowVoicemailRequest(uri);
257        }
258
259        SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
260        checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder);
261
262        final SQLiteDatabase db = mDbHelper.getWritableDatabase();
263        final int matchedUriId = sURIMatcher.match(uri);
264        switch (matchedUriId) {
265            case CALLS:
266                break;
267
268            case CALLS_ID:
269                selectionBuilder.addClause(getEqualityClause(Calls._ID, parseCallIdFromUri(uri)));
270                break;
271
272            default:
273                throw new UnsupportedOperationException("Cannot update URL: " + uri);
274        }
275
276        return getDatabaseModifier(db).update(Tables.CALLS, values, selectionBuilder.build(),
277                selectionArgs);
278    }
279
280    @Override
281    public int delete(Uri uri, String selection, String[] selectionArgs) {
282        SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
283        checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder);
284
285        final SQLiteDatabase db = mDbHelper.getWritableDatabase();
286        final int matchedUriId = sURIMatcher.match(uri);
287        switch (matchedUriId) {
288            case CALLS:
289                return getDatabaseModifier(db).delete(Tables.CALLS,
290                        selectionBuilder.build(), selectionArgs);
291            default:
292                throw new UnsupportedOperationException("Cannot delete that URL: " + uri);
293        }
294    }
295
296    // Work around to let the test code override the context. getContext() is final so cannot be
297    // overridden.
298    protected Context context() {
299        return getContext();
300    }
301
302    /**
303     * Returns a {@link DatabaseModifier} that takes care of sending necessary notifications
304     * after the operation is performed.
305     */
306    private DatabaseModifier getDatabaseModifier(SQLiteDatabase db) {
307        return new DbModifierWithNotification(Tables.CALLS, db, context());
308    }
309
310    /**
311     * Same as {@link #getDatabaseModifier(SQLiteDatabase)} but used for insert helper operations
312     * only.
313     */
314    private DatabaseModifier getDatabaseModifier(DatabaseUtils.InsertHelper insertHelper) {
315        return new DbModifierWithNotification(Tables.CALLS, insertHelper, context());
316    }
317
318    private static final Integer VOICEMAIL_TYPE = new Integer(Calls.VOICEMAIL_TYPE);
319    private boolean hasVoicemailValue(ContentValues values) {
320        return VOICEMAIL_TYPE.equals(values.getAsInteger(Calls.TYPE));
321    }
322
323    /**
324     * Checks if the supplied uri requests to include voicemails and take appropriate
325     * action.
326     * <p> If voicemail is requested, then check for voicemail permissions. Otherwise
327     * modify the selection to restrict to non-voicemail entries only.
328     */
329    private void checkVoicemailPermissionAndAddRestriction(Uri uri,
330            SelectionBuilder selectionBuilder) {
331        if (isAllowVoicemailRequest(uri)) {
332            mVoicemailPermissions.checkCallerHasFullAccess();
333        } else {
334            selectionBuilder.addClause(EXCLUDE_VOICEMAIL_SELECTION);
335        }
336    }
337
338    /**
339     * Determines if the supplied uri has the request to allow voicemails to be
340     * included.
341     */
342    private boolean isAllowVoicemailRequest(Uri uri) {
343        return uri.getBooleanQueryParameter(Calls.ALLOW_VOICEMAILS_PARAM_KEY, false);
344    }
345
346    /**
347     * Checks to ensure that the given uri has allow_voicemail set. Used by
348     * insert and update operations to check that ContentValues with voicemail
349     * call type must use the voicemail uri.
350     * @throws IllegalArgumentException if allow_voicemail is not set.
351     */
352    private void checkIsAllowVoicemailRequest(Uri uri) {
353        if (!isAllowVoicemailRequest(uri)) {
354            throw new IllegalArgumentException(
355                    String.format("Uri %s cannot be used for voicemail record." +
356                            " Please set '%s=true' in the uri.", uri,
357                            Calls.ALLOW_VOICEMAILS_PARAM_KEY));
358        }
359    }
360
361   /**
362    * Parses the call Id from the given uri, assuming that this is a uri that
363    * matches CALLS_ID. For other uri types the behaviour is undefined.
364    * @throws IllegalArgumentException if the id included in the Uri is not a valid long value.
365    */
366    private long parseCallIdFromUri(Uri uri) {
367        try {
368            return Long.parseLong(uri.getPathSegments().get(1));
369        } catch (NumberFormatException e) {
370            throw new IllegalArgumentException("Invalid call id in uri: " + uri, e);
371        }
372    }
373}
374