FontsContract.java revision 3c4be77db95ea716889568bde853be082e764da9
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.app.ActivityThread;
19import android.content.ContentResolver;
20import android.content.ContentUris;
21import android.content.Context;
22import android.content.pm.PackageInfo;
23import android.content.pm.PackageManager;
24import android.content.pm.ProviderInfo;
25import android.content.pm.Signature;
26import android.database.Cursor;
27import android.graphics.Typeface;
28import android.graphics.fonts.FontRequest;
29import android.graphics.fonts.FontResult;
30import android.net.Uri;
31import android.os.Bundle;
32import android.os.Handler;
33import android.os.HandlerThread;
34import android.os.ParcelFileDescriptor;
35import android.os.Process;
36import android.os.ResultReceiver;
37import android.util.Log;
38
39import com.android.internal.annotations.GuardedBy;
40import com.android.internal.annotations.VisibleForTesting;
41
42import java.io.FileNotFoundException;
43import java.util.ArrayList;
44import java.util.HashSet;
45import java.util.List;
46import java.util.Set;
47
48/**
49 * Utility class to deal with Font ContentProviders.
50 */
51public class FontsContract {
52    private static final String TAG = "FontsContract";
53
54    /**
55     * Defines the constants used in a response from a Font Provider. The cursor returned from the
56     * query should have the ID column populated with the content uri ID for the resulting font.
57     * This should point to a real file or shared memory, as the client will mmap the given file
58     * descriptor. Pipes, sockets and other non-mmap-able file descriptors will fail to load in the
59     * client application.
60     */
61    public static final class Columns implements BaseColumns {
62        /**
63         * Constant used to request data from a font provider. The cursor returned from the query
64         * should have this column populated with an int for the ttc index for the resulting font.
65         */
66        public static final String TTC_INDEX = "font_ttc_index";
67        /**
68         * Constant used to request data from a font provider. The cursor returned from the query
69         * may populate this column with the font variation settings String information for the
70         * font.
71         */
72        public static final String VARIATION_SETTINGS = "font_variation_settings";
73        /**
74         * Constant used to request data from a font provider. The cursor returned from the query
75         * should have this column populated with the int style for the resulting font. This should
76         * be one of {@link android.graphics.Typeface#NORMAL},
77         * {@link android.graphics.Typeface#BOLD}, {@link android.graphics.Typeface#ITALIC} or
78         * {@link android.graphics.Typeface#BOLD_ITALIC}
79         */
80        public static final String STYLE = "font_style";
81    }
82
83    /**
84     * Constant used to identify the List of {@link ParcelFileDescriptor} item in the Bundle
85     * returned to the ResultReceiver in getFont.
86     * @hide
87     */
88    public static final String PARCEL_FONT_RESULTS = "font_results";
89
90    /** @hide */
91    public static final int RESULT_CODE_OK = 0;
92    /** @hide */
93    public static final int RESULT_CODE_FONT_NOT_FOUND = 1;
94    /** @hide */
95    public static final int RESULT_CODE_PROVIDER_NOT_FOUND = 2;
96
97    private static final int THREAD_RENEWAL_THRESHOLD_MS = 10000;
98
99    private final Context mContext;
100    private final PackageManager mPackageManager;
101    private final Object mLock = new Object();
102    @GuardedBy("mLock")
103    private Handler mHandler;
104    @GuardedBy("mLock")
105    private HandlerThread mThread;
106
107    /** @hide */
108    public FontsContract() {
109        // TODO: investigate if the system context is the best option here. ApplicationContext or
110        // the one passed by developer?
111        // TODO: Looks like ActivityThread.currentActivityThread() can return null. Check when it
112        // returns null and check if we need to handle null case.
113        mContext = ActivityThread.currentActivityThread().getSystemContext();
114        mPackageManager = mContext.getPackageManager();
115    }
116
117    /** @hide */
118    @VisibleForTesting
119    public FontsContract(Context context, PackageManager packageManager) {
120        mContext = context;
121        mPackageManager = packageManager;
122    }
123
124    // We use a background thread to post the content resolving work for all requests on. This
125    // thread should be quit/stopped after all requests are done.
126    private final Runnable mReplaceDispatcherThreadRunnable = new Runnable() {
127        @Override
128        public void run() {
129            synchronized (mLock) {
130                if (mThread != null) {
131                    mThread.quitSafely();
132                    mThread = null;
133                    mHandler = null;
134                }
135            }
136        }
137    };
138
139    /**
140     * @hide
141     */
142    public void getFont(FontRequest request, ResultReceiver receiver) {
143        synchronized (mLock) {
144            if (mHandler == null) {
145                mThread = new HandlerThread("fonts", Process.THREAD_PRIORITY_BACKGROUND);
146                mThread.start();
147                mHandler = new Handler(mThread.getLooper());
148            }
149            mHandler.post(() -> {
150                ProviderInfo providerInfo = getProvider(request);
151                if (providerInfo == null) {
152                    receiver.send(RESULT_CODE_PROVIDER_NOT_FOUND, null);
153                    return;
154                }
155                getFontFromProvider(request, receiver, providerInfo.authority);
156            });
157            mHandler.removeCallbacks(mReplaceDispatcherThreadRunnable);
158            mHandler.postDelayed(mReplaceDispatcherThreadRunnable, THREAD_RENEWAL_THRESHOLD_MS);
159        }
160    }
161
162    /** @hide */
163    @VisibleForTesting
164    public ProviderInfo getProvider(FontRequest request) {
165        String providerAuthority = request.getProviderAuthority();
166        ProviderInfo info = mPackageManager.resolveContentProvider(providerAuthority, 0);
167        if (info == null) {
168            Log.e(TAG, "Can't find content provider " + providerAuthority);
169            return null;
170        }
171
172        if (!info.packageName.equals(request.getProviderPackage())) {
173            Log.e(TAG, "Found content provider " + providerAuthority + ", but package was not "
174                    + request.getProviderPackage());
175            return null;
176        }
177        // Trust system apps without signature checks
178        if (info.applicationInfo.isSystemApp()) {
179            return info;
180        }
181
182        Set<byte[]> signatures;
183        try {
184            PackageInfo packageInfo = mPackageManager.getPackageInfo(info.packageName,
185                    PackageManager.GET_SIGNATURES);
186            signatures = convertToSet(packageInfo.signatures);
187        } catch (PackageManager.NameNotFoundException e) {
188            Log.e(TAG, "Can't find content provider " + providerAuthority, e);
189            return null;
190        }
191        List<List<byte[]>> requestCertificatesList = request.getCertificates();
192        for (int i = 0; i < requestCertificatesList.size(); ++i) {
193            final Set<byte[]> requestCertificates = convertToSet(requestCertificatesList.get(i));
194            if (signatures.equals(requestCertificates)) {
195                return info;
196            }
197        }
198        Log.e(TAG, "Certificates don't match for given provider " + providerAuthority);
199        return null;
200    }
201
202    private Set<byte[]> convertToSet(Signature[] signatures) {
203        Set<byte[]> shas = new HashSet<>();
204        for (int i = 0; i < signatures.length; ++i) {
205            shas.add(signatures[i].toByteArray());
206        }
207        return shas;
208    }
209
210    private Set<byte[]> convertToSet(List<byte[]> certs) {
211        Set<byte[]> shas = new HashSet<>();
212        shas.addAll(certs);
213        return shas;
214    }
215
216    /** @hide */
217    @VisibleForTesting
218    public void getFontFromProvider(FontRequest request, ResultReceiver receiver,
219            String authority) {
220        ArrayList<FontResult> result = null;
221        Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
222                .authority(authority)
223                .build();
224        try (Cursor cursor = mContext.getContentResolver().query(uri, new String[] { Columns._ID,
225                        Columns.TTC_INDEX, Columns.VARIATION_SETTINGS, Columns.STYLE },
226                "query = ?", new String[] { request.getQuery() }, null);) {
227            // TODO: Should we restrict the amount of fonts that can be returned?
228            // TODO: Write documentation explaining that all results should be from the same family.
229            if (cursor != null && cursor.getCount() > 0) {
230                result = new ArrayList<>();
231                final int idColumnIndex = cursor.getColumnIndex(Columns._ID);
232                final int ttcIndexColumnIndex = cursor.getColumnIndex(Columns.TTC_INDEX);
233                final int vsColumnIndex = cursor.getColumnIndex(Columns.VARIATION_SETTINGS);
234                final int styleColumnIndex = cursor.getColumnIndex(Columns.STYLE);
235                while (cursor.moveToNext()) {
236                    long id = cursor.getLong(idColumnIndex);
237                    Uri fileUri = ContentUris.withAppendedId(uri, id);
238                    try {
239                        ParcelFileDescriptor pfd =
240                                mContext.getContentResolver().openFileDescriptor(fileUri, "r");
241                        final int ttcIndex = ttcIndexColumnIndex != -1
242                                ? cursor.getInt(ttcIndexColumnIndex) : 0;
243                        final String variationSettings = vsColumnIndex != -1
244                                ? cursor.getString(vsColumnIndex) : null;
245                        final int style = styleColumnIndex != -1
246                                ? cursor.getInt(styleColumnIndex) : Typeface.NORMAL;
247                        result.add(new FontResult(pfd, ttcIndex, variationSettings, style));
248                    } catch (FileNotFoundException e) {
249                        Log.e(TAG, "FileNotFoundException raised when interacting with content "
250                                + "provider " + authority, e);
251                    }
252                }
253            }
254        }
255        if (result != null && !result.isEmpty()) {
256            Bundle bundle = new Bundle();
257            bundle.putParcelableArrayList(PARCEL_FONT_RESULTS, result);
258            receiver.send(RESULT_CODE_OK, bundle);
259            return;
260        }
261        receiver.send(RESULT_CODE_FONT_NOT_FOUND, null);
262    }
263}
264