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