1 /*
2  * Copyright (C) 2019 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 android.content.res.loader;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.Context;
22 import android.content.pm.ApplicationInfo;
23 import android.content.res.ApkAssets;
24 import android.content.res.AssetFileDescriptor;
25 import android.os.ParcelFileDescriptor;
26 import android.util.Log;
27 
28 import com.android.internal.annotations.GuardedBy;
29 import com.android.internal.annotations.VisibleForTesting;
30 import com.android.internal.util.ArrayUtils;
31 
32 import java.io.Closeable;
33 import java.io.File;
34 import java.io.IOException;
35 
36 /**
37  * Provides methods to load resources data from APKs ({@code .apk}) and resources tables
38  * (eg. {@code resources.arsc}) for use with {@link ResourcesLoader ResourcesLoader(s)}.
39  */
40 public class ResourcesProvider implements AutoCloseable, Closeable {
41     private static final String TAG = "ResourcesProvider";
42     private final Object mLock = new Object();
43 
44     @GuardedBy("mLock")
45     private boolean mOpen = true;
46 
47     @GuardedBy("mLock")
48     private int mOpenCount = 0;
49 
50     @GuardedBy("mLock")
51     private final ApkAssets mApkAssets;
52 
53     /**
54      * Creates an empty ResourcesProvider with no resource data. This is useful for loading
55      * file-based assets not associated with resource identifiers.
56      *
57      * @param assetsProvider the assets provider that implements the loading of file-based resources
58      */
59     @NonNull
empty(@onNull AssetsProvider assetsProvider)60     public static ResourcesProvider empty(@NonNull AssetsProvider assetsProvider) {
61         return new ResourcesProvider(ApkAssets.loadEmptyForLoader(ApkAssets.PROPERTY_LOADER,
62                 assetsProvider));
63     }
64 
65     /**
66      * Creates a ResourcesProvider from an APK ({@code .apk}) file descriptor.
67      *
68      * <p>The file descriptor is duplicated and the original may be closed by the application at any
69      * time without affecting the ResourcesProvider.
70      *
71      * @param fileDescriptor the file descriptor of the APK to load
72      *
73      * @see ParcelFileDescriptor#open(File, int)
74      * @see android.system.Os#memfd_create(String, int)
75      */
76     @NonNull
loadFromApk(@onNull ParcelFileDescriptor fileDescriptor)77     public static ResourcesProvider loadFromApk(@NonNull ParcelFileDescriptor fileDescriptor)
78             throws IOException {
79         return loadFromApk(fileDescriptor, null /* assetsProvider */);
80     }
81 
82     /**
83      * Creates a ResourcesProvider from an APK ({@code .apk}) file descriptor.
84      *
85      * <p>The file descriptor is duplicated and the original may be closed by the application at any
86      * time without affecting the ResourcesProvider.
87      *
88      * <p>The assets provider can override the loading of files within the APK and can provide
89      * entirely new files that do not exist in the APK.
90      *
91      * @param fileDescriptor the file descriptor of the APK to load
92      * @param assetsProvider the assets provider that overrides the loading of file-based resources
93      *
94      * @see ParcelFileDescriptor#open(File, int)
95      * @see android.system.Os#memfd_create(String, int)
96      */
97     @NonNull
loadFromApk(@onNull ParcelFileDescriptor fileDescriptor, @Nullable AssetsProvider assetsProvider)98     public static ResourcesProvider loadFromApk(@NonNull ParcelFileDescriptor fileDescriptor,
99             @Nullable AssetsProvider assetsProvider)
100             throws IOException {
101         return new ResourcesProvider(ApkAssets.loadFromFd(fileDescriptor.getFileDescriptor(),
102                 fileDescriptor.toString(), ApkAssets.PROPERTY_LOADER, assetsProvider));
103     }
104 
105     /**
106      * Creates a ResourcesProvider from an APK ({@code .apk}) file descriptor.
107      *
108      * <p>The file descriptor is duplicated and the original may be closed by the application at any
109      * time without affecting the ResourcesProvider.
110      *
111      * <p>The assets provider can override the loading of files within the APK and can provide
112      * entirely new files that do not exist in the APK.
113      *
114      * @param fileDescriptor the file descriptor of the APK to load
115      * @param offset The location within the file that the apk starts. This must be 0 if length is
116      *               {@link AssetFileDescriptor#UNKNOWN_LENGTH}.
117      * @param length The number of bytes of the apk, or {@link AssetFileDescriptor#UNKNOWN_LENGTH}
118      *               if it extends to the end of the file.
119      * @param assetsProvider the assets provider that overrides the loading of file-based resources
120      *
121      * @see ParcelFileDescriptor#open(File, int)
122      * @see android.system.Os#memfd_create(String, int)
123      * @hide
124      */
125     @VisibleForTesting
126     @NonNull
loadFromApk(@onNull ParcelFileDescriptor fileDescriptor, long offset, long length, @Nullable AssetsProvider assetsProvider)127     public static ResourcesProvider loadFromApk(@NonNull ParcelFileDescriptor fileDescriptor,
128             long offset, long length, @Nullable AssetsProvider assetsProvider)
129             throws IOException {
130         return new ResourcesProvider(ApkAssets.loadFromFd(fileDescriptor.getFileDescriptor(),
131                 fileDescriptor.toString(), offset, length, ApkAssets.PROPERTY_LOADER,
132                 assetsProvider));
133     }
134 
135     /**
136      * Creates a ResourcesProvider from a resources table ({@code .arsc}) file descriptor.
137      *
138      * <p>The file descriptor is duplicated and the original may be closed by the application at any
139      * time without affecting the ResourcesProvider.
140      *
141      * <p>The resources table format is not an archive format and therefore cannot asset files
142      * within itself. The assets provider can instead provide files that are potentially referenced
143      * by path in the resources table.
144      *
145      * @param fileDescriptor the file descriptor of the resources table to load
146      * @param assetsProvider the assets provider that implements the loading of file-based resources
147      *
148      * @see ParcelFileDescriptor#open(File, int)
149      * @see android.system.Os#memfd_create(String, int)
150      */
151     @NonNull
loadFromTable(@onNull ParcelFileDescriptor fileDescriptor, @Nullable AssetsProvider assetsProvider)152     public static ResourcesProvider loadFromTable(@NonNull ParcelFileDescriptor fileDescriptor,
153             @Nullable AssetsProvider assetsProvider)
154             throws IOException {
155         return new ResourcesProvider(
156                 ApkAssets.loadTableFromFd(fileDescriptor.getFileDescriptor(),
157                         fileDescriptor.toString(), ApkAssets.PROPERTY_LOADER, assetsProvider));
158     }
159 
160     /**
161      * Creates a ResourcesProvider from a resources table ({@code .arsc}) file descriptor.
162      *
163      * The file descriptor is duplicated and the original may be closed by the application at any
164      * time without affecting the ResourcesProvider.
165      *
166      * <p>The resources table format is not an archive format and therefore cannot asset files
167      * within itself. The assets provider can instead provide files that are potentially referenced
168      * by path in the resources table.
169      *
170      * @param fileDescriptor the file descriptor of the resources table to load
171      * @param offset The location within the file that the table starts. This must be 0 if length is
172      *               {@link AssetFileDescriptor#UNKNOWN_LENGTH}.
173      * @param length The number of bytes of the table, or {@link AssetFileDescriptor#UNKNOWN_LENGTH}
174      *               if it extends to the end of the file.
175      * @param assetsProvider the assets provider that overrides the loading of file-based resources
176      *
177      * @see ParcelFileDescriptor#open(File, int)
178      * @see android.system.Os#memfd_create(String, int)
179      * @hide
180      */
181     @VisibleForTesting
182     @NonNull
loadFromTable(@onNull ParcelFileDescriptor fileDescriptor, long offset, long length, @Nullable AssetsProvider assetsProvider)183     public static ResourcesProvider loadFromTable(@NonNull ParcelFileDescriptor fileDescriptor,
184             long offset, long length, @Nullable AssetsProvider assetsProvider)
185             throws IOException {
186         return new ResourcesProvider(
187                 ApkAssets.loadTableFromFd(fileDescriptor.getFileDescriptor(),
188                         fileDescriptor.toString(), offset, length, ApkAssets.PROPERTY_LOADER,
189                         assetsProvider));
190     }
191 
192     /**
193      * Read from a split installed alongside the application, which may not have been
194      * loaded initially because the application requested isolated split loading.
195      *
196      * @param context a context of the package that contains the split
197      * @param splitName the name of the split to load
198      */
199     @NonNull
loadFromSplit(@onNull Context context, @NonNull String splitName)200     public static ResourcesProvider loadFromSplit(@NonNull Context context,
201             @NonNull String splitName) throws IOException {
202         ApplicationInfo appInfo = context.getApplicationInfo();
203         int splitIndex = ArrayUtils.indexOf(appInfo.splitNames, splitName);
204         if (splitIndex < 0) {
205             throw new IllegalArgumentException("Split " + splitName + " not found");
206         }
207 
208         String splitPath = appInfo.getSplitCodePaths()[splitIndex];
209         return new ResourcesProvider(ApkAssets.loadFromPath(splitPath, ApkAssets.PROPERTY_LOADER,
210                 null /* assetsProvider */));
211     }
212 
213     /**
214      * Creates a ResourcesProvider from a directory path.
215      *
216      * File-based resources will be resolved within the directory as if the directory is an APK.
217      *
218      * @param path the path of the directory to treat as an APK
219      * @param assetsProvider the assets provider that overrides the loading of file-based resources
220      */
221     @NonNull
loadFromDirectory(@onNull String path, @Nullable AssetsProvider assetsProvider)222     public static ResourcesProvider loadFromDirectory(@NonNull String path,
223             @Nullable AssetsProvider assetsProvider) throws IOException {
224         return new ResourcesProvider(ApkAssets.loadFromDir(path, ApkAssets.PROPERTY_LOADER,
225                 assetsProvider));
226     }
227 
228 
ResourcesProvider(@onNull ApkAssets apkAssets)229     private ResourcesProvider(@NonNull ApkAssets apkAssets) {
230         this.mApkAssets = apkAssets;
231     }
232 
233     /** @hide */
234     @NonNull
getApkAssets()235     public ApkAssets getApkAssets() {
236         return mApkAssets;
237     }
238 
incrementRefCount()239     final void incrementRefCount() {
240         synchronized (mLock) {
241             if (!mOpen) {
242                 throw new IllegalStateException("Operation failed: resources provider is closed");
243             }
244             mOpenCount++;
245         }
246     }
247 
decrementRefCount()248     final void decrementRefCount() {
249         synchronized (mLock) {
250             mOpenCount--;
251         }
252     }
253 
254     /**
255      * Frees internal data structures. Closed providers can no longer be added to
256      * {@link ResourcesLoader ResourcesLoader(s)}.
257      *
258      * @throws IllegalStateException if provider is currently used by a ResourcesLoader
259      */
260     @Override
close()261     public void close() {
262         synchronized (mLock) {
263             if (!mOpen) {
264                 return;
265             }
266 
267             if (mOpenCount != 0) {
268                 throw new IllegalStateException("Failed to close provider used by " + mOpenCount
269                         + " ResourcesLoader instances");
270             }
271             mOpen = false;
272         }
273 
274         try {
275             mApkAssets.close();
276         } catch (Throwable ignored) {
277         }
278     }
279 
280     @Override
finalize()281     protected void finalize() throws Throwable {
282         synchronized (mLock) {
283             if (mOpenCount != 0) {
284                 Log.w(TAG, "ResourcesProvider " + this + " finalized with non-zero refs: "
285                         + mOpenCount);
286             }
287 
288             if (mOpen) {
289                 mOpen = false;
290                 mApkAssets.close();
291             }
292         }
293     }
294 }
295