• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 import static com.android.providers.contacts.util.DbQueryUtils.concatenateClauses;
20 import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause;
21 
22 import android.content.ContentProvider;
23 import android.content.ContentValues;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.pm.PackageManager;
27 import android.database.Cursor;
28 import android.net.Uri;
29 import android.os.Binder;
30 import android.os.ParcelFileDescriptor;
31 import android.provider.BaseColumns;
32 import android.provider.VoicemailContract;
33 import android.provider.VoicemailContract.Voicemails;
34 import android.util.Log;
35 
36 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
37 import com.android.providers.contacts.util.SelectionBuilder;
38 import com.android.providers.contacts.util.TypedUriMatcherImpl;
39 import com.google.common.annotations.VisibleForTesting;
40 
41 import java.io.FileNotFoundException;
42 import java.util.List;
43 
44 /**
45  * An implementation of the Voicemail content provider. This class in the entry point for both
46  * voicemail content ('calls') table and 'voicemail_status' table. This class performs all common
47  * permission checks and then delegates database level operations to respective table delegate
48  * objects.
49  */
50 public class VoicemailContentProvider extends ContentProvider
51         implements VoicemailTable.DelegateHelper {
52     private VoicemailPermissions mVoicemailPermissions;
53     private VoicemailTable.Delegate mVoicemailContentTable;
54     private VoicemailTable.Delegate mVoicemailStatusTable;
55 
56     @Override
onCreate()57     public boolean onCreate() {
58         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
59             Log.d(Constants.PERFORMANCE_TAG, "VoicemailContentProvider.onCreate start");
60         }
61         Context context = context();
62         mVoicemailPermissions = new VoicemailPermissions(context);
63         mVoicemailContentTable = new VoicemailContentTable(Tables.CALLS, context,
64                 getDatabaseHelper(context), this, createCallLogInsertionHelper(context));
65         mVoicemailStatusTable = new VoicemailStatusTable(Tables.VOICEMAIL_STATUS, context,
66                 getDatabaseHelper(context), this);
67         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
68             Log.d(Constants.PERFORMANCE_TAG, "VoicemailContentProvider.onCreate finish");
69         }
70         return true;
71     }
72 
73     @VisibleForTesting
createCallLogInsertionHelper(Context context)74     /*package*/ CallLogInsertionHelper createCallLogInsertionHelper(Context context) {
75         return DefaultCallLogInsertionHelper.getInstance(context);
76     }
77 
78     @VisibleForTesting
getDatabaseHelper(Context context)79     /*package*/ ContactsDatabaseHelper getDatabaseHelper(Context context) {
80         return ContactsDatabaseHelper.getInstance(context);
81     }
82 
83     @VisibleForTesting
context()84     /*package*/ Context context() {
85         return getContext();
86     }
87 
88     @Override
getType(Uri uri)89     public String getType(Uri uri) {
90         UriData uriData = null;
91         try {
92             uriData = UriData.createUriData(uri);
93         } catch (IllegalArgumentException ignored) {
94             // Special case: for illegal URIs, we return null rather than thrown an exception.
95             return null;
96         }
97         return getTableDelegate(uriData).getType(uriData);
98     }
99 
100     @Override
insert(Uri uri, ContentValues values)101     public Uri insert(Uri uri, ContentValues values) {
102         UriData uriData = checkPermissionsAndCreateUriData(uri, values);
103         return getTableDelegate(uriData).insert(uriData, values);
104     }
105 
106     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)107     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
108             String sortOrder) {
109         UriData uriData = checkPermissionsAndCreateUriDataForReadOperation(uri);
110         SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
111         selectionBuilder.addClause(getPackageRestrictionClause());
112         return getTableDelegate(uriData).query(uriData, projection, selectionBuilder.build(),
113                 selectionArgs, sortOrder);
114     }
115 
116     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)117     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
118         UriData uriData = checkPermissionsAndCreateUriData(uri, values);
119         SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
120         selectionBuilder.addClause(getPackageRestrictionClause());
121         return getTableDelegate(uriData).update(uriData, values, selectionBuilder.build(),
122                 selectionArgs);
123     }
124 
125     @Override
delete(Uri uri, String selection, String[] selectionArgs)126     public int delete(Uri uri, String selection, String[] selectionArgs) {
127         UriData uriData = checkPermissionsAndCreateUriData(uri);
128         SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
129         selectionBuilder.addClause(getPackageRestrictionClause());
130         return getTableDelegate(uriData).delete(uriData, selectionBuilder.build(), selectionArgs);
131     }
132 
133     @Override
openFile(Uri uri, String mode)134     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
135         UriData uriData = null;
136         if (mode.equals("r")) {
137             uriData = checkPermissionsAndCreateUriDataForReadOperation(uri);
138         } else {
139             uriData = checkPermissionsAndCreateUriData(uri);
140         }
141         // openFileHelper() relies on "_data" column to be populated with the file path.
142         return getTableDelegate(uriData).openFile(uriData, mode);
143     }
144 
145     /** Returns the correct table delegate object that can handle this URI. */
getTableDelegate(UriData uriData)146     private VoicemailTable.Delegate getTableDelegate(UriData uriData) {
147         switch (uriData.getUriType()) {
148             case STATUS:
149             case STATUS_ID:
150                 return mVoicemailStatusTable;
151             case VOICEMAILS:
152             case VOICEMAILS_ID:
153                 return mVoicemailContentTable;
154             case NO_MATCH:
155                 throw new IllegalStateException("Invalid uri type for uri: " + uriData.getUri());
156             default:
157                 throw new IllegalStateException("Impossible, all cases are covered.");
158         }
159     }
160 
161     /**
162      * Decorates a URI by providing methods to get various properties from the URI.
163      */
164     public static class UriData {
165         private final Uri mUri;
166         private final String mId;
167         private final String mSourcePackage;
168         private final VoicemailUriType mUriType;
169 
UriData(Uri uri, VoicemailUriType uriType, String id, String sourcePackage)170         public UriData(Uri uri, VoicemailUriType uriType, String id, String sourcePackage) {
171             mUriType = uriType;
172             mUri = uri;
173             mId = id;
174             mSourcePackage = sourcePackage;
175         }
176 
177         /** Gets the original URI to which this {@link UriData} corresponds. */
getUri()178         public final Uri getUri() {
179             return mUri;
180         }
181 
182         /** Tells us if our URI has an individual voicemail id. */
hasId()183         public final boolean hasId() {
184             return mId != null;
185         }
186 
187         /** Gets the ID for the voicemail. */
getId()188         public final String getId() {
189             return mId;
190         }
191 
192         /** Tells us if our URI has a source package string. */
hasSourcePackage()193         public final boolean hasSourcePackage() {
194             return mSourcePackage != null;
195         }
196 
197         /** Gets the source package. */
getSourcePackage()198         public final String getSourcePackage() {
199             return mSourcePackage;
200         }
201 
202         /** Gets the Voicemail URI type. */
getUriType()203         public final VoicemailUriType getUriType() {
204             return mUriType;
205         }
206 
207         /** Builds a where clause from the URI data. */
getWhereClause()208         public final String getWhereClause() {
209             return concatenateClauses(
210                     (hasId() ? getEqualityClause(BaseColumns._ID, getId()) : null),
211                     (hasSourcePackage() ? getEqualityClause(SOURCE_PACKAGE_FIELD,
212                             getSourcePackage()) : null));
213         }
214 
215         /** Create a {@link UriData} corresponding to a given uri. */
createUriData(Uri uri)216         public static UriData createUriData(Uri uri) {
217             String sourcePackage = uri.getQueryParameter(
218                     VoicemailContract.PARAM_KEY_SOURCE_PACKAGE);
219             List<String> segments = uri.getPathSegments();
220             VoicemailUriType uriType = createUriMatcher().match(uri);
221             switch (uriType) {
222                 case VOICEMAILS:
223                 case STATUS:
224                     return new UriData(uri, uriType, null, sourcePackage);
225                 case VOICEMAILS_ID:
226                 case STATUS_ID:
227                     return new UriData(uri, uriType, segments.get(1), sourcePackage);
228                 case NO_MATCH:
229                     throw new IllegalArgumentException("Invalid URI: " + uri);
230                 default:
231                     throw new IllegalStateException("Impossible, all cases are covered");
232             }
233         }
234 
createUriMatcher()235         private static TypedUriMatcherImpl<VoicemailUriType> createUriMatcher() {
236             return new TypedUriMatcherImpl<VoicemailUriType>(
237                     VoicemailContract.AUTHORITY, VoicemailUriType.values());
238         }
239     }
240 
241     @Override
242     // VoicemailTable.DelegateHelper interface.
checkAndAddSourcePackageIntoValues(UriData uriData, ContentValues values)243     public void checkAndAddSourcePackageIntoValues(UriData uriData, ContentValues values) {
244         // If content values don't contain the provider, calculate the right provider to use.
245         if (!values.containsKey(SOURCE_PACKAGE_FIELD)) {
246             String provider = uriData.hasSourcePackage() ?
247                     uriData.getSourcePackage() : getCallingPackage();
248             values.put(SOURCE_PACKAGE_FIELD, provider);
249         }
250         // You must have access to the provider given in values.
251         if (!mVoicemailPermissions.callerHasFullAccess()) {
252             checkPackagesMatch(getCallingPackage(),
253                     values.getAsString(VoicemailContract.SOURCE_PACKAGE_FIELD),
254                     uriData.getUri());
255         }
256     }
257 
258     /**
259      * Checks that the source_package field is same in uriData and ContentValues, if it happens
260      * to be set in both.
261      */
checkSourcePackageSameIfSet(UriData uriData, ContentValues values)262     private void checkSourcePackageSameIfSet(UriData uriData, ContentValues values) {
263         if (uriData.hasSourcePackage() && values.containsKey(SOURCE_PACKAGE_FIELD)) {
264             if (!uriData.getSourcePackage().equals(values.get(SOURCE_PACKAGE_FIELD))) {
265                 throw new SecurityException(
266                         "source_package in URI was " + uriData.getSourcePackage() +
267                         " but doesn't match source_package in ContentValues which was "
268                         + values.get(SOURCE_PACKAGE_FIELD));
269             }
270         }
271     }
272 
273     @Override
274     /** Implementation of  {@link VoicemailTable.DelegateHelper#openDataFile(UriData, String)} */
openDataFile(UriData uriData, String mode)275     public ParcelFileDescriptor openDataFile(UriData uriData, String mode)
276             throws FileNotFoundException {
277         return openFileHelper(uriData.getUri(), mode);
278     }
279 
280     /**
281      * Performs necessary voicemail permission checks common to all operations and returns
282      * the structured representation, {@link UriData}, of the supplied uri.
283      */
checkPermissionsAndCreateUriDataForReadOperation(Uri uri)284     private UriData checkPermissionsAndCreateUriDataForReadOperation(Uri uri) {
285         // If the caller has been explicitly granted read permission to this URI then no need to
286         // check further.
287         if (context().checkCallingUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
288                 == PackageManager.PERMISSION_GRANTED) {
289             return UriData.createUriData(uri);
290         }
291         return checkPermissionsAndCreateUriData(uri);
292     }
293 
294     /**
295      * Performs necessary voicemail permission checks common to all operations and returns
296      * the structured representation, {@link UriData}, of the supplied uri.
297      */
checkPermissionsAndCreateUriData(Uri uri)298     private UriData checkPermissionsAndCreateUriData(Uri uri) {
299         mVoicemailPermissions.checkCallerHasOwnVoicemailAccess();
300         UriData uriData = UriData.createUriData(uri);
301         checkPackagePermission(uriData);
302         return uriData;
303     }
304 
305     /**
306      * Same as {@link #checkPackagePermission(UriData)}. In addition does permission check
307      * on the ContentValues.
308      */
checkPermissionsAndCreateUriData(Uri uri, ContentValues... valuesArray)309     private UriData checkPermissionsAndCreateUriData(Uri uri, ContentValues... valuesArray) {
310         UriData uriData = checkPermissionsAndCreateUriData(uri);
311         for (ContentValues values : valuesArray) {
312             checkSourcePackageSameIfSet(uriData, values);
313         }
314         return uriData;
315     }
316 
317     /**
318      * Checks that the callingPackage is same as voicemailSourcePackage. Throws {@link
319      * SecurityException} if they don't match.
320      */
checkPackagesMatch(String callingPackage, String voicemailSourcePackage, Uri uri)321     private final void checkPackagesMatch(String callingPackage, String voicemailSourcePackage,
322             Uri uri) {
323         if (!voicemailSourcePackage.equals(callingPackage)) {
324             String errorMsg = String.format("Permission denied for URI: %s\n. " +
325                     "Package %s cannot perform this operation for %s. Requires %s permission.",
326                     uri, callingPackage, voicemailSourcePackage,
327                     Manifest.permission.READ_WRITE_ALL_VOICEMAIL);
328             throw new SecurityException(errorMsg);
329         }
330     }
331 
332     /**
333      * Checks that either the caller has READ_WRITE_ALL_VOICEMAIL permission, or has the
334      * ADD_VOICEMAIL permission and is using a URI that matches
335      * /voicemail/?source_package=[source-package] where [source-package] is the same as the calling
336      * package.
337      *
338      * @throws SecurityException if the check fails.
339      */
checkPackagePermission(UriData uriData)340     private void checkPackagePermission(UriData uriData) {
341         if (!mVoicemailPermissions.callerHasFullAccess()) {
342             if (!uriData.hasSourcePackage()) {
343                 // You cannot have a match if this is not a provider URI.
344                 throw new SecurityException(String.format(
345                         "Provider %s does not have %s permission." +
346                                 "\nPlease set query parameter '%s' in the URI.\nURI: %s",
347                         getCallingPackage(), Manifest.permission.READ_WRITE_ALL_VOICEMAIL,
348                         VoicemailContract.PARAM_KEY_SOURCE_PACKAGE, uriData.getUri()));
349             }
350             checkPackagesMatch(getCallingPackage(), uriData.getSourcePackage(), uriData.getUri());
351         }
352     }
353 
354     /**
355      * Gets the name of the calling package.
356      * <p>
357      * It's possible (though unlikely) for there to be more than one calling package (requires that
358      * your manifest say you want to share process ids) in which case we will return an arbitrary
359      * package name. It's also possible (though very unlikely) for us to be unable to work out what
360      * your calling package is, in which case we will return null.
361      */
getCallingPackage()362     /* package for test */String getCallingPackage() {
363         int caller = Binder.getCallingUid();
364         if (caller == 0) {
365             return null;
366         }
367         String[] callerPackages = context().getPackageManager().getPackagesForUid(caller);
368         if (callerPackages == null || callerPackages.length == 0) {
369             return null;
370         }
371         if (callerPackages.length == 1) {
372             return callerPackages[0];
373         }
374         // If we have more than one caller package, which is very unlikely, let's return the one
375         // with the highest permissions. If more than one has the same permission, we don't care
376         // which one we return.
377         String bestSoFar = callerPackages[0];
378         for (String callerPackage : callerPackages) {
379             if (mVoicemailPermissions.packageHasFullAccess(callerPackage)) {
380                 // Full always wins, we can return early.
381                 return callerPackage;
382             }
383             if (mVoicemailPermissions.packageHasOwnVoicemailAccess(callerPackage)) {
384                 bestSoFar = callerPackage;
385             }
386         }
387         return bestSoFar;
388     }
389 
390     /**
391      * Creates a clause to restrict the selection to the calling provider or null if the caller has
392      * access to all data.
393      */
getPackageRestrictionClause()394     private String getPackageRestrictionClause() {
395         if (mVoicemailPermissions.callerHasFullAccess()) {
396             return null;
397         }
398         return getEqualityClause(Voicemails.SOURCE_PACKAGE, getCallingPackage());
399     }
400 }
401