| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.content.res.loader; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.SuppressLint; |
| import android.content.Context; |
| import android.content.om.OverlayInfo; |
| import android.content.om.OverlayManager; |
| import android.content.pm.ApplicationInfo; |
| import android.content.res.ApkAssets; |
| import android.content.res.AssetFileDescriptor; |
| import android.os.ParcelFileDescriptor; |
| import android.util.Log; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.content.om.OverlayManagerImpl; |
| import com.android.internal.util.ArrayUtils; |
| import com.android.internal.util.Preconditions; |
| |
| import java.io.Closeable; |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.util.Objects; |
| |
| /** |
| * Provides methods to load resources data from APKs ({@code .apk}) and resources tables |
| * (eg. {@code resources.arsc}) for use with {@link ResourcesLoader ResourcesLoader(s)}. |
| */ |
| public class ResourcesProvider implements AutoCloseable, Closeable { |
| private static final String TAG = "ResourcesProvider"; |
| private final Object mLock = new Object(); |
| |
| @GuardedBy("mLock") |
| private boolean mOpen = true; |
| |
| @GuardedBy("mLock") |
| private int mOpenCount = 0; |
| |
| @GuardedBy("mLock") |
| private final ApkAssets mApkAssets; |
| |
| /** |
| * Creates an empty ResourcesProvider with no resource data. This is useful for loading |
| * file-based assets not associated with resource identifiers. |
| * |
| * @param assetsProvider the assets provider that implements the loading of file-based resources |
| */ |
| @NonNull |
| public static ResourcesProvider empty(@NonNull AssetsProvider assetsProvider) { |
| return new ResourcesProvider(ApkAssets.loadEmptyForLoader(ApkAssets.PROPERTY_LOADER, |
| assetsProvider)); |
| } |
| |
| /** |
| * Creates a ResourcesProvider instance from the specified overlay information. |
| * |
| * <p>In order to enable the registered overlays, an application can create a {@link |
| * ResourcesProvider} instance according to the specified {@link OverlayInfo} instance and put |
| * them into a {@link ResourcesLoader} instance. The application calls {@link |
| * android.content.res.Resources#addLoaders(ResourcesLoader...)} to load the overlays. |
| * |
| * @param overlayInfo is the information about the specified overlay |
| * @return the resources provider instance for the {@code overlayInfo} |
| * @throws IOException when the files can't be loaded. |
| * @see OverlayManager#getOverlayInfosForTarget(String) to get the list of overlay info. |
| */ |
| @SuppressLint("WrongConstant") // TODO(b/238713267): ApkAssets blocks PROPERTY_LOADER |
| @NonNull |
| public static ResourcesProvider loadOverlay(@NonNull OverlayInfo overlayInfo) |
| throws IOException { |
| Objects.requireNonNull(overlayInfo); |
| Preconditions.checkArgument(overlayInfo.isFabricated(), "Not accepted overlay"); |
| Preconditions.checkStringNotEmpty( |
| overlayInfo.getTargetOverlayableName(), "Without overlayable name"); |
| final String overlayName = |
| OverlayManagerImpl.checkOverlayNameValid(overlayInfo.getOverlayName()); |
| final String path = |
| Preconditions.checkStringNotEmpty( |
| overlayInfo.getBaseCodePath(), "Invalid base path"); |
| |
| final Path frroPath = Path.of(path); |
| if (!Files.isRegularFile(frroPath)) { |
| throw new FileNotFoundException("The frro file not found"); |
| } |
| final Path idmapPath = frroPath.getParent().resolve(overlayName + ".idmap"); |
| if (!Files.isRegularFile(idmapPath)) { |
| throw new FileNotFoundException("The idmap file not found"); |
| } |
| |
| return new ResourcesProvider( |
| ApkAssets.loadOverlayFromPath( |
| idmapPath.toString(), 0 /* flags: self targeting overlay */)); |
| } |
| |
| /** |
| * Creates a ResourcesProvider from an APK ({@code .apk}) file descriptor. |
| * |
| * <p>The file descriptor is duplicated and the original may be closed by the application at any |
| * time without affecting the ResourcesProvider. |
| * |
| * @param fileDescriptor the file descriptor of the APK to load |
| * |
| * @see ParcelFileDescriptor#open(File, int) |
| * @see android.system.Os#memfd_create(String, int) |
| */ |
| @NonNull |
| public static ResourcesProvider loadFromApk(@NonNull ParcelFileDescriptor fileDescriptor) |
| throws IOException { |
| return loadFromApk(fileDescriptor, null /* assetsProvider */); |
| } |
| |
| /** |
| * Creates a ResourcesProvider from an APK ({@code .apk}) file descriptor. |
| * |
| * <p>The file descriptor is duplicated and the original may be closed by the application at any |
| * time without affecting the ResourcesProvider. |
| * |
| * <p>The assets provider can override the loading of files within the APK and can provide |
| * entirely new files that do not exist in the APK. |
| * |
| * @param fileDescriptor the file descriptor of the APK to load |
| * @param assetsProvider the assets provider that overrides the loading of file-based resources |
| * |
| * @see ParcelFileDescriptor#open(File, int) |
| * @see android.system.Os#memfd_create(String, int) |
| */ |
| @NonNull |
| public static ResourcesProvider loadFromApk(@NonNull ParcelFileDescriptor fileDescriptor, |
| @Nullable AssetsProvider assetsProvider) |
| throws IOException { |
| return new ResourcesProvider(ApkAssets.loadFromFd(fileDescriptor.getFileDescriptor(), |
| fileDescriptor.toString(), ApkAssets.PROPERTY_LOADER, assetsProvider)); |
| } |
| |
| /** |
| * Creates a ResourcesProvider from an APK ({@code .apk}) file descriptor. |
| * |
| * <p>The file descriptor is duplicated and the original may be closed by the application at any |
| * time without affecting the ResourcesProvider. |
| * |
| * <p>The assets provider can override the loading of files within the APK and can provide |
| * entirely new files that do not exist in the APK. |
| * |
| * @param fileDescriptor the file descriptor of the APK to load |
| * @param offset The location within the file that the apk starts. This must be 0 if length is |
| * {@link AssetFileDescriptor#UNKNOWN_LENGTH}. |
| * @param length The number of bytes of the apk, or {@link AssetFileDescriptor#UNKNOWN_LENGTH} |
| * if it extends to the end of the file. |
| * @param assetsProvider the assets provider that overrides the loading of file-based resources |
| * |
| * @see ParcelFileDescriptor#open(File, int) |
| * @see android.system.Os#memfd_create(String, int) |
| * @hide |
| */ |
| @VisibleForTesting |
| @NonNull |
| public static ResourcesProvider loadFromApk(@NonNull ParcelFileDescriptor fileDescriptor, |
| long offset, long length, @Nullable AssetsProvider assetsProvider) |
| throws IOException { |
| return new ResourcesProvider(ApkAssets.loadFromFd(fileDescriptor.getFileDescriptor(), |
| fileDescriptor.toString(), offset, length, ApkAssets.PROPERTY_LOADER, |
| assetsProvider)); |
| } |
| |
| /** |
| * Creates a ResourcesProvider from a resources table ({@code .arsc}) file descriptor. |
| * |
| * <p>The file descriptor is duplicated and the original may be closed by the application at any |
| * time without affecting the ResourcesProvider. |
| * |
| * <p>The resources table format is not an archive format and therefore cannot asset files |
| * within itself. The assets provider can instead provide files that are potentially referenced |
| * by path in the resources table. |
| * |
| * @param fileDescriptor the file descriptor of the resources table to load |
| * @param assetsProvider the assets provider that implements the loading of file-based resources |
| * |
| * @see ParcelFileDescriptor#open(File, int) |
| * @see android.system.Os#memfd_create(String, int) |
| */ |
| @NonNull |
| public static ResourcesProvider loadFromTable(@NonNull ParcelFileDescriptor fileDescriptor, |
| @Nullable AssetsProvider assetsProvider) |
| throws IOException { |
| return new ResourcesProvider( |
| ApkAssets.loadTableFromFd(fileDescriptor.getFileDescriptor(), |
| fileDescriptor.toString(), ApkAssets.PROPERTY_LOADER, assetsProvider)); |
| } |
| |
| /** |
| * Creates a ResourcesProvider from a resources table ({@code .arsc}) file descriptor. |
| * |
| * The file descriptor is duplicated and the original may be closed by the application at any |
| * time without affecting the ResourcesProvider. |
| * |
| * <p>The resources table format is not an archive format and therefore cannot asset files |
| * within itself. The assets provider can instead provide files that are potentially referenced |
| * by path in the resources table. |
| * |
| * @param fileDescriptor the file descriptor of the resources table to load |
| * @param offset The location within the file that the table starts. This must be 0 if length is |
| * {@link AssetFileDescriptor#UNKNOWN_LENGTH}. |
| * @param length The number of bytes of the table, or {@link AssetFileDescriptor#UNKNOWN_LENGTH} |
| * if it extends to the end of the file. |
| * @param assetsProvider the assets provider that overrides the loading of file-based resources |
| * |
| * @see ParcelFileDescriptor#open(File, int) |
| * @see android.system.Os#memfd_create(String, int) |
| * @hide |
| */ |
| @VisibleForTesting |
| @NonNull |
| public static ResourcesProvider loadFromTable(@NonNull ParcelFileDescriptor fileDescriptor, |
| long offset, long length, @Nullable AssetsProvider assetsProvider) |
| throws IOException { |
| return new ResourcesProvider( |
| ApkAssets.loadTableFromFd(fileDescriptor.getFileDescriptor(), |
| fileDescriptor.toString(), offset, length, ApkAssets.PROPERTY_LOADER, |
| assetsProvider)); |
| } |
| |
| /** |
| * Read from a split installed alongside the application, which may not have been |
| * loaded initially because the application requested isolated split loading. |
| * |
| * @param context a context of the package that contains the split |
| * @param splitName the name of the split to load |
| */ |
| @NonNull |
| public static ResourcesProvider loadFromSplit(@NonNull Context context, |
| @NonNull String splitName) throws IOException { |
| ApplicationInfo appInfo = context.getApplicationInfo(); |
| int splitIndex = ArrayUtils.indexOf(appInfo.splitNames, splitName); |
| if (splitIndex < 0) { |
| throw new IllegalArgumentException("Split " + splitName + " not found"); |
| } |
| |
| String splitPath = appInfo.getSplitCodePaths()[splitIndex]; |
| return new ResourcesProvider(ApkAssets.loadFromPath(splitPath, ApkAssets.PROPERTY_LOADER, |
| null /* assetsProvider */)); |
| } |
| |
| /** |
| * Creates a ResourcesProvider from a directory path. |
| * |
| * File-based resources will be resolved within the directory as if the directory is an APK. |
| * |
| * @param path the path of the directory to treat as an APK |
| * @param assetsProvider the assets provider that overrides the loading of file-based resources |
| */ |
| @NonNull |
| public static ResourcesProvider loadFromDirectory(@NonNull String path, |
| @Nullable AssetsProvider assetsProvider) throws IOException { |
| return new ResourcesProvider(ApkAssets.loadFromDir(path, ApkAssets.PROPERTY_LOADER, |
| assetsProvider)); |
| } |
| |
| |
| private ResourcesProvider(@NonNull ApkAssets apkAssets) { |
| this.mApkAssets = apkAssets; |
| } |
| |
| /** @hide */ |
| @NonNull |
| public ApkAssets getApkAssets() { |
| return mApkAssets; |
| } |
| |
| final void incrementRefCount() { |
| synchronized (mLock) { |
| if (!mOpen) { |
| throw new IllegalStateException("Operation failed: resources provider is closed"); |
| } |
| mOpenCount++; |
| } |
| } |
| |
| final void decrementRefCount() { |
| synchronized (mLock) { |
| mOpenCount--; |
| } |
| } |
| |
| /** |
| * Frees internal data structures. Closed providers can no longer be added to |
| * {@link ResourcesLoader ResourcesLoader(s)}. |
| * |
| * @throws IllegalStateException if provider is currently used by a ResourcesLoader |
| */ |
| @Override |
| public void close() { |
| synchronized (mLock) { |
| if (!mOpen) { |
| return; |
| } |
| |
| if (mOpenCount != 0) { |
| throw new IllegalStateException("Failed to close provider used by " + mOpenCount |
| + " ResourcesLoader instances"); |
| } |
| mOpen = false; |
| } |
| |
| try { |
| mApkAssets.close(); |
| } catch (Throwable ignored) { |
| } |
| } |
| |
| @Override |
| protected void finalize() throws Throwable { |
| synchronized (mLock) { |
| if (mOpenCount != 0) { |
| Log.w(TAG, "ResourcesProvider " + this + " finalized with non-zero refs: " |
| + mOpenCount); |
| } |
| } |
| } |
| } |