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