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