FontsContract.java revision 43c20cf6d4bd661d85bed76c78953aa656dbcc62
1/*
2 * Copyright (C) 2017 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 android.provider;
17
18import android.content.ContentResolver;
19import android.content.ContentUris;
20import android.content.Context;
21import android.content.pm.PackageInfo;
22import android.content.pm.PackageManager;
23import android.content.pm.ProviderInfo;
24import android.content.pm.Signature;
25import android.database.Cursor;
26import android.graphics.Typeface;
27import android.graphics.fonts.FontRequest;
28import android.graphics.fonts.FontResult;
29import android.net.Uri;
30import android.os.Bundle;
31import android.os.Handler;
32import android.os.HandlerThread;
33import android.os.ParcelFileDescriptor;
34import android.os.Process;
35import android.os.ResultReceiver;
36import android.util.Log;
37
38import com.android.internal.annotations.GuardedBy;
39import com.android.internal.annotations.VisibleForTesting;
40
41import java.io.FileNotFoundException;
42import java.io.IOException;
43import java.util.ArrayList;
44import java.util.Arrays;
45import java.util.Collections;
46import java.util.Comparator;
47import java.util.List;
48
49/**
50 * Utility class to deal with Font ContentProviders.
51 */
52public class FontsContract {
53    private static final String TAG = "FontsContract";
54
55    /**
56     * Defines the constants used in a response from a Font Provider. The cursor returned from the
57     * query should have the ID column populated with the content uri ID for the resulting font.
58     * This should point to a real file or shared memory, as the client will mmap the given file
59     * descriptor. Pipes, sockets and other non-mmap-able file descriptors will fail to load in the
60     * client application.
61     */
62    public static final class Columns implements BaseColumns {
63        /**
64         * Constant used to request data from a font provider. The cursor returned from the query
65         * may populate this column with a long for the font file ID. The client will request a file
66         * descriptor to "file/FILE_ID" with this ID immediately under the top-level content URI. If
67         * not present, the client will request a file descriptor to the top-level URI with the
68         * given base font ID. Note that several results may return the same file ID, e.g. for TTC
69         * files with different indices.
70         */
71        public static final String FILE_ID = "file_id";
72        /**
73         * Constant used to request data from a font provider. The cursor returned from the query
74         * should have this column populated with an int for the ttc index for the resulting font.
75         */
76        public static final String TTC_INDEX = "font_ttc_index";
77        /**
78         * Constant used to request data from a font provider. The cursor returned from the query
79         * may populate this column with the font variation settings String information for the
80         * font.
81         */
82        public static final String VARIATION_SETTINGS = "font_variation_settings";
83        /**
84         * DO NOT USE THIS COLUMN.
85         * This column is kept for preventing demo apps.
86         * TODO: Remove once nobody uses this column.
87         * @hide
88         * @removed
89         */
90        public static final String STYLE = "font_style";
91        /**
92         * Constant used to request data from a font provider. The cursor returned from the query
93         * should have this column populated with the int weight for the resulting font. This value
94         * should be between 100 and 900. The most common values are 400 for regular weight and 700
95         * for bold weight.
96         */
97        public static final String WEIGHT = "font_weight";
98        /**
99         * Constant used to request data from a font provider. The cursor returned from the query
100         * should have this column populated with the int italic for the resulting font. This should
101         * be 0 for regular style and 1 for italic.
102         */
103        public static final String ITALIC = "font_italic";
104        /**
105         * Constant used to request data from a font provider. The cursor returned from the query
106         * should have this column populated to indicate the result status of the
107         * query. This will be checked before any other data in the cursor. Possible values are
108         * {@link #RESULT_CODE_OK}, {@link #RESULT_CODE_FONT_NOT_FOUND},
109         * {@link #RESULT_CODE_MALFORMED_QUERY} and {@link #RESULT_CODE_FONT_UNAVAILABLE}. If not
110         * present, {@link #RESULT_CODE_OK} will be assumed.
111         */
112        public static final String RESULT_CODE = "result_code";
113
114        /**
115         * Constant used to represent a result was retrieved successfully. The given fonts will be
116         * attempted to retrieve immediately via
117         * {@link android.content.ContentProvider#openFile(Uri, String)}. See {@link #RESULT_CODE}.
118         */
119        public static final int RESULT_CODE_OK = 0;
120        /**
121         * Constant used to represent a result was not found. See {@link #RESULT_CODE}.
122         */
123        public static final int RESULT_CODE_FONT_NOT_FOUND = 1;
124        /**
125         * Constant used to represent a result was found, but cannot be provided at this moment. Use
126         * this to indicate, for example, that a font needs to be fetched from the network. See
127         * {@link #RESULT_CODE}.
128         */
129        public static final int RESULT_CODE_FONT_UNAVAILABLE = 2;
130        /**
131         * Constant used to represent that the query was not in a supported format by the provider.
132         * See {@link #RESULT_CODE}.
133         */
134        public static final int RESULT_CODE_MALFORMED_QUERY = 3;
135    }
136
137    /**
138     * Constant used to identify the List of {@link ParcelFileDescriptor} item in the Bundle
139     * returned to the ResultReceiver in getFont.
140     * @hide
141     */
142    public static final String PARCEL_FONT_RESULTS = "font_results";
143
144    // Error codes internal to the system, which can not come from a provider. To keep the number
145    // space open for new provider codes, these should all be negative numbers.
146    /** @hide */
147    public static final int RESULT_CODE_PROVIDER_NOT_FOUND = -1;
148    /** @hide */
149    public static final int RESULT_CODE_WRONG_CERTIFICATES = -2;
150    // Note -3 is used by Typeface to indicate the font failed to load.
151
152    private static final int THREAD_RENEWAL_THRESHOLD_MS = 10000;
153
154    private final Context mContext;
155    private final PackageManager mPackageManager;
156    private final Object mLock = new Object();
157    @GuardedBy("mLock")
158    private Handler mHandler;
159    @GuardedBy("mLock")
160    private HandlerThread mThread;
161
162    /** @hide */
163    public FontsContract(Context context) {
164        mContext = context.getApplicationContext();
165        mPackageManager = mContext.getPackageManager();
166    }
167
168    /** @hide */
169    @VisibleForTesting
170    public FontsContract(Context context, PackageManager packageManager) {
171        mContext = context;
172        mPackageManager = packageManager;
173    }
174
175    // We use a background thread to post the content resolving work for all requests on. This
176    // thread should be quit/stopped after all requests are done.
177    private final Runnable mReplaceDispatcherThreadRunnable = new Runnable() {
178        @Override
179        public void run() {
180            synchronized (mLock) {
181                if (mThread != null) {
182                    mThread.quitSafely();
183                    mThread = null;
184                    mHandler = null;
185                }
186            }
187        }
188    };
189
190    /** @hide */
191    public void getFont(FontRequest request, ResultReceiver receiver) {
192        synchronized (mLock) {
193            if (mHandler == null) {
194                mThread = new HandlerThread("fonts", Process.THREAD_PRIORITY_BACKGROUND);
195                mThread.start();
196                mHandler = new Handler(mThread.getLooper());
197            }
198            mHandler.post(() -> {
199                ProviderInfo providerInfo = getProvider(request, receiver);
200                if (providerInfo == null) {
201                    return;
202                }
203                getFontFromProvider(request, receiver, providerInfo.authority);
204            });
205            mHandler.removeCallbacks(mReplaceDispatcherThreadRunnable);
206            mHandler.postDelayed(mReplaceDispatcherThreadRunnable, THREAD_RENEWAL_THRESHOLD_MS);
207        }
208    }
209
210    /** @hide */
211    @VisibleForTesting
212    public ProviderInfo getProvider(FontRequest request, ResultReceiver receiver) {
213        String providerAuthority = request.getProviderAuthority();
214        ProviderInfo info = mPackageManager.resolveContentProvider(providerAuthority, 0);
215        if (info == null) {
216            Log.e(TAG, "Can't find content provider " + providerAuthority);
217            receiver.send(RESULT_CODE_PROVIDER_NOT_FOUND, null);
218            return null;
219        }
220
221        if (!info.packageName.equals(request.getProviderPackage())) {
222            Log.e(TAG, "Found content provider " + providerAuthority + ", but package was not "
223                    + request.getProviderPackage());
224            receiver.send(RESULT_CODE_PROVIDER_NOT_FOUND, null);
225            return null;
226        }
227        // Trust system apps without signature checks
228        if (info.applicationInfo.isSystemApp()) {
229            return info;
230        }
231
232        List<byte[]> signatures;
233        try {
234            PackageInfo packageInfo = mPackageManager.getPackageInfo(info.packageName,
235                    PackageManager.GET_SIGNATURES);
236            signatures = convertToByteArrayList(packageInfo.signatures);
237            Collections.sort(signatures, sByteArrayComparator);
238        } catch (PackageManager.NameNotFoundException e) {
239            Log.e(TAG, "Can't find content provider " + providerAuthority, e);
240            receiver.send(RESULT_CODE_PROVIDER_NOT_FOUND, null);
241            return null;
242        }
243        List<List<byte[]>> requestCertificatesList = request.getCertificates();
244        for (int i = 0; i < requestCertificatesList.size(); ++i) {
245            // Make a copy so we can sort it without modifying the incoming data.
246            List<byte[]> requestSignatures = new ArrayList<>(requestCertificatesList.get(i));
247            Collections.sort(requestSignatures, sByteArrayComparator);
248            if (equalsByteArrayList(signatures, requestSignatures)) {
249                return info;
250            }
251        }
252        Log.e(TAG, "Certificates don't match for given provider " + providerAuthority);
253        receiver.send(RESULT_CODE_WRONG_CERTIFICATES, null);
254        return null;
255    }
256
257    private static final Comparator<byte[]> sByteArrayComparator = (l, r) -> {
258        if (l.length != r.length) {
259            return l.length - r.length;
260        }
261        for (int i = 0; i < l.length; ++i) {
262            if (l[i] != r[i]) {
263                return l[i] - r[i];
264            }
265        }
266        return 0;
267    };
268
269    private boolean equalsByteArrayList(List<byte[]> signatures, List<byte[]> requestSignatures) {
270        if (signatures.size() != requestSignatures.size()) {
271            return false;
272        }
273        for (int i = 0; i < signatures.size(); ++i) {
274            if (!Arrays.equals(signatures.get(i), requestSignatures.get(i))) {
275                return false;
276            }
277        }
278        return true;
279    }
280
281    private List<byte[]> convertToByteArrayList(Signature[] signatures) {
282        List<byte[]> shas = new ArrayList<>();
283        for (int i = 0; i < signatures.length; ++i) {
284            shas.add(signatures[i].toByteArray());
285        }
286        return shas;
287    }
288
289    /** @hide */
290    @VisibleForTesting
291    public void getFontFromProvider(FontRequest request, ResultReceiver receiver,
292            String authority) {
293        ArrayList<FontResult> result = null;
294        final Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
295                .authority(authority)
296                .build();
297        final Uri fileBaseUri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
298                .authority(authority)
299                .appendPath("file")
300                .build();
301        try (Cursor cursor = mContext.getContentResolver().query(uri, new String[] { Columns._ID,
302                        Columns.FILE_ID, Columns.TTC_INDEX, Columns.VARIATION_SETTINGS,
303                        Columns.STYLE, Columns.WEIGHT, Columns.ITALIC, Columns.RESULT_CODE },
304                "query = ?", new String[] { request.getQuery() }, null);) {
305            // TODO: Should we restrict the amount of fonts that can be returned?
306            // TODO: Write documentation explaining that all results should be from the same family.
307            if (cursor != null && cursor.getCount() > 0) {
308                final int resultCodeColumnIndex = cursor.getColumnIndex(Columns.RESULT_CODE);
309                int resultCode = -1;
310                result = new ArrayList<>();
311                final int idColumnIndex = cursor.getColumnIndexOrThrow(Columns._ID);
312                final int fileIdColumnIndex = cursor.getColumnIndex(Columns.FILE_ID);
313                final int ttcIndexColumnIndex = cursor.getColumnIndex(Columns.TTC_INDEX);
314                final int vsColumnIndex = cursor.getColumnIndex(Columns.VARIATION_SETTINGS);
315                final int weightColumnIndex = cursor.getColumnIndex(Columns.WEIGHT);
316                final int italicColumnIndex = cursor.getColumnIndex(Columns.ITALIC);
317                final int styleColumnIndex = cursor.getColumnIndex(Columns.STYLE);
318                while (cursor.moveToNext()) {
319                    resultCode = resultCodeColumnIndex != -1
320                            ? cursor.getInt(resultCodeColumnIndex) : Columns.RESULT_CODE_OK;
321                    if (resultCode != Columns.RESULT_CODE_OK) {
322                        if (resultCode < 0) {
323                            // Negative values are reserved for the internal errors.
324                            resultCode = Columns.RESULT_CODE_FONT_NOT_FOUND;
325                        }
326                        for (int i = 0; i < result.size(); ++i) {
327                            try {
328                                result.get(i).getFileDescriptor().close();
329                            } catch (IOException e) {
330                                // Ignore, as we are closing fds for cleanup.
331                            }
332                        }
333                        receiver.send(resultCode, null);
334                        return;
335                    }
336                    Uri fileUri;
337                    if (fileIdColumnIndex == -1) {
338                        long id = cursor.getLong(idColumnIndex);
339                        fileUri = ContentUris.withAppendedId(uri, id);
340                    } else {
341                        long id = cursor.getLong(fileIdColumnIndex);
342                        fileUri = ContentUris.withAppendedId(fileBaseUri, id);
343                    }
344                    try {
345                        ParcelFileDescriptor pfd =
346                                mContext.getContentResolver().openFileDescriptor(fileUri, "r");
347                        final int ttcIndex = ttcIndexColumnIndex != -1
348                                ? cursor.getInt(ttcIndexColumnIndex) : 0;
349                        final String variationSettings = vsColumnIndex != -1
350                                ? cursor.getString(vsColumnIndex) : null;
351                        // TODO: Stop using STYLE column and enforce WEIGHT/ITALIC column.
352                        int weight;
353                        boolean italic;
354                        if (weightColumnIndex != -1 && italicColumnIndex != -1) {
355                            weight = cursor.getInt(weightColumnIndex);
356                            italic = cursor.getInt(italicColumnIndex) == 1;
357                        } else if (styleColumnIndex != -1) {
358                            final int style = cursor.getInt(styleColumnIndex);
359                            weight = (style & Typeface.BOLD) != 0 ? 700 : 400;
360                            italic = (style & Typeface.ITALIC) != 0;
361                        } else {
362                            weight = 400;
363                            italic = false;
364                        }
365                        result.add(
366                                new FontResult(pfd, ttcIndex, variationSettings, weight, italic));
367                    } catch (FileNotFoundException e) {
368                        Log.e(TAG, "FileNotFoundException raised when interacting with content "
369                                + "provider " + authority, e);
370                    }
371                }
372            }
373        }
374        if (result != null && !result.isEmpty()) {
375            Bundle bundle = new Bundle();
376            bundle.putParcelableArrayList(PARCEL_FONT_RESULTS, result);
377            receiver.send(Columns.RESULT_CODE_OK, bundle);
378            return;
379        }
380        receiver.send(Columns.RESULT_CODE_FONT_NOT_FOUND, null);
381    }
382}
383