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