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 */
16package com.android.providers.contacts;
17
18import static android.provider.VoicemailContract.SOURCE_PACKAGE_FIELD;
19import static com.android.providers.contacts.util.DbQueryUtils.concatenateClauses;
20import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause;
21
22import android.content.ContentProvider;
23import android.content.ContentValues;
24import android.content.Context;
25import android.content.Intent;
26import android.content.pm.PackageManager;
27import android.database.Cursor;
28import android.net.Uri;
29import android.os.Binder;
30import android.os.ParcelFileDescriptor;
31import android.provider.BaseColumns;
32import android.provider.VoicemailContract;
33import android.provider.VoicemailContract.Voicemails;
34import android.util.Log;
35
36import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
37import com.android.providers.contacts.util.SelectionBuilder;
38import com.android.providers.contacts.util.TypedUriMatcherImpl;
39import com.google.common.annotations.VisibleForTesting;
40
41import java.io.FileNotFoundException;
42import 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 */
50public 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
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
74    /*package*/ CallLogInsertionHelper createCallLogInsertionHelper(Context context) {
75        return DefaultCallLogInsertionHelper.getInstance(context);
76    }
77
78    @VisibleForTesting
79    /*package*/ ContactsDatabaseHelper getDatabaseHelper(Context context) {
80        return ContactsDatabaseHelper.getInstance(context);
81    }
82
83    @VisibleForTesting
84    /*package*/ Context context() {
85        return getContext();
86    }
87
88    @Override
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
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
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
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
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
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. */
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
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. */
178        public final Uri getUri() {
179            return mUri;
180        }
181
182        /** Tells us if our URI has an individual voicemail id. */
183        public final boolean hasId() {
184            return mId != null;
185        }
186
187        /** Gets the ID for the voicemail. */
188        public final String getId() {
189            return mId;
190        }
191
192        /** Tells us if our URI has a source package string. */
193        public final boolean hasSourcePackage() {
194            return mSourcePackage != null;
195        }
196
197        /** Gets the source package. */
198        public final String getSourcePackage() {
199            return mSourcePackage;
200        }
201
202        /** Gets the Voicemail URI type. */
203        public final VoicemailUriType getUriType() {
204            return mUriType;
205        }
206
207        /** Builds a where clause from the URI data. */
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. */
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
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.
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     */
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)} */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
394    private String getPackageRestrictionClause() {
395        if (mVoicemailPermissions.callerHasFullAccess()) {
396            return null;
397        }
398        return getEqualityClause(Voicemails.SOURCE_PACKAGE, getCallingPackage());
399    }
400}
401