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