1 /*
2  * Copyright (C) 2011 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 package com.android.providers.contacts;
17 
18 import static android.provider.VoicemailContract.SOURCE_PACKAGE_FIELD;
19 
20 import static com.android.providers.contacts.util.DbQueryUtils.concatenateClauses;
21 import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause;
22 
23 import android.app.AppOpsManager;
24 import android.content.ContentProvider;
25 import android.content.ContentResolver;
26 import android.content.ContentValues;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.database.Cursor;
30 import android.net.Uri;
31 import android.os.Binder;
32 import android.os.ParcelFileDescriptor;
33 import android.provider.BaseColumns;
34 import android.provider.VoicemailContract;
35 import android.provider.VoicemailContract.Status;
36 import android.provider.VoicemailContract.Voicemails;
37 import android.util.ArraySet;
38 import android.util.Log;
39 
40 import com.android.providers.contacts.CallLogDatabaseHelper.Tables;
41 import com.android.providers.contacts.util.ContactsPermissions;
42 import com.android.providers.contacts.util.PackageUtils;
43 import com.android.providers.contacts.util.SelectionBuilder;
44 import com.android.providers.contacts.util.TypedUriMatcherImpl;
45 import com.android.providers.contacts.util.UserUtils;
46 
47 import com.google.common.annotations.VisibleForTesting;
48 
49 import java.io.FileNotFoundException;
50 import java.util.Arrays;
51 import java.util.List;
52 import java.util.Set;
53 
54 /**
55  * An implementation of the Voicemail content provider. This class in the entry point for both
56  * voicemail content ('calls') table and 'voicemail_status' table. This class performs all common
57  * permission checks and then delegates database level operations to respective table delegate
58  * objects.
59  */
60 public class VoicemailContentProvider extends ContentProvider
61         implements VoicemailTable.DelegateHelper {
62     private static final String TAG = "VoicemailProvider";
63 
64     public static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
65 
66     private static final int BACKGROUND_TASK_SCAN_STALE_PACKAGES = 0;
67 
68     private ContactsTaskScheduler mTaskScheduler;
69 
70     private VoicemailPermissions mVoicemailPermissions;
71     private VoicemailTable.Delegate mVoicemailContentTable;
72     private VoicemailTable.Delegate mVoicemailStatusTable;
73 
74     @Override
onCreate()75     public boolean onCreate() {
76         if (VERBOSE_LOGGING) {
77             Log.v(TAG, "onCreate: " + this.getClass().getSimpleName()
78                     + " user=" + android.os.Process.myUserHandle().getIdentifier());
79         }
80         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.INFO)) {
81             Log.i(Constants.PERFORMANCE_TAG, "VoicemailContentProvider.onCreate start");
82         }
83         Context context = context();
84 
85         // ADD_VOICEMAIL permission guards read and write. We do the same with app ops.
86         // The permission name doesn't reflect its function but we cannot rename it.
87         setAppOps(AppOpsManager.OP_ADD_VOICEMAIL, AppOpsManager.OP_ADD_VOICEMAIL);
88 
89         mVoicemailPermissions = new VoicemailPermissions(context);
90         mVoicemailContentTable = new VoicemailContentTable(Tables.CALLS, context,
91                 getDatabaseHelper(context), this, createCallLogInsertionHelper(context));
92         mVoicemailStatusTable = new VoicemailStatusTable(Tables.VOICEMAIL_STATUS, context,
93                 getDatabaseHelper(context), this);
94 
95         mTaskScheduler = new ContactsTaskScheduler(getClass().getSimpleName()) {
96             @Override
97             public void onPerformTask(int taskId, Object arg) {
98                 performBackgroundTask(taskId, arg);
99             }
100         };
101 
102         scheduleScanStalePackages();
103 
104         ContactsPackageMonitor.start(getContext());
105 
106         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.INFO)) {
107             Log.i(Constants.PERFORMANCE_TAG, "VoicemailContentProvider.onCreate finish");
108         }
109         return true;
110     }
111 
112     @VisibleForTesting
scheduleScanStalePackages()113     void scheduleScanStalePackages() {
114         scheduleTask(BACKGROUND_TASK_SCAN_STALE_PACKAGES, null);
115     }
116 
117     @VisibleForTesting
scheduleTask(int taskId, Object arg)118     void scheduleTask(int taskId, Object arg) {
119         mTaskScheduler.scheduleTask(taskId, arg);
120     }
121 
122     @VisibleForTesting
createCallLogInsertionHelper(Context context)123     /*package*/ CallLogInsertionHelper createCallLogInsertionHelper(Context context) {
124         return DefaultCallLogInsertionHelper.getInstance(context);
125     }
126 
127     @VisibleForTesting
getDatabaseHelper(Context context)128     /*package*/ CallLogDatabaseHelper getDatabaseHelper(Context context) {
129         return CallLogDatabaseHelper.getInstance(context);
130     }
131 
132     @VisibleForTesting
context()133     /*package*/ Context context() {
134         return getContext();
135     }
136 
137     @Override
getType(Uri uri)138     public String getType(Uri uri) {
139         UriData uriData = null;
140         try {
141             uriData = UriData.createUriData(uri);
142         } catch (IllegalArgumentException ignored) {
143             // Special case: for illegal URIs, we return null rather than thrown an exception.
144             return null;
145         }
146         return getTableDelegate(uriData).getType(uriData);
147     }
148 
149     @Override
insert(Uri uri, ContentValues values)150     public Uri insert(Uri uri, ContentValues values) {
151         if (VERBOSE_LOGGING) {
152             Log.v(TAG, "insert: uri=" + uri + "  values=[" + values + "]" +
153                     " CPID=" + Binder.getCallingPid());
154         }
155         UriData uriData = checkPermissionsAndCreateUriDataForWrite(uri, values);
156         return getTableDelegate(uriData).insert(uriData, values);
157     }
158 
159     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)160     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
161             String sortOrder) {
162         if (VERBOSE_LOGGING) {
163             Log.v(TAG, "query: uri=" + uri + "  projection=" + Arrays.toString(projection) +
164                     "  selection=[" + selection + "]  args=" + Arrays.toString(selectionArgs) +
165                     "  order=[" + sortOrder + "] CPID=" + Binder.getCallingPid() +
166                     " User=" + UserUtils.getCurrentUserHandle(getContext()));
167         }
168         UriData uriData = checkPermissionsAndCreateUriDataForRead(uri);
169         SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
170         selectionBuilder.addClause(getPackageRestrictionClause(true/*isQuery*/));
171         return getTableDelegate(uriData).query(uriData, projection, selectionBuilder.build(),
172                 selectionArgs, sortOrder);
173     }
174 
175     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)176     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
177         if (VERBOSE_LOGGING) {
178             Log.v(TAG, "update: uri=" + uri +
179                     "  selection=[" + selection + "]  args=" + Arrays.toString(selectionArgs) +
180                     "  values=[" + values + "] CPID=" + Binder.getCallingPid() +
181                     " User=" + UserUtils.getCurrentUserHandle(getContext()));
182         }
183         UriData uriData = checkPermissionsAndCreateUriDataForWrite(uri, values);
184         SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
185         selectionBuilder.addClause(getPackageRestrictionClause(false/*isQuery*/));
186         return getTableDelegate(uriData).update(uriData, values, selectionBuilder.build(),
187                 selectionArgs);
188     }
189 
190     @Override
delete(Uri uri, String selection, String[] selectionArgs)191     public int delete(Uri uri, String selection, String[] selectionArgs) {
192         if (VERBOSE_LOGGING) {
193             Log.v(TAG, "delete: uri=" + uri +
194                     "  selection=[" + selection + "]  args=" + Arrays.toString(selectionArgs) +
195                     " CPID=" + Binder.getCallingPid() +
196                     " User=" + UserUtils.getCurrentUserHandle(getContext()));
197         }
198         UriData uriData = checkPermissionsAndCreateUriDataForWrite(uri);
199         SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
200         selectionBuilder.addClause(getPackageRestrictionClause(false/*isQuery*/));
201         return getTableDelegate(uriData).delete(uriData, selectionBuilder.build(), selectionArgs);
202     }
203 
204     @Override
openFile(Uri uri, String mode)205     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
206         boolean success = false;
207         try {
208             UriData uriData = null;
209             if (mode.equals("r")) {
210                 uriData = checkPermissionsAndCreateUriDataForRead(uri);
211             } else {
212                 uriData = checkPermissionsAndCreateUriDataForWrite(uri);
213             }
214             // openFileHelper() relies on "_data" column to be populated with the file path.
215             final ParcelFileDescriptor ret = getTableDelegate(uriData).openFile(uriData, mode);
216             success = true;
217             return ret;
218         } finally {
219             if (VERBOSE_LOGGING) {
220                 Log.v(TAG, "openFile uri=" + uri + " mode=" + mode + " success=" + success +
221                         " CPID=" + Binder.getCallingPid() +
222                         " User=" + UserUtils.getCurrentUserHandle(getContext()));
223             }
224         }
225     }
226 
227     /** Returns the correct table delegate object that can handle this URI. */
getTableDelegate(UriData uriData)228     private VoicemailTable.Delegate getTableDelegate(UriData uriData) {
229         switch (uriData.getUriType()) {
230             case STATUS:
231             case STATUS_ID:
232                 return mVoicemailStatusTable;
233             case VOICEMAILS:
234             case VOICEMAILS_ID:
235                 return mVoicemailContentTable;
236             case NO_MATCH:
237                 throw new IllegalStateException("Invalid uri type for uri: " + uriData.getUri());
238             default:
239                 throw new IllegalStateException("Impossible, all cases are covered.");
240         }
241     }
242 
243     /**
244      * Decorates a URI by providing methods to get various properties from the URI.
245      */
246     public static class UriData {
247         private final Uri mUri;
248         private final String mId;
249         private final String mSourcePackage;
250         private final VoicemailUriType mUriType;
251 
UriData(Uri uri, VoicemailUriType uriType, String id, String sourcePackage)252         private UriData(Uri uri, VoicemailUriType uriType, String id, String sourcePackage) {
253             mUriType = uriType;
254             mUri = uri;
255             mId = id;
256             mSourcePackage = sourcePackage;
257         }
258 
259         /** Gets the original URI to which this {@link UriData} corresponds. */
getUri()260         public final Uri getUri() {
261             return mUri;
262         }
263 
264         /** Tells us if our URI has an individual voicemail id. */
hasId()265         public final boolean hasId() {
266             return mId != null;
267         }
268 
269         /** Gets the ID for the voicemail. */
getId()270         public final String getId() {
271             return mId;
272         }
273 
274         /** Tells us if our URI has a source package string. */
hasSourcePackage()275         public final boolean hasSourcePackage() {
276             return mSourcePackage != null;
277         }
278 
279         /** Gets the source package. */
getSourcePackage()280         public final String getSourcePackage() {
281             return mSourcePackage;
282         }
283 
284         /** Gets the Voicemail URI type. */
getUriType()285         public final VoicemailUriType getUriType() {
286             return mUriType;
287         }
288 
289         /** Builds a where clause from the URI data. */
getWhereClause()290         public final String getWhereClause() {
291             return concatenateClauses(
292                     (hasId() ? getEqualityClause(BaseColumns._ID, getId()) : null),
293                     (hasSourcePackage() ? getEqualityClause(SOURCE_PACKAGE_FIELD,
294                             getSourcePackage()) : null));
295         }
296 
297         /** Create a {@link UriData} corresponding to a given uri. */
createUriData(Uri uri)298         public static UriData createUriData(Uri uri) {
299             String sourcePackage = uri.getQueryParameter(
300                     VoicemailContract.PARAM_KEY_SOURCE_PACKAGE);
301             List<String> segments = uri.getPathSegments();
302             VoicemailUriType uriType = createUriMatcher().match(uri);
303             switch (uriType) {
304                 case VOICEMAILS:
305                 case STATUS:
306                     return new UriData(uri, uriType, null, sourcePackage);
307                 case VOICEMAILS_ID:
308                 case STATUS_ID:
309                     return new UriData(uri, uriType, segments.get(1), sourcePackage);
310                 case NO_MATCH:
311                     throw new IllegalArgumentException("Invalid URI: " + uri);
312                 default:
313                     throw new IllegalStateException("Impossible, all cases are covered");
314             }
315         }
316 
createUriMatcher()317         private static TypedUriMatcherImpl<VoicemailUriType> createUriMatcher() {
318             return new TypedUriMatcherImpl<VoicemailUriType>(
319                     VoicemailContract.AUTHORITY, VoicemailUriType.values());
320         }
321     }
322 
323     @Override
324     // VoicemailTable.DelegateHelper interface.
checkAndAddSourcePackageIntoValues(UriData uriData, ContentValues values)325     public void checkAndAddSourcePackageIntoValues(UriData uriData, ContentValues values) {
326         // If content values don't contain the provider, calculate the right provider to use.
327         if (!values.containsKey(SOURCE_PACKAGE_FIELD)) {
328             String provider = uriData.hasSourcePackage() ?
329                     uriData.getSourcePackage() : getInjectedCallingPackage();
330             values.put(SOURCE_PACKAGE_FIELD, provider);
331         }
332 
333         // You must have access to the provider given in values.
334         if (!mVoicemailPermissions.callerHasWriteAccess(getCallingPackage())) {
335             checkPackagesMatch(getInjectedCallingPackage(),
336                     values.getAsString(VoicemailContract.SOURCE_PACKAGE_FIELD),
337                     uriData.getUri());
338         }
339     }
340 
341     /**
342      * Checks that the source_package field is same in uriData and ContentValues, if it happens
343      * to be set in both.
344      */
checkSourcePackageSameIfSet(UriData uriData, ContentValues values)345     private void checkSourcePackageSameIfSet(UriData uriData, ContentValues values) {
346         if (uriData.hasSourcePackage() && values.containsKey(SOURCE_PACKAGE_FIELD)) {
347             if (!uriData.getSourcePackage().equals(values.get(SOURCE_PACKAGE_FIELD))) {
348                 throw new SecurityException(
349                         "source_package in URI was " + uriData.getSourcePackage() +
350                         " but doesn't match source_package in ContentValues which was "
351                         + values.get(SOURCE_PACKAGE_FIELD));
352             }
353         }
354     }
355 
356     @Override
357     /** Implementation of  {@link VoicemailTable.DelegateHelper#openDataFile(UriData, String)} */
openDataFile(UriData uriData, String mode)358     public ParcelFileDescriptor openDataFile(UriData uriData, String mode)
359             throws FileNotFoundException {
360         return openFileHelper(uriData.getUri(), mode);
361     }
362 
363     /**
364      * Ensures that the caller has the permissions to perform a query/read operation, and
365      * then returns the structured representation {@link UriData} of the supplied uri.
366      */
checkPermissionsAndCreateUriDataForRead(Uri uri)367     private UriData checkPermissionsAndCreateUriDataForRead(Uri uri) {
368         // If the caller has been explicitly granted read permission to this URI then no need to
369         // check further.
370         if (ContactsPermissions.hasCallerUriPermission(
371                 getContext(), uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)) {
372             return UriData.createUriData(uri);
373         }
374 
375         if (mVoicemailPermissions.callerHasReadAccess(getCallingPackage())) {
376             return UriData.createUriData(uri);
377         }
378 
379         return checkPermissionsAndCreateUriData(uri, true);
380     }
381 
382     /**
383      * Performs necessary voicemail permission checks common to all operations and returns
384      * the structured representation, {@link UriData}, of the supplied uri.
385      */
checkPermissionsAndCreateUriData(Uri uri, boolean read)386     private UriData checkPermissionsAndCreateUriData(Uri uri, boolean read) {
387         UriData uriData = UriData.createUriData(uri);
388         if (!hasReadWritePermission(read)) {
389             mVoicemailPermissions.checkCallerHasOwnVoicemailAccess();
390             checkPackagePermission(uriData);
391         }
392         return uriData;
393     }
394 
395     /**
396      * Ensures that the caller has the permissions to perform an update/delete operation, and
397      * then returns the structured representation {@link UriData} of the supplied uri.
398      * Also does a permission check on the ContentValues.
399      */
checkPermissionsAndCreateUriDataForWrite(Uri uri, ContentValues... valuesArray)400     private UriData checkPermissionsAndCreateUriDataForWrite(Uri uri, ContentValues... valuesArray) {
401         UriData uriData = checkPermissionsAndCreateUriData(uri, false);
402         for (ContentValues values : valuesArray) {
403             checkSourcePackageSameIfSet(uriData, values);
404         }
405         return uriData;
406     }
407 
408     /**
409      * Checks that the callingPackage is same as voicemailSourcePackage. Throws {@link
410      * SecurityException} if they don't match.
411      */
checkPackagesMatch(String callingPackage, String voicemailSourcePackage, Uri uri)412     private final void checkPackagesMatch(String callingPackage, String voicemailSourcePackage,
413             Uri uri) {
414         if (!voicemailSourcePackage.equals(callingPackage)) {
415             String errorMsg = String.format("Permission denied for URI: %s\n. " +
416                     "Package %s cannot perform this operation for %s. Requires %s permission.",
417                     uri, callingPackage, voicemailSourcePackage,
418                     android.Manifest.permission.WRITE_VOICEMAIL);
419             throw new SecurityException(errorMsg);
420         }
421     }
422 
423     /**
424      * Checks that either the caller has the MANAGE_VOICEMAIL permission,
425      * or has the ADD_VOICEMAIL permission and is using a URI that matches
426      * /voicemail/?source_package=[source-package] where [source-package] is the same as the calling
427      * package.
428      *
429      * @throws SecurityException if the check fails.
430      */
checkPackagePermission(UriData uriData)431     private void checkPackagePermission(UriData uriData) {
432         if (!mVoicemailPermissions.callerHasWriteAccess(getCallingPackage())) {
433             if (!uriData.hasSourcePackage()) {
434                 // You cannot have a match if this is not a provider URI.
435                 throw new SecurityException(String.format(
436                         "Provider %s does not have %s permission." +
437                                 "\nPlease set query parameter '%s' in the URI.\nURI: %s",
438                         getInjectedCallingPackage(), android.Manifest.permission.WRITE_VOICEMAIL,
439                         VoicemailContract.PARAM_KEY_SOURCE_PACKAGE, uriData.getUri()));
440             }
441             checkPackagesMatch(getInjectedCallingPackage(), uriData.getSourcePackage(),
442                     uriData.getUri());
443         }
444     }
445 
446     @VisibleForTesting
getInjectedCallingPackage()447     String getInjectedCallingPackage() {
448         return super.getCallingPackage();
449     }
450 
451     /**
452      * Creates a clause to restrict the selection to the calling provider or null if the caller has
453      * access to all data.
454      */
getPackageRestrictionClause(boolean isQuery)455     private String getPackageRestrictionClause(boolean isQuery) {
456         if (hasReadWritePermission(isQuery)) {
457             return null;
458         }
459         return getEqualityClause(Voicemails.SOURCE_PACKAGE, getInjectedCallingPackage());
460     }
461 
462     /**
463      * Whether or not the calling package has the appropriate read/write permission. The user
464      * selected default and/or system dialers are always allowed to read and write to the
465      * VoicemailContentProvider.
466      *
467      * @param read Whether or not this operation is a read
468      *
469      * @return True if the package has the permission required to perform the read/write operation
470      */
hasReadWritePermission(boolean read)471     private boolean hasReadWritePermission(boolean read) {
472         return read ? mVoicemailPermissions.callerHasReadAccess(getCallingPackage()) :
473             mVoicemailPermissions.callerHasWriteAccess(getCallingPackage());
474     }
475 
476     /** Remove all records from a given source package. */
removeBySourcePackage(String packageName)477     public void removeBySourcePackage(String packageName) {
478         delete(Voicemails.buildSourceUri(packageName), null, null);
479         delete(Status.buildSourceUri(packageName), null, null);
480     }
481 
482     @VisibleForTesting
performBackgroundTask(int task, Object arg)483     void performBackgroundTask(int task, Object arg) {
484         switch (task) {
485             case BACKGROUND_TASK_SCAN_STALE_PACKAGES:
486                 removeStalePackages();
487                 break;
488         }
489     }
490 
491     /**
492      * Remove all records made by packages that no longer exist.
493      */
removeStalePackages()494     private void removeStalePackages() {
495         if (VERBOSE_LOGGING) {
496             Log.v(TAG, "scanStalePackages start");
497         }
498 
499         // Make sure all source tables still exists.
500 
501         // First, list all source packages.
502         final ArraySet<String> packages = mVoicemailContentTable.getSourcePackages();
503         packages.addAll(mVoicemailStatusTable.getSourcePackages());
504 
505         // Remove the ones that still exist.
506         for (int i = packages.size() - 1; i >= 0; i--) {
507             final String pkg = packages.valueAt(i);
508             final boolean installed = PackageUtils.isPackageInstalled(getContext(), pkg);
509             if (VERBOSE_LOGGING) {
510                 Log.v(TAG, "  " + pkg + (installed ? " installed" : " removed"));
511             }
512             if (!installed) {
513                 removeBySourcePackage(pkg);
514             }
515         }
516 
517         if (VERBOSE_LOGGING) {
518             Log.v(TAG, "scanStalePackages finish");
519         }
520     }
521 }
522