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 */ 16 17 package androidx.core.graphics; 18 19 import static androidx.annotation.RestrictTo.Scope.LIBRARY; 20 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; 21 22 import android.content.ContentResolver; 23 import android.content.Context; 24 import android.content.res.Resources; 25 import android.net.Uri; 26 import android.os.CancellationSignal; 27 import android.os.ParcelFileDescriptor; 28 import android.os.Process; 29 import android.os.StrictMode; 30 import android.util.Log; 31 32 import androidx.annotation.RestrictTo; 33 import androidx.core.provider.FontsContractCompat; 34 35 import org.jspecify.annotations.NonNull; 36 import org.jspecify.annotations.Nullable; 37 38 import java.io.Closeable; 39 import java.io.File; 40 import java.io.FileInputStream; 41 import java.io.FileOutputStream; 42 import java.io.IOException; 43 import java.io.InputStream; 44 import java.nio.ByteBuffer; 45 import java.nio.channels.FileChannel; 46 import java.util.Collections; 47 import java.util.HashMap; 48 import java.util.Map; 49 50 /** 51 * Utility methods for TypefaceCompat. 52 */ 53 @RestrictTo(LIBRARY_GROUP_PREFIX) 54 public class TypefaceCompatUtil { 55 private static final String TAG = "TypefaceCompatUtil"; 56 TypefaceCompatUtil()57 private TypefaceCompatUtil() {} // Do not instantiate. 58 59 private static final String CACHE_FILE_PREFIX = ".font"; 60 61 /** 62 * Creates a temp file. 63 * 64 * Returns null if failed to create temp file. 65 */ getTempFile(@onNull Context context)66 public static @Nullable File getTempFile(@NonNull Context context) { 67 File cacheDir = context.getCacheDir(); 68 if (cacheDir == null) { 69 return null; 70 } 71 72 final String prefix = CACHE_FILE_PREFIX + Process.myPid() + "-" + Process.myTid() + "-"; 73 for (int i = 0; i < 100; ++i) { 74 final File file = new File(cacheDir, prefix + i); 75 try { 76 if (file.createNewFile()) { 77 return file; 78 } 79 } catch (IOException e) { 80 // ignore. Try next file. 81 } 82 } 83 return null; 84 } 85 86 /** 87 * Copy the file contents to the direct byte buffer. 88 */ mmap(File file)89 private static @Nullable ByteBuffer mmap(File file) { 90 try (FileInputStream fis = new FileInputStream(file)) { 91 FileChannel channel = fis.getChannel(); 92 final long size = channel.size(); 93 return channel.map(FileChannel.MapMode.READ_ONLY, 0, size); 94 } catch (IOException e) { 95 return null; 96 } 97 } 98 99 /** 100 * Copy the file contents to the direct byte buffer. 101 */ mmap(@onNull Context context, @Nullable CancellationSignal cancellationSignal, @NonNull Uri uri)102 public static @Nullable ByteBuffer mmap(@NonNull Context context, 103 @Nullable CancellationSignal cancellationSignal, @NonNull Uri uri) { 104 final ContentResolver resolver = context.getContentResolver(); 105 try { 106 try (ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r", 107 cancellationSignal)) { 108 if (pfd == null) { 109 return null; 110 } 111 try (FileInputStream fis = new FileInputStream(pfd.getFileDescriptor())) { 112 FileChannel channel = fis.getChannel(); 113 final long size = channel.size(); 114 return channel.map(FileChannel.MapMode.READ_ONLY, 0, size); 115 } 116 } 117 } catch (IOException e) { 118 return null; 119 } 120 } 121 122 /** 123 * Copy the resource contents to the direct byte buffer. 124 */ 125 @SuppressWarnings("ResultOfMethodCallIgnored") copyToDirectBuffer(@onNull Context context, @NonNull Resources res, int id)126 public static @Nullable ByteBuffer copyToDirectBuffer(@NonNull Context context, 127 @NonNull Resources res, int id) { 128 File tmpFile = getTempFile(context); 129 if (tmpFile == null) { 130 return null; 131 } 132 try { 133 if (!copyToFile(tmpFile, res, id)) { 134 return null; 135 } 136 return mmap(tmpFile); 137 } finally { 138 tmpFile.delete(); 139 } 140 } 141 142 /** 143 * Copy the input stream contents to file. 144 */ copyToFile(@onNull File file, @NonNull InputStream is)145 public static boolean copyToFile(@NonNull File file, @NonNull InputStream is) { 146 FileOutputStream os = null; 147 StrictMode.ThreadPolicy old = StrictMode.allowThreadDiskWrites(); 148 try { 149 os = new FileOutputStream(file, false); 150 byte[] buffer = new byte[1024]; 151 int readLen; 152 while ((readLen = is.read(buffer)) != -1) { 153 os.write(buffer, 0, readLen); 154 } 155 return true; 156 } catch (IOException e) { 157 Log.e(TAG, "Error copying resource contents to temp file: " + e.getMessage()); 158 return false; 159 } finally { 160 closeQuietly(os); 161 StrictMode.setThreadPolicy(old); 162 } 163 } 164 165 /** 166 * Copy the resource contents to file. 167 */ 168 @SuppressWarnings("BooleanMethodIsAlwaysInverted") copyToFile(@onNull File file, @NonNull Resources res, int id)169 public static boolean copyToFile(@NonNull File file, @NonNull Resources res, int id) { 170 InputStream is = null; 171 try { 172 is = res.openRawResource(id); 173 return copyToFile(file, is); 174 } finally { 175 closeQuietly(is); 176 } 177 } 178 179 /** 180 * Attempts to close a Closeable, swallowing any resulting IOException. 181 * 182 * @param c the closeable to close 183 */ closeQuietly(@ullable Closeable c)184 public static void closeQuietly(@Nullable Closeable c) { 185 if (c != null) { 186 try { 187 c.close(); 188 } catch (IOException e) { 189 // Quietly! 190 } 191 } 192 } 193 194 /** 195 * A helper function to create a mapping from {@link Uri} to {@link ByteBuffer}. 196 * 197 * Skip if the file contents is not ready to be read. 198 * 199 * @param context A {@link Context} to be used for resolving content URI in 200 * {@link FontsContractCompat.FontInfo}. 201 * @param fonts An array of {@link FontsContractCompat.FontInfo}. 202 * @return A map from {@link Uri} to {@link ByteBuffer}. 203 */ 204 @RestrictTo(LIBRARY) readFontInfoIntoByteBuffer( @onNull Context context, FontsContractCompat.FontInfo @NonNull [] fonts, @Nullable CancellationSignal cancellationSignal )205 public static @NonNull Map<Uri, ByteBuffer> readFontInfoIntoByteBuffer( 206 @NonNull Context context, 207 FontsContractCompat.FontInfo @NonNull [] fonts, 208 @Nullable CancellationSignal cancellationSignal 209 ) { 210 final HashMap<Uri, ByteBuffer> out = new HashMap<>(); 211 212 for (FontsContractCompat.FontInfo font : fonts) { 213 if (font.getResultCode() != FontsContractCompat.Columns.RESULT_CODE_OK) { 214 continue; 215 } 216 217 final Uri uri = font.getUri(); 218 if (out.containsKey(uri)) { 219 continue; 220 } 221 222 ByteBuffer buffer = TypefaceCompatUtil.mmap(context, cancellationSignal, uri); 223 out.put(uri, buffer); 224 } 225 return Collections.unmodifiableMap(out); 226 } 227 } 228