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