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