• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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  
17  package com.android.providers.contacts;
18  
19  import static com.android.providers.contacts.util.DbQueryUtils.checkForSupportedColumns;
20  import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause;
21  import static com.android.providers.contacts.util.DbQueryUtils.getInequalityClause;
22  
23  import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
24  import com.android.providers.contacts.util.SelectionBuilder;
25  import com.google.common.annotations.VisibleForTesting;
26  
27  import android.content.ContentProvider;
28  import android.content.ContentUris;
29  import android.content.ContentValues;
30  import android.content.Context;
31  import android.content.UriMatcher;
32  import android.database.Cursor;
33  import android.database.DatabaseUtils;
34  import android.database.sqlite.SQLiteDatabase;
35  import android.database.sqlite.SQLiteQueryBuilder;
36  import android.net.Uri;
37  import android.provider.CallLog;
38  import android.provider.CallLog.Calls;
39  import android.util.Log;
40  
41  import java.util.HashMap;
42  
43  /**
44   * Call log content provider.
45   */
46  public 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, Integer.toString(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 {
sURIMatcher.addURI(CallLog.AUTHORITY, "calls", CALLS)59          sURIMatcher.addURI(CallLog.AUTHORITY, "calls", CALLS);
sURIMatcher.addURI(CallLog.AUTHORITY, "calls/#", CALLS_ID)60          sURIMatcher.addURI(CallLog.AUTHORITY, "calls/#", CALLS_ID);
sURIMatcher.addURI(CallLog.AUTHORITY, "calls/filter/*", CALLS_FILTER)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>();
sCallsProjectionMap.put(Calls._ID, Calls._ID)69          sCallsProjectionMap.put(Calls._ID, Calls._ID);
sCallsProjectionMap.put(Calls.NUMBER, Calls.NUMBER)70          sCallsProjectionMap.put(Calls.NUMBER, Calls.NUMBER);
sCallsProjectionMap.put(Calls.DATE, Calls.DATE)71          sCallsProjectionMap.put(Calls.DATE, Calls.DATE);
sCallsProjectionMap.put(Calls.DURATION, Calls.DURATION)72          sCallsProjectionMap.put(Calls.DURATION, Calls.DURATION);
sCallsProjectionMap.put(Calls.TYPE, Calls.TYPE)73          sCallsProjectionMap.put(Calls.TYPE, Calls.TYPE);
sCallsProjectionMap.put(Calls.NEW, Calls.NEW)74          sCallsProjectionMap.put(Calls.NEW, Calls.NEW);
sCallsProjectionMap.put(Calls.VOICEMAIL_URI, Calls.VOICEMAIL_URI)75          sCallsProjectionMap.put(Calls.VOICEMAIL_URI, Calls.VOICEMAIL_URI);
sCallsProjectionMap.put(Calls.IS_READ, Calls.IS_READ)76          sCallsProjectionMap.put(Calls.IS_READ, Calls.IS_READ);
sCallsProjectionMap.put(Calls.CACHED_NAME, Calls.CACHED_NAME)77          sCallsProjectionMap.put(Calls.CACHED_NAME, Calls.CACHED_NAME);
sCallsProjectionMap.put(Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_TYPE)78          sCallsProjectionMap.put(Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_TYPE);
sCallsProjectionMap.put(Calls.CACHED_NUMBER_LABEL, Calls.CACHED_NUMBER_LABEL)79          sCallsProjectionMap.put(Calls.CACHED_NUMBER_LABEL, Calls.CACHED_NUMBER_LABEL);
sCallsProjectionMap.put(Calls.COUNTRY_ISO, Calls.COUNTRY_ISO)80          sCallsProjectionMap.put(Calls.COUNTRY_ISO, Calls.COUNTRY_ISO);
sCallsProjectionMap.put(Calls.GEOCODED_LOCATION, Calls.GEOCODED_LOCATION)81          sCallsProjectionMap.put(Calls.GEOCODED_LOCATION, Calls.GEOCODED_LOCATION);
sCallsProjectionMap.put(Calls.CACHED_LOOKUP_URI, Calls.CACHED_LOOKUP_URI)82          sCallsProjectionMap.put(Calls.CACHED_LOOKUP_URI, Calls.CACHED_LOOKUP_URI);
sCallsProjectionMap.put(Calls.CACHED_MATCHED_NUMBER, Calls.CACHED_MATCHED_NUMBER)83          sCallsProjectionMap.put(Calls.CACHED_MATCHED_NUMBER, Calls.CACHED_MATCHED_NUMBER);
sCallsProjectionMap.put(Calls.CACHED_NORMALIZED_NUMBER, Calls.CACHED_NORMALIZED_NUMBER)84          sCallsProjectionMap.put(Calls.CACHED_NORMALIZED_NUMBER, Calls.CACHED_NORMALIZED_NUMBER);
sCallsProjectionMap.put(Calls.CACHED_PHOTO_ID, Calls.CACHED_PHOTO_ID)85          sCallsProjectionMap.put(Calls.CACHED_PHOTO_ID, Calls.CACHED_PHOTO_ID);
sCallsProjectionMap.put(Calls.CACHED_FORMATTED_NUMBER, Calls.CACHED_FORMATTED_NUMBER)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
onCreate()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
createCallLogInsertionHelper(final Context context)114      protected CallLogInsertionHelper createCallLogInsertionHelper(final Context context) {
115          return DefaultCallLogInsertionHelper.getInstance(context);
116      }
117  
118      @VisibleForTesting
getDatabaseHelper(final Context context)119      protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
120          return ContactsDatabaseHelper.getInstance(context);
121      }
122  
123      @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)124      public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
125              String sortOrder) {
126          SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
127          qb.setTables(Tables.CALLS);
128          qb.setProjectionMap(sCallsProjectionMap);
129          qb.setStrict(true);
130  
131          SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
132          checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder);
133  
134          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 SQLiteDatabase db = mDbHelper.getReadableDatabase();
158          Cursor c = qb.query(db, projection, selectionBuilder.build(), selectionArgs, null, null,
159                  sortOrder, null);
160          if (c != null) {
161              c.setNotificationUri(getContext().getContentResolver(), CallLog.CONTENT_URI);
162          }
163          return c;
164      }
165  
166      @Override
getType(Uri uri)167      public String getType(Uri uri) {
168          int match = sURIMatcher.match(uri);
169          switch (match) {
170              case CALLS:
171                  return Calls.CONTENT_TYPE;
172              case CALLS_ID:
173                  return Calls.CONTENT_ITEM_TYPE;
174              case CALLS_FILTER:
175                  return Calls.CONTENT_TYPE;
176              default:
177                  throw new IllegalArgumentException("Unknown URI: " + uri);
178          }
179      }
180  
181      @Override
insert(Uri uri, ContentValues values)182      public Uri insert(Uri uri, ContentValues values) {
183          checkForSupportedColumns(sCallsProjectionMap, values);
184          // Inserting a voicemail record through call_log requires the voicemail
185          // permission and also requires the additional voicemail param set.
186          if (hasVoicemailValue(values)) {
187              checkIsAllowVoicemailRequest(uri);
188              mVoicemailPermissions.checkCallerHasFullAccess();
189          }
190          if (mCallsInserter == null) {
191              SQLiteDatabase db = mDbHelper.getWritableDatabase();
192              mCallsInserter = new DatabaseUtils.InsertHelper(db, Tables.CALLS);
193          }
194  
195          ContentValues copiedValues = new ContentValues(values);
196  
197          // Add the computed fields to the copied values.
198          mCallLogInsertionHelper.addComputedValues(copiedValues);
199  
200          long rowId = getDatabaseModifier(mCallsInserter).insert(copiedValues);
201          if (rowId > 0) {
202              return ContentUris.withAppendedId(uri, rowId);
203          }
204          return null;
205      }
206  
207      @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)208      public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
209          checkForSupportedColumns(sCallsProjectionMap, values);
210          // Request that involves changing record type to voicemail requires the
211          // voicemail param set in the uri.
212          if (hasVoicemailValue(values)) {
213              checkIsAllowVoicemailRequest(uri);
214          }
215  
216          SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
217          checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder);
218  
219          final SQLiteDatabase db = mDbHelper.getWritableDatabase();
220          final int matchedUriId = sURIMatcher.match(uri);
221          switch (matchedUriId) {
222              case CALLS:
223                  break;
224  
225              case CALLS_ID:
226                  selectionBuilder.addClause(getEqualityClause(Calls._ID, parseCallIdFromUri(uri)));
227                  break;
228  
229              default:
230                  throw new UnsupportedOperationException("Cannot update URL: " + uri);
231          }
232  
233          return getDatabaseModifier(db).update(Tables.CALLS, values, selectionBuilder.build(),
234                  selectionArgs);
235      }
236  
237      @Override
delete(Uri uri, String selection, String[] selectionArgs)238      public int delete(Uri uri, String selection, String[] selectionArgs) {
239          SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
240          checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder);
241  
242          final SQLiteDatabase db = mDbHelper.getWritableDatabase();
243          final int matchedUriId = sURIMatcher.match(uri);
244          switch (matchedUriId) {
245              case CALLS:
246                  return getDatabaseModifier(db).delete(Tables.CALLS,
247                          selectionBuilder.build(), selectionArgs);
248              default:
249                  throw new UnsupportedOperationException("Cannot delete that URL: " + uri);
250          }
251      }
252  
253      // Work around to let the test code override the context. getContext() is final so cannot be
254      // overridden.
context()255      protected Context context() {
256          return getContext();
257      }
258  
259      /**
260       * Returns a {@link DatabaseModifier} that takes care of sending necessary notifications
261       * after the operation is performed.
262       */
getDatabaseModifier(SQLiteDatabase db)263      private DatabaseModifier getDatabaseModifier(SQLiteDatabase db) {
264          return new DbModifierWithNotification(Tables.CALLS, db, context());
265      }
266  
267      /**
268       * Same as {@link #getDatabaseModifier(SQLiteDatabase)} but used for insert helper operations
269       * only.
270       */
getDatabaseModifier(DatabaseUtils.InsertHelper insertHelper)271      private DatabaseModifier getDatabaseModifier(DatabaseUtils.InsertHelper insertHelper) {
272          return new DbModifierWithNotification(Tables.CALLS, insertHelper, context());
273      }
274  
hasVoicemailValue(ContentValues values)275      private boolean hasVoicemailValue(ContentValues values) {
276          return values.containsKey(Calls.TYPE) &&
277                  values.getAsInteger(Calls.TYPE).equals(Calls.VOICEMAIL_TYPE);
278      }
279  
280      /**
281       * Checks if the supplied uri requests to include voicemails and take appropriate
282       * action.
283       * <p> If voicemail is requested, then check for voicemail permissions. Otherwise
284       * modify the selection to restrict to non-voicemail entries only.
285       */
checkVoicemailPermissionAndAddRestriction(Uri uri, SelectionBuilder selectionBuilder)286      private void checkVoicemailPermissionAndAddRestriction(Uri uri,
287              SelectionBuilder selectionBuilder) {
288          if (isAllowVoicemailRequest(uri)) {
289              mVoicemailPermissions.checkCallerHasFullAccess();
290          } else {
291              selectionBuilder.addClause(EXCLUDE_VOICEMAIL_SELECTION);
292          }
293      }
294  
295      /**
296       * Determines if the supplied uri has the request to allow voicemails to be
297       * included.
298       */
isAllowVoicemailRequest(Uri uri)299      private boolean isAllowVoicemailRequest(Uri uri) {
300          return uri.getBooleanQueryParameter(Calls.ALLOW_VOICEMAILS_PARAM_KEY, false);
301      }
302  
303      /**
304       * Checks to ensure that the given uri has allow_voicemail set. Used by
305       * insert and update operations to check that ContentValues with voicemail
306       * call type must use the voicemail uri.
307       * @throws IllegalArgumentException if allow_voicemail is not set.
308       */
checkIsAllowVoicemailRequest(Uri uri)309      private void checkIsAllowVoicemailRequest(Uri uri) {
310          if (!isAllowVoicemailRequest(uri)) {
311              throw new IllegalArgumentException(
312                      String.format("Uri %s cannot be used for voicemail record." +
313                              " Please set '%s=true' in the uri.", uri,
314                              Calls.ALLOW_VOICEMAILS_PARAM_KEY));
315          }
316      }
317  
318     /**
319      * Parses the call Id from the given uri, assuming that this is a uri that
320      * matches CALLS_ID. For other uri types the behaviour is undefined.
321      * @throws IllegalArgumentException if the id included in the Uri is not a valid long value.
322      */
parseCallIdFromUri(Uri uri)323      private String parseCallIdFromUri(Uri uri) {
324          try {
325              Long id = Long.valueOf(uri.getPathSegments().get(1));
326              return id.toString();
327          } catch (NumberFormatException e) {
328              throw new IllegalArgumentException("Invalid call id in uri: " + uri, e);
329          }
330      }
331  }
332