diff options
112 files changed, 4760 insertions, 539 deletions
diff --git a/apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java b/apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java index 9039f921b3ba..e27670c34fb2 100644 --- a/apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java +++ b/apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java @@ -17,10 +17,13 @@ package android.os; import android.annotation.NonNull; +import android.annotation.RequiresPermission; import android.annotation.SystemService; import android.annotation.TestApi; import android.content.Context; +import java.util.List; + /** * Access to the service that keeps track of device idleness and drives low power mode based on * that. @@ -66,4 +69,19 @@ public class DeviceIdleManager { return new String[0]; } } + + /** + * Add the specified packages to the power save whitelist. + * + * @return the number of packages that were successfully added to the whitelist + */ + @RequiresPermission(android.Manifest.permission.DEVICE_POWER) + public int addPowerSaveWhitelistApps(@NonNull List<String> packageNames) { + try { + return mService.addPowerSaveWhitelistApps(packageNames); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + return 0; + } + } } diff --git a/apex/jobscheduler/framework/java/android/os/IDeviceIdleController.aidl b/apex/jobscheduler/framework/java/android/os/IDeviceIdleController.aidl index 9d5becbf77cd..20fb000b36d3 100644 --- a/apex/jobscheduler/framework/java/android/os/IDeviceIdleController.aidl +++ b/apex/jobscheduler/framework/java/android/os/IDeviceIdleController.aidl @@ -21,6 +21,7 @@ import android.os.UserHandle; /** @hide */ interface IDeviceIdleController { void addPowerSaveWhitelistApp(String name); + int addPowerSaveWhitelistApps(in List<String> packageNames); void removePowerSaveWhitelistApp(String name); /* Removes an app from the system whitelist. Calling restoreSystemPowerWhitelistApp will add the app back into the system whitelist */ diff --git a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java index 20ee06405a86..4ee46f453bca 100644 --- a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java +++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java @@ -105,6 +105,8 @@ import java.io.IOException; import java.io.PrintWriter; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.stream.Collectors; /** @@ -1549,11 +1551,20 @@ public class DeviceIdleController extends SystemService if (DEBUG) { Slog.i(TAG, "addPowerSaveWhitelistApp(name = " + name + ")"); } + addPowerSaveWhitelistApps(Collections.singletonList(name)); + } + + @Override + public int addPowerSaveWhitelistApps(List<String> packageNames) { + if (DEBUG) { + Slog.i(TAG, + "addPowerSaveWhitelistApps(name = " + packageNames + ")"); + } getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, null); long ident = Binder.clearCallingIdentity(); try { - addPowerSaveWhitelistAppInternal(name); + return addPowerSaveWhitelistAppsInternal(packageNames); } finally { Binder.restoreCallingIdentity(ident); } @@ -2188,21 +2199,35 @@ public class DeviceIdleController extends SystemService } } - public boolean addPowerSaveWhitelistAppInternal(String name) { + private int addPowerSaveWhitelistAppsInternal(List<String> pkgNames) { + int numAdded = 0; + int numErrors = 0; synchronized (this) { - try { - ApplicationInfo ai = getContext().getPackageManager().getApplicationInfo(name, - PackageManager.MATCH_ANY_USER); - if (mPowerSaveWhitelistUserApps.put(name, UserHandle.getAppId(ai.uid)) == null) { - reportPowerSaveWhitelistChangedLocked(); - updateWhitelistAppIdsLocked(); - writeConfigFileLocked(); + for (int i = pkgNames.size() - 1; i >= 0; --i) { + final String name = pkgNames.get(i); + if (name == null) { + numErrors++; + continue; } - return true; - } catch (PackageManager.NameNotFoundException e) { - return false; + try { + ApplicationInfo ai = getContext().getPackageManager().getApplicationInfo(name, + PackageManager.MATCH_ANY_USER); + if (mPowerSaveWhitelistUserApps.put(name, UserHandle.getAppId(ai.uid)) + == null) { + numAdded++; + } + } catch (PackageManager.NameNotFoundException e) { + Slog.e(TAG, "Tried to add unknown package to power save whitelist: " + name); + numErrors++; + } + } + if (numAdded > 0) { + reportPowerSaveWhitelistChangedLocked(); + updateWhitelistAppIdsLocked(); + writeConfigFileLocked(); } } + return pkgNames.size() - numErrors; } public boolean removePowerSaveWhitelistAppInternal(String name) { @@ -4070,7 +4095,8 @@ public class DeviceIdleController extends SystemService char op = arg.charAt(0); String pkg = arg.substring(1); if (op == '+') { - if (addPowerSaveWhitelistAppInternal(pkg)) { + if (addPowerSaveWhitelistAppsInternal(Collections.singletonList(pkg)) + == 1) { pw.println("Added: " + pkg); } else { pw.println("Unknown package: " + pkg); diff --git a/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java b/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java index b97da59f8d17..aa7696df6dbd 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java +++ b/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java @@ -17,13 +17,8 @@ package com.android.server.job.restrictions; import android.app.job.JobParameters; -import android.content.Context; -import android.os.IThermalService; -import android.os.IThermalStatusListener; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.Temperature; -import android.util.Slog; +import android.os.PowerManager; +import android.os.PowerManager.OnThermalStatusChangedListener; import android.util.proto.ProtoOutputStream; import com.android.internal.util.IndentingPrintWriter; @@ -36,31 +31,29 @@ public class ThermalStatusRestriction extends JobRestriction { private volatile boolean mIsThermalRestricted = false; + private PowerManager mPowerManager; + public ThermalStatusRestriction(JobSchedulerService service) { super(service, JobParameters.REASON_DEVICE_THERMAL); } @Override public void onSystemServicesReady() { - final IThermalService thermalService = IThermalService.Stub.asInterface( - ServiceManager.getService(Context.THERMAL_SERVICE)); - if (thermalService != null) { - try { - thermalService.registerThermalStatusListener(new IThermalStatusListener.Stub() { - @Override - public void onStatusChange(int status) { - final boolean shouldBeActive = status >= Temperature.THROTTLING_SEVERE; - if (mIsThermalRestricted == shouldBeActive) { - return; - } - mIsThermalRestricted = shouldBeActive; - mService.onControllerStateChanged(); - } - }); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to register thermal callback.", e); + mPowerManager = mService.getContext().getSystemService(PowerManager.class); + // Use MainExecutor + mPowerManager.addThermalStatusListener(new OnThermalStatusChangedListener() { + @Override + public void onThermalStatusChanged(int status) { + // This is called on the main thread. Do not do any slow operations in it. + // mService.onControllerStateChanged() will just post a message, which is okay. + final boolean shouldBeActive = status >= PowerManager.THERMAL_STATUS_SEVERE; + if (mIsThermalRestricted == shouldBeActive) { + return; + } + mIsThermalRestricted = shouldBeActive; + mService.onControllerStateChanged(); } - } + }); } @Override diff --git a/api/current.txt b/api/current.txt index 2598dee83318..603dec283c66 100644 --- a/api/current.txt +++ b/api/current.txt @@ -9805,7 +9805,7 @@ package android.content { method public abstract java.io.File[] getExternalCacheDirs(); method @Nullable public abstract java.io.File getExternalFilesDir(@Nullable String); method public abstract java.io.File[] getExternalFilesDirs(String); - method public abstract java.io.File[] getExternalMediaDirs(); + method @Deprecated public abstract java.io.File[] getExternalMediaDirs(); method public abstract java.io.File getFileStreamPath(String); method public abstract java.io.File getFilesDir(); method public java.util.concurrent.Executor getMainExecutor(); @@ -12402,6 +12402,8 @@ package android.content.res { public class Resources { ctor @Deprecated public Resources(android.content.res.AssetManager, android.util.DisplayMetrics, android.content.res.Configuration); + method public void addLoader(@NonNull android.content.res.loader.ResourceLoader, @NonNull android.content.res.loader.ResourcesProvider, @IntRange(from=0) int); + method public int addLoader(@NonNull android.content.res.loader.ResourceLoader, @NonNull android.content.res.loader.ResourcesProvider); method public final void finishPreloading(); method public final void flushLayoutCache(); method @NonNull public android.content.res.XmlResourceParser getAnimation(@AnimatorRes @AnimRes int) throws android.content.res.Resources.NotFoundException; @@ -12428,6 +12430,7 @@ package android.content.res { method @NonNull public int[] getIntArray(@ArrayRes int) throws android.content.res.Resources.NotFoundException; method public int getInteger(@IntegerRes int) throws android.content.res.Resources.NotFoundException; method @NonNull public android.content.res.XmlResourceParser getLayout(@LayoutRes int) throws android.content.res.Resources.NotFoundException; + method @NonNull public java.util.List<android.util.Pair<android.content.res.loader.ResourceLoader,android.content.res.loader.ResourcesProvider>> getLoaders(); method @Deprecated public android.graphics.Movie getMovie(@RawRes int) throws android.content.res.Resources.NotFoundException; method @NonNull public String getQuantityString(@PluralsRes int, int, java.lang.Object...) throws android.content.res.Resources.NotFoundException; method @NonNull public String getQuantityString(@PluralsRes int, int) throws android.content.res.Resources.NotFoundException; @@ -12455,6 +12458,8 @@ package android.content.res { method public android.content.res.AssetFileDescriptor openRawResourceFd(@RawRes int) throws android.content.res.Resources.NotFoundException; method public void parseBundleExtra(String, android.util.AttributeSet, android.os.Bundle) throws org.xmlpull.v1.XmlPullParserException; method public void parseBundleExtras(android.content.res.XmlResourceParser, android.os.Bundle) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException; + method public int removeLoader(@NonNull android.content.res.loader.ResourceLoader); + method public void setLoaders(@Nullable java.util.List<android.util.Pair<android.content.res.loader.ResourceLoader,android.content.res.loader.ResourcesProvider>>); method @Deprecated public void updateConfiguration(android.content.res.Configuration, android.util.DisplayMetrics); field @AnyRes public static final int ID_NULL = 0; // 0x0 } @@ -12522,6 +12527,33 @@ package android.content.res { } +package android.content.res.loader { + + public class DirectoryResourceLoader implements android.content.res.loader.ResourceLoader { + ctor public DirectoryResourceLoader(@NonNull java.io.File); + method @Nullable public java.io.File findFile(@NonNull String); + method @NonNull public java.io.File getDirectory(); + } + + public interface ResourceLoader { + method @Nullable public default java.io.InputStream loadAsset(@NonNull String, int) throws java.io.IOException; + method @Nullable public default android.os.ParcelFileDescriptor loadAssetFd(@NonNull String) throws java.io.IOException; + method @Nullable public default android.graphics.drawable.Drawable loadDrawable(@NonNull android.util.TypedValue, int, int, @Nullable android.content.res.Resources.Theme); + method @Nullable public default android.content.res.XmlResourceParser loadXmlResourceParser(@NonNull String, @AnyRes int); + } + + public final class ResourcesProvider implements java.lang.AutoCloseable java.io.Closeable { + method public void close(); + method @NonNull public static android.content.res.loader.ResourcesProvider empty(); + method @NonNull public static android.content.res.loader.ResourcesProvider loadFromApk(@NonNull android.os.ParcelFileDescriptor) throws java.io.IOException; + method @NonNull public static android.content.res.loader.ResourcesProvider loadFromApk(@NonNull android.os.SharedMemory) throws java.io.IOException; + method @NonNull public static android.content.res.loader.ResourcesProvider loadFromArsc(@NonNull android.os.ParcelFileDescriptor) throws java.io.IOException; + method @NonNull public static android.content.res.loader.ResourcesProvider loadFromArsc(@NonNull android.os.SharedMemory) throws java.io.IOException; + method @NonNull public static android.content.res.loader.ResourcesProvider loadFromSplit(@NonNull android.content.Context, @NonNull String) throws java.io.IOException; + } + +} + package android.database { public abstract class AbstractCursor implements android.database.CrossProcessCursor { @@ -25415,6 +25447,9 @@ package android.media { field public static final int METADATA_KEY_BITRATE = 20; // 0x14 field public static final int METADATA_KEY_CAPTURE_FRAMERATE = 25; // 0x19 field public static final int METADATA_KEY_CD_TRACK_NUMBER = 0; // 0x0 + field public static final int METADATA_KEY_COLOR_RANGE = 37; // 0x25 + field public static final int METADATA_KEY_COLOR_STANDARD = 35; // 0x23 + field public static final int METADATA_KEY_COLOR_TRANSFER = 36; // 0x24 field public static final int METADATA_KEY_COMPILATION = 15; // 0xf field public static final int METADATA_KEY_COMPOSER = 4; // 0x4 field public static final int METADATA_KEY_DATE = 5; // 0x5 @@ -38752,6 +38787,9 @@ package android.provider { field public static final String ARTIST = "artist"; field public static final String BOOKMARK = "bookmark"; field public static final String CATEGORY = "category"; + field public static final String COLOR_RANGE = "color_range"; + field public static final String COLOR_STANDARD = "color_standard"; + field public static final String COLOR_TRANSFER = "color_transfer"; field public static final String DESCRIPTION = "description"; field public static final String IS_PRIVATE = "isprivate"; field public static final String LANGUAGE = "language"; diff --git a/api/system-current.txt b/api/system-current.txt index 447ba30382a8..92315368d182 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -3420,7 +3420,9 @@ package android.location { public class Location implements android.os.Parcelable { method public boolean isComplete(); method public void makeComplete(); + method public void setExtraLocation(@Nullable String, @Nullable android.location.Location); method public void setIsFromMockProvider(boolean); + field public static final String EXTRA_NO_GPS_LOCATION = "noGPSLocation"; } public class LocationManager { @@ -9576,17 +9578,17 @@ package android.telephony.ims.stub { public class ImsSmsImplBase { ctor public ImsSmsImplBase(); - method public void acknowledgeSms(int, int, int); - method public void acknowledgeSmsReport(int, int, int); + method public void acknowledgeSms(int, @IntRange(from=0, to=65535) int, int); + method public void acknowledgeSmsReport(int, @IntRange(from=0, to=65535) int, int); method public String getSmsFormat(); method public void onReady(); - method @Deprecated public final void onSendSmsResult(int, int, int, int) throws java.lang.RuntimeException; - method public final void onSendSmsResultError(int, int, int, int, int) throws java.lang.RuntimeException; - method public final void onSendSmsResultSuccess(int, int) throws java.lang.RuntimeException; + method @Deprecated public final void onSendSmsResult(int, @IntRange(from=0, to=65535) int, int, int) throws java.lang.RuntimeException; + method public final void onSendSmsResultError(int, @IntRange(from=0, to=65535) int, int, int, int) throws java.lang.RuntimeException; + method public final void onSendSmsResultSuccess(int, @IntRange(from=0, to=65535) int) throws java.lang.RuntimeException; method public final void onSmsReceived(int, String, byte[]) throws java.lang.RuntimeException; - method @Deprecated public final void onSmsStatusReportReceived(int, int, String, byte[]) throws java.lang.RuntimeException; + method @Deprecated public final void onSmsStatusReportReceived(int, @IntRange(from=0, to=65535) int, String, byte[]) throws java.lang.RuntimeException; method public final void onSmsStatusReportReceived(int, String, byte[]) throws java.lang.RuntimeException; - method public void sendSms(int, int, String, String, boolean, byte[]); + method public void sendSms(int, @IntRange(from=0, to=65535) int, String, String, boolean, byte[]); field public static final int DELIVER_STATUS_ERROR_GENERIC = 2; // 0x2 field public static final int DELIVER_STATUS_ERROR_NO_MEMORY = 3; // 0x3 field public static final int DELIVER_STATUS_ERROR_REQUEST_NOT_SUPPORTED = 4; // 0x4 diff --git a/api/test-current.txt b/api/test-current.txt index 700be90a206e..d292e0173761 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -788,7 +788,9 @@ package android.content.res { public final class AssetManager implements java.lang.AutoCloseable { method @NonNull public String[] getApkPaths(); + method @Nullable public String getLastResourceResolution(); method @Nullable public String getOverlayablesToString(String); + method public void setResourceResolutionLoggingEnabled(boolean); } public final class Configuration implements java.lang.Comparable<android.content.res.Configuration> android.os.Parcelable { @@ -1089,6 +1091,8 @@ package android.location { public class Location implements android.os.Parcelable { method public void makeComplete(); + method public void setExtraLocation(@Nullable String, @Nullable android.location.Location); + field public static final String EXTRA_NO_GPS_LOCATION = "noGPSLocation"; } public class LocationManager { @@ -1741,6 +1745,7 @@ package android.os { } public class DeviceIdleManager { + method @RequiresPermission("android.permission.DEVICE_POWER") public int addPowerSaveWhitelistApps(@NonNull java.util.List<java.lang.String>); method @NonNull public String[] getSystemPowerWhitelist(); method @NonNull public String[] getSystemPowerWhitelistExceptIdle(); } diff --git a/cmds/statsd/src/external/StatsPullerManager.cpp b/cmds/statsd/src/external/StatsPullerManager.cpp index 43e33f59f612..5a76d1f9c80d 100644 --- a/cmds/statsd/src/external/StatsPullerManager.cpp +++ b/cmds/statsd/src/external/StatsPullerManager.cpp @@ -122,9 +122,10 @@ std::map<int, PullAtomInfo> StatsPullerManager::kAllPullAtomInfo = { {.puller = new StatsCompanionServicePuller(android::util::BLUETOOTH_ACTIVITY_INFO)}}, // system_elapsed_realtime {android::util::SYSTEM_ELAPSED_REALTIME, - {.pullTimeoutNs = NS_PER_SEC / 2, - .coolDownNs = NS_PER_SEC, - .puller = new StatsCompanionServicePuller(android::util::SYSTEM_ELAPSED_REALTIME)}}, + {.coolDownNs = NS_PER_SEC, + .puller = new StatsCompanionServicePuller(android::util::SYSTEM_ELAPSED_REALTIME), + .pullTimeoutNs = NS_PER_SEC / 2, + }}, // system_uptime {android::util::SYSTEM_UPTIME, {.puller = new StatsCompanionServicePuller(android::util::SYSTEM_UPTIME)}}, diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java index cb9ebac728ec..9e6054c715d6 100644 --- a/core/java/android/app/ResourcesManager.java +++ b/core/java/android/app/ResourcesManager.java @@ -32,6 +32,7 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.ResourcesImpl; import android.content.res.ResourcesKey; +import android.content.res.loader.ResourceLoader; import android.hardware.display.DisplayManagerGlobal; import android.os.IBinder; import android.os.Process; @@ -45,6 +46,7 @@ import android.util.Slog; import android.view.Display; import android.view.DisplayAdjustments; +import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.IndentingPrintWriter; @@ -53,7 +55,11 @@ import java.io.IOException; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.WeakHashMap; import java.util.function.Predicate; @@ -92,6 +98,52 @@ public class ResourcesManager { new ArrayMap<>(); /** + * A list of {@link Resources} that contain unique {@link ResourcesImpl}s. + * + * These are isolated so that {@link ResourceLoader}s can be added and removed without + * affecting other instances. + * + * When a reference is added here, it is guaranteed that the {@link ResourcesImpl} + * it contains is unique to itself and will never be set to a shared reference. + */ + @GuardedBy("this") + private List<ResourcesWithLoaders> mResourcesWithLoaders = Collections.emptyList(); + + private static class ResourcesWithLoaders { + + private WeakReference<Resources> mResources; + private ResourcesKey mResourcesKey; + + @Nullable + private WeakReference<IBinder> mActivityToken; + + ResourcesWithLoaders(Resources resources, ResourcesKey resourcesKey, + IBinder activityToken) { + this.mResources = new WeakReference<>(resources); + this.mResourcesKey = resourcesKey; + this.mActivityToken = new WeakReference<>(activityToken); + } + + @Nullable + Resources resources() { + return mResources.get(); + } + + @Nullable + IBinder activityToken() { + return mActivityToken == null ? null : mActivityToken.get(); + } + + ResourcesKey resourcesKey() { + return mResourcesKey; + } + + void updateKey(ResourcesKey newKey) { + mResourcesKey = newKey; + } + } + + /** * A list of Resource references that can be reused. */ @UnsupportedAppUsage @@ -182,15 +234,36 @@ public class ResourcesManager { public void invalidatePath(String path) { synchronized (this) { int count = 0; - for (int i = 0; i < mResourceImpls.size();) { + + for (int i = mResourceImpls.size() - 1; i >= 0; i--) { final ResourcesKey key = mResourceImpls.keyAt(i); if (key.isPathReferenced(path)) { - cleanupResourceImpl(key); + ResourcesImpl impl = mResourceImpls.removeAt(i).get(); + if (impl != null) { + impl.flushLayoutCache(); + } + count++; + } + } + + for (int i = mResourcesWithLoaders.size() - 1; i >= 0; i--) { + ResourcesWithLoaders resourcesWithLoaders = mResourcesWithLoaders.get(i); + Resources resources = resourcesWithLoaders.resources(); + if (resources == null) { + continue; + } + + final ResourcesKey key = resourcesWithLoaders.resourcesKey(); + if (key.isPathReferenced(path)) { + mResourcesWithLoaders.remove(i); + ResourcesImpl impl = resources.getImpl(); + if (impl != null) { + impl.flushLayoutCache(); + } count++; - } else { - i++; } } + Log.i(TAG, "Invalidated " + count + " asset managers that referenced " + path); for (int i = mCachedApkAssets.size() - 1; i >= 0; i--) { @@ -317,15 +390,6 @@ public class ResourcesManager { } } - private void cleanupResourceImpl(ResourcesKey removedKey) { - // Remove resource key to resource impl mapping and flush cache - final ResourcesImpl res = mResourceImpls.remove(removedKey).get(); - - if (res != null) { - res.flushLayoutCache(); - } - } - private static String overlayPathToIdmapPath(String path) { return "/data/resource-cache/" + path.substring(1).replace('/', '@') + "@idmap"; } @@ -499,6 +563,16 @@ public class ResourcesManager { pw.print("resource impls: "); pw.println(countLiveReferences(mResourceImpls.values())); + + int resourcesWithLoadersCount = 0; + for (int index = 0; index < mResourcesWithLoaders.size(); index++) { + if (mResourcesWithLoaders.get(index).resources() != null) { + resourcesWithLoadersCount++; + } + } + + pw.print("resources with loaders: "); + pw.println(resourcesWithLoadersCount); } } @@ -579,11 +653,24 @@ public class ResourcesManager { */ private @Nullable ResourcesKey findKeyForResourceImplLocked( @NonNull ResourcesImpl resourceImpl) { - final int refCount = mResourceImpls.size(); + int size = mResourcesWithLoaders.size(); + for (int index = 0; index < size; index++) { + ResourcesWithLoaders resourcesWithLoaders = mResourcesWithLoaders.get(index); + Resources resources = resourcesWithLoaders.resources(); + if (resources == null) { + continue; + } + + if (resourceImpl == resources.getImpl()) { + return resourcesWithLoaders.resourcesKey(); + } + } + + int refCount = mResourceImpls.size(); for (int i = 0; i < refCount; i++) { WeakReference<ResourcesImpl> weakImplRef = mResourceImpls.valueAt(i); ResourcesImpl impl = weakImplRef != null ? weakImplRef.get() : null; - if (impl != null && resourceImpl == impl) { + if (resourceImpl == impl) { return mResourceImpls.keyAt(i); } } @@ -625,31 +712,55 @@ public class ResourcesManager { return activityResources; } - /** - * Gets an existing Resources object tied to this Activity, or creates one if it doesn't exist - * or the class loader is different. - */ - private @NonNull Resources getOrCreateResourcesForActivityLocked(@NonNull IBinder activityToken, - @NonNull ClassLoader classLoader, @NonNull ResourcesImpl impl, - @NonNull CompatibilityInfo compatInfo) { - final ActivityResources activityResources = getOrCreateActivityResourcesStructLocked( - activityToken); + @Nullable + private Resources findResourcesForActivityLocked(@NonNull IBinder targetActivityToken, + @NonNull ResourcesKey targetKey, @NonNull ClassLoader targetClassLoader) { + int size = mResourcesWithLoaders.size(); + for (int index = 0; index < size; index++) { + ResourcesWithLoaders resourcesWithLoaders = mResourcesWithLoaders.get(index); + Resources resources = resourcesWithLoaders.resources(); + if (resources == null) { + continue; + } - final int refCount = activityResources.activityResources.size(); - for (int i = 0; i < refCount; i++) { - WeakReference<Resources> weakResourceRef = activityResources.activityResources.get(i); - Resources resources = weakResourceRef.get(); + IBinder activityToken = resourcesWithLoaders.activityToken(); + ResourcesKey key = resourcesWithLoaders.resourcesKey(); - if (resources != null - && Objects.equals(resources.getClassLoader(), classLoader) - && resources.getImpl() == impl) { - if (DEBUG) { - Slog.d(TAG, "- using existing ref=" + resources); - } + ClassLoader classLoader = resources.getClassLoader(); + + if (Objects.equals(activityToken, targetActivityToken) + && Objects.equals(key, targetKey) + && Objects.equals(classLoader, targetClassLoader)) { + return resources; + } + } + + ActivityResources activityResources = getOrCreateActivityResourcesStructLocked( + targetActivityToken); + + size = activityResources.activityResources.size(); + for (int index = 0; index < size; index++) { + WeakReference<Resources> ref = activityResources.activityResources.get(index); + Resources resources = ref.get(); + ResourcesKey key = resources == null ? null : findKeyForResourceImplLocked( + resources.getImpl()); + + if (key != null + && Objects.equals(resources.getClassLoader(), targetClassLoader) + && Objects.equals(key, targetKey)) { return resources; } } + return null; + } + + private @NonNull Resources createResourcesForActivityLocked(@NonNull IBinder activityToken, + @NonNull ClassLoader classLoader, @NonNull ResourcesImpl impl, + @NonNull CompatibilityInfo compatInfo) { + final ActivityResources activityResources = getOrCreateActivityResourcesStructLocked( + activityToken); + Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader) : new Resources(classLoader); resources.setImpl(impl); @@ -661,28 +772,8 @@ public class ResourcesManager { return resources; } - /** - * Gets an existing Resources object if the class loader and ResourcesImpl are the same, - * otherwise creates a new Resources object. - */ - private @NonNull Resources getOrCreateResourcesLocked(@NonNull ClassLoader classLoader, + private @NonNull Resources createResourcesLocked(@NonNull ClassLoader classLoader, @NonNull ResourcesImpl impl, @NonNull CompatibilityInfo compatInfo) { - // Find an existing Resources that has this ResourcesImpl set. - final int refCount = mResourceReferences.size(); - for (int i = 0; i < refCount; i++) { - WeakReference<Resources> weakResourceRef = mResourceReferences.get(i); - Resources resources = weakResourceRef.get(); - if (resources != null && - Objects.equals(resources.getClassLoader(), classLoader) && - resources.getImpl() == impl) { - if (DEBUG) { - Slog.d(TAG, "- using existing ref=" + resources); - } - return resources; - } - } - - // Create a new Resources reference and use the existing ResourcesImpl object. Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader) : new Resources(classLoader); resources.setImpl(impl); @@ -750,16 +841,70 @@ public class ResourcesManager { updateResourcesForActivity(activityToken, overrideConfig, displayId, false /* movedToDifferentDisplay */); + cleanupReferences(activityToken); + rebaseKeyForActivity(activityToken, key); + + synchronized (this) { + Resources resources = findResourcesForActivityLocked(activityToken, key, + classLoader); + if (resources != null) { + return resources; + } + } + // Now request an actual Resources object. - return getOrCreateResources(activityToken, key, classLoader); + return createResources(activityToken, key, classLoader); } finally { Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); } } /** - * Gets an existing Resources object set with a ResourcesImpl object matching the given key, - * or creates one if it doesn't exist. + * Rebases a key's override config on top of the Activity's base override. + */ + private void rebaseKeyForActivity(IBinder activityToken, ResourcesKey key) { + final ActivityResources activityResources = + getOrCreateActivityResourcesStructLocked(activityToken); + + // Clean up any dead references so they don't pile up. + ArrayUtils.unstableRemoveIf(activityResources.activityResources, + sEmptyReferencePredicate); + + // Rebase the key's override config on top of the Activity's base override. + if (key.hasOverrideConfiguration() + && !activityResources.overrideConfig.equals(Configuration.EMPTY)) { + final Configuration temp = new Configuration(activityResources.overrideConfig); + temp.updateFrom(key.mOverrideConfiguration); + key.mOverrideConfiguration.setTo(temp); + } + } + + /** + * Check WeakReferences and remove any dead references so they don't pile up. + * @param activityToken optional token to clean up Activity resources + */ + private void cleanupReferences(IBinder activityToken) { + if (activityToken != null) { + ActivityResources activityResources = mActivityResourceReferences.get(activityToken); + if (activityResources != null) { + ArrayUtils.unstableRemoveIf(activityResources.activityResources, + sEmptyReferencePredicate); + } + } else { + ArrayUtils.unstableRemoveIf(mResourceReferences, sEmptyReferencePredicate); + } + + for (int index = mResourcesWithLoaders.size() - 1; index >= 0; index--) { + ResourcesWithLoaders resourcesWithLoaders = mResourcesWithLoaders.get(index); + Resources resources = resourcesWithLoaders.resources(); + if (resources == null) { + mResourcesWithLoaders.remove(index); + } + } + } + + /** + * Creates a Resources object set with a ResourcesImpl object matching the given key. * * @param activityToken The Activity this Resources object should be associated with. * @param key The key describing the parameters of the ResourcesImpl object. @@ -769,7 +914,7 @@ public class ResourcesManager { * {@link #applyConfigurationToResourcesLocked(Configuration, CompatibilityInfo)} * is called. */ - private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken, + private @Nullable Resources createResources(@Nullable IBinder activityToken, @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) { synchronized (this) { if (DEBUG) { @@ -778,66 +923,17 @@ public class ResourcesManager { Slog.w(TAG, "!! Get resources for activity=" + activityToken + " key=" + key, here); } - if (activityToken != null) { - final ActivityResources activityResources = - getOrCreateActivityResourcesStructLocked(activityToken); - - // Clean up any dead references so they don't pile up. - ArrayUtils.unstableRemoveIf(activityResources.activityResources, - sEmptyReferencePredicate); - - // Rebase the key's override config on top of the Activity's base override. - if (key.hasOverrideConfiguration() - && !activityResources.overrideConfig.equals(Configuration.EMPTY)) { - final Configuration temp = new Configuration(activityResources.overrideConfig); - temp.updateFrom(key.mOverrideConfiguration); - key.mOverrideConfiguration.setTo(temp); - } - - ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key); - if (resourcesImpl != null) { - if (DEBUG) { - Slog.d(TAG, "- using existing impl=" + resourcesImpl); - } - return getOrCreateResourcesForActivityLocked(activityToken, classLoader, - resourcesImpl, key.mCompatInfo); - } - - // We will create the ResourcesImpl object outside of holding this lock. - - } else { - // Clean up any dead references so they don't pile up. - ArrayUtils.unstableRemoveIf(mResourceReferences, sEmptyReferencePredicate); - - // Not tied to an Activity, find a shared Resources that has the right ResourcesImpl - ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key); - if (resourcesImpl != null) { - if (DEBUG) { - Slog.d(TAG, "- using existing impl=" + resourcesImpl); - } - return getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo); - } - - // We will create the ResourcesImpl object outside of holding this lock. - } - - // If we're here, we didn't find a suitable ResourcesImpl to use, so create one now. - ResourcesImpl resourcesImpl = createResourcesImpl(key); + ResourcesImpl resourcesImpl = findOrCreateResourcesImplForKeyLocked(key); if (resourcesImpl == null) { return null; } - // Add this ResourcesImpl to the cache. - mResourceImpls.put(key, new WeakReference<>(resourcesImpl)); - - final Resources resources; if (activityToken != null) { - resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader, + return createResourcesForActivityLocked(activityToken, classLoader, resourcesImpl, key.mCompatInfo); } else { - resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo); + return createResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo); } - return resources; } } @@ -868,7 +964,8 @@ public class ResourcesManager { * {@link ClassLoader#getSystemClassLoader()} is used. * @return a Resources object from which to access resources. */ - public @Nullable Resources getResources(@Nullable IBinder activityToken, + public @Nullable Resources getResources( + @Nullable IBinder activityToken, @Nullable String resDir, @Nullable String[] splitResDirs, @Nullable String[] overlayDirs, @@ -888,7 +985,14 @@ public class ResourcesManager { overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy compatInfo); classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader(); - return getOrCreateResources(activityToken, key, classLoader); + + cleanupReferences(activityToken); + + if (activityToken != null) { + rebaseKeyForActivity(activityToken, key); + } + + return createResources(activityToken, key, classLoader); } finally { Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); } @@ -944,67 +1048,40 @@ public class ResourcesManager { here); } - final boolean activityHasOverrideConfig = - !activityResources.overrideConfig.equals(Configuration.EMPTY); // Rebase each Resources associated with this Activity. final int refCount = activityResources.activityResources.size(); for (int i = 0; i < refCount; i++) { WeakReference<Resources> weakResRef = activityResources.activityResources.get( i); + Resources resources = weakResRef.get(); if (resources == null) { continue; } - // Extract the ResourcesKey that was last used to create the Resources for this - // activity. - final ResourcesKey oldKey = findKeyForResourceImplLocked(resources.getImpl()); - if (oldKey == null) { - Slog.e(TAG, "can't find ResourcesKey for resources impl=" - + resources.getImpl()); - continue; - } - - // Build the new override configuration for this ResourcesKey. - final Configuration rebasedOverrideConfig = new Configuration(); - if (overrideConfig != null) { - rebasedOverrideConfig.setTo(overrideConfig); - } + ResourcesKey newKey = rebaseActivityOverrideConfig(resources, oldConfig, + overrideConfig, displayId); + updateActivityResources(resources, newKey, false); + } - if (activityHasOverrideConfig && oldKey.hasOverrideConfiguration()) { - // Generate a delta between the old base Activity override configuration and - // the actual final override configuration that was used to figure out the - // real delta this Resources object wanted. - Configuration overrideOverrideConfig = Configuration.generateDelta( - oldConfig, oldKey.mOverrideConfiguration); - rebasedOverrideConfig.updateFrom(overrideOverrideConfig); + // Also find loaders that are associated with an Activity + final int loaderCount = mResourcesWithLoaders.size(); + for (int index = loaderCount - 1; index >= 0; index--) { + ResourcesWithLoaders resourcesWithLoaders = mResourcesWithLoaders.get( + index); + Resources resources = resourcesWithLoaders.resources(); + if (resources == null + || resourcesWithLoaders.activityToken() != activityToken) { + continue; } - // Create the new ResourcesKey with the rebased override config. - final ResourcesKey newKey = new ResourcesKey(oldKey.mResDir, - oldKey.mSplitResDirs, - oldKey.mOverlayDirs, oldKey.mLibDirs, displayId, - rebasedOverrideConfig, oldKey.mCompatInfo); - - if (DEBUG) { - Slog.d(TAG, "rebasing ref=" + resources + " from oldKey=" + oldKey - + " to newKey=" + newKey + ", displayId=" + displayId); - } + ResourcesKey newKey = rebaseActivityOverrideConfig(resources, oldConfig, + overrideConfig, displayId); - ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(newKey); - if (resourcesImpl == null) { - resourcesImpl = createResourcesImpl(newKey); - if (resourcesImpl != null) { - mResourceImpls.put(newKey, new WeakReference<>(resourcesImpl)); - } - } + updateActivityResources(resources, newKey, true); - if (resourcesImpl != null && resourcesImpl != resources.getImpl()) { - // Set the ResourcesImpl, updating it for all users of this Resources - // object. - resources.setImpl(resourcesImpl); - } + resourcesWithLoaders.updateKey(newKey); } } } finally { @@ -1012,6 +1089,70 @@ public class ResourcesManager { } } + /** + * Rebases an updated override config over any old override config and returns the new one + * that an Activity's Resources should be set to. + */ + private ResourcesKey rebaseActivityOverrideConfig(Resources resources, + Configuration oldOverrideConfig, @Nullable Configuration newOverrideConfig, + int displayId) { + // Extract the ResourcesKey that was last used to create the Resources for this + // activity. + final ResourcesKey oldKey = findKeyForResourceImplLocked(resources.getImpl()); + if (oldKey == null) { + Slog.e(TAG, "can't find ResourcesKey for resources impl=" + + resources.getImpl()); + return null; + } + + // Build the new override configuration for this ResourcesKey. + final Configuration rebasedOverrideConfig = new Configuration(); + if (newOverrideConfig != null) { + rebasedOverrideConfig.setTo(newOverrideConfig); + } + + final boolean hadOverrideConfig = !oldOverrideConfig.equals(Configuration.EMPTY); + if (hadOverrideConfig && oldKey.hasOverrideConfiguration()) { + // Generate a delta between the old base Activity override configuration and + // the actual final override configuration that was used to figure out the + // real delta this Resources object wanted. + Configuration overrideOverrideConfig = Configuration.generateDelta( + oldOverrideConfig, oldKey.mOverrideConfiguration); + rebasedOverrideConfig.updateFrom(overrideOverrideConfig); + } + + // Create the new ResourcesKey with the rebased override config. + final ResourcesKey newKey = new ResourcesKey(oldKey.mResDir, + oldKey.mSplitResDirs, + oldKey.mOverlayDirs, oldKey.mLibDirs, displayId, + rebasedOverrideConfig, oldKey.mCompatInfo); + + if (DEBUG) { + Slog.d(TAG, "rebasing ref=" + resources + " from oldKey=" + oldKey + + " to newKey=" + newKey + ", displayId=" + displayId); + } + + return newKey; + } + + private void updateActivityResources(Resources resources, ResourcesKey newKey, + boolean hasLoader) { + final ResourcesImpl resourcesImpl; + + if (hasLoader) { + // Loaders always get new Impls because they cannot be shared + resourcesImpl = createResourcesImpl(newKey); + } else { + resourcesImpl = findOrCreateResourcesImplForKeyLocked(newKey); + } + + if (resourcesImpl != null && resourcesImpl != resources.getImpl()) { + // Set the ResourcesImpl, updating it for all users of this Resources + // object. + resources.setImpl(resourcesImpl); + } + } + @TestApi public final boolean applyConfigurationToResources(@NonNull Configuration config, @Nullable CompatibilityInfo compat) { @@ -1050,61 +1191,77 @@ public class ResourcesManager { ApplicationPackageManager.configurationChanged(); //Slog.i(TAG, "Configuration changed in " + currentPackageName()); - Configuration tmpConfig = null; + Configuration tmpConfig = new Configuration(); for (int i = mResourceImpls.size() - 1; i >= 0; i--) { ResourcesKey key = mResourceImpls.keyAt(i); WeakReference<ResourcesImpl> weakImplRef = mResourceImpls.valueAt(i); ResourcesImpl r = weakImplRef != null ? weakImplRef.get() : null; if (r != null) { - if (DEBUG || DEBUG_CONFIGURATION) Slog.v(TAG, "Changing resources " - + r + " config to: " + config); - int displayId = key.mDisplayId; - boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY); - DisplayMetrics dm = defaultDisplayMetrics; - final boolean hasOverrideConfiguration = key.hasOverrideConfiguration(); - if (!isDefaultDisplay || hasOverrideConfiguration) { - if (tmpConfig == null) { - tmpConfig = new Configuration(); - } - tmpConfig.setTo(config); - - // Get new DisplayMetrics based on the DisplayAdjustments given - // to the ResourcesImpl. Update a copy if the CompatibilityInfo - // changed, because the ResourcesImpl object will handle the - // update internally. - DisplayAdjustments daj = r.getDisplayAdjustments(); - if (compat != null) { - daj = new DisplayAdjustments(daj); - daj.setCompatibilityInfo(compat); - } - dm = getDisplayMetrics(displayId, daj); - - if (!isDefaultDisplay) { - applyNonDefaultDisplayMetricsToConfiguration(dm, tmpConfig); - } - - if (hasOverrideConfiguration) { - tmpConfig.updateFrom(key.mOverrideConfiguration); - } - r.updateConfiguration(tmpConfig, dm, compat); - } else { - r.updateConfiguration(config, dm, compat); - } - //Slog.i(TAG, "Updated app resources " + v.getKey() - // + " " + r + ": " + r.getConfiguration()); + applyConfigurationToResourcesLocked(config, compat, tmpConfig, + defaultDisplayMetrics, key, r); } else { - //Slog.i(TAG, "Removing old resources " + v.getKey()); mResourceImpls.removeAt(i); } } + for (int index = mResourcesWithLoaders.size() - 1; index >= 0; index--) { + ResourcesWithLoaders resourcesWithLoaders = mResourcesWithLoaders.get(index); + Resources resources = resourcesWithLoaders.resources(); + if (resources == null) { + mResourcesWithLoaders.remove(index); + continue; + } + + applyConfigurationToResourcesLocked(config, compat, tmpConfig, + defaultDisplayMetrics, resourcesWithLoaders.resourcesKey(), + resources.getImpl()); + } + return changes != 0; } finally { Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); } } + private void applyConfigurationToResourcesLocked(@NonNull Configuration config, + @Nullable CompatibilityInfo compat, Configuration tmpConfig, + DisplayMetrics defaultDisplayMetrics, ResourcesKey key, ResourcesImpl resourcesImpl) { + if (DEBUG || DEBUG_CONFIGURATION) { + Slog.v(TAG, "Changing resources " + + resourcesImpl + " config to: " + config); + } + int displayId = key.mDisplayId; + boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY); + DisplayMetrics dm = defaultDisplayMetrics; + final boolean hasOverrideConfiguration = key.hasOverrideConfiguration(); + if (!isDefaultDisplay || hasOverrideConfiguration) { + tmpConfig.setTo(config); + + // Get new DisplayMetrics based on the DisplayAdjustments given + // to the ResourcesImpl. Update a copy if the CompatibilityInfo + // changed, because the ResourcesImpl object will handle the + // update internally. + DisplayAdjustments daj = resourcesImpl.getDisplayAdjustments(); + if (compat != null) { + daj = new DisplayAdjustments(daj); + daj.setCompatibilityInfo(compat); + } + dm = getDisplayMetrics(displayId, daj); + + if (!isDefaultDisplay) { + applyNonDefaultDisplayMetricsToConfiguration(dm, tmpConfig); + } + + if (hasOverrideConfiguration) { + tmpConfig.updateFrom(key.mOverrideConfiguration); + } + resourcesImpl.updateConfiguration(tmpConfig, dm, compat); + } else { + resourcesImpl.updateConfiguration(config, dm, compat); + } + } + /** * Appends the library asset path to any ResourcesImpl object that contains the main * assetPath. @@ -1140,7 +1297,7 @@ public class ResourcesManager { ArrayUtils.appendElement(String.class, newLibAssets, libAsset); } - if (newLibAssets != key.mLibDirs) { + if (!Arrays.equals(newLibAssets, key.mLibDirs)) { updatedResourceKeys.put(impl, new ResourcesKey( key.mResDir, key.mSplitResDirs, @@ -1153,10 +1310,106 @@ public class ResourcesManager { } } + final int count = mResourcesWithLoaders.size(); + for (int index = 0; index < count; index++) { + ResourcesWithLoaders resourcesWithLoaders = mResourcesWithLoaders.get(index); + Resources resources = resourcesWithLoaders.resources(); + if (resources == null) { + continue; + } + + ResourcesKey key = resourcesWithLoaders.resourcesKey(); + if (Objects.equals(key.mResDir, assetPath)) { + String[] newLibAssets = key.mLibDirs; + for (String libAsset : libAssets) { + newLibAssets = + ArrayUtils.appendElement(String.class, newLibAssets, libAsset); + } + + if (!Arrays.equals(newLibAssets, key.mLibDirs)) { + updatedResourceKeys.put(resources.getImpl(), + new ResourcesKey( + key.mResDir, + key.mSplitResDirs, + key.mOverlayDirs, + newLibAssets, + key.mDisplayId, + key.mOverrideConfiguration, + key.mCompatInfo)); + } + } + } + redirectResourcesToNewImplLocked(updatedResourceKeys); } } + /** + * Mark a {@link Resources} as containing a {@link ResourceLoader}. + * + * This removes its {@link ResourcesImpl} from the shared pool and creates it a new one. It is + * then tracked separately, kept unique, and restored properly across {@link Activity} + * recreations. + */ + public void registerForLoaders(Resources resources) { + synchronized (this) { + boolean found = false; + IBinder activityToken = null; + + // Remove the Resources from the reference list as it's now tracked separately + int size = mResourceReferences.size(); + for (int index = 0; index < size; index++) { + WeakReference<Resources> reference = mResourceReferences.get(index); + if (reference.get() == resources) { + mResourceReferences.remove(index); + found = true; + break; + } + } + + if (!found) { + // Do the same removal for any Activity Resources + for (Map.Entry<IBinder, ActivityResources> entry : + mActivityResourceReferences.entrySet()) { + ArrayList<WeakReference<Resources>> activityResourcesList = + entry.getValue().activityResources; + final int resCount = activityResourcesList.size(); + for (int index = 0; index < resCount; index++) { + WeakReference<Resources> reference = activityResourcesList.get(index); + if (reference.get() == resources) { + activityToken = entry.getKey(); + activityResourcesList.remove(index); + found = true; + break; + } + } + + if (found) { + break; + } + } + } + + if (!found) { + throw new IllegalArgumentException("Resources " + resources + + " registered for loaders but was not previously tracked by" + + " ResourcesManager"); + } + + ResourcesKey key = findKeyForResourceImplLocked(resources.getImpl()); + ResourcesImpl impl = createResourcesImpl(key); + + if (mResourcesWithLoaders == Collections.EMPTY_LIST) { + mResourcesWithLoaders = Collections.synchronizedList(new ArrayList<>()); + } + + mResourcesWithLoaders.add(new ResourcesWithLoaders(resources, key, activityToken)); + + // Set the new Impl, which is now guaranteed to be unique per Resources object + resources.setImpl(impl); + } + } + // TODO(adamlesinski): Make this accept more than just overlay directories. final void applyNewResourceDirsLocked(@NonNull final ApplicationInfo appInfo, @Nullable final String[] oldPaths) { @@ -1201,6 +1454,32 @@ public class ResourcesManager { } } + final int count = mResourcesWithLoaders.size(); + for (int index = 0; index < count; index++) { + ResourcesWithLoaders resourcesWithLoaders = mResourcesWithLoaders.get(index); + Resources resources = resourcesWithLoaders.resources(); + if (resources == null) { + continue; + } + + ResourcesKey key = resourcesWithLoaders.resourcesKey(); + + if (key.mResDir == null + || key.mResDir.equals(baseCodePath) + || ArrayUtils.contains(oldPaths, key.mResDir)) { + updatedResourceKeys.put(resources.getImpl(), + new ResourcesKey( + baseCodePath, + copiedSplitDirs, + copiedResourceDirs, + key.mLibDirs, + key.mDisplayId, + key.mOverrideConfiguration, + key.mCompatInfo + )); + } + } + redirectResourcesToNewImplLocked(updatedResourceKeys); } finally { Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); @@ -1250,5 +1529,25 @@ public class ResourcesManager { } } } + + // Update any references that need to be re-built with loaders. These are intentionally not + // part of either of the above lists. + final int loaderCount = mResourcesWithLoaders.size(); + for (int index = loaderCount - 1; index >= 0; index--) { + ResourcesWithLoaders resourcesWithLoaders = mResourcesWithLoaders.get(index); + Resources resources = resourcesWithLoaders.resources(); + if (resources == null) { + continue; + } + + ResourcesKey newKey = updatedResourceKeys.get(resources.getImpl()); + if (newKey == null) { + continue; + } + + resourcesWithLoaders.updateKey(newKey); + resourcesWithLoaders.resources() + .setImpl(createResourcesImpl(newKey)); + } } } diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 2dde3ae5909c..227684b1bd14 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -1559,7 +1559,14 @@ public abstract class Context { * @see Environment#getExternalStorageState(File) * @see Environment#isExternalStorageEmulated(File) * @see Environment#isExternalStorageRemovable(File) + * @deprecated These directories still exist and are scanned, but developers + * are encouraged to migrate to inserting content into a + * {@link MediaStore} collection directly, as any app can + * contribute new media to {@link MediaStore} with no + * permissions required, starting in + * {@link android.os.Build.VERSION_CODES#Q}. */ + @Deprecated public abstract File[] getExternalMediaDirs(); /** diff --git a/core/java/android/content/res/ApkAssets.java b/core/java/android/content/res/ApkAssets.java index a35ad567ed81..de1d514d0a5b 100644 --- a/core/java/android/content/res/ApkAssets.java +++ b/core/java/android/content/res/ApkAssets.java @@ -16,7 +16,10 @@ package android.content.res; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.UnsupportedAppUsage; +import android.content.res.loader.ResourcesProvider; +import android.text.TextUtils; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.Preconditions; @@ -36,10 +39,14 @@ import java.io.IOException; */ public final class ApkAssets { @GuardedBy("this") private final long mNativePtr; + + @Nullable @GuardedBy("this") private final StringBlock mStringBlock; @GuardedBy("this") private boolean mOpen = true; + private final boolean mForLoader; + /** * Creates a new ApkAssets instance from the given path on disk. * @@ -48,7 +55,8 @@ public final class ApkAssets { * @throws IOException if a disk I/O error or parsing error occurred. */ public static @NonNull ApkAssets loadFromPath(@NonNull String path) throws IOException { - return new ApkAssets(path, false /*system*/, false /*forceSharedLib*/, false /*overlay*/); + return new ApkAssets(path, false /*system*/, false /*forceSharedLib*/, false /*overlay*/, + false /*arscOnly*/, false /*forLoader*/); } /** @@ -61,7 +69,8 @@ public final class ApkAssets { */ public static @NonNull ApkAssets loadFromPath(@NonNull String path, boolean system) throws IOException { - return new ApkAssets(path, system, false /*forceSharedLib*/, false /*overlay*/); + return new ApkAssets(path, system, false /*forceSharedLib*/, false /*overlay*/, + false /*arscOnly*/, false /*forLoader*/); } /** @@ -76,7 +85,8 @@ public final class ApkAssets { */ public static @NonNull ApkAssets loadFromPath(@NonNull String path, boolean system, boolean forceSharedLibrary) throws IOException { - return new ApkAssets(path, system, forceSharedLibrary, false /*overlay*/); + return new ApkAssets(path, system, forceSharedLibrary, false /*overlay*/, + false /*arscOnly*/, false /*forLoader*/); } /** @@ -96,7 +106,8 @@ public final class ApkAssets { public static @NonNull ApkAssets loadFromFd(@NonNull FileDescriptor fd, @NonNull String friendlyName, boolean system, boolean forceSharedLibrary) throws IOException { - return new ApkAssets(fd, friendlyName, system, forceSharedLibrary); + return new ApkAssets(fd, friendlyName, system, forceSharedLibrary, false /*arscOnly*/, + false /*forLoader*/); } /** @@ -110,21 +121,90 @@ public final class ApkAssets { */ public static @NonNull ApkAssets loadOverlayFromPath(@NonNull String idmapPath, boolean system) throws IOException { - return new ApkAssets(idmapPath, system, false /*forceSharedLibrary*/, true /*overlay*/); + return new ApkAssets(idmapPath, system, false /*forceSharedLibrary*/, true /*overlay*/, + false /*arscOnly*/, false /*forLoader*/); + } + + /** + * Creates a new ApkAssets instance from the given path on disk for use with a + * {@link ResourcesProvider}. + * + * @param path The path to an APK on disk. + * @return a new instance of ApkAssets. + * @throws IOException if a disk I/O error or parsing error occurred. + */ + public static @NonNull ApkAssets loadApkForLoader(@NonNull String path) + throws IOException { + return new ApkAssets(path, false /*system*/, false /*forceSharedLibrary*/, + false /*overlay*/, false /*arscOnly*/, true /*forLoader*/); + } + + /** + * Creates a new ApkAssets instance from the given file descriptor for use with a + * {@link ResourcesProvider}. + * + * Performs a dup of the underlying fd, so you must take care of still closing + * the FileDescriptor yourself (and can do that whenever you want). + * + * @param fd The FileDescriptor of an open, readable APK. + * @return a new instance of ApkAssets. + * @throws IOException if a disk I/O error or parsing error occurred. + */ + @NonNull + public static ApkAssets loadApkForLoader(@NonNull FileDescriptor fd) throws IOException { + return new ApkAssets(fd, TextUtils.emptyIfNull(fd.toString()), + false /*system*/, false /*forceSharedLib*/, false /*arscOnly*/, true /*forLoader*/); } - private ApkAssets(@NonNull String path, boolean system, boolean forceSharedLib, boolean overlay) + /** + * Creates a new ApkAssets instance from the given file descriptor representing an ARSC + * for use with a {@link ResourcesProvider}. + * + * Performs a dup of the underlying fd, so you must take care of still closing + * the FileDescriptor yourself (and can do that whenever you want). + * + * @param fd The FileDescriptor of an open, readable .arsc. + * @return a new instance of ApkAssets. + * @throws IOException if a disk I/O error or parsing error occurred. + */ + public static @NonNull ApkAssets loadArscForLoader(@NonNull FileDescriptor fd) throws IOException { + return new ApkAssets(fd, TextUtils.emptyIfNull(fd.toString()), + false /*system*/, false /*forceSharedLib*/, true /*arscOnly*/, true /*forLoader*/); + } + + /** + * Generates an entirely empty ApkAssets. Needed because the ApkAssets instance and presence + * is required for a lot of APIs, and it's easier to have a non-null reference rather than + * tracking a separate identifier. + */ + @NonNull + public static ApkAssets loadEmptyForLoader() { + return new ApkAssets(true); + } + + private ApkAssets(boolean forLoader) { + mForLoader = forLoader; + mNativePtr = nativeLoadEmpty(forLoader); + mStringBlock = null; + } + + private ApkAssets(@NonNull String path, boolean system, boolean forceSharedLib, boolean overlay, + boolean arscOnly, boolean forLoader) throws IOException { + mForLoader = forLoader; Preconditions.checkNotNull(path, "path"); - mNativePtr = nativeLoad(path, system, forceSharedLib, overlay); + mNativePtr = arscOnly ? nativeLoadArsc(path, forLoader) + : nativeLoad(path, system, forceSharedLib, overlay, forLoader); mStringBlock = new StringBlock(nativeGetStringBlock(mNativePtr), true /*useSparse*/); } private ApkAssets(@NonNull FileDescriptor fd, @NonNull String friendlyName, boolean system, - boolean forceSharedLib) throws IOException { + boolean forceSharedLib, boolean arscOnly, boolean forLoader) throws IOException { + mForLoader = forLoader; Preconditions.checkNotNull(fd, "fd"); Preconditions.checkNotNull(friendlyName, "friendlyName"); - mNativePtr = nativeLoadFromFd(fd, friendlyName, system, forceSharedLib); + mNativePtr = arscOnly ? nativeLoadArscFromFd(fd, friendlyName, forLoader) + : nativeLoadFromFd(fd, friendlyName, system, forceSharedLib, forLoader); mStringBlock = new StringBlock(nativeGetStringBlock(mNativePtr), true /*useSparse*/); } @@ -136,11 +216,19 @@ public final class ApkAssets { } CharSequence getStringFromPool(int idx) { + if (mStringBlock == null) { + return null; + } + synchronized (this) { return mStringBlock.get(idx); } } + public boolean isForLoader() { + return mForLoader; + } + /** * Retrieve a parser for a compiled XML file. This is associated with a single APK and * <em>NOT</em> a full AssetManager. This means that shared-library references will not be @@ -192,18 +280,26 @@ public final class ApkAssets { synchronized (this) { if (mOpen) { mOpen = false; - mStringBlock.close(); + if (mStringBlock != null) { + mStringBlock.close(); + } nativeDestroy(mNativePtr); } } } - private static native long nativeLoad( - @NonNull String path, boolean system, boolean forceSharedLib, boolean overlay) + private static native long nativeLoad(@NonNull String path, boolean system, + boolean forceSharedLib, boolean overlay, boolean forLoader) throws IOException; private static native long nativeLoadFromFd(@NonNull FileDescriptor fd, - @NonNull String friendlyName, boolean system, boolean forceSharedLib) + @NonNull String friendlyName, boolean system, boolean forceSharedLib, + boolean forLoader) + throws IOException; + private static native long nativeLoadArsc(@NonNull String path, boolean forLoader) throws IOException; + private static native long nativeLoadArscFromFd(@NonNull FileDescriptor fd, + @NonNull String friendlyName, boolean forLoader) throws IOException; + private static native long nativeLoadEmpty(boolean forLoader); private static native void nativeDestroy(long ptr); private static native @NonNull String nativeGetAssetPath(long ptr); private static native long nativeGetStringBlock(long ptr); diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java index 7d6dc97e82cb..23e772075ad6 100644 --- a/core/java/android/content/res/AssetManager.java +++ b/core/java/android/content/res/AssetManager.java @@ -27,9 +27,13 @@ import android.annotation.TestApi; import android.annotation.UnsupportedAppUsage; import android.content.pm.ActivityInfo; import android.content.res.Configuration.NativeConfig; +import android.content.res.loader.ResourceLoader; +import android.content.res.loader.ResourceLoaderManager; +import android.content.res.loader.ResourcesProvider; import android.os.ParcelFileDescriptor; import android.util.ArraySet; import android.util.Log; +import android.util.Pair; import android.util.SparseArray; import android.util.TypedValue; @@ -39,15 +43,19 @@ import com.android.internal.util.Preconditions; import libcore.io.IoUtils; import java.io.BufferedReader; +import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.channels.FileLock; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.List; +import java.util.Locale; import java.util.Map; /** @@ -110,6 +118,13 @@ public final class AssetManager implements AutoCloseable { @GuardedBy("this") private int mNumRefs = 1; @GuardedBy("this") private HashMap<Long, RuntimeException> mRefStacks; + private ResourceLoaderManager mResourceLoaderManager; + + /** @hide */ + public void setResourceLoaderManager(ResourceLoaderManager resourceLoaderManager) { + mResourceLoaderManager = resourceLoaderManager; + } + /** * A Builder class that helps create an AssetManager with only a single invocation of * {@link AssetManager#setApkAssets(ApkAssets[], boolean)}. Without using this builder, @@ -507,7 +522,7 @@ public final class AssetManager implements AutoCloseable { outValue.changingConfigurations); if (outValue.type == TypedValue.TYPE_STRING) { - outValue.string = mApkAssets[cookie - 1].getStringFromPool(outValue.data); + outValue.string = getPooledStringForCookie(cookie, outValue.data); } return true; } @@ -554,7 +569,7 @@ public final class AssetManager implements AutoCloseable { outValue.changingConfigurations); if (outValue.type == TypedValue.TYPE_STRING) { - return mApkAssets[cookie - 1].getStringFromPool(outValue.data); + return getPooledStringForCookie(cookie, outValue.data); } return outValue.coerceToString(); } @@ -632,7 +647,7 @@ public final class AssetManager implements AutoCloseable { int cookie = rawInfoArray[i]; int index = rawInfoArray[i + 1]; retArray[j] = (index >= 0 && cookie > 0) - ? mApkAssets[cookie - 1].getStringFromPool(index) : null; + ? getPooledStringForCookie(cookie, index) : null; } return retArray; } @@ -688,7 +703,7 @@ public final class AssetManager implements AutoCloseable { outValue.changingConfigurations); if (outValue.type == TypedValue.TYPE_STRING) { - outValue.string = mApkAssets[cookie - 1].getStringFromPool(outValue.data); + outValue.string = getPooledStringForCookie(cookie, outValue.data); } return true; } @@ -753,6 +768,7 @@ public final class AssetManager implements AutoCloseable { * * @hide */ + @TestApi public void setResourceResolutionLoggingEnabled(boolean enabled) { synchronized (this) { ensureValidLocked(); @@ -768,6 +784,7 @@ public final class AssetManager implements AutoCloseable { * * @hide */ + @TestApi public @Nullable String getLastResourceResolution() { synchronized (this) { ensureValidLocked(); @@ -814,6 +831,13 @@ public final class AssetManager implements AutoCloseable { Preconditions.checkNotNull(fileName, "fileName"); synchronized (this) { ensureOpenLocked(); + + String path = Paths.get("assets", fileName).toString(); + InputStream inputStream = searchLoaders(0, path, accessMode); + if (inputStream != null) { + return inputStream; + } + final long asset = nativeOpenAsset(mObject, fileName, accessMode); if (asset == 0) { throw new FileNotFoundException("Asset file: " + fileName); @@ -838,6 +862,13 @@ public final class AssetManager implements AutoCloseable { Preconditions.checkNotNull(fileName, "fileName"); synchronized (this) { ensureOpenLocked(); + + String path = Paths.get("assets", fileName).toString(); + AssetFileDescriptor fileDescriptor = searchLoadersFd(0, path); + if (fileDescriptor != null) { + return fileDescriptor; + } + final ParcelFileDescriptor pfd = nativeOpenAssetFd(mObject, fileName, mOffsets); if (pfd == null) { throw new FileNotFoundException("Asset file: " + fileName); @@ -931,6 +962,12 @@ public final class AssetManager implements AutoCloseable { Preconditions.checkNotNull(fileName, "fileName"); synchronized (this) { ensureOpenLocked(); + + InputStream inputStream = searchLoaders(cookie, fileName, accessMode); + if (inputStream != null) { + return inputStream; + } + final long asset = nativeOpenNonAsset(mObject, cookie, fileName, accessMode); if (asset == 0) { throw new FileNotFoundException("Asset absolute file: " + fileName); @@ -970,6 +1007,12 @@ public final class AssetManager implements AutoCloseable { Preconditions.checkNotNull(fileName, "fileName"); synchronized (this) { ensureOpenLocked(); + + AssetFileDescriptor fileDescriptor = searchLoadersFd(cookie, fileName); + if (fileDescriptor != null) { + return fileDescriptor; + } + final ParcelFileDescriptor pfd = nativeOpenNonAssetFd(mObject, cookie, fileName, mOffsets); if (pfd == null) { @@ -1031,7 +1074,16 @@ public final class AssetManager implements AutoCloseable { Preconditions.checkNotNull(fileName, "fileName"); synchronized (this) { ensureOpenLocked(); - final long xmlBlock = nativeOpenXmlAsset(mObject, cookie, fileName); + + final long xmlBlock; + AssetFileDescriptor fileDescriptor = searchLoadersFd(cookie, fileName); + if (fileDescriptor != null) { + xmlBlock = nativeOpenXmlAssetFd(mObject, cookie, + fileDescriptor.getFileDescriptor()); + } else { + xmlBlock = nativeOpenXmlAsset(mObject, cookie, fileName); + } + if (xmlBlock == 0) { throw new FileNotFoundException("Asset XML file: " + fileName); } @@ -1041,6 +1093,85 @@ public final class AssetManager implements AutoCloseable { } } + private InputStream searchLoaders(int cookie, @NonNull String fileName, int accessMode) + throws IOException { + if (mResourceLoaderManager == null) { + return null; + } + + List<Pair<ResourceLoader, ResourcesProvider>> loaders = + mResourceLoaderManager.getInternalList(); + + // A cookie of 0 means no specific ApkAssets, so search everything + if (cookie == 0) { + for (int index = loaders.size() - 1; index >= 0; index--) { + Pair<ResourceLoader, ResourcesProvider> pair = loaders.get(index); + try { + InputStream inputStream = pair.first.loadAsset(fileName, accessMode); + if (inputStream != null) { + return inputStream; + } + } catch (IOException ignored) { + // When searching, ignore read failures + } + } + + return null; + } + + ApkAssets apkAssets = mApkAssets[cookie - 1]; + for (int index = loaders.size() - 1; index >= 0; index--) { + Pair<ResourceLoader, ResourcesProvider> pair = loaders.get(index); + if (pair.second.getApkAssets() == apkAssets) { + return pair.first.loadAsset(fileName, accessMode); + } + } + + return null; + } + + private AssetFileDescriptor searchLoadersFd(int cookie, @NonNull String fileName) + throws IOException { + if (mResourceLoaderManager == null) { + return null; + } + + List<Pair<ResourceLoader, ResourcesProvider>> loaders = + mResourceLoaderManager.getInternalList(); + + // A cookie of 0 means no specific ApkAssets, so search everything + if (cookie == 0) { + for (int index = loaders.size() - 1; index >= 0; index--) { + Pair<ResourceLoader, ResourcesProvider> pair = loaders.get(index); + try { + ParcelFileDescriptor fileDescriptor = pair.first.loadAssetFd(fileName); + if (fileDescriptor != null) { + return new AssetFileDescriptor(fileDescriptor, 0, + AssetFileDescriptor.UNKNOWN_LENGTH); + } + } catch (IOException ignored) { + // When searching, ignore read failures + } + } + + return null; + } + + ApkAssets apkAssets = mApkAssets[cookie - 1]; + for (int index = loaders.size() - 1; index >= 0; index--) { + Pair<ResourceLoader, ResourcesProvider> pair = loaders.get(index); + if (pair.second.getApkAssets() == apkAssets) { + ParcelFileDescriptor fileDescriptor = pair.first.loadAssetFd(fileName); + if (fileDescriptor != null) { + return new AssetFileDescriptor(fileDescriptor, 0, + AssetFileDescriptor.UNKNOWN_LENGTH); + } + return null; + } + } + return null; + } + void xmlBlockGone(int id) { synchronized (this) { decRefsLocked(id); @@ -1296,7 +1427,7 @@ public final class AssetManager implements AutoCloseable { * * <p>On SDK 21 (Android 5.0: Lollipop) and above, Locale strings are valid * <a href="https://tools.ietf.org/html/bcp47">BCP-47</a> language tags and can be - * parsed using {@link java.util.Locale#forLanguageTag(String)}. + * parsed using {@link Locale#forLanguageTag(String)}. * * <p>On SDK 20 (Android 4.4W: Kitkat for watches) and below, locale strings * are of the form {@code ll_CC} where {@code ll} is a two letter language code, @@ -1439,6 +1570,8 @@ public final class AssetManager implements AutoCloseable { private static native @Nullable ParcelFileDescriptor nativeOpenNonAssetFd(long ptr, int cookie, @NonNull String fileName, @NonNull long[] outOffsets) throws IOException; private static native long nativeOpenXmlAsset(long ptr, int cookie, @NonNull String fileName); + private static native long nativeOpenXmlAssetFd(long ptr, int cookie, + @NonNull FileDescriptor fileDescriptor); // Primitive resource native methods. private static native int nativeGetResourceValue(long ptr, @AnyRes int resId, short density, diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index d7e4e1452cfe..2698c2de4c61 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -30,6 +30,7 @@ import android.annotation.DimenRes; import android.annotation.DrawableRes; import android.annotation.FontRes; import android.annotation.FractionRes; +import android.annotation.IntRange; import android.annotation.IntegerRes; import android.annotation.LayoutRes; import android.annotation.NonNull; @@ -41,8 +42,12 @@ import android.annotation.StyleRes; import android.annotation.StyleableRes; import android.annotation.UnsupportedAppUsage; import android.annotation.XmlRes; +import android.app.ResourcesManager; import android.content.pm.ActivityInfo; import android.content.pm.ActivityInfo.Config; +import android.content.res.loader.ResourceLoader; +import android.content.res.loader.ResourceLoaderManager; +import android.content.res.loader.ResourcesProvider; import android.graphics.Movie; import android.graphics.Typeface; import android.graphics.drawable.Drawable; @@ -54,13 +59,16 @@ import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; import android.util.LongSparseArray; +import android.util.Pair; import android.util.Pools.SynchronizedPool; import android.util.TypedValue; import android.view.DisplayAdjustments; import android.view.ViewDebug; import android.view.ViewHierarchyEncoder; +import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.ArrayUtils; import com.android.internal.util.GrowingArrayUtils; import com.android.internal.util.XmlUtils; @@ -71,6 +79,8 @@ import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** * Class for accessing an application's resources. This sits on top of the @@ -133,6 +143,11 @@ public class Resources { @UnsupportedAppUsage final ClassLoader mClassLoader; + private final Object mResourceLoaderLock = new Object(); + + @GuardedBy("mResourceLoaderLock") + private ResourceLoaderManager mResourceLoaderManager; + /** * WeakReferences to Themes that were constructed from this Resources object. * We keep track of these in case our underlying implementation is changed, in which case @@ -148,6 +163,8 @@ public class Resources { private static final int MIN_THEME_REFS_FLUSH_SIZE = 32; private int mThemeRefsNextFlushSize = MIN_THEME_REFS_FLUSH_SIZE; + private int mBaseApkAssetsSize; + /** * Returns the most appropriate default theme for the specified target SDK version. * <ul> @@ -283,8 +300,15 @@ public class Resources { return; } + mBaseApkAssetsSize = ArrayUtils.size(impl.getAssets().getApkAssets()); mResourcesImpl = impl; + synchronized (mResourceLoaderLock) { + if (mResourceLoaderManager != null) { + mResourceLoaderManager.onImplUpdate(mResourcesImpl); + } + } + // Create new ThemeImpls that are identical to the ones we have. synchronized (mThemeRefs) { final int count = mThemeRefs.size(); @@ -903,7 +927,7 @@ public class Resources { try { final ResourcesImpl impl = mResourcesImpl; impl.getValueForDensity(id, density, value, true); - return impl.loadDrawable(this, value, id, density, theme); + return loadDrawable(value, id, density, theme); } finally { releaseTempTypedValue(value); } @@ -913,6 +937,14 @@ public class Resources { @UnsupportedAppUsage Drawable loadDrawable(@NonNull TypedValue value, int id, int density, @Nullable Theme theme) throws NotFoundException { + ResourceLoader loader = findLoader(value.assetCookie); + if (loader != null) { + Drawable drawable = loader.loadDrawable(value, id, density, theme); + if (drawable != null) { + return drawable; + } + } + return mResourcesImpl.loadDrawable(this, value, id, density, theme); } @@ -2280,7 +2312,7 @@ public class Resources { final ResourcesImpl impl = mResourcesImpl; impl.getValue(id, value, true); if (value.type == TypedValue.TYPE_STRING) { - return impl.loadXmlResourceParser(value.string.toString(), id, + return loadXmlResourceParser(value.string.toString(), id, value.assetCookie, type); } throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id) @@ -2304,6 +2336,14 @@ public class Resources { @UnsupportedAppUsage XmlResourceParser loadXmlResourceParser(String file, int id, int assetCookie, String type) throws NotFoundException { + ResourceLoader loader = findLoader(assetCookie); + if (loader != null) { + XmlResourceParser xml = loader.loadXmlResourceParser(file, id); + if (xml != null) { + return xml; + } + } + return mResourcesImpl.loadXmlResourceParser(file, id, assetCookie, type); } @@ -2329,4 +2369,137 @@ public class Resources { } return theme.obtainStyledAttributes(set, attrs, 0, 0); } + + private ResourceLoader findLoader(int assetCookie) { + ApkAssets[] apkAssetsArray = mResourcesImpl.getAssets().getApkAssets(); + int apkAssetsIndex = assetCookie - 1; + if (apkAssetsIndex < apkAssetsArray.length && apkAssetsIndex >= 0) { + ApkAssets apkAssets = apkAssetsArray[apkAssetsIndex]; + if (apkAssets.isForLoader()) { + List<Pair<ResourceLoader, ResourcesProvider>> loaders; + // Since we don't lock the entire resolution path anyways, + // only lock here instead of entire method. The list is copied + // and effectively a snapshot is used. + synchronized (mResourceLoaderLock) { + loaders = mResourceLoaderManager.getInternalList(); + } + + if (!ArrayUtils.isEmpty(loaders)) { + int size = loaders.size(); + for (int index = 0; index < size; index++) { + Pair<ResourceLoader, ResourcesProvider> pair = loaders.get(index); + if (pair.second.getApkAssets() == apkAssets) { + return pair.first; + } + } + } + } + } + + return null; + } + + /** + * @return copied list of loaders and providers previously added + */ + @NonNull + public List<Pair<ResourceLoader, ResourcesProvider>> getLoaders() { + synchronized (mResourceLoaderLock) { + return mResourceLoaderManager == null + ? Collections.emptyList() + : mResourceLoaderManager.getLoaders(); + } + } + + /** + * Add a custom {@link ResourceLoader} which is added to the paths searched by + * {@link AssetManager} when resolving a resource. + * + * Resources are resolved as if the loader was a resource overlay, meaning the latest + * in the list, of equal or better config, is returned. + * + * {@link ResourcesProvider}s passed in here are not managed and a reference should be held + * to remove, re-use, or close them when necessary. + * + * @param resourceLoader an interface used to resolve file paths for drawables/XML files; + * a reference should be kept to remove the loader if necessary + * @param resourcesProvider an .apk or .arsc file representation + * @param index where to add the loader in the list + * @throws IllegalArgumentException if the resourceLoader is already added + * @throws IndexOutOfBoundsException if the index is invalid + */ + public void addLoader(@NonNull ResourceLoader resourceLoader, + @NonNull ResourcesProvider resourcesProvider, @IntRange(from = 0) int index) { + synchronized (mResourceLoaderLock) { + if (mResourceLoaderManager == null) { + ResourcesManager.getInstance().registerForLoaders(this); + mResourceLoaderManager = new ResourceLoaderManager(mResourcesImpl); + } + + mResourceLoaderManager.addLoader(resourceLoader, resourcesProvider, index); + } + } + + /** + * @see #addLoader(ResourceLoader, ResourcesProvider, int). + * + * Adds to the end of the list. + * + * @return index the loader was added at + */ + public int addLoader(@NonNull ResourceLoader resourceLoader, + @NonNull ResourcesProvider resourcesProvider) { + synchronized (mResourceLoaderLock) { + int index = getLoaders().size(); + addLoader(resourceLoader, resourcesProvider, index); + return index; + } + } + + /** + * Remove a loader previously added by + * {@link #addLoader(ResourceLoader, ResourcesProvider, int)} + * + * The caller maintains responsibility for holding a reference to the matching + * {@link ResourcesProvider} and closing it after this method has been called. + * + * @param resourceLoader the same reference passed into [addLoader + * @return the index the loader was at in the list, or -1 if the loader was not found + */ + public int removeLoader(@NonNull ResourceLoader resourceLoader) { + synchronized (mResourceLoaderLock) { + if (mResourceLoaderManager == null) { + return -1; + } + + return mResourceLoaderManager.removeLoader(resourceLoader); + } + } + + /** + * Swap the current set of loaders. Preferred to multiple remove/add calls as this doesn't + * update the resource data structures after each modification. + * + * Set to null or an empty list to clear the set of loaders. + * + * The caller maintains responsibility for holding references to the added + * {@link ResourcesProvider}s and closing them after this method has been called. + * + * @param resourceLoadersAndProviders a list of pairs to add + */ + public void setLoaders( + @Nullable List<Pair<ResourceLoader, ResourcesProvider>> resourceLoadersAndProviders) { + synchronized (mResourceLoaderLock) { + if (mResourceLoaderManager == null) { + if (ArrayUtils.isEmpty(resourceLoadersAndProviders)) { + return; + } + + ResourcesManager.getInstance().registerForLoaders(this); + mResourceLoaderManager = new ResourceLoaderManager(mResourcesImpl); + } + + mResourceLoaderManager.setLoaders(resourceLoadersAndProviders); + } + } } diff --git a/core/java/android/content/res/ResourcesImpl.java b/core/java/android/content/res/ResourcesImpl.java index b72544c02d6a..84489cfb768c 100644 --- a/core/java/android/content/res/ResourcesImpl.java +++ b/core/java/android/content/res/ResourcesImpl.java @@ -57,6 +57,8 @@ import android.view.DisplayAdjustments; import com.android.internal.util.GrowingArrayUtils; +import libcore.io.IoUtils; + import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -376,7 +378,7 @@ public class ResourcesImpl { Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesImpl#updateConfiguration"); try { synchronized (mAccessLock) { - if (false) { + if (DEBUG_CONFIG) { Slog.i(TAG, "**** Updating config of " + this + ": old config is " + mConfiguration + " old compat is " + mDisplayAdjustments.getCompatibilityInfo()); @@ -572,6 +574,20 @@ public class ResourcesImpl { } } + /** + * Wipe all caches that might be read and return an outdated object when resolving a resource. + */ + public void clearAllCaches() { + synchronized (mAccessLock) { + mDrawableCache.clear(); + mColorDrawableCache.clear(); + mComplexColorCache.clear(); + mAnimatorCache.clear(); + mStateListAnimatorCache.clear(); + flushLayoutCache(); + } + } + @Nullable Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id, int density, @Nullable Resources.Theme theme) @@ -802,6 +818,27 @@ public class ResourcesImpl { } /** + * Loads a Drawable from an encoded image stream, or null. + * + * This call will handle closing the {@link InputStream}. + */ + @Nullable + private Drawable decodeImageDrawable(@NonNull InputStream inputStream, + @NonNull Resources wrapper, @NonNull TypedValue value) { + ImageDecoder.Source src = ImageDecoder.createSource(wrapper, inputStream, value.density); + try { + return ImageDecoder.decodeDrawable(src, (decoder, info, s) -> + decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE)); + } catch (IOException ignored) { + // This is okay. This may be something that ImageDecoder does not + // support, like SVG. + return null; + } finally { + IoUtils.closeQuietly(inputStream); + } + } + + /** * Loads a drawable from XML or resources stream. * * @return Drawable, or null if Drawable cannot be decoded. @@ -865,8 +902,12 @@ public class ResourcesImpl { } else { final InputStream is = mAssets.openNonAsset( value.assetCookie, file, AssetManager.ACCESS_STREAMING); - AssetInputStream ais = (AssetInputStream) is; - dr = decodeImageDrawable(ais, wrapper, value); + if (is instanceof AssetInputStream) { + AssetInputStream ais = (AssetInputStream) is; + dr = decodeImageDrawable(ais, wrapper, value); + } else { + dr = decodeImageDrawable(is, wrapper, value); + } } } finally { stack.pop(); diff --git a/core/java/android/content/res/StringBlock.java b/core/java/android/content/res/StringBlock.java index 2ae1932c3437..d43bd36b4c74 100644 --- a/core/java/android/content/res/StringBlock.java +++ b/core/java/android/content/res/StringBlock.java @@ -47,6 +47,7 @@ import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; +import java.io.Closeable; import java.util.Arrays; /** @@ -54,7 +55,7 @@ import java.util.Arrays; * * {@hide} */ -final class StringBlock { +public final class StringBlock implements Closeable { private static final String TAG = "AssetManager"; private static final boolean localLOGV = false; @@ -175,6 +176,7 @@ final class StringBlock { } } + @Override public void close() { synchronized (this) { if (mOpen) { @@ -517,7 +519,7 @@ final class StringBlock { * of this newly creating StringBlock. */ @UnsupportedAppUsage - StringBlock(long obj, boolean useSparse) { + public StringBlock(long obj, boolean useSparse) { mNative = obj; mUseSparse = useSparse; mOwnsNative = false; diff --git a/core/java/android/content/res/ThemedResourceCache.java b/core/java/android/content/res/ThemedResourceCache.java index 06cafdb2bb91..968ab401ccba 100644 --- a/core/java/android/content/res/ThemedResourceCache.java +++ b/core/java/android/content/res/ThemedResourceCache.java @@ -22,8 +22,8 @@ import android.annotation.UnsupportedAppUsage; import android.content.pm.ActivityInfo.Config; import android.content.res.Resources.Theme; import android.content.res.Resources.ThemeKey; -import android.util.LongSparseArray; import android.util.ArrayMap; +import android.util.LongSparseArray; import java.lang.ref.WeakReference; @@ -234,4 +234,18 @@ abstract class ThemedResourceCache<T> { return entry == null || (configChanges != 0 && shouldInvalidateEntry(entry, configChanges)); } + + public synchronized void clear() { + if (mThemedEntries != null) { + mThemedEntries.clear(); + } + + if (mUnthemedEntries != null) { + mUnthemedEntries.clear(); + } + + if (mNullThemedEntries != null) { + mNullThemedEntries.clear(); + } + } } diff --git a/core/java/android/content/res/loader/DirectoryResourceLoader.java b/core/java/android/content/res/loader/DirectoryResourceLoader.java new file mode 100644 index 000000000000..7d90e72ab07e --- /dev/null +++ b/core/java/android/content/res/loader/DirectoryResourceLoader.java @@ -0,0 +1,74 @@ +/* + * 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.os.ParcelFileDescriptor; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * A {@link ResourceLoader} that searches a directory for assets. + * + * Assumes that resource paths are resolvable child paths of the directory passed in. + */ +public class DirectoryResourceLoader implements ResourceLoader { + + @NonNull + private final File mDirectory; + + public DirectoryResourceLoader(@NonNull File directory) { + this.mDirectory = directory; + } + + @Nullable + @Override + public InputStream loadAsset(@NonNull String path, int accessMode) throws IOException { + File file = findFile(path); + if (file == null || !file.exists()) { + return null; + } + return new FileInputStream(file); + } + + @Nullable + @Override + public ParcelFileDescriptor loadAssetFd(@NonNull String path) throws IOException { + File file = findFile(path); + if (file == null || !file.exists()) { + return null; + } + return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); + } + + /** + * Find the file for the given path encoded into the resource table. + */ + @Nullable + public File findFile(@NonNull String path) { + return mDirectory.toPath().resolve(path).toFile(); + } + + @NonNull + public File getDirectory() { + return mDirectory; + } +} diff --git a/core/java/android/content/res/loader/ResourceLoader.java b/core/java/android/content/res/loader/ResourceLoader.java new file mode 100644 index 000000000000..af32aa2c6875 --- /dev/null +++ b/core/java/android/content/res/loader/ResourceLoader.java @@ -0,0 +1,116 @@ +/* + * 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.AnyRes; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.graphics.drawable.Drawable; +import android.os.ParcelFileDescriptor; +import android.util.TypedValue; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Exposes methods for overriding file-based resource loading from a {@link Resources}. + * + * To be used with {@link Resources#addLoader(ResourceLoader, ResourcesProvider, int)} and related + * methods to override resource loading. + * + * Note that this class doesn't actually contain any resource data. Non-file-based resources are + * loaded directly from the {@link ResourcesProvider}'s .arsc representation. + * + * An instance's methods will only be called if its corresponding {@link ResourcesProvider}'s + * resources table contains an entry for the resource ID being resolved, + * with the exception of the non-cookie variants of {@link AssetManager}'s openAsset and + * openNonAsset. + * + * Those methods search backwards through all {@link ResourceLoader}s and then any paths provided + * by the application or system. + * + * Otherwise, an ARSC that defines R.drawable.some_id must be provided if a {@link ResourceLoader} + * wants to point R.drawable.some_id to a different file on disk. + */ +public interface ResourceLoader { + + /** + * Given the value resolved from the string pool of the {@link ResourcesProvider} passed to + * {@link Resources#addLoader(ResourceLoader, ResourcesProvider, int)}, return a + * {@link Drawable} which should be returned by the parent + * {@link Resources#getDrawable(int, Resources.Theme)}. + * + * @param value the resolved {@link TypedValue} before it has been converted to a Drawable + * object + * @param id the R.drawable ID this resolution is for + * @param density the requested density + * @param theme the {@link Resources.Theme} resolved under + * @return null if resolution should try to find an entry inside the {@link ResourcesProvider}, + * including calling through to {@link #loadAsset(String, int)} or {@link #loadAssetFd(String)} + */ + @Nullable + default Drawable loadDrawable(@NonNull TypedValue value, int id, int density, + @Nullable Resources.Theme theme) { + return null; + } + + /** + * Given the value resolved from the string pool of the {@link ResourcesProvider} passed to + * {@link Resources#addLoader(ResourceLoader, ResourcesProvider, int)}, return an + * {@link XmlResourceParser} which should be returned by the parent + * {@link Resources#getDrawable(int, Resources.Theme)}. + * + * @param path the string that was found in the string pool + * @param id the XML ID this resolution is for, can be R.anim, R.layout, or R.xml + * @return null if resolution should try to find an entry inside the {@link ResourcesProvider}, + * including calling through to {@link #loadAssetFd(String)} (String, int)} + */ + @Nullable + default XmlResourceParser loadXmlResourceParser(@NonNull String path, @AnyRes int id) { + return null; + } + + /** + * Given the value resolved from the string pool of the {@link ResourcesProvider} passed to + * {@link Resources#addLoader(ResourceLoader, ResourcesProvider, int)}, return an + * {@link InputStream} which should be returned when an asset is loaded by {@link AssetManager}. + * Assets will be loaded from a provider's root, with anything in its assets subpath prefixed + * with "assets/". + * + * @param path the asset path to load + * @param accessMode {@link AssetManager} access mode; does not have to be respected + * @return null if resolution should try to find an entry inside the {@link ResourcesProvider} + */ + @Nullable + default InputStream loadAsset(@NonNull String path, int accessMode) throws IOException { + return null; + } + + /** + * {@link ParcelFileDescriptor} variant of {@link #loadAsset(String, int)}. + * + * @param path the asset path to load + * @return null if resolution should try to find an entry inside the {@link ResourcesProvider} + */ + @Nullable + default ParcelFileDescriptor loadAssetFd(@NonNull String path) throws IOException { + return null; + } +} diff --git a/core/java/android/content/res/loader/ResourceLoaderManager.java b/core/java/android/content/res/loader/ResourceLoaderManager.java new file mode 100644 index 000000000000..ddbfa81390e4 --- /dev/null +++ b/core/java/android/content/res/loader/ResourceLoaderManager.java @@ -0,0 +1,189 @@ +/* + * 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.Nullable; +import android.content.res.ApkAssets; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.content.res.ResourcesImpl; +import android.util.Pair; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.ArrayUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * @hide + */ +public class ResourceLoaderManager { + + private final Object mLock = new Object(); + + @GuardedBy("mLock") + private final List<Pair<ResourceLoader, ResourcesProvider>> mResourceLoaders = + new ArrayList<>(); + + @GuardedBy("mLock") + private ResourcesImpl mResourcesImpl; + + public ResourceLoaderManager(ResourcesImpl resourcesImpl) { + this.mResourcesImpl = resourcesImpl; + this.mResourcesImpl.getAssets().setResourceLoaderManager(this); + } + + /** + * Copies the list to ensure that ongoing mutations don't affect the list if it's being used + * as a search set. + * + * @see Resources#getLoaders() + */ + public List<Pair<ResourceLoader, ResourcesProvider>> getLoaders() { + synchronized (mLock) { + return new ArrayList<>(mResourceLoaders); + } + } + + /** + * Returns a list for searching for a loader. Locks and copies the list to ensure that + * ongoing mutations don't affect the search set. + */ + public List<Pair<ResourceLoader, ResourcesProvider>> getInternalList() { + synchronized (mLock) { + return new ArrayList<>(mResourceLoaders); + } + } + + /** + * TODO(b/136251855): Consider optional boolean ignoreConfigurations to allow ResourceLoader + * to override every configuration in the target package + * + * @see Resources#addLoader(ResourceLoader, ResourcesProvider) + */ + public void addLoader(ResourceLoader resourceLoader, ResourcesProvider resourcesProvider, + int index) { + synchronized (mLock) { + for (int listIndex = 0; listIndex < mResourceLoaders.size(); listIndex++) { + if (Objects.equals(mResourceLoaders.get(listIndex).first, resourceLoader)) { + throw new IllegalArgumentException("Cannot add the same ResourceLoader twice"); + } + } + + mResourceLoaders.add(index, Pair.create(resourceLoader, resourcesProvider)); + updateLoaders(); + } + } + + /** + * @see Resources#removeLoader(ResourceLoader) + */ + public int removeLoader(ResourceLoader resourceLoader) { + synchronized (mLock) { + int indexOfLoader = -1; + + for (int index = 0; index < mResourceLoaders.size(); index++) { + if (mResourceLoaders.get(index).first == resourceLoader) { + indexOfLoader = index; + break; + } + } + + if (indexOfLoader < 0) { + return indexOfLoader; + } + + mResourceLoaders.remove(indexOfLoader); + updateLoaders(); + return indexOfLoader; + } + } + + /** + * @see Resources#setLoaders(List) + */ + public void setLoaders( + @Nullable List<Pair<ResourceLoader, ResourcesProvider>> newLoadersAndProviders) { + synchronized (mLock) { + if (ArrayUtils.isEmpty(newLoadersAndProviders)) { + mResourceLoaders.clear(); + updateLoaders(); + return; + } + + int size = newLoadersAndProviders.size(); + for (int newIndex = 0; newIndex < size; newIndex++) { + ResourceLoader resourceLoader = newLoadersAndProviders.get(newIndex).first; + for (int oldIndex = 0; oldIndex < mResourceLoaders.size(); oldIndex++) { + if (Objects.equals(mResourceLoaders.get(oldIndex).first, resourceLoader)) { + throw new IllegalArgumentException( + "Cannot add the same ResourceLoader twice"); + } + } + } + + mResourceLoaders.clear(); + mResourceLoaders.addAll(newLoadersAndProviders); + + updateLoaders(); + } + } + + /** + * Swap the tracked {@link ResourcesImpl} and reattach any loaders to it. + */ + public void onImplUpdate(ResourcesImpl resourcesImpl) { + synchronized (mLock) { + this.mResourcesImpl = resourcesImpl; + updateLoaders(); + } + } + + private void updateLoaders() { + synchronized (mLock) { + AssetManager assetManager = mResourcesImpl.getAssets(); + ApkAssets[] existingApkAssets = assetManager.getApkAssets(); + int baseApkAssetsSize = 0; + for (int index = existingApkAssets.length - 1; index >= 0; index--) { + // Loaders are always last, so the first non-loader is the end of the base assets + if (!existingApkAssets[index].isForLoader()) { + baseApkAssetsSize = index + 1; + break; + } + } + + List<ApkAssets> newAssets = new ArrayList<>(); + for (int index = 0; index < baseApkAssetsSize; index++) { + newAssets.add(existingApkAssets[index]); + } + + int size = mResourceLoaders.size(); + for (int index = 0; index < size; index++) { + ApkAssets apkAssets = mResourceLoaders.get(index).second.getApkAssets(); + newAssets.add(apkAssets); + } + + assetManager.setApkAssets(newAssets.toArray(new ApkAssets[0]), true); + + // Short of resolving every resource, it's too difficult to determine what has changed + // when a resource loader is changed, so just clear everything. + mResourcesImpl.clearAllCaches(); + } + } +} diff --git a/core/java/android/content/res/loader/ResourcesProvider.java b/core/java/android/content/res/loader/ResourcesProvider.java new file mode 100644 index 000000000000..050aeb7c5fda --- /dev/null +++ b/core/java/android/content/res/loader/ResourcesProvider.java @@ -0,0 +1,139 @@ +/* + * 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.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.res.ApkAssets; +import android.content.res.Resources; +import android.os.ParcelFileDescriptor; +import android.os.SharedMemory; + +import com.android.internal.util.ArrayUtils; + +import java.io.Closeable; +import java.io.IOException; + +/** + * Provides methods to load resources from an .apk or .arsc file to pass to + * {@link Resources#addLoader(ResourceLoader, ResourcesProvider, int)}. + * + * It is the responsibility of the app to close any instances. + */ +public final class ResourcesProvider implements AutoCloseable, Closeable { + + /** + * Contains no data, assuming that any resource loading behavior will be handled in the + * corresponding {@link ResourceLoader}. + */ + @NonNull + public static ResourcesProvider empty() { + return new ResourcesProvider(ApkAssets.loadEmptyForLoader()); + } + + /** + * Read from an .apk file descriptor. + * + * The file descriptor is duplicated and the one passed in may be closed by the application + * at any time. + */ + @NonNull + public static ResourcesProvider loadFromApk(@NonNull ParcelFileDescriptor fileDescriptor) + throws IOException { + return new ResourcesProvider( + ApkAssets.loadApkForLoader(fileDescriptor.getFileDescriptor())); + } + + /** + * Read from an .apk file representation in memory. + */ + @NonNull + public static ResourcesProvider loadFromApk(@NonNull SharedMemory sharedMemory) + throws IOException { + return new ResourcesProvider( + ApkAssets.loadApkForLoader(sharedMemory.getFileDescriptor())); + } + + /** + * Read from an .arsc file descriptor. + * + * The file descriptor is duplicated and the one passed in may be closed by the application + * at any time. + */ + @NonNull + public static ResourcesProvider loadFromArsc(@NonNull ParcelFileDescriptor fileDescriptor) + throws IOException { + return new ResourcesProvider( + ApkAssets.loadArscForLoader(fileDescriptor.getFileDescriptor())); + } + + /** + * Read from an .arsc file representation in memory. + */ + @NonNull + public static ResourcesProvider loadFromArsc(@NonNull SharedMemory sharedMemory) + throws IOException { + return new ResourcesProvider( + ApkAssets.loadArscForLoader(sharedMemory.getFileDescriptor())); + } + + /** + * Read from a split installed alongside the application, which may not have been + * loaded initially because the application requested isolated split loading. + */ + @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.loadApkForLoader(splitPath)); + } + + + @NonNull + private final ApkAssets mApkAssets; + + private ResourcesProvider(@NonNull ApkAssets apkAssets) { + this.mApkAssets = apkAssets; + } + + /** @hide */ + @NonNull + public ApkAssets getApkAssets() { + return mApkAssets; + } + + @Override + public void close() { + try { + mApkAssets.close(); + } catch (Throwable ignored) { + } + } + + @Override + protected void finalize() throws Throwable { + close(); + super.finalize(); + } +} diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java index bcb94ce2d2d5..fdb44e7050e1 100644 --- a/core/java/android/os/ParcelFileDescriptor.java +++ b/core/java/android/os/ParcelFileDescriptor.java @@ -35,12 +35,15 @@ import android.annotation.TestApi; import android.annotation.UnsupportedAppUsage; import android.content.BroadcastReceiver; import android.content.ContentProvider; +import android.content.ContentResolver; +import android.net.Uri; import android.os.MessageQueue.OnFileDescriptorEventListener; import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; import android.system.StructStat; import android.util.Log; +import android.util.Size; import dalvik.system.CloseGuard; import dalvik.system.VMRuntime; @@ -204,6 +207,10 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { /** * Create a new ParcelFileDescriptor accessing a given file. + * <p> + * This method should only be used for files that you have direct access to; + * if you'd like to work with files hosted outside your app, use an API like + * {@link ContentResolver#openFile(Uri, String, CancellationSignal)}. * * @param file The file to be opened. * @param mode The desired access mode, must be one of @@ -226,6 +233,10 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { /** * Create a new ParcelFileDescriptor accessing a given file. + * <p> + * This method should only be used for files that you have direct access to; + * if you'd like to work with files hosted outside your app, use an API like + * {@link ContentResolver#openFile(Uri, String, CancellationSignal)}. * * @param file The file to be opened. * @param mode The desired access mode, must be one of diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java index 079a42ddaa6c..493f9a2ce123 100644 --- a/core/java/android/provider/MediaStore.java +++ b/core/java/android/provider/MediaStore.java @@ -47,6 +47,7 @@ import android.graphics.Point; import android.graphics.PostProcessor; import android.media.ExifInterface; import android.media.MediaFile; +import android.media.MediaFormat; import android.net.Uri; import android.os.Bundle; import android.os.CancellationSignal; @@ -3007,22 +3008,32 @@ public final class MediaStore { public static final String BOOKMARK = "bookmark"; /** - * The standard of color aspects - * @hide + * The color standard of this media file, if available. + * + * @see MediaFormat#COLOR_STANDARD_BT709 + * @see MediaFormat#COLOR_STANDARD_BT601_PAL + * @see MediaFormat#COLOR_STANDARD_BT601_NTSC + * @see MediaFormat#COLOR_STANDARD_BT2020 */ @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true) public static final String COLOR_STANDARD = "color_standard"; /** - * The transfer of color aspects - * @hide + * The color transfer of this media file, if available. + * + * @see MediaFormat#COLOR_TRANSFER_LINEAR + * @see MediaFormat#COLOR_TRANSFER_SDR_VIDEO + * @see MediaFormat#COLOR_TRANSFER_ST2084 + * @see MediaFormat#COLOR_TRANSFER_HLG */ @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true) public static final String COLOR_TRANSFER = "color_transfer"; /** - * The range of color aspects - * @hide + * The color range of this media file, if available. + * + * @see MediaFormat#COLOR_RANGE_LIMITED + * @see MediaFormat#COLOR_RANGE_FULL */ @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true) public static final String COLOR_RANGE = "color_range"; diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index 407a85f1bb05..068056f091d7 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -388,21 +388,24 @@ public class ResolverActivity extends Activity { mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top, mSystemWindowInsets.right, 0); - View emptyView = findViewById(R.id.empty); - if (emptyView != null) { - emptyView.setPadding(0, 0, 0, mSystemWindowInsets.bottom - + getResources().getDimensionPixelSize( - R.dimen.chooser_edge_margin_normal) * 2); - } - - if (mFooterSpacer == null) { - mFooterSpacer = new Space(getApplicationContext()); + // Need extra padding so the list can fully scroll up + if (useLayoutWithDefault()) { + if (mFooterSpacer == null) { + mFooterSpacer = new Space(getApplicationContext()); + } else { + ((ListView) mAdapterView).removeFooterView(mFooterSpacer); + } + mFooterSpacer.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT, + mSystemWindowInsets.bottom)); + ((ListView) mAdapterView).addFooterView(mFooterSpacer); } else { - ((ListView) mAdapterView).removeFooterView(mFooterSpacer); + View emptyView = findViewById(R.id.empty); + if (emptyView != null) { + emptyView.setPadding(0, 0, 0, mSystemWindowInsets.bottom + + getResources().getDimensionPixelSize( + R.dimen.chooser_edge_margin_normal) * 2); + } } - mFooterSpacer.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT, - mSystemWindowInsets.bottom)); - ((ListView) mAdapterView).addFooterView(mFooterSpacer); resetButtonBar(); @@ -561,7 +564,7 @@ public class ResolverActivity extends Activity { intent.getData().getHost(), mAdapter.getFilteredItem().getDisplayLabel()); } else if (mAdapter.areAllTargetsBrowsers()) { - dialogTitle = getString(ActionTitle.BROWSABLE_TITLE_RES); + dialogTitle = getString(ActionTitle.BROWSABLE_TITLE_RES); } else { dialogTitle = getString(ActionTitle.BROWSABLE_HOST_TITLE_RES, intent.getData().getHost()); @@ -1304,6 +1307,7 @@ public class ResolverActivity extends Activity { // In case this method is called again (due to activity recreation), avoid adding a new // header if one is already present. if (useHeader && listView != null && listView.getHeaderViewsCount() == 0) { + listView.setHeaderDividersEnabled(true); listView.addHeaderView(LayoutInflater.from(this).inflate( R.layout.resolver_different_item_header, listView, false)); } @@ -1346,11 +1350,13 @@ public class ResolverActivity extends Activity { final ViewGroup buttonLayout = findViewById(R.id.button_bar); if (buttonLayout != null) { buttonLayout.setVisibility(View.VISIBLE); - int inset = mSystemWindowInsets != null ? mSystemWindowInsets.bottom : 0; - buttonLayout.setPadding(buttonLayout.getPaddingLeft(), buttonLayout.getPaddingTop(), - buttonLayout.getPaddingRight(), getResources().getDimensionPixelSize( - R.dimen.resolver_button_bar_spacing) + inset); + if (!useLayoutWithDefault()) { + int inset = mSystemWindowInsets != null ? mSystemWindowInsets.bottom : 0; + buttonLayout.setPadding(buttonLayout.getPaddingLeft(), buttonLayout.getPaddingTop(), + buttonLayout.getPaddingRight(), getResources().getDimensionPixelSize( + R.dimen.resolver_button_bar_spacing) + inset); + } mOnceButton = (Button) buttonLayout.findViewById(R.id.button_once); mAlwaysButton = (Button) buttonLayout.findViewById(R.id.button_always); @@ -2057,7 +2063,9 @@ public class ResolverActivity extends Activity { CharSequence subLabel = info.getExtendedInfo(); if (TextUtils.equals(label, subLabel)) subLabel = null; - if (!TextUtils.equals(holder.text2.getText(), subLabel)) { + if (!TextUtils.equals(holder.text2.getText(), subLabel) + && !TextUtils.isEmpty(subLabel)) { + holder.text2.setVisibility(View.VISIBLE); holder.text2.setText(subLabel); } diff --git a/core/jni/android_content_res_ApkAssets.cpp b/core/jni/android_content_res_ApkAssets.cpp index bd4862dfb08d..637025329e37 100644 --- a/core/jni/android_content_res_ApkAssets.cpp +++ b/core/jni/android_content_res_ApkAssets.cpp @@ -16,6 +16,7 @@ #define ATRACE_TAG ATRACE_TAG_RESOURCES +#include "android-base/logging.h" #include "android-base/macros.h" #include "android-base/stringprintf.h" #include "android-base/unique_fd.h" @@ -32,7 +33,7 @@ using ::android::base::unique_fd; namespace android { static jlong NativeLoad(JNIEnv* env, jclass /*clazz*/, jstring java_path, jboolean system, - jboolean force_shared_lib, jboolean overlay) { + jboolean force_shared_lib, jboolean overlay, jboolean for_loader) { ScopedUtfChars path(env, java_path); if (path.c_str() == nullptr) { return 0; @@ -46,7 +47,7 @@ static jlong NativeLoad(JNIEnv* env, jclass /*clazz*/, jstring java_path, jboole } else if (force_shared_lib) { apk_assets = ApkAssets::LoadAsSharedLibrary(path.c_str(), system); } else { - apk_assets = ApkAssets::Load(path.c_str(), system); + apk_assets = ApkAssets::Load(path.c_str(), system, for_loader); } if (apk_assets == nullptr) { @@ -58,7 +59,8 @@ static jlong NativeLoad(JNIEnv* env, jclass /*clazz*/, jstring java_path, jboole } static jlong NativeLoadFromFd(JNIEnv* env, jclass /*clazz*/, jobject file_descriptor, - jstring friendly_name, jboolean system, jboolean force_shared_lib) { + jstring friendly_name, jboolean system, jboolean force_shared_lib, + jboolean for_loader) { ScopedUtfChars friendly_name_utf8(env, friendly_name); if (friendly_name_utf8.c_str() == nullptr) { return 0; @@ -80,7 +82,9 @@ static jlong NativeLoadFromFd(JNIEnv* env, jclass /*clazz*/, jobject file_descri std::unique_ptr<const ApkAssets> apk_assets = ApkAssets::LoadFromFd(std::move(dup_fd), friendly_name_utf8.c_str(), - system, force_shared_lib); + system, force_shared_lib, + for_loader); + if (apk_assets == nullptr) { std::string error_msg = base::StringPrintf("Failed to load asset path %s from fd %d", friendly_name_utf8.c_str(), dup_fd.get()); @@ -90,6 +94,60 @@ static jlong NativeLoadFromFd(JNIEnv* env, jclass /*clazz*/, jobject file_descri return reinterpret_cast<jlong>(apk_assets.release()); } +static jlong NativeLoadArsc(JNIEnv* env, jclass /*clazz*/, jstring java_path, + jboolean for_loader) { + ScopedUtfChars path(env, java_path); + if (path.c_str() == nullptr) { + return 0; + } + + ATRACE_NAME(base::StringPrintf("LoadApkAssetsArsc(%s)", path.c_str()).c_str()); + + std::unique_ptr<const ApkAssets> apk_assets = ApkAssets::LoadArsc(path.c_str(), for_loader); + + if (apk_assets == nullptr) { + std::string error_msg = base::StringPrintf("Failed to load asset path %s", path.c_str()); + jniThrowException(env, "java/io/IOException", error_msg.c_str()); + return 0; + } + return reinterpret_cast<jlong>(apk_assets.release()); +} + +static jlong NativeLoadArscFromFd(JNIEnv* env, jclass /*clazz*/, jobject file_descriptor, + jstring friendly_name, jboolean for_loader) { + ScopedUtfChars friendly_name_utf8(env, friendly_name); + if (friendly_name_utf8.c_str() == nullptr) { + return 0; + } + + int fd = jniGetFDFromFileDescriptor(env, file_descriptor); + ATRACE_NAME(base::StringPrintf("LoadApkAssetsArscFd(%d)", fd).c_str()); + if (fd < 0) { + jniThrowException(env, "java/lang/IllegalArgumentException", "Bad FileDescriptor"); + return 0; + } + + unique_fd dup_fd(::fcntl(fd, F_DUPFD_CLOEXEC, 0)); + if (dup_fd < 0) { + jniThrowIOException(env, errno); + return 0; + } + + std::unique_ptr<const ApkAssets> apk_assets = + ApkAssets::LoadArsc(std::move(dup_fd), friendly_name_utf8.c_str(), for_loader); + if (apk_assets == nullptr) { + std::string error_msg = base::StringPrintf("Failed to load asset path from fd %d", fd); + jniThrowException(env, "java/io/IOException", error_msg.c_str()); + return 0; + } + return reinterpret_cast<jlong>(apk_assets.release()); +} + +static jlong NativeLoadEmpty(JNIEnv* env, jclass /*clazz*/, jboolean for_loader) { + std::unique_ptr<const ApkAssets> apk_assets = ApkAssets::LoadEmpty(for_loader); + return reinterpret_cast<jlong>(apk_assets.release()); +} + static void NativeDestroy(JNIEnv* /*env*/, jclass /*clazz*/, jlong ptr) { delete reinterpret_cast<ApkAssets*>(ptr); } @@ -138,9 +196,13 @@ static jlong NativeOpenXml(JNIEnv* env, jclass /*clazz*/, jlong ptr, jstring fil // JNI registration. static const JNINativeMethod gApkAssetsMethods[] = { - {"nativeLoad", "(Ljava/lang/String;ZZZ)J", (void*)NativeLoad}, - {"nativeLoadFromFd", "(Ljava/io/FileDescriptor;Ljava/lang/String;ZZ)J", + {"nativeLoad", "(Ljava/lang/String;ZZZZ)J", (void*)NativeLoad}, + {"nativeLoadFromFd", "(Ljava/io/FileDescriptor;Ljava/lang/String;ZZZ)J", (void*)NativeLoadFromFd}, + {"nativeLoadArsc", "(Ljava/lang/String;Z)J", (void*)NativeLoadArsc}, + {"nativeLoadArscFromFd", "(Ljava/io/FileDescriptor;Ljava/lang/String;Z)J", + (void*)NativeLoadArscFromFd}, + {"nativeLoadEmpty", "(Z)J", (void*)NativeLoadEmpty}, {"nativeDestroy", "(J)V", (void*)NativeDestroy}, {"nativeGetAssetPath", "(J)Ljava/lang/String;", (void*)NativeGetAssetPath}, {"nativeGetStringBlock", "(J)J", (void*)NativeGetStringBlock}, diff --git a/core/jni/android_os_Debug.cpp b/core/jni/android_os_Debug.cpp index 9445319e47ec..d62d2d967d85 100644 --- a/core/jni/android_os_Debug.cpp +++ b/core/jni/android_os_Debug.cpp @@ -139,8 +139,8 @@ static stat_field_names stat_field_names[_NUM_CORE_HEAP] = { "nativePrivateClean", "nativeSharedClean", "nativeSwappedOut", "nativeSwappedOutPss" } }; -jfieldID otherStats_field; -jfieldID hasSwappedOutPss_field; +static jfieldID otherStats_field; +static jfieldID hasSwappedOutPss_field; struct stats_t { int pss; diff --git a/core/jni/android_util_AssetManager.cpp b/core/jni/android_util_AssetManager.cpp index daf33f61105c..c7b36d0f8fc9 100644 --- a/core/jni/android_util_AssetManager.cpp +++ b/core/jni/android_util_AssetManager.cpp @@ -108,7 +108,7 @@ static struct arraymap_offsets_t { jmethodID put; } gArrayMapOffsets; -jclass g_stringClass = nullptr; +static jclass g_stringClass = nullptr; // ---------------------------------------------------------------------------- @@ -763,6 +763,41 @@ static jlong NativeOpenXmlAsset(JNIEnv* env, jobject /*clazz*/, jlong ptr, jint return reinterpret_cast<jlong>(xml_tree.release()); } +static jlong NativeOpenXmlAssetFd(JNIEnv* env, jobject /*clazz*/, jlong ptr, int jcookie, + jobject file_descriptor) { + int fd = jniGetFDFromFileDescriptor(env, file_descriptor); + ATRACE_NAME(base::StringPrintf("AssetManager::OpenXmlAssetFd(%d)", fd).c_str()); + if (fd < 0) { + jniThrowException(env, "java/lang/IllegalArgumentException", "Bad FileDescriptor"); + return 0; + } + + base::unique_fd dup_fd(::fcntl(fd, F_DUPFD_CLOEXEC, 0)); + if (dup_fd < 0) { + jniThrowIOException(env, errno); + return 0; + } + + std::unique_ptr<Asset> + asset(Asset::createFromFd(dup_fd.release(), nullptr, Asset::AccessMode::ACCESS_BUFFER)); + + ScopedLock<AssetManager2> assetmanager(AssetManagerFromLong(ptr)); + ApkAssetsCookie cookie = JavaCookieToApkAssetsCookie(jcookie); + + // May be nullptr. + const DynamicRefTable* dynamic_ref_table = assetmanager->GetDynamicRefTableForCookie(cookie); + + std::unique_ptr<ResXMLTree> xml_tree = util::make_unique<ResXMLTree>(dynamic_ref_table); + status_t err = xml_tree->setTo(asset->getBuffer(true), asset->getLength(), true); + asset.reset(); + + if (err != NO_ERROR) { + jniThrowException(env, "java/io/FileNotFoundException", "Corrupt XML binary file"); + return 0; + } + return reinterpret_cast<jlong>(xml_tree.release()); +} + static jint NativeGetResourceValue(JNIEnv* env, jclass /*clazz*/, jlong ptr, jint resid, jshort density, jobject typed_value, jboolean resolve_references) { @@ -1564,6 +1599,7 @@ static const JNINativeMethod gAssetManagerMethods[] = { {"nativeOpenNonAssetFd", "(JILjava/lang/String;[J)Landroid/os/ParcelFileDescriptor;", (void*)NativeOpenNonAssetFd}, {"nativeOpenXmlAsset", "(JILjava/lang/String;)J", (void*)NativeOpenXmlAsset}, + {"nativeOpenXmlAssetFd", "(JILjava/io/FileDescriptor;)J", (void*)NativeOpenXmlAssetFd}, // AssetManager resource methods. {"nativeGetResourceValue", "(JISLandroid/util/TypedValue;Z)I", (void*)NativeGetResourceValue}, diff --git a/core/res/res/layout/resolve_list_item.xml b/core/res/res/layout/resolve_list_item.xml index 0bdb25a8d307..485709523e66 100644 --- a/core/res/res/layout/resolve_list_item.xml +++ b/core/res/res/layout/resolve_list_item.xml @@ -22,8 +22,6 @@ android:layout_height="wrap_content" android:layout_width="match_parent" android:minHeight="?attr/listPreferredItemHeightSmall" - android:paddingTop="4dp" - android:paddingBottom="4dp" android:background="?attr/activatedBackgroundIndicator"> <!-- Activity icon when presenting dialog @@ -32,7 +30,8 @@ android:layout_width="@dimen/resolver_icon_size" android:layout_height="@dimen/resolver_icon_size" android:layout_gravity="start|center_vertical" - android:layout_marginStart="?attr/listPreferredItemPaddingStart" + android:layout_marginStart="@dimen/resolver_icon_margin" + android:layout_marginEnd="@dimen/resolver_icon_margin" android:layout_marginTop="12dp" android:layout_marginBottom="12dp" android:scaleType="fitCenter" /> @@ -40,8 +39,7 @@ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:gravity="start|center_vertical" android:orientation="vertical" - android:paddingStart="?attr/listPreferredItemPaddingStart" - android:paddingEnd="?attr/listPreferredItemPaddingEnd" + android:paddingEnd="@dimen/resolver_edge_margin" android:layout_height="wrap_content" android:layout_width="wrap_content" android:layout_gravity="start|center_vertical"> @@ -49,14 +47,20 @@ <TextView android:id="@android:id/text1" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textAppearance="?attr/textAppearanceMedium" - android:textColor="?attr/textColorPrimary" + android:layout_gravity="start|center_vertical" + android:textColor="?android:attr/textColorPrimary" + android:fontFamily="@android:string/config_bodyFontFamily" + android:textSize="16sp" android:minLines="1" android:maxLines="1" android:ellipsize="marquee" /> <!-- Extended activity info to distinguish between duplicate activity names --> <TextView android:id="@android:id/text2" - android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="?android:attr/textColorSecondary" + android:fontFamily="@android:string/config_bodyFontFamily" + android:layout_gravity="start|center_vertical" + android:textSize="14sp" + android:visibility="gone" android:layout_width="wrap_content" android:layout_height="wrap_content" android:minLines="1" diff --git a/core/res/res/layout/resolver_different_item_header.xml b/core/res/res/layout/resolver_different_item_header.xml index 7d9ffd72870d..0a35edc42329 100644 --- a/core/res/res/layout/resolver_different_item_header.xml +++ b/core/res/res/layout/resolver_different_item_header.xml @@ -22,12 +22,12 @@ android:layout_height="wrap_content" android:layout_alwaysShow="true" android:text="@string/use_a_different_app" - android:minHeight="56dp" - android:textAppearance="?android:attr/textAppearanceMedium" + android:textColor="?android:attr/textColorPrimary" + android:fontFamily="@android:string/config_headlineFontFamilyMedium" + android:textSize="16sp" android:gravity="start|center_vertical" - android:paddingStart="16dp" - android:paddingEnd="16dp" - android:paddingTop="8dp" - android:paddingBottom="8dp" - android:elevation="8dp" - /> + android:paddingStart="@dimen/resolver_edge_margin" + android:paddingEnd="@dimen/resolver_edge_margin" + android:paddingTop="@dimen/resolver_small_margin" + android:paddingBottom="@dimen/resolver_edge_margin" + android:elevation="1dp" /> diff --git a/core/res/res/layout/resolver_list.xml b/core/res/res/layout/resolver_list.xml index 1dd420746e8a..6e45e7a4c509 100644 --- a/core/res/res/layout/resolver_list.xml +++ b/core/res/res/layout/resolver_list.xml @@ -29,16 +29,18 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alwaysShow="true" - android:elevation="8dp" - android:background="?attr/colorBackgroundFloating"> + android:elevation="@dimen/resolver_elevation" + android:paddingTop="@dimen/resolver_small_margin" + android:paddingStart="@dimen/resolver_edge_margin" + android:paddingEnd="@dimen/resolver_edge_margin" + android:paddingBottom="@dimen/resolver_edge_margin" + android:background="@drawable/bottomsheet_background"> <TextView android:id="@+id/profile_button" android:layout_width="wrap_content" android:layout_height="48dp" android:layout_marginEnd="8dp" - android:paddingStart="8dp" - android:paddingEnd="8dp" android:visibility="gone" style="?attr/borderlessButtonStyle" android:textAppearance="?attr/textAppearanceButton" @@ -50,36 +52,49 @@ <TextView android:id="@+id/title" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:minHeight="56dp" - android:textAppearance="?attr/textAppearanceMedium" - android:gravity="start|center_vertical" - android:paddingStart="?attr/dialogPreferredPadding" - android:paddingEnd="?attr/dialogPreferredPadding" - android:paddingTop="8dp" android:layout_below="@id/profile_button" android:layout_alignParentStart="true" - android:paddingBottom="8dp" /> + android:textColor="?android:attr/textColorPrimary" + android:fontFamily="@android:string/config_headlineFontFamilyMedium" + android:textSize="16sp" + android:gravity="start|center_vertical" /> </RelativeLayout> + <View + android:layout_alwaysShow="true" + android:layout_width="match_parent" + android:layout_height="1dp" + android:background="?attr/colorBackgroundFloating" + android:foreground="?attr/dividerVertical" /> <ListView android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/resolver_list" android:clipToPadding="false" - android:scrollbarStyle="outsideOverlay" android:background="?attr/colorBackgroundFloating" - android:elevation="8dp" + android:elevation="@dimen/resolver_elevation" android:nestedScrollingEnabled="true" + android:scrollbarStyle="outsideOverlay" android:scrollIndicators="top|bottom" - android:divider="@null" /> + android:divider="?attr/dividerVertical" + android:footerDividersEnabled="false" + android:headerDividersEnabled="false" + android:dividerHeight="1dp" /> + <View + android:layout_alwaysShow="true" + android:layout_width="match_parent" + android:layout_height="1dp" + android:background="?attr/colorBackgroundFloating" + android:foreground="?attr/dividerVertical" /> + <TextView android:id="@+id/empty" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?attr/colorBackgroundFloating" - android:elevation="8dp" + android:elevation="@dimen/resolver_elevation" android:layout_alwaysShow="true" android:text="@string/noApplications" android:padding="32dp" @@ -102,18 +117,19 @@ android:background="?attr/colorBackgroundFloating" android:paddingTop="@dimen/resolver_button_bar_spacing" android:paddingBottom="@dimen/resolver_button_bar_spacing" - android:paddingStart="12dp" - android:paddingEnd="12dp" - android:elevation="8dp"> + android:paddingStart="@dimen/resolver_edge_margin" + android:paddingEnd="@dimen/resolver_small_margin" + android:elevation="@dimen/resolver_elevation"> <Button android:id="@+id/button_once" android:layout_width="wrap_content" android:layout_gravity="start" android:maxLines="2" - style="?attr/buttonBarNegativeButtonStyle" - android:minHeight="@dimen/alert_dialog_button_bar_height" + style="?attr/buttonBarButtonStyle" + android:fontFamily="@android:string/config_headlineFontFamilyMedium" android:layout_height="wrap_content" + android:textAllCaps="false" android:enabled="false" android:text="@string/activity_resolver_use_once" android:onClick="onButtonClick" /> @@ -123,8 +139,9 @@ android:layout_width="wrap_content" android:layout_gravity="end" android:maxLines="2" - android:minHeight="@dimen/alert_dialog_button_bar_height" - style="?attr/buttonBarPositiveButtonStyle" + style="?attr/buttonBarButtonStyle" + android:fontFamily="@android:string/config_headlineFontFamilyMedium" + android:textAllCaps="false" android:layout_height="wrap_content" android:enabled="false" android:text="@string/activity_resolver_use_always" diff --git a/core/res/res/layout/resolver_list_with_default.xml b/core/res/res/layout/resolver_list_with_default.xml index 740a7eb9374e..dbba0b7bcc25 100644 --- a/core/res/res/layout/resolver_list_with_default.xml +++ b/core/res/res/layout/resolver_list_with_default.xml @@ -29,22 +29,22 @@ android:layout_height="wrap_content" android:layout_alwaysShow="true" android:orientation="vertical" - android:background="?attr/colorBackgroundFloating" - android:elevation="8dp"> + android:background="@drawable/bottomsheet_background" + android:paddingTop="@dimen/resolver_small_margin" + android:elevation="@dimen/resolver_elevation"> <LinearLayout android:layout_width="match_parent" - android:layout_height="64dp" - android:orientation="horizontal"> - + android:layout_height="wrap_content" + android:orientation="horizontal" + android:paddingBottom="@dimen/resolver_edge_margin" + android:paddingEnd="@dimen/resolver_edge_margin"> <ImageView android:id="@+id/icon" - android:layout_width="24dp" - android:layout_height="24dp" + android:layout_width="@dimen/resolver_icon_size" + android:layout_height="@dimen/resolver_icon_size" android:layout_gravity="start|top" - android:layout_marginStart="16dp" - android:layout_marginEnd="16dp" - android:layout_marginTop="20dp" + android:layout_marginStart="@dimen/resolver_icon_margin" android:src="@drawable/resolver_icon_placeholder" android:scaleType="fitCenter" /> @@ -52,9 +52,11 @@ android:id="@+id/title" android:layout_width="0dp" android:layout_weight="1" - android:layout_height="?attr/listPreferredItemHeight" - android:layout_marginStart="16dp" - android:textAppearance="?attr/textAppearanceMedium" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/resolver_icon_margin" + android:textColor="?android:attr/textColorPrimary" + android:fontFamily="@android:string/config_headlineFontFamilyMedium" + android:textSize="16sp" android:gravity="start|center_vertical" android:paddingEnd="16dp" /> @@ -107,21 +109,22 @@ android:orientation="horizontal" android:layoutDirection="locale" android:measureWithLargestChild="true" - android:paddingTop="8dp" - android:paddingBottom="8dp" - android:paddingStart="12dp" - android:paddingEnd="12dp" - android:elevation="8dp"> + android:paddingTop="@dimen/resolver_button_bar_spacing" + android:paddingBottom="@dimen/resolver_button_bar_spacing" + android:paddingStart="@dimen/resolver_edge_margin" + android:paddingEnd="@dimen/resolver_small_margin" + android:elevation="@dimen/resolver_elevation"> <Button android:id="@+id/button_once" android:layout_width="wrap_content" android:layout_gravity="start" android:maxLines="2" - style="?attr/buttonBarNegativeButtonStyle" - android:minHeight="@dimen/alert_dialog_button_bar_height" + style="?attr/buttonBarButtonStyle" + android:fontFamily="@android:string/config_headlineFontFamilyMedium" android:layout_height="wrap_content" android:enabled="false" + android:textAllCaps="false" android:text="@string/activity_resolver_use_once" android:onClick="onButtonClick" /> @@ -130,29 +133,40 @@ android:layout_width="wrap_content" android:layout_gravity="end" android:maxLines="2" - android:minHeight="@dimen/alert_dialog_button_bar_height" - style="?attr/buttonBarPositiveButtonStyle" + style="?attr/buttonBarButtonStyle" + android:fontFamily="@android:string/config_headlineFontFamilyMedium" android:layout_height="wrap_content" android:enabled="false" + android:textAllCaps="false" android:text="@string/activity_resolver_use_always" android:onClick="onButtonClick" /> </LinearLayout> - - <View - android:layout_width="match_parent" - android:layout_height="1dp" - android:background="?attr/dividerVertical" /> </LinearLayout> + <View + android:layout_alwaysShow="true" + android:layout_width="match_parent" + android:layout_height="1dp" + android:background="?attr/colorBackgroundFloating" + android:foreground="?attr/dividerVertical" /> <ListView android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/resolver_list" android:clipToPadding="false" - android:scrollbarStyle="outsideOverlay" android:background="?attr/colorBackgroundFloating" - android:elevation="8dp" + android:elevation="@dimen/resolver_elevation" android:nestedScrollingEnabled="true" - android:divider="@null" /> - + android:scrollbarStyle="outsideOverlay" + android:scrollIndicators="top|bottom" + android:divider="?attr/dividerVertical" + android:footerDividersEnabled="false" + android:headerDividersEnabled="false" + android:dividerHeight="1dp" /> + <View + android:layout_alwaysShow="true" + android:layout_width="match_parent" + android:layout_height="1dp" + android:background="?attr/colorBackgroundFloating" + android:foreground="?attr/dividerVertical" /> </com.android.internal.widget.ResolverDrawerLayout> diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 609659b62948..a01bbe38f296 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -750,7 +750,7 @@ <dimen name="seekbar_thumb_exclusion_max_size">48dp</dimen> - <!-- chooser (sharesheet) spacing --> + <!-- chooser/resolver (sharesheet) spacing --> <dimen name="chooser_corner_radius">8dp</dimen> <dimen name="chooser_row_text_option_translate">25dp</dimen> <dimen name="chooser_view_spacing">18dp</dimen> @@ -759,11 +759,15 @@ <dimen name="chooser_preview_image_font_size">20sp</dimen> <dimen name="chooser_preview_image_border">1dp</dimen> <dimen name="chooser_preview_width">-1px</dimen> - <dimen name="resolver_icon_size">42dp</dimen> - <dimen name="resolver_button_bar_spacing">8dp</dimen> - <dimen name="resolver_badge_size">18dp</dimen> <dimen name="chooser_target_width">90dp</dimen> <dimen name="chooser_header_scroll_elevation">4dp</dimen> <dimen name="chooser_max_collapsed_height">288dp</dimen> <dimen name="chooser_direct_share_label_placeholder_max_width">72dp</dimen> + <dimen name="resolver_icon_size">32dp</dimen> + <dimen name="resolver_button_bar_spacing">8dp</dimen> + <dimen name="resolver_badge_size">18dp</dimen> + <dimen name="resolver_icon_margin">16dp</dimen> + <dimen name="resolver_small_margin">18dp</dimen> + <dimen name="resolver_edge_margin">24dp</dimen> + <dimen name="resolver_elevation">1dp</dimen> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 363bc9ddd75c..c5a0dfca4bd1 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3819,6 +3819,10 @@ <java-symbol type="dimen" name="resolver_icon_size"/> <java-symbol type="dimen" name="resolver_badge_size"/> <java-symbol type="dimen" name="resolver_button_bar_spacing"/> + <java-symbol type="dimen" name="resolver_icon_margin"/> + <java-symbol type="dimen" name="resolver_small_margin"/> + <java-symbol type="dimen" name="resolver_edge_margin"/> + <java-symbol type="dimen" name="resolver_elevation"/> <!-- For DropBox --> <java-symbol type="integer" name="config_dropboxLowPriorityBroadcastRateLimitPeriod" /> diff --git a/core/tests/ResourceLoaderTests/Android.bp b/core/tests/ResourceLoaderTests/Android.bp new file mode 100644 index 000000000000..53db8322f7b8 --- /dev/null +++ b/core/tests/ResourceLoaderTests/Android.bp @@ -0,0 +1,63 @@ +// +// 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. +// + +android_test { + name: "FrameworksResourceLoaderTests", + srcs: [ + "src/**/*.kt" + ], + libs: [ + "android.test.runner", + "android.test.base", + ], + static_libs: [ + "androidx.test.espresso.core", + "androidx.test.ext.junit", + "androidx.test.runner", + "androidx.test.rules", + "mockito-target-minus-junit4", + "truth-prebuilt", + ], + resource_zips: [ ":FrameworksResourceLoaderTestsAssets" ], + test_suites: ["device-tests"], + sdk_version: "test_current", + aaptflags: [ + "--no-compress", + ], + data: [ + ":FrameworksResourceLoaderTestsOverlay", + ":FrameworksResourceLoaderTestsSplitOne", + ":FrameworksResourceLoaderTestsSplitTwo", + ], + java_resources: [ "NonAsset.txt" ] +} + +filegroup { + name: "FrameworksResourceLoaderTestsResources", + srcs: ["resources"], +} + +genrule { + name: "FrameworksResourceLoaderTestsAssets", + srcs: [ + ":framework-res", + ":FrameworksResourceLoaderTestsResources", + ], + tools: [ ":aapt2", ":soong_zip" ], + tool_files: [ "resources/compileAndLink.sh" ], + cmd: "$(location resources/compileAndLink.sh) $(location :aapt2) $(location :soong_zip) $(genDir) $(in) $(in)", + out: [ "out.zip" ] +} diff --git a/core/tests/ResourceLoaderTests/AndroidManifest.xml b/core/tests/ResourceLoaderTests/AndroidManifest.xml new file mode 100644 index 000000000000..00b4ccbd8030 --- /dev/null +++ b/core/tests/ResourceLoaderTests/AndroidManifest.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> + +<!-- Split loading is tested separately, so this must be marked isolated --> +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="android.content.res.loader.test" + android:isolatedSplits="true" + > + + <uses-sdk android:minSdkVersion="29"/> + + <application> + <uses-library android:name="android.test.runner"/> + + <activity + android:name=".TestActivity" + android:configChanges="orientation" + /> + </application> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:label="ResourceLoaderTests" + android:targetPackage="android.content.res.loader.test" + /> + +</manifest> diff --git a/core/tests/ResourceLoaderTests/AndroidTest.xml b/core/tests/ResourceLoaderTests/AndroidTest.xml new file mode 100644 index 000000000000..702151d01110 --- /dev/null +++ b/core/tests/ResourceLoaderTests/AndroidTest.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<configuration description="Test module config for ResourceLoaderTests"> + <option name="test-tag" value="ResourceLoaderTests" /> + + <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <option name="cleanup-apks" value="true" /> + <!-- The following value cannot be multi-line as whitespace is parsed by the installer --> + <option name="split-apk-file-names" + value="FrameworksResourceLoaderTests.apk,FrameworksResourceLoaderTestsSplitOne.apk,FrameworksResourceLoaderTestsSplitTwo.apk" /> + <option name="test-file-name" value="FrameworksResourceLoaderTestsOverlay.apk" /> + </target_preparer> + + <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> + <option name="run-command" + value="cmd overlay disable android.content.res.loader.test.overlay" /> + </target_preparer> + + <test class="com.android.tradefed.testtype.AndroidJUnitTest"> + <option name="package" value="android.content.res.loader.test" /> + </test> +</configuration> diff --git a/core/tests/ResourceLoaderTests/NonAsset.txt b/core/tests/ResourceLoaderTests/NonAsset.txt new file mode 100644 index 000000000000..5c0b2cc98d64 --- /dev/null +++ b/core/tests/ResourceLoaderTests/NonAsset.txt @@ -0,0 +1 @@ +Outside assets directory diff --git a/core/tests/ResourceLoaderTests/SplitOne/Android.bp b/core/tests/ResourceLoaderTests/SplitOne/Android.bp new file mode 100644 index 000000000000..897897fbf254 --- /dev/null +++ b/core/tests/ResourceLoaderTests/SplitOne/Android.bp @@ -0,0 +1,19 @@ +// +// 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. +// + +android_test_helper_app { + name: "FrameworksResourceLoaderTestsSplitOne" +} diff --git a/core/tests/ResourceLoaderTests/SplitOne/AndroidManifest.xml b/core/tests/ResourceLoaderTests/SplitOne/AndroidManifest.xml new file mode 100644 index 000000000000..b14bd8600f31 --- /dev/null +++ b/core/tests/ResourceLoaderTests/SplitOne/AndroidManifest.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="android.content.res.loader.test" + split="split_one" + > + + <uses-sdk android:minSdkVersion="1" android:targetSdkVersion="1" /> + <application android:hasCode="false" /> + +</manifest> diff --git a/core/tests/ResourceLoaderTests/SplitOne/res/values/string_split_one.xml b/core/tests/ResourceLoaderTests/SplitOne/res/values/string_split_one.xml new file mode 100644 index 000000000000..3c215ebc287c --- /dev/null +++ b/core/tests/ResourceLoaderTests/SplitOne/res/values/string_split_one.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<resources> + <public type="string" name="split_overlaid" id="0x7f040001" /> + <string name="split_overlaid">Split ONE Overlaid</string> +</resources> diff --git a/core/tests/ResourceLoaderTests/assets/Asset.txt b/core/tests/ResourceLoaderTests/assets/Asset.txt new file mode 100644 index 000000000000..03f9a0fd146a --- /dev/null +++ b/core/tests/ResourceLoaderTests/assets/Asset.txt @@ -0,0 +1 @@ +In assets directory diff --git a/core/tests/ResourceLoaderTests/lib/kotlin-reflect-sources.jar b/core/tests/ResourceLoaderTests/lib/kotlin-reflect-sources.jar Binary files differnew file mode 100644 index 000000000000..a12e33a34aee --- /dev/null +++ b/core/tests/ResourceLoaderTests/lib/kotlin-reflect-sources.jar diff --git a/core/tests/ResourceLoaderTests/lib/kotlin-reflect.jar b/core/tests/ResourceLoaderTests/lib/kotlin-reflect.jar Binary files differnew file mode 100644 index 000000000000..182cbabadfe6 --- /dev/null +++ b/core/tests/ResourceLoaderTests/lib/kotlin-reflect.jar diff --git a/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-jdk7-sources.jar b/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-jdk7-sources.jar Binary files differnew file mode 100644 index 000000000000..e6b5f15b8a57 --- /dev/null +++ b/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-jdk7-sources.jar diff --git a/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-jdk7.jar b/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-jdk7.jar Binary files differnew file mode 100644 index 000000000000..e9c743c60289 --- /dev/null +++ b/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-jdk7.jar diff --git a/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-jdk8-sources.jar b/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-jdk8-sources.jar Binary files differnew file mode 100644 index 000000000000..cd0536042662 --- /dev/null +++ b/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-jdk8-sources.jar diff --git a/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-jdk8.jar b/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-jdk8.jar Binary files differnew file mode 100644 index 000000000000..dc8aa90385fd --- /dev/null +++ b/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-jdk8.jar diff --git a/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-sources.jar b/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-sources.jar Binary files differnew file mode 100644 index 000000000000..8a672bac4685 --- /dev/null +++ b/core/tests/ResourceLoaderTests/lib/kotlin-stdlib-sources.jar diff --git a/core/tests/ResourceLoaderTests/lib/kotlin-stdlib.jar b/core/tests/ResourceLoaderTests/lib/kotlin-stdlib.jar Binary files differnew file mode 100644 index 000000000000..56f3d1e385e4 --- /dev/null +++ b/core/tests/ResourceLoaderTests/lib/kotlin-stdlib.jar diff --git a/core/tests/ResourceLoaderTests/lib/kotlin-test-sources.jar b/core/tests/ResourceLoaderTests/lib/kotlin-test-sources.jar Binary files differnew file mode 100644 index 000000000000..663d3128dd54 --- /dev/null +++ b/core/tests/ResourceLoaderTests/lib/kotlin-test-sources.jar diff --git a/core/tests/ResourceLoaderTests/lib/kotlin-test.jar b/core/tests/ResourceLoaderTests/lib/kotlin-test.jar Binary files differnew file mode 100644 index 000000000000..5f6e4b8cc988 --- /dev/null +++ b/core/tests/ResourceLoaderTests/lib/kotlin-test.jar diff --git a/core/tests/ResourceLoaderTests/overlay/Android.bp b/core/tests/ResourceLoaderTests/overlay/Android.bp new file mode 100644 index 000000000000..63e7e61d797a --- /dev/null +++ b/core/tests/ResourceLoaderTests/overlay/Android.bp @@ -0,0 +1,20 @@ +// Copyright (C) 2018 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. + +android_test { + name: "FrameworksResourceLoaderTestsOverlay", + sdk_version: "current", + + aaptflags: ["--no-resource-removal"], +} diff --git a/core/tests/ResourceLoaderTests/overlay/AndroidManifest.xml b/core/tests/ResourceLoaderTests/overlay/AndroidManifest.xml new file mode 100644 index 000000000000..942f7da9aa27 --- /dev/null +++ b/core/tests/ResourceLoaderTests/overlay/AndroidManifest.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="android.content.res.loader.test.overlay" + > + + <application android:hasCode="false" /> + + <overlay android:targetPackage="android.content.res.loader.test" /> + +</manifest> diff --git a/core/tests/ResourceLoaderTests/overlay/res/values/strings.xml b/core/tests/ResourceLoaderTests/overlay/res/values/strings.xml new file mode 100644 index 000000000000..348bb353611a --- /dev/null +++ b/core/tests/ResourceLoaderTests/overlay/res/values/strings.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<resources> + + <string name="loader_path_change_test">Overlaid</string> + +</resources> diff --git a/core/tests/ResourceLoaderTests/res/drawable-nodpi/non_asset_bitmap.png b/core/tests/ResourceLoaderTests/res/drawable-nodpi/non_asset_bitmap.png Binary files differnew file mode 100644 index 000000000000..efd71ee039e2 --- /dev/null +++ b/core/tests/ResourceLoaderTests/res/drawable-nodpi/non_asset_bitmap.png diff --git a/core/tests/ResourceLoaderTests/res/drawable-nodpi/non_asset_drawable.xml b/core/tests/ResourceLoaderTests/res/drawable-nodpi/non_asset_drawable.xml new file mode 100644 index 000000000000..d1211c50a203 --- /dev/null +++ b/core/tests/ResourceLoaderTests/res/drawable-nodpi/non_asset_drawable.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<color + xmlns:android="http://schemas.android.com/apk/res/android" + android:color="#B2D2F2" + /> diff --git a/core/tests/ResourceLoaderTests/res/layout/layout.xml b/core/tests/ResourceLoaderTests/res/layout/layout.xml new file mode 100644 index 000000000000..d59059b453d6 --- /dev/null +++ b/core/tests/ResourceLoaderTests/res/layout/layout.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + /> + diff --git a/core/tests/ResourceLoaderTests/res/values/strings.xml b/core/tests/ResourceLoaderTests/res/values/strings.xml new file mode 100644 index 000000000000..28b8f73d45a6 --- /dev/null +++ b/core/tests/ResourceLoaderTests/res/values/strings.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<resources> + + <string name="loader_path_change_test">Not overlaid</string> + <string name="split_overlaid">Not overlaid</string> + +</resources> diff --git a/core/tests/ResourceLoaderTests/resources/AndroidManifestApp.xml b/core/tests/ResourceLoaderTests/resources/AndroidManifestApp.xml new file mode 100644 index 000000000000..5dd8a966e2b7 --- /dev/null +++ b/core/tests/ResourceLoaderTests/resources/AndroidManifestApp.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="android.content.res.loader.test" + > + + <uses-sdk android:minSdkVersion="1" android:targetSdkVersion="1" /> + <application/> + +</manifest> diff --git a/core/tests/ResourceLoaderTests/resources/AndroidManifestFramework.xml b/core/tests/ResourceLoaderTests/resources/AndroidManifestFramework.xml new file mode 100644 index 000000000000..5a92ae9e662b --- /dev/null +++ b/core/tests/ResourceLoaderTests/resources/AndroidManifestFramework.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<!-- Mocks the framework package name so that AAPT2 assigns the correct package --> +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="android" + > + + <uses-sdk android:minSdkVersion="1" android:targetSdkVersion="1" /> + <application/> + +</manifest> diff --git a/core/tests/ResourceLoaderTests/resources/compileAndLink.sh b/core/tests/ResourceLoaderTests/resources/compileAndLink.sh new file mode 100755 index 000000000000..885f681f4261 --- /dev/null +++ b/core/tests/ResourceLoaderTests/resources/compileAndLink.sh @@ -0,0 +1,108 @@ +#!/bin/bash +# 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. + +aapt2=$1 +soong_zip=$2 +genDir=$3 +FRAMEWORK_RES_APK=$4 +inDir=$5 + +# (String name, boolean retainFiles = false, String... files) +function compileAndLink { + moduleName=$1 + mkdir "$genDir"/out/"$moduleName" + + args="" + for arg in "${@:4}"; do + if [[ $arg == res* ]]; then + args="$args $inDir/$arg" + else + args="$args $arg" + fi + done + + $aapt2 compile -o "$genDir"/out/"$moduleName" $args + + $aapt2 link \ + -I "$FRAMEWORK_RES_APK" \ + --manifest "$inDir"/"$3" \ + -o "$genDir"/out/"$moduleName"/apk.apk \ + "$genDir"/out/"$moduleName"/*.flat \ + --no-compress + + unzip -qq "$genDir"/out/"$moduleName"/apk.apk -d "$genDir"/out/"$moduleName"/unzip + + if [[ "$2" == "APK_WITHOUT_FILE" || "$2" == "BOTH_WITHOUT_FILE" ]]; then + zip -q -d "$genDir"/out/"$moduleName"/apk.apk "res/*" + cp "$genDir"/out/"$moduleName"/apk.apk "$genDir"/output/raw/"$moduleName"Apk.apk + elif [[ "$2" == "APK" || "$2" == "BOTH" ]]; then + cp "$genDir"/out/"$moduleName"/apk.apk "$genDir"/output/raw/"$moduleName"Apk.apk + fi + + if [[ "$2" == "ARSC" || "$2" == "BOTH" || "$2" == "BOTH_WITHOUT_FILE" ]]; then + zip -d "$genDir"/out/"$moduleName"/apk.apk "res/*" + cp "$genDir"/out/"$moduleName"/unzip/resources.arsc "$genDir"/output/raw/"$moduleName"Arsc.arsc + fi +} + +rm -r "$genDir"/out +rm -r "$genDir"/output +rm -r "$genDir"/temp + +mkdir "$genDir"/out +mkdir -p "$genDir"/output/raw +mkdir -p "$genDir"/temp/res/drawable-nodpi +mkdir -p "$genDir"/temp/res/layout + +compileAndLink stringOne BOTH AndroidManifestFramework.xml res/values/string_one.xml +compileAndLink stringTwo BOTH AndroidManifestFramework.xml res/values/string_two.xml + +compileAndLink dimenOne BOTH AndroidManifestFramework.xml res/values/dimen_one.xml +compileAndLink dimenTwo BOTH AndroidManifestFramework.xml res/values/dimen_two.xml + +compileAndLink drawableMdpiWithoutFile BOTH_WITHOUT_FILE AndroidManifestFramework.xml res/values/drawable_one.xml res/drawable-mdpi/ic_delete.png +compileAndLink drawableMdpiWithFile APK AndroidManifestFramework.xml res/values/drawable_one.xml res/drawable-mdpi/ic_delete.png + +compileAndLink layoutWithoutFile BOTH_WITHOUT_FILE AndroidManifestFramework.xml res/values/activity_list_item_id.xml res/layout/activity_list_item.xml +compileAndLink layoutWithFile APK AndroidManifestFramework.xml res/values/activity_list_item_id.xml res/layout/activity_list_item.xml + +cp -f "$inDir"/res/layout/layout_one.xml "$genDir"/temp/res/layout/layout.xml +compileAndLink layoutOne ARSC AndroidManifestApp.xml "$genDir"/temp/res/layout/layout.xml res/values/layout_id.xml +cp -f "$genDir"/out/layoutOne/unzip/res/layout/layout.xml "$genDir"/output/raw/layoutOne.xml + +cp -f "$inDir"/res/layout/layout_two.xml "$genDir"/temp/res/layout/layout.xml +compileAndLink layoutTwo ARSC AndroidManifestApp.xml "$genDir"/temp/res/layout/layout.xml res/values/layout_id.xml +cp -f "$genDir"/out/layoutTwo/unzip/res/layout/layout.xml "$genDir"/output/raw/layoutTwo.xml + +drawableNoDpi="/res/drawable-nodpi" +inDirDrawableNoDpi="$inDir$drawableNoDpi" + +cp -f "$inDirDrawableNoDpi"/nonAssetDrawableOne.xml "$genDir"/temp/res/drawable-nodpi/non_asset_drawable.xml +compileAndLink nonAssetDrawableOne ARSC AndroidManifestApp.xml "$genDir"/temp/res/drawable-nodpi/non_asset_drawable.xml res/values/non_asset_drawable_id.xml +cp -f "$genDir"/out/nonAssetDrawableOne/unzip/res/drawable-nodpi-v4/non_asset_drawable.xml "$genDir"/output/raw/nonAssetDrawableOne.xml + +cp -f "$inDirDrawableNoDpi"/nonAssetDrawableTwo.xml "$genDir"/temp/res/drawable-nodpi/non_asset_drawable.xml +compileAndLink nonAssetDrawableTwo ARSC AndroidManifestApp.xml "$genDir"/temp/res/drawable-nodpi/non_asset_drawable.xml res/values/non_asset_drawable_id.xml +cp -f "$genDir"/out/nonAssetDrawableTwo/unzip/res/drawable-nodpi-v4/non_asset_drawable.xml "$genDir"/output/raw/nonAssetDrawableTwo.xml + +cp -f "$inDirDrawableNoDpi"/nonAssetBitmapGreen.png "$genDir"/temp/res/drawable-nodpi/non_asset_bitmap.png +compileAndLink nonAssetBitmapGreen BOTH AndroidManifestApp.xml "$genDir"/temp/res/drawable-nodpi/non_asset_bitmap.png res/values/non_asset_bitmap_id.xml +cp -f "$genDir"/out/nonAssetBitmapGreen/unzip/res/drawable-nodpi-v4/non_asset_bitmap.png "$genDir"/output/raw/nonAssetBitmapGreen.png + +cp -f "$inDirDrawableNoDpi"/nonAssetBitmapBlue.png "$genDir"/temp/res/drawable-nodpi/non_asset_bitmap.png +compileAndLink nonAssetBitmapBlue ARSC AndroidManifestApp.xml "$genDir"/temp/res/drawable-nodpi/non_asset_bitmap.png res/values/non_asset_bitmap_id.xml +cp -f "$genDir"/out/nonAssetBitmapBlue/unzip/res/drawable-nodpi-v4/non_asset_bitmap.png "$genDir"/output/raw/nonAssetBitmapBlue.png + +$soong_zip -o "$genDir"/out.zip -C "$genDir"/output/ -D "$genDir"/output/ diff --git a/core/tests/ResourceLoaderTests/resources/res/drawable-mdpi/ic_delete.png b/core/tests/ResourceLoaderTests/resources/res/drawable-mdpi/ic_delete.png Binary files differnew file mode 100644 index 000000000000..f3e53d7596c1 --- /dev/null +++ b/core/tests/ResourceLoaderTests/resources/res/drawable-mdpi/ic_delete.png diff --git a/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetBitmapBlue.png b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetBitmapBlue.png Binary files differnew file mode 100644 index 000000000000..5231d175569e --- /dev/null +++ b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetBitmapBlue.png diff --git a/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetBitmapGreen.png b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetBitmapGreen.png Binary files differnew file mode 100644 index 000000000000..671d6d00be31 --- /dev/null +++ b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetBitmapGreen.png diff --git a/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetDrawableOne.xml b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetDrawableOne.xml new file mode 100644 index 000000000000..f1a93d2d2f21 --- /dev/null +++ b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetDrawableOne.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<color + xmlns:android="http://schemas.android.com/apk/res/android" + android:color="#A3C3E3" + /> diff --git a/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetDrawableTwo.xml b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetDrawableTwo.xml new file mode 100644 index 000000000000..7c455a57fb0b --- /dev/null +++ b/core/tests/ResourceLoaderTests/resources/res/drawable-nodpi/nonAssetDrawableTwo.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<color + xmlns:android="http://schemas.android.com/apk/res/android" + android:color="#3A3C3E" + /> diff --git a/core/tests/ResourceLoaderTests/resources/res/layout/activity_list_item.xml b/core/tests/ResourceLoaderTests/resources/res/layout/activity_list_item.xml new file mode 100644 index 000000000000..d59059b453d6 --- /dev/null +++ b/core/tests/ResourceLoaderTests/resources/res/layout/activity_list_item.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + /> + diff --git a/core/tests/ResourceLoaderTests/resources/res/layout/layout_one.xml b/core/tests/ResourceLoaderTests/resources/res/layout/layout_one.xml new file mode 100644 index 000000000000..ede3838be8de --- /dev/null +++ b/core/tests/ResourceLoaderTests/resources/res/layout/layout_one.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + /> + diff --git a/core/tests/ResourceLoaderTests/resources/res/layout/layout_two.xml b/core/tests/ResourceLoaderTests/resources/res/layout/layout_two.xml new file mode 100644 index 000000000000..d8bff90d56d8 --- /dev/null +++ b/core/tests/ResourceLoaderTests/resources/res/layout/layout_two.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + /> + diff --git a/core/tests/ResourceLoaderTests/resources/res/values/activity_list_item_id.xml b/core/tests/ResourceLoaderTests/resources/res/values/activity_list_item_id.xml new file mode 100644 index 000000000000..a552431e23be --- /dev/null +++ b/core/tests/ResourceLoaderTests/resources/res/values/activity_list_item_id.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> + +<resources> + <public type="layout" name="activity_list_item" id="0x01090000" /> +</resources> diff --git a/core/tests/ResourceLoaderTests/resources/res/values/dimen_one.xml b/core/tests/ResourceLoaderTests/resources/res/values/dimen_one.xml new file mode 100644 index 000000000000..69ecf2316284 --- /dev/null +++ b/core/tests/ResourceLoaderTests/resources/res/values/dimen_one.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> + +<resources> + <public type="dimen" name="app_icon_size" id="0x01050000" /> + <dimen name="app_icon_size">564716dp</dimen> +</resources> diff --git a/core/tests/ResourceLoaderTests/resources/res/values/dimen_two.xml b/core/tests/ResourceLoaderTests/resources/res/values/dimen_two.xml new file mode 100644 index 000000000000..4d55deffbd2a --- /dev/null +++ b/core/tests/ResourceLoaderTests/resources/res/values/dimen_two.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> + +<resources> + <public type="dimen" name="app_icon_size" id="0x01050000" /> + <dimen name="app_icon_size">565717dp</dimen> +</resources> diff --git a/core/tests/ResourceLoaderTests/resources/res/values/drawable_one.xml b/core/tests/ResourceLoaderTests/resources/res/values/drawable_one.xml new file mode 100644 index 000000000000..b5b4dfd22231 --- /dev/null +++ b/core/tests/ResourceLoaderTests/resources/res/values/drawable_one.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> + +<resources> + <public type="drawable" name="ic_delete" id="0x0108001d" /> +</resources> diff --git a/core/tests/ResourceLoaderTests/resources/res/values/layout_id.xml b/core/tests/ResourceLoaderTests/resources/res/values/layout_id.xml new file mode 100644 index 000000000000..4962a07bc8c7 --- /dev/null +++ b/core/tests/ResourceLoaderTests/resources/res/values/layout_id.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<resources> + <public type="layout" name="layout" id="0x7f020000" /> +</resources> diff --git a/core/tests/ResourceLoaderTests/resources/res/values/non_asset_bitmap_id.xml b/core/tests/ResourceLoaderTests/resources/res/values/non_asset_bitmap_id.xml new file mode 100644 index 000000000000..38b152beb76f --- /dev/null +++ b/core/tests/ResourceLoaderTests/resources/res/values/non_asset_bitmap_id.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<resources> + <public type="drawable" name="non_asset_bitmap" id="0x7f010000" /> +</resources> diff --git a/core/tests/ResourceLoaderTests/resources/res/values/non_asset_drawable_id.xml b/core/tests/ResourceLoaderTests/resources/res/values/non_asset_drawable_id.xml new file mode 100644 index 000000000000..bdd6f58e5824 --- /dev/null +++ b/core/tests/ResourceLoaderTests/resources/res/values/non_asset_drawable_id.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<resources> + <public type="drawable" name="non_asset_drawable" id="0x7f010001" /> +</resources> diff --git a/core/tests/ResourceLoaderTests/resources/res/values/string_one.xml b/core/tests/ResourceLoaderTests/resources/res/values/string_one.xml new file mode 100644 index 000000000000..4fc52723946e --- /dev/null +++ b/core/tests/ResourceLoaderTests/resources/res/values/string_one.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> + +<resources> + <public type="string" name="cancel" id="0x01040000" /> + <string name="cancel">SomeRidiculouslyUnlikelyStringOne</string> +</resources> diff --git a/core/tests/ResourceLoaderTests/resources/res/values/string_two.xml b/core/tests/ResourceLoaderTests/resources/res/values/string_two.xml new file mode 100644 index 000000000000..3604d7b21cf5 --- /dev/null +++ b/core/tests/ResourceLoaderTests/resources/res/values/string_two.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> + +<resources> + <public type="string" name="cancel" id="0x01040000" /> + <string name="cancel">SomeRidiculouslyUnlikelyStringTwo</string> +</resources> diff --git a/core/tests/ResourceLoaderTests/splits/Android.bp b/core/tests/ResourceLoaderTests/splits/Android.bp new file mode 100644 index 000000000000..4582808934df --- /dev/null +++ b/core/tests/ResourceLoaderTests/splits/Android.bp @@ -0,0 +1,19 @@ +// +// 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. +// + +android_test_helper_app { + name: "FrameworksResourceLoaderTestsSplitTwo" +} diff --git a/core/tests/ResourceLoaderTests/splits/AndroidManifest.xml b/core/tests/ResourceLoaderTests/splits/AndroidManifest.xml new file mode 100644 index 000000000000..aad8c27a1a3b --- /dev/null +++ b/core/tests/ResourceLoaderTests/splits/AndroidManifest.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="android.content.res.loader.test" + split="split_two" + > + + <uses-sdk android:minSdkVersion="1" android:targetSdkVersion="1" /> + <application android:hasCode="false" /> + +</manifest> diff --git a/core/tests/ResourceLoaderTests/splits/res/values/string_split_two.xml b/core/tests/ResourceLoaderTests/splits/res/values/string_split_two.xml new file mode 100644 index 000000000000..a367063dd43e --- /dev/null +++ b/core/tests/ResourceLoaderTests/splits/res/values/string_split_two.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<resources> + <public type="string" name="split_overlaid" id="0x7f040001" /> + <string name="split_overlaid">Split TWO Overlaid</string> +</resources> diff --git a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/DirectoryResourceLoaderTest.kt b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/DirectoryResourceLoaderTest.kt new file mode 100644 index 000000000000..b1bdc967e68f --- /dev/null +++ b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/DirectoryResourceLoaderTest.kt @@ -0,0 +1,101 @@ +/* + * 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.test + +import android.content.res.loader.DirectoryResourceLoader +import android.content.res.loader.ResourceLoader +import android.graphics.Color +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.ColorDrawable +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestName +import java.io.File + +class DirectoryResourceLoaderTest : ResourceLoaderTestBase() { + + @get:Rule + val testName = TestName() + + private lateinit var testDir: File + private lateinit var loader: ResourceLoader + + @Before + fun setUpTestDir() { + testDir = context.filesDir.resolve("DirectoryResourceLoaderTest_${testName.methodName}") + loader = DirectoryResourceLoader(testDir) + } + + @After + fun deleteTestFiles() { + testDir.deleteRecursively() + } + + @Test + fun loadDrawableXml() { + "nonAssetDrawableOne" writeTo "res/drawable-nodpi-v4/non_asset_drawable.xml" + val provider = openArsc("nonAssetDrawableOne") + + fun getValue() = (resources.getDrawable(R.drawable.non_asset_drawable) as ColorDrawable) + .color + + assertThat(getValue()).isEqualTo(Color.parseColor("#B2D2F2")) + + addLoader(loader to provider) + + assertThat(getValue()).isEqualTo(Color.parseColor("#A3C3E3")) + } + + @Test + fun loadDrawableBitmap() { + "nonAssetBitmapGreen" writeTo "res/drawable-nodpi-v4/non_asset_bitmap.png" + val provider = openArsc("nonAssetBitmapGreen") + + fun getValue() = (resources.getDrawable(R.drawable.non_asset_bitmap) as BitmapDrawable) + .bitmap.getColor(0, 0).toArgb() + + assertThat(getValue()).isEqualTo(Color.RED) + + addLoader(loader to provider) + + assertThat(getValue()).isEqualTo(Color.GREEN) + } + + @Test + fun loadXml() { + "layoutOne" writeTo "res/layout/layout.xml" + val provider = openArsc("layoutOne") + + fun getValue() = resources.getLayout(R.layout.layout).advanceToRoot().name + + assertThat(getValue()).isEqualTo("FrameLayout") + + addLoader(loader to provider) + + assertThat(getValue()).isEqualTo("RelativeLayout") + } + + private infix fun String.writeTo(path: String) { + val testFile = testDir.resolve(path) + testFile.parentFile!!.mkdirs() + resources.openRawResource(rawFile(this)) + .copyTo(testFile.outputStream()) + } +} diff --git a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderAssetTest.kt b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderAssetTest.kt new file mode 100644 index 000000000000..a6a83789c082 --- /dev/null +++ b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderAssetTest.kt @@ -0,0 +1,169 @@ +/* + * 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.test + +import android.content.res.AssetManager +import android.content.res.loader.DirectoryResourceLoader +import android.content.res.loader.ResourceLoader +import android.content.res.loader.ResourcesProvider +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestName +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.anyString +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.eq +import org.mockito.Mockito.inOrder +import org.mockito.Mockito.mock +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import java.nio.file.Paths + +@RunWith(Parameterized::class) +class ResourceLoaderAssetTest : ResourceLoaderTestBase() { + + companion object { + private const val BASE_TEST_PATH = "android/content/res/loader/test/file.txt" + private const val TEST_TEXT = "some text" + + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun parameters(): Array<Array<out Any?>> { + val fromInputStream: ResourceLoader.(String) -> Any? = { + loadAsset(eq(it), anyInt()) + } + + val fromFileDescriptor: ResourceLoader.(String) -> Any? = { + loadAssetFd(eq(it)) + } + + val openAsset: AssetManager.() -> String? = { + open(BASE_TEST_PATH).reader().readText() + } + + val openNonAsset: AssetManager.() -> String? = { + openNonAssetFd(BASE_TEST_PATH).readText() + } + + return arrayOf( + arrayOf("assets", fromInputStream, openAsset), + arrayOf("", fromFileDescriptor, openNonAsset) + ) + } + } + + @get:Rule + val testName = TestName() + + @JvmField + @field:Parameterized.Parameter(0) + var prefix: String? = null + + @field:Parameterized.Parameter(1) + lateinit var loadAssetFunction: ResourceLoader.(String) -> Any? + + @field:Parameterized.Parameter(2) + lateinit var openAssetFunction: AssetManager.() -> String? + + private val testPath: String + get() = Paths.get(prefix.orEmpty(), BASE_TEST_PATH).toString() + + private fun ResourceLoader.loadAsset() = loadAssetFunction(testPath) + + private fun AssetManager.openAsset() = openAssetFunction() + + private lateinit var testDir: File + + @Before + fun setUpTestDir() { + testDir = context.filesDir.resolve("DirectoryResourceLoaderTest_${testName.methodName}") + testDir.resolve(testPath).apply { parentFile!!.mkdirs() }.writeText(TEST_TEXT) + } + + @Test + fun multipleLoadersSearchesBackwards() { + // DirectoryResourceLoader relies on a private field and can't be spied directly, so wrap it + val loader = DirectoryResourceLoader(testDir) + val loaderWrapper = mock(ResourceLoader::class.java).apply { + doAnswer { loader.loadAsset(it.arguments[0] as String, it.arguments[1] as Int) } + .`when`(this).loadAsset(anyString(), anyInt()) + doAnswer { loader.loadAssetFd(it.arguments[0] as String) } + .`when`(this).loadAssetFd(anyString()) + } + + val one = loaderWrapper to ResourcesProvider.empty() + val two = mockLoader { + doReturn(null).`when`(it).loadAsset() + } + + addLoader(one, two) + + assertOpenedAsset() + inOrder(two.first, one.first).apply { + verify(two.first).loadAsset() + verify(one.first).loadAsset() + } + } + + @Test(expected = FileNotFoundException::class) + fun failToFindThrowsFileNotFound() { + val one = mockLoader { + doReturn(null).`when`(it).loadAsset() + } + val two = mockLoader { + doReturn(null).`when`(it).loadAsset() + } + + addLoader(one, two) + + assertOpenedAsset() + } + + @Test + fun throwingIOExceptionIsSkipped() { + val one = DirectoryResourceLoader(testDir) to ResourcesProvider.empty() + val two = mockLoader { + doAnswer { throw IOException() }.`when`(it).loadAsset() + } + + addLoader(one, two) + + assertOpenedAsset() + } + + @Test(expected = IllegalStateException::class) + fun throwingNonIOExceptionCausesFailure() { + val one = DirectoryResourceLoader(testDir) to ResourcesProvider.empty() + val two = mockLoader { + doAnswer { throw IllegalStateException() }.`when`(it).loadAsset() + } + + addLoader(one, two) + + assertOpenedAsset() + } + + private fun assertOpenedAsset() { + assertThat(resources.assets.openAsset()).isEqualTo(TEST_TEXT) + } +} diff --git a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderChangesTest.kt b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderChangesTest.kt new file mode 100644 index 000000000000..e01e254b1f16 --- /dev/null +++ b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderChangesTest.kt @@ -0,0 +1,236 @@ +/* + * 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.test + +import android.app.Activity +import android.app.Instrumentation +import android.app.UiAutomation +import android.content.res.Configuration +import android.content.res.Resources +import android.graphics.Color +import android.os.Bundle +import android.os.ParcelFileDescriptor +import android.widget.FrameLayout +import androidx.test.InstrumentationRegistry +import androidx.test.rule.ActivityTestRule +import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry +import androidx.test.runner.lifecycle.Stage +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import java.util.Arrays +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executor +import java.util.concurrent.FutureTask +import java.util.concurrent.TimeUnit + +// @Ignore("UiAutomation is crashing with not connected, not sure why") +@RunWith(Parameterized::class) +class ResourceLoaderChangesTest : ResourceLoaderTestBase() { + + companion object { + private const val TIMEOUT = 30L + private const val OVERLAY_PACKAGE = "android.content.res.loader.test.overlay" + + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun data() = arrayOf(DataType.APK, DataType.ARSC) + } + + @field:Parameterized.Parameter(0) + override lateinit var dataType: DataType + + @get:Rule + val activityRule: ActivityTestRule<TestActivity> = + ActivityTestRule<TestActivity>(TestActivity::class.java, false, true) + + // Redirect to the Activity's resources + override val resources: Resources + get() = activityRule.getActivity().resources + + private val activity: TestActivity + get() = activityRule.getActivity() + + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + + @Before + @After + fun disableOverlay() { +// enableOverlay(OVERLAY_PACKAGE, false) + } + + @Test + fun activityRecreate() = verifySameBeforeAndAfter { + val oldActivity = activity + var newActivity: Activity? = null + instrumentation.runOnMainSync { oldActivity.recreate() } + instrumentation.waitForIdleSync() + instrumentation.runOnMainSync { + newActivity = ActivityLifecycleMonitorRegistry.getInstance() + .getActivitiesInStage(Stage.RESUMED) + .single() + } + + assertThat(newActivity).isNotNull() + assertThat(newActivity).isNotSameAs(oldActivity) + + // Return the new resources to assert on + return@verifySameBeforeAndAfter newActivity!!.resources + } + + @Test + fun activityHandledOrientationChange() = verifySameBeforeAndAfter { + val latch = CountDownLatch(1) + val oldConfig = Configuration().apply { setTo(resources.configuration) } + var changedConfig: Configuration? = null + + activity.callback = object : TestActivity.Callback { + override fun onConfigurationChanged(newConfig: Configuration) { + changedConfig = newConfig + latch.countDown() + } + } + + val isPortrait = resources.displayMetrics.run { widthPixels < heightPixels } + val newRotation = if (isPortrait) { + UiAutomation.ROTATION_FREEZE_90 + } else { + UiAutomation.ROTATION_FREEZE_0 + } + + instrumentation.uiAutomation.setRotation(newRotation) + + assertThat(latch.await(TIMEOUT, TimeUnit.SECONDS)).isTrue() + assertThat(changedConfig).isNotEqualTo(oldConfig) + return@verifySameBeforeAndAfter activity.resources + } + + @Test + fun enableOverlayCausingPathChange() = verifySameBeforeAndAfter { + assertThat(getString(R.string.loader_path_change_test)).isEqualTo("Not overlaid") + + enableOverlay(OVERLAY_PACKAGE, true) + + assertThat(getString(R.string.loader_path_change_test)).isEqualTo("Overlaid") + + return@verifySameBeforeAndAfter activity.resources + } + + @Test + fun enableOverlayChildContextUnaffected() { + val childContext = activity.createConfigurationContext(Configuration()) + val childResources = childContext.resources + val originalValue = childResources.getString(android.R.string.cancel) + assertThat(childResources.getString(R.string.loader_path_change_test)) + .isEqualTo("Not overlaid") + + verifySameBeforeAndAfter { + enableOverlay(OVERLAY_PACKAGE, true) + return@verifySameBeforeAndAfter activity.resources + } + + // Loader not applied, but overlay change propagated + assertThat(childResources.getString(android.R.string.cancel)).isEqualTo(originalValue) + assertThat(childResources.getString(R.string.loader_path_change_test)) + .isEqualTo("Overlaid") + } + + // All these tests assert for the exact same loaders/values, so extract that logic out + private fun verifySameBeforeAndAfter(block: () -> Resources) { + // TODO(chiuwinson): atest doesn't work with @Ignore, UiAutomation not connected error + Assume.assumeFalse(true) + + val originalValue = resources.getString(android.R.string.cancel) + + val loader = "stringOne".openLoader() + addLoader(loader) + + val oldLoaders = resources.loaders + val oldValue = resources.getString(android.R.string.cancel) + + assertThat(oldValue).isNotEqualTo(originalValue) + + val newResources = block() + + val newLoaders = newResources.loaders + val newValue = newResources.getString(android.R.string.cancel) + + assertThat(newValue).isEqualTo(oldValue) + assertThat(newLoaders).isEqualTo(oldLoaders) + } + + // Copied from overlaytests LocalOverlayManager + private fun enableOverlay(packageName: String, enable: Boolean) { + val executor = Executor { Thread(it).start() } + val pattern = (if (enable) "[x]" else "[ ]") + " " + packageName + if (executeShellCommand("cmd overlay list").contains(pattern)) { + // nothing to do, overlay already in the requested state + return + } + + val oldApkPaths = resources.assets.apkPaths + val task = FutureTask { + while (true) { + if (!Arrays.equals(oldApkPaths, resources.assets.apkPaths)) { + return@FutureTask true + } + Thread.sleep(10) + } + + @Suppress("UNREACHABLE_CODE") + return@FutureTask false + } + + val command = if (enable) "enable" else "disable" + executeShellCommand("cmd overlay $command $packageName") + executor.execute(task) + assertThat(task.get(TIMEOUT, TimeUnit.SECONDS)).isTrue() + } + + private fun executeShellCommand(command: String): String { + val uiAutomation = instrumentation.uiAutomation + val pfd = uiAutomation.executeShellCommand(command) + return ParcelFileDescriptor.AutoCloseInputStream(pfd).use { it.reader().readText() } + } +} + +class TestActivity : Activity() { + + var callback: Callback? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(FrameLayout(this).apply { + setBackgroundColor(Color.BLUE) + }) + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + callback?.onConfigurationChanged(newConfig) + } + + interface Callback { + fun onConfigurationChanged(newConfig: Configuration) + } +} diff --git a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderDrawableTest.kt b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderDrawableTest.kt new file mode 100644 index 000000000000..09fd27e02b59 --- /dev/null +++ b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderDrawableTest.kt @@ -0,0 +1,183 @@ +/* + * 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.test + +import android.content.res.Resources +import android.content.res.loader.ResourceLoader +import android.content.res.loader.ResourcesProvider +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import com.google.common.truth.Truth.assertThat +import org.hamcrest.CoreMatchers.not +import org.junit.Assume.assumeThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mockito.`when` +import org.mockito.Mockito.any +import org.mockito.Mockito.argThat +import org.mockito.Mockito.eq +import org.mockito.Mockito.never +import org.mockito.Mockito.verify + +@RunWith(Parameterized::class) +class ResourceLoaderDrawableTest : ResourceLoaderTestBase() { + + companion object { + + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun data() = arrayOf(DataType.APK, DataType.ARSC) + } + + @field:Parameterized.Parameter(0) + override lateinit var dataType: DataType + + @Test + fun matchingConfig() { + val original = getDrawable(android.R.drawable.ic_delete) + val loader = "drawableMdpiWithoutFile".openLoader() + `when`(loader.first.loadDrawable(any(), anyInt(), anyInt(), any())) + .thenReturn(ColorDrawable(Color.BLUE)) + + addLoader(loader) + + updateConfiguration { densityDpi = 160 /* mdpi */ } + + val drawable = getDrawable(android.R.drawable.ic_delete) + + loader.verifyLoadDrawableCalled() + + assertThat(drawable).isNotEqualTo(original) + assertThat(drawable).isInstanceOf(ColorDrawable::class.java) + assertThat((drawable as ColorDrawable).color).isEqualTo(Color.BLUE) + } + + @Test + fun worseConfig() { + val loader = "drawableMdpiWithoutFile".openLoader() + addLoader(loader) + + updateConfiguration { densityDpi = 480 /* xhdpi */ } + + getDrawable(android.R.drawable.ic_delete) + + verify(loader.first, never()).loadDrawable(any(), anyInt(), anyInt(), any()) + } + + @Test + fun multipleLoaders() { + val original = getDrawable(android.R.drawable.ic_delete) + val loaderOne = "drawableMdpiWithoutFile".openLoader() + val loaderTwo = "drawableMdpiWithoutFile".openLoader() + + `when`(loaderTwo.first.loadDrawable(any(), anyInt(), anyInt(), any())) + .thenReturn(ColorDrawable(Color.BLUE)) + + addLoader(loaderOne, loaderTwo) + + updateConfiguration { densityDpi = 160 /* mdpi */ } + + val drawable = getDrawable(android.R.drawable.ic_delete) + loaderOne.verifyLoadDrawableNotCalled() + loaderTwo.verifyLoadDrawableCalled() + + assertThat(drawable).isNotEqualTo(original) + assertThat(drawable).isInstanceOf(ColorDrawable::class.java) + assertThat((drawable as ColorDrawable).color).isEqualTo(Color.BLUE) + } + + @Test(expected = Resources.NotFoundException::class) + fun multipleLoadersNoReturnWithoutFile() { + val loaderOne = "drawableMdpiWithoutFile".openLoader() + val loaderTwo = "drawableMdpiWithoutFile".openLoader() + + addLoader(loaderOne, loaderTwo) + + updateConfiguration { densityDpi = 160 /* mdpi */ } + + try { + getDrawable(android.R.drawable.ic_delete) + } finally { + // We expect the call to fail because at least the loader won't resolve the overridden + // drawable, but we should still verify that both loaders were called before allowing + // the exception to propagate. + loaderOne.verifyLoadDrawableNotCalled() + loaderTwo.verifyLoadDrawableCalled() + } + } + + @Test + fun multipleLoadersReturnWithFile() { + // Can't return a file if an ARSC + assumeThat(dataType, not(DataType.ARSC)) + + val original = getDrawable(android.R.drawable.ic_delete) + val loaderOne = "drawableMdpiWithFile".openLoader() + val loaderTwo = "drawableMdpiWithFile".openLoader() + + addLoader(loaderOne, loaderTwo) + + updateConfiguration { densityDpi = 160 /* mdpi */ } + + val drawable = getDrawable(android.R.drawable.ic_delete) + loaderOne.verifyLoadDrawableNotCalled() + loaderTwo.verifyLoadDrawableCalled() + + assertThat(drawable).isNotNull() + assertThat(drawable).isInstanceOf(original.javaClass) + } + + @Test + fun unhandledResourceIgnoresLoaders() { + val loader = "drawableMdpiWithoutFile".openLoader() + `when`(loader.first.loadDrawable(any(), anyInt(), anyInt(), any())) + .thenReturn(ColorDrawable(Color.BLUE)) + addLoader(loader) + + getDrawable(android.R.drawable.ic_menu_add) + + loader.verifyLoadDrawableNotCalled() + + getDrawable(android.R.drawable.ic_delete) + + loader.verifyLoadDrawableCalled() + } + + private fun Pair<ResourceLoader, ResourcesProvider>.verifyLoadDrawableCalled() { + verify(first).loadDrawable( + argThat { + it.density == 160 && + it.resourceId == android.R.drawable.ic_delete && + it.string == "res/drawable-mdpi-v4/ic_delete.png" + }, + eq(android.R.drawable.ic_delete), + eq(0), + any() + ) + } + + private fun Pair<ResourceLoader, ResourcesProvider>.verifyLoadDrawableNotCalled() { + verify(first, never()).loadDrawable( + any(), + anyInt(), + anyInt(), + any() + ) + } +} diff --git a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderLayoutTest.kt b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderLayoutTest.kt new file mode 100644 index 000000000000..1ec209486c18 --- /dev/null +++ b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderLayoutTest.kt @@ -0,0 +1,153 @@ +/* + * 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.test + +import android.content.res.Resources +import android.content.res.XmlResourceParser +import android.content.res.loader.ResourceLoader +import android.content.res.loader.ResourcesProvider +import com.google.common.truth.Truth.assertThat +import org.hamcrest.CoreMatchers.not +import org.junit.Assume.assumeThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mockito.`when` +import org.mockito.Mockito.any +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify + +@RunWith(Parameterized::class) +class ResourceLoaderLayoutTest : ResourceLoaderTestBase() { + + companion object { + + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun data() = arrayOf(DataType.APK, DataType.ARSC) + } + + @field:Parameterized.Parameter(0) + override lateinit var dataType: DataType + + @Test + fun singleLoader() { + val original = getLayout(android.R.layout.activity_list_item) + val mockXml = mock(XmlResourceParser::class.java) + val loader = "layoutWithoutFile".openLoader() + `when`(loader.first.loadXmlResourceParser(any(), anyInt())) + .thenReturn(mockXml) + + addLoader(loader) + + val layout = getLayout(android.R.layout.activity_list_item) + loader.verifyLoadLayoutCalled() + + assertThat(layout).isNotEqualTo(original) + assertThat(layout).isSameAs(mockXml) + } + + @Test + fun multipleLoaders() { + val original = getLayout(android.R.layout.activity_list_item) + val loaderOne = "layoutWithoutFile".openLoader() + val loaderTwo = "layoutWithoutFile".openLoader() + + val mockXml = mock(XmlResourceParser::class.java) + `when`(loaderTwo.first.loadXmlResourceParser(any(), anyInt())) + .thenReturn(mockXml) + + addLoader(loaderOne, loaderTwo) + + val layout = getLayout(android.R.layout.activity_list_item) + loaderOne.verifyLoadLayoutNotCalled() + loaderTwo.verifyLoadLayoutCalled() + + assertThat(layout).isNotEqualTo(original) + assertThat(layout).isSameAs(mockXml) + } + + @Test(expected = Resources.NotFoundException::class) + fun multipleLoadersNoReturnWithoutFile() { + val loaderOne = "layoutWithoutFile".openLoader() + val loaderTwo = "layoutWithoutFile".openLoader() + + addLoader(loaderOne, loaderTwo) + + try { + getLayout(android.R.layout.activity_list_item) + } finally { + // We expect the call to fail because at least one loader must resolve the overridden + // layout, but we should still verify that both loaders were called before allowing + // the exception to propagate. + loaderOne.verifyLoadLayoutNotCalled() + loaderTwo.verifyLoadLayoutCalled() + } + } + + @Test + fun multipleLoadersReturnWithFile() { + // Can't return a file if an ARSC + assumeThat(dataType, not(DataType.ARSC)) + + val loaderOne = "layoutWithFile".openLoader() + val loaderTwo = "layoutWithFile".openLoader() + + addLoader(loaderOne, loaderTwo) + + val xml = getLayout(android.R.layout.activity_list_item) + loaderOne.verifyLoadLayoutNotCalled() + loaderTwo.verifyLoadLayoutCalled() + + assertThat(xml).isNotNull() + } + + @Test + fun unhandledResourceIgnoresLoaders() { + val loader = "layoutWithoutFile".openLoader() + val mockXml = mock(XmlResourceParser::class.java) + `when`(loader.first.loadXmlResourceParser(any(), anyInt())) + .thenReturn(mockXml) + addLoader(loader) + + getLayout(android.R.layout.preference_category) + + verify(loader.first, never()) + .loadXmlResourceParser(anyString(), anyInt()) + + getLayout(android.R.layout.activity_list_item) + + loader.verifyLoadLayoutCalled() + } + + private fun Pair<ResourceLoader, ResourcesProvider>.verifyLoadLayoutCalled() { + verify(first).loadXmlResourceParser( + "res/layout/activity_list_item.xml", + android.R.layout.activity_list_item + ) + } + + private fun Pair<ResourceLoader, ResourcesProvider>.verifyLoadLayoutNotCalled() { + verify(first, never()).loadXmlResourceParser( + anyString(), + anyInt() + ) + } +} diff --git a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderTestBase.kt b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderTestBase.kt new file mode 100644 index 000000000000..5af453d526e4 --- /dev/null +++ b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderTestBase.kt @@ -0,0 +1,226 @@ +/* + * 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.test + +import android.content.Context +import android.content.res.AssetManager +import android.content.res.Configuration +import android.content.res.Resources +import android.content.res.loader.ResourceLoader +import android.content.res.loader.ResourcesProvider +import android.os.ParcelFileDescriptor +import android.util.TypedValue +import androidx.annotation.DimenRes +import androidx.annotation.DrawableRes +import androidx.annotation.LayoutRes +import androidx.annotation.StringRes +import androidx.test.InstrumentationRegistry +import org.junit.After +import org.junit.Before +import org.mockito.ArgumentMatchers +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.argThat +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import java.io.Closeable + +abstract class ResourceLoaderTestBase { + + open lateinit var dataType: DataType + + protected lateinit var context: Context + protected open val resources: Resources + get() = context.resources + protected open val assets: AssetManager + get() = resources.assets + + // Track opened streams and ResourcesProviders to close them after testing + private val openedObjects = mutableListOf<Closeable>() + + @Before + fun setUpBase() { + context = InstrumentationRegistry.getTargetContext() + } + + @After + fun removeAllLoaders() { + resources.setLoaders(null) + openedObjects.forEach { + try { + it.close() + } catch (ignored: Exception) { + } + } + } + + protected fun getString(@StringRes stringRes: Int, debugLog: Boolean = false) = + logResolution(debugLog) { getString(stringRes) } + + protected fun getDrawable(@DrawableRes drawableRes: Int, debugLog: Boolean = false) = + logResolution(debugLog) { getDrawable(drawableRes) } + + protected fun getLayout(@LayoutRes layoutRes: Int, debugLog: Boolean = false) = + logResolution(debugLog) { getLayout(layoutRes) } + + protected fun getDimensionPixelSize(@DimenRes dimenRes: Int, debugLog: Boolean = false) = + logResolution(debugLog) { getDimensionPixelSize(dimenRes) } + + private fun <T> logResolution(debugLog: Boolean = false, block: Resources.() -> T): T { + if (debugLog) { + resources.assets.setResourceResolutionLoggingEnabled(true) + } + + var thrown = false + + try { + return resources.block() + } catch (t: Throwable) { + // No good way to log to test output other than throwing an exception + if (debugLog) { + thrown = true + throw IllegalStateException(resources.assets.lastResourceResolution, t) + } else { + throw t + } + } finally { + if (!thrown && debugLog) { + throw IllegalStateException(resources.assets.lastResourceResolution) + } + } + } + + protected fun updateConfiguration(block: Configuration.() -> Unit) { + val configuration = Configuration().apply { + setTo(resources.configuration) + block() + } + + resources.updateConfiguration(configuration, resources.displayMetrics) + } + + protected fun String.openLoader( + dataType: DataType = this@ResourceLoaderTestBase.dataType + ): Pair<ResourceLoader, ResourcesProvider> = when (dataType) { + DataType.APK -> { + mock(ResourceLoader::class.java) to context.copiedRawFile("${this}Apk").use { + ResourcesProvider.loadFromApk(it) + }.also { openedObjects += it } + } + DataType.ARSC -> { + mock(ResourceLoader::class.java) to openArsc(this) + } + DataType.SPLIT -> { + mock(ResourceLoader::class.java) to ResourcesProvider.loadFromSplit(context, this) + } + DataType.ASSET -> mockLoader { + doAnswer { byteInputStream() }.`when`(it) + .loadAsset(eq("assets/Asset.txt"), anyInt()) + } + DataType.ASSET_FD -> mockLoader { + doAnswer { + val file = context.filesDir.resolve("Asset.txt") + file.writeText(this) + ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY) + }.`when`(it).loadAssetFd("assets/Asset.txt") + } + DataType.NON_ASSET -> mockLoader { + doAnswer { + val file = context.filesDir.resolve("NonAsset.txt") + file.writeText(this) + ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY) + }.`when`(it).loadAssetFd("NonAsset.txt") + } + DataType.NON_ASSET_DRAWABLE -> mockLoader(openArsc(this)) { + doReturn(null).`when`(it).loadDrawable(argThat { value -> + value.type == TypedValue.TYPE_STRING && + value.resourceId == 0x7f010001 && + value.string == "res/drawable-nodpi-v4/non_asset_drawable.xml" + }, eq(0x7f010001), anyInt(), ArgumentMatchers.any()) + + doAnswer { context.copiedRawFile(this) }.`when`(it) + .loadAssetFd("res/drawable-nodpi-v4/non_asset_drawable.xml") + } + DataType.NON_ASSET_BITMAP -> mockLoader(openArsc(this)) { + doReturn(null).`when`(it).loadDrawable(argThat { value -> + value.type == TypedValue.TYPE_STRING && + value.resourceId == 0x7f010000 && + value.string == "res/drawable-nodpi-v4/non_asset_bitmap.png" + }, eq(0x7f010000), anyInt(), ArgumentMatchers.any()) + + doAnswer { resources.openRawResourceFd(rawFile(this)).createInputStream() } + .`when`(it) + .loadAsset(eq("res/drawable-nodpi-v4/non_asset_bitmap.png"), anyInt()) + } + DataType.NON_ASSET_LAYOUT -> mockLoader(openArsc(this)) { + doReturn(null).`when`(it) + .loadXmlResourceParser("res/layout/layout.xml", 0x7f020000) + + doAnswer { context.copiedRawFile(this) }.`when`(it) + .loadAssetFd("res/layout/layout.xml") + } + } + + protected fun mockLoader( + provider: ResourcesProvider = ResourcesProvider.empty(), + block: (ResourceLoader) -> Unit = {} + ): Pair<ResourceLoader, ResourcesProvider> { + return mock(ResourceLoader::class.java, Utils.ANSWER_THROWS) + .apply(block) to provider + } + + protected fun openArsc(rawName: String): ResourcesProvider { + return context.copiedRawFile("${rawName}Arsc") + .use { ResourcesProvider.loadFromArsc(it) } + .also { openedObjects += it } + } + + // This specifically uses addLoader so both behaviors are tested + protected fun addLoader(vararg pairs: Pair<out ResourceLoader, ResourcesProvider>) { + pairs.forEach { resources.addLoader(it.first, it.second) } + } + + protected fun setLoaders(vararg pairs: Pair<out ResourceLoader, ResourcesProvider>) { + resources.setLoaders(pairs.map { android.util.Pair(it.first, it.second) }) + } + + protected fun addLoader(pair: Pair<out ResourceLoader, ResourcesProvider>, index: Int) { + resources.addLoader(pair.first, pair.second, index) + } + + protected fun removeLoader(vararg pairs: Pair<out ResourceLoader, ResourcesProvider>) { + pairs.forEach { resources.removeLoader(it.first) } + } + + protected fun getLoaders(): MutableList<Pair<ResourceLoader, ResourcesProvider>> { + // Cast instead of toMutableList to maintain the same object + return resources.getLoaders() as MutableList<Pair<ResourceLoader, ResourcesProvider>> + } + + enum class DataType { + APK, + ARSC, + SPLIT, + ASSET, + ASSET_FD, + NON_ASSET, + NON_ASSET_DRAWABLE, + NON_ASSET_BITMAP, + NON_ASSET_LAYOUT, + } +} diff --git a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderValuesTest.kt b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderValuesTest.kt new file mode 100644 index 000000000000..017552a02152 --- /dev/null +++ b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderValuesTest.kt @@ -0,0 +1,354 @@ +/* + * 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.test + +import android.graphics.Color +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.ColorDrawable +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +/** + * Tests generic ResourceLoader behavior. Intentionally abstract in its test methodology because + * the behavior being verified isn't specific to any resource type. As long as it can pass an + * equals check. + * + * Currently tests strings and dimens since String and any Number seemed most relevant to verify. + */ +@RunWith(Parameterized::class) +class ResourceLoaderValuesTest : ResourceLoaderTestBase() { + + companion object { + @Parameterized.Parameters(name = "{1} {0}") + @JvmStatic + fun parameters(): Array<Any> { + val parameters = mutableListOf<Parameter>() + + // R.string + parameters += Parameter( + { getString(android.R.string.cancel) }, + "stringOne", { "SomeRidiculouslyUnlikelyStringOne" }, + "stringTwo", { "SomeRidiculouslyUnlikelyStringTwo" }, + listOf(DataType.APK, DataType.ARSC) + ) + + // R.dimen + parameters += Parameter( + { resources.getDimensionPixelSize(android.R.dimen.app_icon_size) }, + "dimenOne", { 564716.dpToPx(resources) }, + "dimenTwo", { 565717.dpToPx(resources) }, + listOf(DataType.APK, DataType.ARSC) + ) + + // File in the assets directory + parameters += Parameter( + { assets.open("Asset.txt").reader().readText() }, + "assetOne", { "assetOne" }, + "assetTwo", { "assetTwo" }, + listOf(DataType.ASSET) + ) + + // From assets directory returning file descriptor + parameters += Parameter( + { assets.openFd("Asset.txt").readText() }, + "assetOne", { "assetOne" }, + "assetTwo", { "assetTwo" }, + listOf(DataType.ASSET_FD) + ) + + // From root directory returning file descriptor + parameters += Parameter( + { assets.openNonAssetFd("NonAsset.txt").readText() }, + "NonAssetOne", { "NonAssetOne" }, + "NonAssetTwo", { "NonAssetTwo" }, + listOf(DataType.NON_ASSET) + ) + + // Asset as compiled XML drawable + parameters += Parameter( + { (getDrawable(R.drawable.non_asset_drawable) as ColorDrawable).color }, + "nonAssetDrawableOne", { Color.parseColor("#A3C3E3") }, + "nonAssetDrawableTwo", { Color.parseColor("#3A3C3E") }, + listOf(DataType.NON_ASSET_DRAWABLE) + ) + + // Asset as compiled bitmap drawable + parameters += Parameter( + { + (getDrawable(R.drawable.non_asset_bitmap) as BitmapDrawable) + .bitmap.getColor(0, 0).toArgb() + }, + "nonAssetBitmapGreen", { Color.GREEN }, + "nonAssetBitmapBlue", { Color.BLUE }, + listOf(DataType.NON_ASSET_BITMAP) + ) + + // Asset as compiled XML layout + parameters += Parameter( + { getLayout(R.layout.layout).advanceToRoot().name }, + "layoutOne", { "RelativeLayout" }, + "layoutTwo", { "LinearLayout" }, + listOf(DataType.NON_ASSET_LAYOUT) + ) + + // Isolated resource split + parameters += Parameter( + { getString(R.string.split_overlaid) }, + "split_one", { "Split ONE Overlaid" }, + "split_two", { "Split TWO Overlaid" }, + listOf(DataType.SPLIT) + ) + + return parameters.flatMap { parameter -> + parameter.dataTypes.map { dataType -> + arrayOf(dataType, parameter) + } + }.toTypedArray() + } + } + + @Suppress("LateinitVarOverridesLateinitVar") + @field:Parameterized.Parameter(0) + override lateinit var dataType: DataType + + @field:Parameterized.Parameter(1) + lateinit var parameter: Parameter + + private val valueOne by lazy { parameter.valueOne(this) } + private val valueTwo by lazy { parameter.valueTwo(this) } + + private fun openOne() = parameter.loaderOne.openLoader() + private fun openTwo() = parameter.loaderTwo.openLoader() + + // Class method for syntax highlighting purposes + private fun getValue() = parameter.getValue(this) + + @Test + fun verifyValueUniqueness() { + // Ensure the parameters are valid in case of coding errors + assertNotEquals(valueOne, getValue()) + assertNotEquals(valueTwo, getValue()) + assertNotEquals(valueOne, valueTwo) + } + + @Test + fun addMultipleLoaders() { + val originalValue = getValue() + val testOne = openOne() + val testTwo = openTwo() + + addLoader(testOne, testTwo) + + assertEquals(valueTwo, getValue()) + + removeLoader(testTwo) + + assertEquals(valueOne, getValue()) + + removeLoader(testOne) + + assertEquals(originalValue, getValue()) + } + + @Test + fun setMultipleLoaders() { + val originalValue = getValue() + val testOne = openOne() + val testTwo = openTwo() + + setLoaders(testOne, testTwo) + + assertEquals(valueTwo, getValue()) + + removeLoader(testTwo) + + assertEquals(valueOne, getValue()) + + setLoaders() + + assertEquals(originalValue, getValue()) + } + + @Test + fun getLoadersContainsAll() { + val testOne = openOne() + val testTwo = openTwo() + + addLoader(testOne, testTwo) + + assertThat(getLoaders()).containsAllOf(testOne, testTwo) + } + + @Test + fun getLoadersDoesNotLeakMutability() { + val originalValue = getValue() + val testOne = openOne() + val testTwo = openTwo() + + addLoader(testOne) + + assertEquals(valueOne, getValue()) + + val loaders = getLoaders() + loaders += testTwo + + assertEquals(valueOne, getValue()) + + removeLoader(testOne) + + assertEquals(originalValue, getValue()) + } + + @Test(expected = IllegalArgumentException::class) + fun alreadyAddedThrows() { + val testOne = openOne() + val testTwo = openTwo() + + addLoader(testOne) + addLoader(testTwo) + addLoader(testOne) + } + + @Test(expected = IllegalArgumentException::class) + fun alreadyAddedAndSetThrows() { + val testOne = openOne() + val testTwo = openTwo() + + addLoader(testOne) + addLoader(testTwo) + setLoaders(testTwo) + } + + @Test + fun repeatedRemoveSucceeds() { + val originalValue = getValue() + val testOne = openOne() + + addLoader(testOne) + + assertNotEquals(originalValue, getValue()) + + removeLoader(testOne) + + assertEquals(originalValue, getValue()) + + removeLoader(testOne) + + assertEquals(originalValue, getValue()) + } + + @Test + fun addToFront() { + val testOne = openOne() + val testTwo = openTwo() + + addLoader(testOne) + + assertEquals(valueOne, getValue()) + + addLoader(testTwo, 0) + + assertEquals(valueOne, getValue()) + + // Remove top loader, so previously added to front should now resolve + removeLoader(testOne) + assertEquals(valueTwo, getValue()) + } + + @Test + fun addToEnd() { + val testOne = openOne() + val testTwo = openTwo() + + addLoader(testOne) + + assertEquals(valueOne, getValue()) + + addLoader(testTwo, 1) + + assertEquals(valueTwo, getValue()) + } + + @Test(expected = IndexOutOfBoundsException::class) + fun addPastEnd() { + val testOne = openOne() + val testTwo = openTwo() + + addLoader(testOne) + + assertEquals(valueOne, getValue()) + + addLoader(testTwo, 2) + } + + @Test(expected = IndexOutOfBoundsException::class) + fun addBeforeFront() { + val testOne = openOne() + val testTwo = openTwo() + + addLoader(testOne) + + assertEquals(valueOne, getValue()) + + addLoader(testTwo, -1) + } + + @Test + fun reorder() { + val originalValue = getValue() + val testOne = openOne() + val testTwo = openTwo() + + addLoader(testOne, testTwo) + + assertEquals(valueTwo, getValue()) + + removeLoader(testOne) + + assertEquals(valueTwo, getValue()) + + addLoader(testOne) + + assertEquals(valueOne, getValue()) + + removeLoader(testTwo) + + assertEquals(valueOne, getValue()) + + removeLoader(testOne) + + assertEquals(originalValue, getValue()) + } + + data class Parameter( + val getValue: ResourceLoaderValuesTest.() -> Any, + val loaderOne: String, + val valueOne: ResourceLoaderValuesTest.() -> Any, + val loaderTwo: String, + val valueTwo: ResourceLoaderValuesTest.() -> Any, + val dataTypes: List<DataType> + ) { + override fun toString(): String { + val prefix = loaderOne.commonPrefixWith(loaderTwo) + return "$prefix${loaderOne.removePrefix(prefix)}|${loaderTwo.removePrefix(prefix)}" + } + } +} diff --git a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/Utils.kt b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/Utils.kt new file mode 100644 index 000000000000..df2d09adf503 --- /dev/null +++ b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/Utils.kt @@ -0,0 +1,72 @@ +/* + * 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.test + +import android.content.Context +import android.content.res.AssetFileDescriptor +import android.content.res.Resources +import android.os.ParcelFileDescriptor +import android.util.TypedValue +import org.mockito.Answers +import org.mockito.stubbing.Answer +import org.xmlpull.v1.XmlPullParser +import java.io.File + +// Enforce use of [android.util.Pair] instead of Kotlin's so it matches the ResourceLoader APIs +typealias Pair<F, S> = android.util.Pair<F, S> +infix fun <A, B> A.to(that: B): Pair<A, B> = Pair.create(this, that)!! + +object Utils { + val ANSWER_THROWS = Answer<Any> { + when (val name = it.method.name) { + "toString" -> return@Answer Answers.CALLS_REAL_METHODS.answer(it) + else -> throw UnsupportedOperationException("$name with " + + "${it.arguments?.joinToString()} should not be called") + } + } +} + +fun Int.dpToPx(resources: Resources) = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + this.toFloat(), + resources.displayMetrics +).toInt() + +fun AssetFileDescriptor.readText() = createInputStream().reader().readText() + +fun rawFile(fileName: String) = R.raw::class.java.getDeclaredField(fileName).getInt(null) + +fun XmlPullParser.advanceToRoot() = apply { + while (next() != XmlPullParser.START_TAG) { + // Empty + } +} + +fun Context.copiedRawFile(fileName: String): ParcelFileDescriptor { + return resources.openRawResourceFd(rawFile(fileName)).use { asset -> + // AssetManager doesn't expose a direct file descriptor to the asset, so copy it to + // an individual file so one can be created manually. + val copiedFile = File(filesDir, fileName) + asset.createInputStream().use { input -> + copiedFile.outputStream().use { output -> + input.copyTo(output) + } + } + + ParcelFileDescriptor.open(copiedFile, ParcelFileDescriptor.MODE_READ_WRITE) + } +} diff --git a/data/etc/car/com.google.android.car.kitchensink.xml b/data/etc/car/com.google.android.car.kitchensink.xml index d36a82684e9e..61281eea7134 100644 --- a/data/etc/car/com.google.android.car.kitchensink.xml +++ b/data/etc/car/com.google.android.car.kitchensink.xml @@ -16,17 +16,30 @@ --> <permissions> <privapp-permissions package="com.google.android.car.kitchensink"> + <permission name="android.permission.ACCESS_NETWORK_STATE"/> + <permission name="android.permission.ACCESS_WIFI_STATE"/> + <permission name="android.permission.ACTIVITY_EMBEDDING"/> + <permission name="android.permission.INJECT_EVENTS"/> + <!-- use for CarServiceUnitTest and CarServiceTest --> + <permission name="android.permission.INTERACT_ACROSS_USERS"/> + <!-- use for CarServiceUnitTest --> + <permission name="android.permission.INTERACT_ACROSS_USERS_FULL"/> <permission name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS"/> <permission name="android.permission.LOCATION_HARDWARE"/> <permission name="android.permission.MANAGE_USB"/> <permission name="android.permission.MANAGE_USERS"/> + <!-- use for CarServiceTest --> + <permission name="android.permission.MEDIA_CONTENT_CONTROL"/> <permission name="android.permission.MODIFY_AUDIO_ROUTING"/> <permission name="android.permission.MODIFY_DAY_NIGHT_MODE"/> <permission name="android.permission.MODIFY_PHONE_STATE"/> <permission name="android.permission.PROVIDE_TRUST_AGENT"/> + <permission name="android.permission.OVERRIDE_WIFI_CONFIG"/> <permission name="android.permission.REAL_GET_TASKS"/> <permission name="android.permission.READ_LOGS"/> <permission name="android.permission.REBOOT"/> + <!-- use for CarServiceTest --> + <permission name="android.permission.SET_ACTIVITY_WATCHER"/> <permission name="android.permission.WRITE_SECURE_SETTINGS"/> </privapp-permissions> </permissions> diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp index eeaefc5b157c..a34a6c0b3724 100644 --- a/libs/androidfw/Android.bp +++ b/libs/androidfw/Android.bp @@ -166,7 +166,10 @@ cc_test { static_libs: common_test_libs + ["liblog", "libz"], }, }, - data: ["tests/data/**/*.apk"], + data: [ + "tests/data/**/*.apk", + "tests/data/**/*.arsc", + ], test_suites: ["device-tests"], } diff --git a/libs/androidfw/ApkAssets.cpp b/libs/androidfw/ApkAssets.cpp index cf2ef3070385..b309621435b5 100644 --- a/libs/androidfw/ApkAssets.cpp +++ b/libs/androidfw/ApkAssets.cpp @@ -42,12 +42,16 @@ static const std::string kResourcesArsc("resources.arsc"); ApkAssets::ApkAssets(ZipArchiveHandle unmanaged_handle, const std::string& path, - time_t last_mod_time) - : zip_handle_(unmanaged_handle, ::CloseArchive), path_(path), last_mod_time_(last_mod_time) { + time_t last_mod_time, + bool for_loader) + : zip_handle_(unmanaged_handle, ::CloseArchive), path_(path), last_mod_time_(last_mod_time), + for_loader(for_loader) { } -std::unique_ptr<const ApkAssets> ApkAssets::Load(const std::string& path, bool system) { - return LoadImpl({} /*fd*/, path, nullptr, nullptr, system, false /*load_as_shared_library*/); +std::unique_ptr<const ApkAssets> ApkAssets::Load(const std::string& path, bool system, + bool for_loader) { + return LoadImpl({} /*fd*/, path, nullptr, nullptr, system, false /*load_as_shared_library*/, + for_loader); } std::unique_ptr<const ApkAssets> ApkAssets::LoadAsSharedLibrary(const std::string& path, @@ -76,9 +80,21 @@ std::unique_ptr<const ApkAssets> ApkAssets::LoadOverlay(const std::string& idmap std::unique_ptr<const ApkAssets> ApkAssets::LoadFromFd(unique_fd fd, const std::string& friendly_name, - bool system, bool force_shared_lib) { + bool system, bool force_shared_lib, + bool for_loader) { return LoadImpl(std::move(fd), friendly_name, nullptr /*idmap_asset*/, nullptr /*loaded_idmap*/, - system, force_shared_lib); + system, force_shared_lib, for_loader); +} + +std::unique_ptr<const ApkAssets> ApkAssets::LoadArsc(const std::string& path, + bool for_loader) { + return LoadArscImpl({} /*fd*/, path, for_loader); +} + +std::unique_ptr<const ApkAssets> ApkAssets::LoadArsc(unique_fd fd, + const std::string& friendly_name, + bool for_loader) { + return LoadArscImpl(std::move(fd), friendly_name, for_loader); } std::unique_ptr<Asset> ApkAssets::CreateAssetFromFile(const std::string& path) { @@ -104,7 +120,8 @@ std::unique_ptr<Asset> ApkAssets::CreateAssetFromFile(const std::string& path) { std::unique_ptr<const ApkAssets> ApkAssets::LoadImpl( unique_fd fd, const std::string& path, std::unique_ptr<Asset> idmap_asset, - std::unique_ptr<const LoadedIdmap> loaded_idmap, bool system, bool load_as_shared_library) { + std::unique_ptr<const LoadedIdmap> loaded_idmap, bool system, bool load_as_shared_library, + bool for_loader) { ::ZipArchiveHandle unmanaged_handle; int32_t result; if (fd >= 0) { @@ -123,7 +140,8 @@ std::unique_ptr<const ApkAssets> ApkAssets::LoadImpl( time_t last_mod_time = getFileModDate(path.c_str()); // Wrap the handle in a unique_ptr so it gets automatically closed. - std::unique_ptr<ApkAssets> loaded_apk(new ApkAssets(unmanaged_handle, path, last_mod_time)); + std::unique_ptr<ApkAssets> + loaded_apk(new ApkAssets(unmanaged_handle, path, last_mod_time, for_loader)); // Find the resource table. ::ZipEntry entry; @@ -152,7 +170,7 @@ std::unique_ptr<const ApkAssets> ApkAssets::LoadImpl( reinterpret_cast<const char*>(loaded_apk->resources_asset_->getBuffer(true /*wordAligned*/)), loaded_apk->resources_asset_->getLength()); loaded_apk->loaded_arsc_ = - LoadedArsc::Load(data, loaded_idmap.get(), system, load_as_shared_library); + LoadedArsc::Load(data, loaded_idmap.get(), system, load_as_shared_library, for_loader); if (loaded_apk->loaded_arsc_ == nullptr) { LOG(ERROR) << "Failed to load '" << kResourcesArsc << "' in APK '" << path << "'."; return {}; @@ -162,8 +180,53 @@ std::unique_ptr<const ApkAssets> ApkAssets::LoadImpl( return std::move(loaded_apk); } +std::unique_ptr<const ApkAssets> ApkAssets::LoadArscImpl(unique_fd fd, + const std::string& path, + bool for_loader) { + std::unique_ptr<Asset> resources_asset; + + if (fd >= 0) { + resources_asset = std::unique_ptr<Asset>(Asset::createFromFd(fd.release(), nullptr, + Asset::AccessMode::ACCESS_BUFFER)); + } else { + resources_asset = CreateAssetFromFile(path); + } + + if (resources_asset == nullptr) { + LOG(ERROR) << "Failed to open ARSC '" << path; + return {}; + } + + time_t last_mod_time = getFileModDate(path.c_str()); + + std::unique_ptr<ApkAssets> loaded_apk(new ApkAssets(nullptr, path, last_mod_time, for_loader)); + loaded_apk->resources_asset_ = std::move(resources_asset); + + const StringPiece data( + reinterpret_cast<const char*>(loaded_apk->resources_asset_->getBuffer(true /*wordAligned*/)), + loaded_apk->resources_asset_->getLength()); + loaded_apk->loaded_arsc_ = LoadedArsc::Load(data, nullptr, false, false, for_loader); + if (loaded_apk->loaded_arsc_ == nullptr) { + LOG(ERROR) << "Failed to load '" << kResourcesArsc << path; + return {}; + } + + // Need to force a move for mingw32. + return std::move(loaded_apk); +} + +std::unique_ptr<const ApkAssets> ApkAssets::LoadEmpty(bool for_loader) { + std::unique_ptr<ApkAssets> loaded_apk(new ApkAssets(nullptr, "", -1, for_loader)); + loaded_apk->loaded_arsc_ = LoadedArsc::CreateEmpty(); + // Need to force a move for mingw32. + return std::move(loaded_apk); +} + std::unique_ptr<Asset> ApkAssets::Open(const std::string& path, Asset::AccessMode mode) const { - CHECK(zip_handle_ != nullptr); + // If this is a resource loader from an .arsc, there will be no zip handle + if (zip_handle_ == nullptr) { + return {}; + } ::ZipEntry entry; int32_t result = ::FindEntry(zip_handle_.get(), path, &entry); @@ -205,7 +268,10 @@ std::unique_ptr<Asset> ApkAssets::Open(const std::string& path, Asset::AccessMod bool ApkAssets::ForEachFile(const std::string& root_path, const std::function<void(const StringPiece&, FileType)>& f) const { - CHECK(zip_handle_ != nullptr); + // If this is a resource loader from an .arsc, there will be no zip handle + if (zip_handle_ == nullptr) { + return false; + } std::string root_path_full = root_path; if (root_path_full.back() != '/') { @@ -252,6 +318,11 @@ bool ApkAssets::ForEachFile(const std::string& root_path, } bool ApkAssets::IsUpToDate() const { + // Loaders are invalidated by the app, not the system, so assume up to date + if (for_loader) { + return true; + } + return last_mod_time_ == getFileModDate(path_.c_str()); } diff --git a/libs/androidfw/Asset.cpp b/libs/androidfw/Asset.cpp index 92125c9da8bb..c132f343713f 100644 --- a/libs/androidfw/Asset.cpp +++ b/libs/androidfw/Asset.cpp @@ -133,14 +133,24 @@ Asset::Asset(void) */ /*static*/ Asset* Asset::createFromFile(const char* fileName, AccessMode mode) { + return createFromFd(open(fileName, O_RDONLY | O_BINARY), fileName, mode); +} + +/* + * Create a new Asset from a file on disk. There is a fair chance that + * the file doesn't actually exist. + * + * We can use "mode" to decide how we want to go about it. + */ +/*static*/ Asset* Asset::createFromFd(const int fd, const char* fileName, AccessMode mode) +{ + if (fd < 0) { + return NULL; + } + _FileAsset* pAsset; status_t result; off64_t length; - int fd; - - fd = open(fileName, O_RDONLY | O_BINARY); - if (fd < 0) - return NULL; /* * Under Linux, the lseek fails if we actually opened a directory. To diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp index eec49df79630..e914f37bcac4 100644 --- a/libs/androidfw/AssetManager2.cpp +++ b/libs/androidfw/AssetManager2.cpp @@ -493,8 +493,12 @@ ApkAssetsCookie AssetManager2::FindEntry(uint32_t resid, uint16_t density_overri type_flags |= type_spec->GetFlagsForEntryIndex(local_entry_idx); - // If the package is an overlay, then even configurations that are the same MUST be chosen. + + // If the package is an overlay or custom loader, + // then even configurations that are the same MUST be chosen. const bool package_is_overlay = loaded_package->IsOverlay(); + const bool package_is_loader = loaded_package->IsCustomLoader(); + const bool should_overlay = package_is_overlay || package_is_loader; if (use_fast_path) { const FilteredConfigGroup& filtered_group = loaded_package_impl.filtered_configs_[type_idx]; @@ -508,10 +512,28 @@ ApkAssetsCookie AssetManager2::FindEntry(uint32_t resid, uint16_t density_overri if (best_config == nullptr) { resolution_type = Resolution::Step::Type::INITIAL; } else if (this_config.isBetterThan(*best_config, desired_config)) { - resolution_type = Resolution::Step::Type::BETTER_MATCH; - } else if (package_is_overlay && this_config.compare(*best_config) == 0) { - resolution_type = Resolution::Step::Type::OVERLAID; + if (package_is_loader) { + resolution_type = Resolution::Step::Type::BETTER_MATCH_LOADER; + } else { + resolution_type = Resolution::Step::Type::BETTER_MATCH; + } + } else if (should_overlay && this_config.compare(*best_config) == 0) { + if (package_is_loader) { + resolution_type = Resolution::Step::Type::OVERLAID_LOADER; + } else if (package_is_overlay) { + resolution_type = Resolution::Step::Type::OVERLAID; + } } else { + if (resource_resolution_logging_enabled_) { + if (package_is_loader) { + resolution_type = Resolution::Step::Type::SKIPPED_LOADER; + } else { + resolution_type = Resolution::Step::Type::SKIPPED; + } + resolution_steps.push_back(Resolution::Step{resolution_type, + this_config.toString(), + &loaded_package->GetPackageName()}); + } continue; } @@ -520,6 +542,16 @@ ApkAssetsCookie AssetManager2::FindEntry(uint32_t resid, uint16_t density_overri const ResTable_type* type = filtered_group.types[i]; const uint32_t offset = LoadedPackage::GetEntryOffset(type, local_entry_idx); if (offset == ResTable_type::NO_ENTRY) { + if (resource_resolution_logging_enabled_) { + if (package_is_loader) { + resolution_type = Resolution::Step::Type::NO_ENTRY_LOADER; + } else { + resolution_type = Resolution::Step::Type::NO_ENTRY; + } + resolution_steps.push_back(Resolution::Step{Resolution::Step::Type::NO_ENTRY, + this_config.toString(), + &loaded_package->GetPackageName()}); + } continue; } @@ -554,9 +586,17 @@ ApkAssetsCookie AssetManager2::FindEntry(uint32_t resid, uint16_t density_overri if (best_config == nullptr) { resolution_type = Resolution::Step::Type::INITIAL; } else if (this_config.isBetterThan(*best_config, desired_config)) { - resolution_type = Resolution::Step::Type::BETTER_MATCH; - } else if (package_is_overlay && this_config.compare(*best_config) == 0) { - resolution_type = Resolution::Step::Type::OVERLAID; + if (package_is_loader) { + resolution_type = Resolution::Step::Type::BETTER_MATCH_LOADER; + } else { + resolution_type = Resolution::Step::Type::BETTER_MATCH; + } + } else if (should_overlay && this_config.compare(*best_config) == 0) { + if (package_is_overlay) { + resolution_type = Resolution::Step::Type::OVERLAID; + } else if (package_is_loader) { + resolution_type = Resolution::Step::Type::OVERLAID_LOADER; + } } else { continue; } @@ -678,9 +718,27 @@ std::string AssetManager2::GetLastResourceResolution() const { case Resolution::Step::Type::BETTER_MATCH: prefix = "Found better"; break; + case Resolution::Step::Type::BETTER_MATCH_LOADER: + prefix = "Found better in loader"; + break; case Resolution::Step::Type::OVERLAID: prefix = "Overlaid"; break; + case Resolution::Step::Type::OVERLAID_LOADER: + prefix = "Overlaid by loader"; + break; + case Resolution::Step::Type::SKIPPED: + prefix = "Skipped"; + break; + case Resolution::Step::Type::SKIPPED_LOADER: + prefix = "Skipped loader"; + break; + case Resolution::Step::Type::NO_ENTRY: + prefix = "No entry"; + break; + case Resolution::Step::Type::NO_ENTRY_LOADER: + prefix = "No entry for loader"; + break; } if (!prefix.empty()) { diff --git a/libs/androidfw/LoadedArsc.cpp b/libs/androidfw/LoadedArsc.cpp index 72873abc6a42..882dc0d71759 100644 --- a/libs/androidfw/LoadedArsc.cpp +++ b/libs/androidfw/LoadedArsc.cpp @@ -401,7 +401,9 @@ const LoadedPackage* LoadedArsc::GetPackageById(uint8_t package_id) const { std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk, const LoadedIdmap* loaded_idmap, - bool system, bool load_as_shared_library) { + bool system, + bool load_as_shared_library, + bool for_loader) { ATRACE_NAME("LoadedPackage::Load"); std::unique_ptr<LoadedPackage> loaded_package(new LoadedPackage()); @@ -430,6 +432,10 @@ std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk, loaded_package->overlay_ = true; } + if (for_loader) { + loaded_package->custom_loader_ = true; + } + if (header->header.headerSize >= sizeof(ResTable_package)) { uint32_t type_id_offset = dtohl(header->typeIdOffset); if (type_id_offset > std::numeric_limits<uint8_t>::max()) { @@ -696,7 +702,7 @@ std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk, } bool LoadedArsc::LoadTable(const Chunk& chunk, const LoadedIdmap* loaded_idmap, - bool load_as_shared_library) { + bool load_as_shared_library, bool for_loader) { const ResTable_header* header = chunk.header<ResTable_header>(); if (header == nullptr) { LOG(ERROR) << "RES_TABLE_TYPE too small."; @@ -735,7 +741,11 @@ bool LoadedArsc::LoadTable(const Chunk& chunk, const LoadedIdmap* loaded_idmap, packages_seen++; std::unique_ptr<const LoadedPackage> loaded_package = - LoadedPackage::Load(child_chunk, loaded_idmap, system_, load_as_shared_library); + LoadedPackage::Load(child_chunk, + loaded_idmap, + system_, + load_as_shared_library, + for_loader); if (!loaded_package) { return false; } @@ -758,9 +768,11 @@ bool LoadedArsc::LoadTable(const Chunk& chunk, const LoadedIdmap* loaded_idmap, } std::unique_ptr<const LoadedArsc> LoadedArsc::Load(const StringPiece& data, - const LoadedIdmap* loaded_idmap, bool system, - bool load_as_shared_library) { - ATRACE_NAME("LoadedArsc::LoadTable"); + const LoadedIdmap* loaded_idmap, + bool system, + bool load_as_shared_library, + bool for_loader) { + ATRACE_NAME("LoadedArsc::Load"); // Not using make_unique because the constructor is private. std::unique_ptr<LoadedArsc> loaded_arsc(new LoadedArsc()); @@ -771,7 +783,10 @@ std::unique_ptr<const LoadedArsc> LoadedArsc::Load(const StringPiece& data, const Chunk chunk = iter.Next(); switch (chunk.type()) { case RES_TABLE_TYPE: - if (!loaded_arsc->LoadTable(chunk, loaded_idmap, load_as_shared_library)) { + if (!loaded_arsc->LoadTable(chunk, + loaded_idmap, + load_as_shared_library, + for_loader)) { return {}; } break; diff --git a/libs/androidfw/include/androidfw/ApkAssets.h b/libs/androidfw/include/androidfw/ApkAssets.h index 49fc82bff11e..625b68207d83 100644 --- a/libs/androidfw/include/androidfw/ApkAssets.h +++ b/libs/androidfw/include/androidfw/ApkAssets.h @@ -40,7 +40,8 @@ class ApkAssets { // Creates an ApkAssets. // If `system` is true, the package is marked as a system package, and allows some functions to // filter out this package when computing what configurations/resources are available. - static std::unique_ptr<const ApkAssets> Load(const std::string& path, bool system = false); + static std::unique_ptr<const ApkAssets> Load(const std::string& path, bool system = false, + bool for_loader = false); // Creates an ApkAssets, but forces any package with ID 0x7f to be loaded as a shared library. // If `system` is true, the package is marked as a system package, and allows some functions to @@ -63,7 +64,21 @@ class ApkAssets { // If `force_shared_lib` is true, any package with ID 0x7f is loaded as a shared library. static std::unique_ptr<const ApkAssets> LoadFromFd(base::unique_fd fd, const std::string& friendly_name, bool system, - bool force_shared_lib); + bool force_shared_lib, + bool for_loader = false); + + // Creates an empty wrapper ApkAssets from the given path which points to an .arsc. + static std::unique_ptr<const ApkAssets> LoadArsc(const std::string& path, + bool for_loader = false); + + // Creates an empty wrapper ApkAssets from the given file descriptor which points to an .arsc, + // Takes ownership of the file descriptor. + static std::unique_ptr<const ApkAssets> LoadArsc(base::unique_fd fd, + const std::string& friendly_name, + bool resource_loader = false); + + // Creates a totally empty ApkAssets with no resources table and no file entries. + static std::unique_ptr<const ApkAssets> LoadEmpty(bool resource_loader = false); std::unique_ptr<Asset> Open(const std::string& path, Asset::AccessMode mode = Asset::AccessMode::ACCESS_RANDOM) const; @@ -86,24 +101,33 @@ class ApkAssets { bool IsUpToDate() const; + // Creates an Asset from any file on the file system. + static std::unique_ptr<Asset> CreateAssetFromFile(const std::string& path); + private: DISALLOW_COPY_AND_ASSIGN(ApkAssets); static std::unique_ptr<const ApkAssets> LoadImpl(base::unique_fd fd, const std::string& path, std::unique_ptr<Asset> idmap_asset, std::unique_ptr<const LoadedIdmap> loaded_idmap, - bool system, bool load_as_shared_library); + bool system, bool load_as_shared_library, + bool resource_loader = false); - // Creates an Asset from any file on the file system. - static std::unique_ptr<Asset> CreateAssetFromFile(const std::string& path); + static std::unique_ptr<const ApkAssets> LoadArscImpl(base::unique_fd fd, + const std::string& path, + bool resource_loader = false); - ApkAssets(ZipArchiveHandle unmanaged_handle, const std::string& path, time_t last_mod_time); + ApkAssets(ZipArchiveHandle unmanaged_handle, + const std::string& path, + time_t last_mod_time, + bool for_loader = false); - using ZipArchivePtr = std::unique_ptr<ZipArchive, void(*)(ZipArchiveHandle)>; + using ZipArchivePtr = std::unique_ptr<ZipArchive, void (*)(ZipArchiveHandle)>; ZipArchivePtr zip_handle_; const std::string path_; time_t last_mod_time_; + bool for_loader; std::unique_ptr<Asset> resources_asset_; std::unique_ptr<Asset> idmap_asset_; std::unique_ptr<const LoadedArsc> loaded_arsc_; diff --git a/libs/androidfw/include/androidfw/Asset.h b/libs/androidfw/include/androidfw/Asset.h index 9d12a35395c9..053dbb7864c6 100644 --- a/libs/androidfw/include/androidfw/Asset.h +++ b/libs/androidfw/include/androidfw/Asset.h @@ -121,6 +121,11 @@ public: */ const char* getAssetSource(void) const { return mAssetSource.string(); } + /* + * Create the asset from a file descriptor. + */ + static Asset* createFromFd(const int fd, const char* fileName, AccessMode mode); + protected: /* * Adds this Asset to the global Asset list for debugging and diff --git a/libs/androidfw/include/androidfw/AssetManager2.h b/libs/androidfw/include/androidfw/AssetManager2.h index de46081a6aa3..c7348b180648 100644 --- a/libs/androidfw/include/androidfw/AssetManager2.h +++ b/libs/androidfw/include/androidfw/AssetManager2.h @@ -382,7 +382,13 @@ class AssetManager2 { enum class Type { INITIAL, BETTER_MATCH, - OVERLAID + BETTER_MATCH_LOADER, + OVERLAID, + OVERLAID_LOADER, + SKIPPED, + SKIPPED_LOADER, + NO_ENTRY, + NO_ENTRY_LOADER, }; // Marks what kind of override this step was. diff --git a/libs/androidfw/include/androidfw/LoadedArsc.h b/libs/androidfw/include/androidfw/LoadedArsc.h index 950f5413f550..1a56876b9686 100644 --- a/libs/androidfw/include/androidfw/LoadedArsc.h +++ b/libs/androidfw/include/androidfw/LoadedArsc.h @@ -137,7 +137,8 @@ class LoadedPackage { static std::unique_ptr<const LoadedPackage> Load(const Chunk& chunk, const LoadedIdmap* loaded_idmap, bool system, - bool load_as_shared_library); + bool load_as_shared_library, + bool load_as_custom_loader); ~LoadedPackage(); @@ -187,6 +188,11 @@ class LoadedPackage { return overlay_; } + // Returns true if this package is a custom loader and should behave like an overlay + inline bool IsCustomLoader() const { + return custom_loader_; + } + // Returns the map of package name to package ID used in this LoadedPackage. At runtime, a // package could have been assigned a different package ID than what this LoadedPackage was // compiled with. AssetManager rewrites the package IDs so that they are compatible at runtime. @@ -260,6 +266,7 @@ class LoadedPackage { bool dynamic_ = false; bool system_ = false; bool overlay_ = false; + bool custom_loader_ = false; bool defines_overlayable_ = false; ByteBucketArray<TypeSpecPtr> type_specs_; @@ -282,7 +289,8 @@ class LoadedArsc { static std::unique_ptr<const LoadedArsc> Load(const StringPiece& data, const LoadedIdmap* loaded_idmap = nullptr, bool system = false, - bool load_as_shared_library = false); + bool load_as_shared_library = false, + bool for_loader = false); // Create an empty LoadedArsc. This is used when an APK has no resources.arsc. static std::unique_ptr<const LoadedArsc> CreateEmpty(); @@ -311,7 +319,19 @@ class LoadedArsc { DISALLOW_COPY_AND_ASSIGN(LoadedArsc); LoadedArsc() = default; - bool LoadTable(const Chunk& chunk, const LoadedIdmap* loaded_idmap, bool load_as_shared_library); + bool LoadTable( + const Chunk& chunk, + const LoadedIdmap* loaded_idmap, + bool load_as_shared_library, + bool for_loader + ); + + static std::unique_ptr<const LoadedArsc> LoadData(std::unique_ptr<LoadedArsc>& loaded_arsc, + const char* data, + size_t length, + const LoadedIdmap* loaded_idmap = nullptr, + bool load_as_shared_library = false, + bool for_loader = false); ResStringPool global_string_pool_; std::vector<std::unique_ptr<const LoadedPackage>> packages_; diff --git a/libs/androidfw/tests/LoadedArsc_test.cpp b/libs/androidfw/tests/LoadedArsc_test.cpp index d58e8d20c8aa..fd57a92c216b 100644 --- a/libs/androidfw/tests/LoadedArsc_test.cpp +++ b/libs/androidfw/tests/LoadedArsc_test.cpp @@ -25,6 +25,7 @@ #include "data/overlayable/R.h" #include "data/sparse/R.h" #include "data/styles/R.h" +#include "data/system/R.h" namespace app = com::android::app; namespace basic = com::android::basic; @@ -387,6 +388,39 @@ TEST(LoadedArscTest, GetOverlayableMap) { ASSERT_EQ(map.at("OverlayableResources2"), "overlay://com.android.overlayable"); } +TEST(LoadedArscTest, LoadCustomLoader) { + std::string contents; + + std::unique_ptr<Asset> + asset = ApkAssets::CreateAssetFromFile(GetTestDataPath() + "/loader/resources.arsc"); + + MockLoadedIdmap loaded_idmap; + const StringPiece data( + reinterpret_cast<const char*>(asset->getBuffer(true /*wordAligned*/)), + asset->getLength()); + + std::unique_ptr<const LoadedArsc> loaded_arsc = + LoadedArsc::Load(data, nullptr, false, false, true); + ASSERT_THAT(loaded_arsc, NotNull()); + + const LoadedPackage* package = + loaded_arsc->GetPackageById(get_package_id(android::R::string::cancel)); + ASSERT_THAT(package, NotNull()); + EXPECT_THAT(package->GetPackageName(), StrEq("android")); + EXPECT_THAT(package->GetPackageId(), Eq(0x01)); + + const uint8_t type_index = get_type_id(android::R::string::cancel) - 1; + const uint16_t entry_index = get_entry_id(android::R::string::cancel); + + const TypeSpec* type_spec = package->GetTypeSpecByTypeIndex(type_index); + ASSERT_THAT(type_spec, NotNull()); + ASSERT_THAT(type_spec->type_count, Ge(1u)); + + const ResTable_type* type = type_spec->types[0]; + ASSERT_THAT(type, NotNull()); + ASSERT_THAT(LoadedPackage::GetEntry(type, entry_index), NotNull()); +} + // structs with size fields (like Res_value, ResTable_entry) should be // backwards and forwards compatible (aka checking the size field against // sizeof(Res_value) might not be backwards compatible. diff --git a/libs/androidfw/tests/data/loader/resources.arsc b/libs/androidfw/tests/data/loader/resources.arsc Binary files differnew file mode 100644 index 000000000000..2c881f2cdfe5 --- /dev/null +++ b/libs/androidfw/tests/data/loader/resources.arsc diff --git a/libs/androidfw/tests/data/system/R.h b/libs/androidfw/tests/data/system/R.h index becb38830fb3..374107484784 100644 --- a/libs/androidfw/tests/data/system/R.h +++ b/libs/androidfw/tests/data/system/R.h @@ -40,6 +40,12 @@ struct R { number = 0x01030000, // sv }; }; + + struct string { + enum : uint32_t { + cancel = 0x01040000, + }; + }; }; } // namespace android diff --git a/location/java/android/location/Location.java b/location/java/android/location/Location.java index 9c36d76cf370..6824be8e1e3b 100644 --- a/location/java/android/location/Location.java +++ b/location/java/android/location/Location.java @@ -16,6 +16,7 @@ package android.location; +import android.annotation.Nullable; import android.annotation.SystemApi; import android.annotation.TestApi; import android.annotation.UnsupportedAppUsage; @@ -79,6 +80,8 @@ public class Location implements Parcelable { * * @hide */ + @TestApi + @SystemApi public static final String EXTRA_NO_GPS_LOCATION = "noGPSLocation"; /** @@ -1214,8 +1217,9 @@ public class Location implements Parcelable { * @param value the Location to attach * @hide */ - @UnsupportedAppUsage - public void setExtraLocation(String key, Location value) { + @TestApi + @SystemApi + public void setExtraLocation(@Nullable String key, @Nullable Location value) { if (mExtras == null) { mExtras = new Bundle(); } diff --git a/media/java/android/media/MediaMetadataRetriever.java b/media/java/android/media/MediaMetadataRetriever.java index f421029909bd..7ed431d4660d 100644 --- a/media/java/android/media/MediaMetadataRetriever.java +++ b/media/java/android/media/MediaMetadataRetriever.java @@ -1028,8 +1028,6 @@ public class MediaMetadataRetriever implements AutoCloseable { * @see MediaFormat#COLOR_STANDARD_BT601_PAL * @see MediaFormat#COLOR_STANDARD_BT601_NTSC * @see MediaFormat#COLOR_STANDARD_BT2020 - * - * @hide */ public static final int METADATA_KEY_COLOR_STANDARD = 35; @@ -1040,8 +1038,6 @@ public class MediaMetadataRetriever implements AutoCloseable { * @see MediaFormat#COLOR_TRANSFER_SDR_VIDEO * @see MediaFormat#COLOR_TRANSFER_ST2084 * @see MediaFormat#COLOR_TRANSFER_HLG - * - * @hide */ public static final int METADATA_KEY_COLOR_TRANSFER = 36; @@ -1050,8 +1046,6 @@ public class MediaMetadataRetriever implements AutoCloseable { * * @see MediaFormat#COLOR_RANGE_LIMITED * @see MediaFormat#COLOR_RANGE_FULL - * - * @hide */ public static final int METADATA_KEY_COLOR_RANGE = 37; // Add more here... diff --git a/media/java/android/media/ThumbnailUtils.java b/media/java/android/media/ThumbnailUtils.java index fb581b532dd2..a315c1eefb52 100644 --- a/media/java/android/media/ThumbnailUtils.java +++ b/media/java/android/media/ThumbnailUtils.java @@ -139,6 +139,12 @@ public class ThumbnailUtils { /** * Create a thumbnail for given audio file. + * <p> + * This method should only be used for files that you have direct access to; + * if you'd like to work with media hosted outside your app, consider using + * {@link ContentResolver#loadThumbnail(Uri, Size, CancellationSignal)} + * which enables remote providers to efficiently cache and invalidate + * thumbnails. * * @param file The audio file. * @param size The desired thumbnail size. @@ -231,6 +237,12 @@ public class ThumbnailUtils { /** * Create a thumbnail for given image file. + * <p> + * This method should only be used for files that you have direct access to; + * if you'd like to work with media hosted outside your app, consider using + * {@link ContentResolver#loadThumbnail(Uri, Size, CancellationSignal)} + * which enables remote providers to efficiently cache and invalidate + * thumbnails. * * @param file The audio file. * @param size The desired thumbnail size. @@ -334,6 +346,12 @@ public class ThumbnailUtils { /** * Create a thumbnail for given video file. + * <p> + * This method should only be used for files that you have direct access to; + * if you'd like to work with media hosted outside your app, consider using + * {@link ContentResolver#loadThumbnail(Uri, Size, CancellationSignal)} + * which enables remote providers to efficiently cache and invalidate + * thumbnails. * * @param file The video file. * @param size The desired thumbnail size. diff --git a/packages/PackageInstaller/res/values-television/themes.xml b/packages/PackageInstaller/res/values-television/themes.xml new file mode 100644 index 000000000000..5ae4957b494d --- /dev/null +++ b/packages/PackageInstaller/res/values-television/themes.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> + +<resources> + + <style name="Theme.AlertDialogActivity.NoAnimation"> + <item name="android:windowAnimationStyle">@null</item> + </style> + + <style name="Theme.AlertDialogActivity" + parent="@android:style/Theme.DeviceDefault.Light.Dialog.Alert" /> + + <style name="Theme.AlertDialogActivity.NoActionBar" + parent="@android:style/Theme.DeviceDefault.Light.NoActionBar"> + </style> + +</resources> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java index a7e7f085ffd7..c6d051d74239 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java @@ -29,6 +29,7 @@ import android.view.DisplayCutout; import android.view.Gravity; import android.view.View; import android.view.ViewTreeObserver; +import android.view.WindowInsets; import androidx.collection.ArraySet; @@ -390,7 +391,12 @@ public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable, } private void updateRegionForNotch(Region region) { - DisplayCutout cutout = mStatusBarWindowView.getRootWindowInsets().getDisplayCutout(); + WindowInsets windowInsets = mStatusBarWindowView.getRootWindowInsets(); + if (windowInsets == null) { + Log.w(TAG, "StatusBarWindowView is not attached."); + return; + } + DisplayCutout cutout = windowInsets.getDisplayCutout(); if (cutout == null) { return; } diff --git a/services/core/java/com/android/server/integrity/engine/RuleEvaluator.java b/services/core/java/com/android/server/integrity/engine/RuleEvaluator.java index 4ed581d6e915..a4bf2b045361 100644 --- a/services/core/java/com/android/server/integrity/engine/RuleEvaluator.java +++ b/services/core/java/com/android/server/integrity/engine/RuleEvaluator.java @@ -88,11 +88,8 @@ final class RuleEvaluator { // NOT connector has only 1 formula attached. return !isMatch(openFormula.getFormulas().get(0), appInstallMetadata); case AND: - boolean result = true; - for (Formula subFormula : openFormula.getFormulas()) { - result &= isMatch(subFormula, appInstallMetadata); - } - return result; + return openFormula.getFormulas().stream().allMatch( + subFormula -> isMatch(subFormula, appInstallMetadata)); default: Slog.i(TAG, String.format("Returned no match for unknown connector %s", openFormula.getConnector())); diff --git a/services/core/java/com/android/server/integrity/model/Rule.java b/services/core/java/com/android/server/integrity/model/Rule.java index ff21d6f70fb1..63b9b911ff4f 100644 --- a/services/core/java/com/android/server/integrity/model/Rule.java +++ b/services/core/java/com/android/server/integrity/model/Rule.java @@ -78,8 +78,8 @@ public final class Rule { return false; } Rule that = (Rule) o; - return mFormula.equals(that.mFormula) - && mEffect == that.mEffect; + return Objects.equals(mFormula, that.mFormula) + && Objects.equals(mEffect, that.mEffect); } @Override diff --git a/telephony/java/android/telephony/ICellInfoCallback.aidl b/telephony/java/android/telephony/ICellInfoCallback.aidl index ee3c1b1be6d9..60732a3db59a 100644 --- a/telephony/java/android/telephony/ICellInfoCallback.aidl +++ b/telephony/java/android/telephony/ICellInfoCallback.aidl @@ -16,7 +16,6 @@ package android.telephony; -import android.os.ParcelableException; import android.telephony.CellInfo; import java.util.List; @@ -28,5 +27,5 @@ import java.util.List; oneway interface ICellInfoCallback { void onCellInfo(in List<CellInfo> state); - void onError(in int errorCode, in ParcelableException detail); + void onError(in int errorCode, in String exceptionName, in String message); } diff --git a/telephony/java/android/telephony/SmsManager.java b/telephony/java/android/telephony/SmsManager.java index f4330fa0b725..2d35f8eae816 100644 --- a/telephony/java/android/telephony/SmsManager.java +++ b/telephony/java/android/telephony/SmsManager.java @@ -1814,6 +1814,36 @@ public final class SmsManager { // SMS send failure result codes + /** @hide */ + @IntDef(prefix = { "RESULT" }, value = { + RESULT_ERROR_NONE, + RESULT_ERROR_GENERIC_FAILURE, + RESULT_ERROR_RADIO_OFF, + RESULT_ERROR_NULL_PDU, + RESULT_ERROR_NO_SERVICE, + RESULT_ERROR_LIMIT_EXCEEDED, + RESULT_ERROR_FDN_CHECK_FAILURE, + RESULT_ERROR_SHORT_CODE_NOT_ALLOWED, + RESULT_ERROR_SHORT_CODE_NEVER_ALLOWED, + RESULT_RADIO_NOT_AVAILABLE, + RESULT_NETWORK_REJECT, + RESULT_INVALID_ARGUMENTS, + RESULT_INVALID_STATE, + RESULT_NO_MEMORY, + RESULT_INVALID_SMS_FORMAT, + RESULT_SYSTEM_ERROR, + RESULT_MODEM_ERROR, + RESULT_NETWORK_ERROR, + RESULT_INVALID_SMSC_ADDRESS, + RESULT_OPERATION_NOT_ALLOWED, + RESULT_INTERNAL_ERROR, + RESULT_NO_RESOURCES, + RESULT_CANCELLED, + RESULT_REQUEST_NOT_SUPPORTED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Result {} + /** * No error. * @hide diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index ee76a6f3992d..03e57e728610 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -5570,18 +5570,20 @@ public class TelephonyManager { telephony.requestCellInfoUpdate( getSubId(), new ICellInfoCallback.Stub() { + @Override public void onCellInfo(List<CellInfo> cellInfo) { Binder.withCleanCallingIdentity(() -> executor.execute(() -> callback.onCellInfo(cellInfo))); } - public void onError(int errorCode, android.os.ParcelableException detail) { + @Override + public void onError(int errorCode, String exceptionName, String message) { Binder.withCleanCallingIdentity(() -> executor.execute(() -> callback.onError( - errorCode, detail.getCause()))); + errorCode, + createThrowableByClassName(exceptionName, message)))); } }, getOpPackageName()); - } catch (RemoteException ex) { } } @@ -5610,21 +5612,36 @@ public class TelephonyManager { telephony.requestCellInfoUpdateWithWorkSource( getSubId(), new ICellInfoCallback.Stub() { + @Override public void onCellInfo(List<CellInfo> cellInfo) { Binder.withCleanCallingIdentity(() -> executor.execute(() -> callback.onCellInfo(cellInfo))); } - public void onError(int errorCode, android.os.ParcelableException detail) { + @Override + public void onError(int errorCode, String exceptionName, String message) { Binder.withCleanCallingIdentity(() -> executor.execute(() -> callback.onError( - errorCode, detail.getCause()))); + errorCode, + createThrowableByClassName(exceptionName, message)))); } }, getOpPackageName(), workSource); } catch (RemoteException ex) { } } + private static Throwable createThrowableByClassName(String className, String message) { + if (className == null) { + return null; + } + try { + Class<?> c = Class.forName(className); + return (Throwable) c.getConstructor(String.class).newInstance(message); + } catch (ReflectiveOperationException | ClassCastException e) { + } + return new RuntimeException(className + ": " + message); + } + /** * Sets the minimum time in milli-seconds between {@link PhoneStateListener#onCellInfoChanged * PhoneStateListener.onCellInfoChanged} will be invoked. diff --git a/telephony/java/android/telephony/ims/stub/ImsSmsImplBase.java b/telephony/java/android/telephony/ims/stub/ImsSmsImplBase.java index 175769bd34e4..36ece958d501 100644 --- a/telephony/java/android/telephony/ims/stub/ImsSmsImplBase.java +++ b/telephony/java/android/telephony/ims/stub/ImsSmsImplBase.java @@ -17,6 +17,7 @@ package android.telephony.ims.stub; import android.annotation.IntDef; +import android.annotation.IntRange; import android.annotation.SystemApi; import android.os.RemoteException; import android.telephony.SmsManager; @@ -148,14 +149,16 @@ public class ImsSmsImplBase { * * @param token unique token generated by the platform that should be used when triggering * callbacks for this specific message. - * @param messageRef the message reference. - * @param format the format of the message. Valid values are {@link SmsMessage#FORMAT_3GPP} and - * {@link SmsMessage#FORMAT_3GPP2}. + * @param messageRef the message reference, which may be 1 byte if it is in + * {@link SmsMessage#FORMAT_3GPP} format (see TS.123.040) or 2 bytes if it is in + * {@link SmsMessage#FORMAT_3GPP2} format (see 3GPP2 C.S0015-B). + * @param format the format of the message. * @param smsc the Short Message Service Center address. * @param isRetry whether it is a retry of an already attempted message or not. * @param pdu PDU representing the contents of the message. */ - public void sendSms(int token, int messageRef, String format, String smsc, boolean isRetry, + public void sendSms(int token, @IntRange(from = 0, to = 65535) int messageRef, + @SmsMessage.Format String format, String smsc, boolean isRetry, byte[] pdu) { // Base implementation returns error. Should be overridden. try { @@ -172,14 +175,13 @@ public class ImsSmsImplBase { * provider. * * @param token token provided in {@link #onSmsReceived(int, String, byte[])} - * @param messageRef the message reference - * @param result result of delivering the message. Valid values are: - * {@link #DELIVER_STATUS_OK}, - * {@link #DELIVER_STATUS_ERROR_GENERIC}, - * {@link #DELIVER_STATUS_ERROR_NO_MEMORY}, - * {@link #DELIVER_STATUS_ERROR_REQUEST_NOT_SUPPORTED} + * @param messageRef the message reference, which may be 1 byte if it is in + * {@link SmsMessage#FORMAT_3GPP} format (see TS.123.040) or 2 bytes if it is in + * {@link SmsMessage#FORMAT_3GPP2} format (see 3GPP2 C.S0015-B). + * @param result result of delivering the message. */ - public void acknowledgeSms(int token, int messageRef, @DeliverStatusResult int result) { + public void acknowledgeSms(int token, @IntRange(from = 0, to = 65535) int messageRef, + @DeliverStatusResult int result) { Log.e(LOG_TAG, "acknowledgeSms() not implemented."); } @@ -191,12 +193,13 @@ public class ImsSmsImplBase { * * @param token token provided in {@link #onSmsStatusReportReceived(int, int, String, byte[])} * or {@link #onSmsStatusReportReceived(int, String, byte[])} - * @param messageRef the message reference - * @param result result of delivering the message. Valid values are: - * {@link #STATUS_REPORT_STATUS_OK}, - * {@link #STATUS_REPORT_STATUS_ERROR} + * @param messageRef the message reference, which may be 1 byte if it is in + * {@link SmsMessage#FORMAT_3GPP} format (see TS.123.040) or 2 bytes if it is in + * {@link SmsMessage#FORMAT_3GPP2} format (see 3GPP2 C.S0015-B). + * @param result result of delivering the message. */ - public void acknowledgeSmsReport(int token, int messageRef, @StatusReportResult int result) { + public void acknowledgeSmsReport(int token, @IntRange(from = 0, to = 65535) int messageRef, + @StatusReportResult int result) { Log.e(LOG_TAG, "acknowledgeSmsReport() not implemented."); } @@ -210,12 +213,12 @@ public class ImsSmsImplBase { * {@link #DELIVER_STATUS_ERROR_GENERIC} result code. * @param token unique token generated by IMS providers that the platform will use to trigger * callbacks for this message. - * @param format the format of the message. Valid values are {@link SmsMessage#FORMAT_3GPP} and - * {@link SmsMessage#FORMAT_3GPP2}. + * @param format the format of the message. * @param pdu PDU representing the contents of the message. * @throws RuntimeException if called before {@link #onReady()} is triggered. */ - public final void onSmsReceived(int token, String format, byte[] pdu) throws RuntimeException { + public final void onSmsReceived(int token, @SmsMessage.Format String format, byte[] pdu) + throws RuntimeException { synchronized (mLock) { if (mListener == null) { throw new RuntimeException("Feature not ready."); @@ -241,13 +244,16 @@ public class ImsSmsImplBase { * sent successfully. * * @param token token provided in {@link #sendSms(int, int, String, String, boolean, byte[])} - * @param messageRef the message reference. Should be between 0 and 255 per TS.123.040 + * @param messageRef the message reference, which may be 1 byte if it is in + * {@link SmsMessage#FORMAT_3GPP} format (see TS.123.040) or 2 bytes if it is in + * {@link SmsMessage#FORMAT_3GPP2} format (see 3GPP2 C.S0015-B). * * @throws RuntimeException if called before {@link #onReady()} is triggered or if the * connection to the framework is not available. If this happens attempting to send the SMS * should be aborted. */ - public final void onSendSmsResultSuccess(int token, int messageRef) throws RuntimeException { + public final void onSendSmsResultSuccess(int token, + @IntRange(from = 0, to = 65535) int messageRef) throws RuntimeException { synchronized (mLock) { if (mListener == null) { throw new RuntimeException("Feature not ready."); @@ -266,34 +272,11 @@ public class ImsSmsImplBase { * to the platform. * * @param token token provided in {@link #sendSms(int, int, String, String, boolean, byte[])} - * @param messageRef the message reference. Should be between 0 and 255 per TS.123.040 + * @param messageRef the message reference, which may be 1 byte if it is in + * {@link SmsMessage#FORMAT_3GPP} format (see TS.123.040) or 2 bytes if it is in + * {@link SmsMessage#FORMAT_3GPP2} format (see 3GPP2 C.S0015-B). * @param status result of sending the SMS. - * @param reason reason in case status is failure. Valid values are: - * {@link SmsManager#RESULT_ERROR_NONE}, - * {@link SmsManager#RESULT_ERROR_GENERIC_FAILURE}, - * {@link SmsManager#RESULT_ERROR_RADIO_OFF}, - * {@link SmsManager#RESULT_ERROR_NULL_PDU}, - * {@link SmsManager#RESULT_ERROR_NO_SERVICE}, - * {@link SmsManager#RESULT_ERROR_LIMIT_EXCEEDED}, - * {@link SmsManager#RESULT_ERROR_FDN_CHECK_FAILURE}, - * {@link SmsManager#RESULT_ERROR_SHORT_CODE_NOT_ALLOWED}, - * {@link SmsManager#RESULT_ERROR_SHORT_CODE_NEVER_ALLOWED}, - * {@link SmsManager#RESULT_RADIO_NOT_AVAILABLE}, - * {@link SmsManager#RESULT_NETWORK_REJECT}, - * {@link SmsManager#RESULT_INVALID_ARGUMENTS}, - * {@link SmsManager#RESULT_INVALID_STATE}, - * {@link SmsManager#RESULT_NO_MEMORY}, - * {@link SmsManager#RESULT_INVALID_SMS_FORMAT}, - * {@link SmsManager#RESULT_SYSTEM_ERROR}, - * {@link SmsManager#RESULT_MODEM_ERROR}, - * {@link SmsManager#RESULT_NETWORK_ERROR}, - * {@link SmsManager#RESULT_ENCODING_ERROR}, - * {@link SmsManager#RESULT_INVALID_SMSC_ADDRESS}, - * {@link SmsManager#RESULT_OPERATION_NOT_ALLOWED}, - * {@link SmsManager#RESULT_INTERNAL_ERROR}, - * {@link SmsManager#RESULT_NO_RESOURCES}, - * {@link SmsManager#RESULT_CANCELLED}, - * {@link SmsManager#RESULT_REQUEST_NOT_SUPPORTED} + * @param reason reason in case status is failure. * * @throws RuntimeException if called before {@link #onReady()} is triggered or if the * connection to the framework is not available. If this happens attempting to send the SMS @@ -303,8 +286,8 @@ public class ImsSmsImplBase { * send result. */ @Deprecated - public final void onSendSmsResult(int token, int messageRef, @SendStatusResult int status, - int reason) throws RuntimeException { + public final void onSendSmsResult(int token, @IntRange(from = 0, to = 65535) int messageRef, + @SendStatusResult int status, @SmsManager.Result int reason) throws RuntimeException { synchronized (mLock) { if (mListener == null) { throw new RuntimeException("Feature not ready."); @@ -324,34 +307,10 @@ public class ImsSmsImplBase { * network. * * @param token token provided in {@link #sendSms(int, int, String, String, boolean, byte[])} - * @param messageRef the message reference. Should be between 0 and 255 per TS.123.040 + * @param messageRef the message reference, which may be 1 byte if it is in + * {@link SmsMessage#FORMAT_3GPP} format (see TS.123.040) or 2 bytes if it is in + * {@link SmsMessage#FORMAT_3GPP2} format (see 3GPP2 C.S0015-B). * @param status result of sending the SMS. - * @param reason Valid values are: - * {@link SmsManager#RESULT_ERROR_NONE}, - * {@link SmsManager#RESULT_ERROR_GENERIC_FAILURE}, - * {@link SmsManager#RESULT_ERROR_RADIO_OFF}, - * {@link SmsManager#RESULT_ERROR_NULL_PDU}, - * {@link SmsManager#RESULT_ERROR_NO_SERVICE}, - * {@link SmsManager#RESULT_ERROR_LIMIT_EXCEEDED}, - * {@link SmsManager#RESULT_ERROR_FDN_CHECK_FAILURE}, - * {@link SmsManager#RESULT_ERROR_SHORT_CODE_NOT_ALLOWED}, - * {@link SmsManager#RESULT_ERROR_SHORT_CODE_NEVER_ALLOWED}, - * {@link SmsManager#RESULT_RADIO_NOT_AVAILABLE}, - * {@link SmsManager#RESULT_NETWORK_REJECT}, - * {@link SmsManager#RESULT_INVALID_ARGUMENTS}, - * {@link SmsManager#RESULT_INVALID_STATE}, - * {@link SmsManager#RESULT_NO_MEMORY}, - * {@link SmsManager#RESULT_INVALID_SMS_FORMAT}, - * {@link SmsManager#RESULT_SYSTEM_ERROR}, - * {@link SmsManager#RESULT_MODEM_ERROR}, - * {@link SmsManager#RESULT_NETWORK_ERROR}, - * {@link SmsManager#RESULT_ENCODING_ERROR}, - * {@link SmsManager#RESULT_INVALID_SMSC_ADDRESS}, - * {@link SmsManager#RESULT_OPERATION_NOT_ALLOWED}, - * {@link SmsManager#RESULT_INTERNAL_ERROR}, - * {@link SmsManager#RESULT_NO_RESOURCES}, - * {@link SmsManager#RESULT_CANCELLED}, - * {@link SmsManager#RESULT_REQUEST_NOT_SUPPORTED} * @param networkErrorCode the error code reported by the carrier network if sending this SMS * has resulted in an error or {@link #RESULT_NO_NETWORK_ERROR} if no network error was * generated. See 3GPP TS 24.011 Section 7.3.4 for valid error codes and more information. @@ -360,9 +319,9 @@ public class ImsSmsImplBase { * connection to the framework is not available. If this happens attempting to send the SMS * should be aborted. */ - public final void onSendSmsResultError(int token, int messageRef, @SendStatusResult int status, - int reason, int networkErrorCode) - throws RuntimeException { + public final void onSendSmsResultError(int token, + @IntRange(from = 0, to = 65535) int messageRef, @SendStatusResult int status, + @SmsManager.Result int reason, int networkErrorCode) throws RuntimeException { synchronized (mLock) { if (mListener == null) { throw new RuntimeException("Feature not ready."); @@ -384,9 +343,10 @@ public class ImsSmsImplBase { * the platform is not available, {@link #acknowledgeSmsReport(int, int, int)} will be called * with the {@link #STATUS_REPORT_STATUS_ERROR} result code. * @param token token provided in {@link #sendSms(int, int, String, String, boolean, byte[])} - * @param messageRef the message reference. - * @param format the format of the message. Valid values are {@link SmsMessage#FORMAT_3GPP} and - * {@link SmsMessage#FORMAT_3GPP2}. + * @param messageRef the message reference, which may be 1 byte if it is in + * {@link SmsMessage#FORMAT_3GPP} format or 2 bytes if it is in + * {@link SmsMessage#FORMAT_3GPP2} format (see 3GPP2 C.S0015-B). + * @param format the format of the message. * @param pdu PDU representing the content of the status report. * @throws RuntimeException if called before {@link #onReady()} is triggered * @@ -394,7 +354,8 @@ public class ImsSmsImplBase { * message reference. */ @Deprecated - public final void onSmsStatusReportReceived(int token, int messageRef, String format, + public final void onSmsStatusReportReceived(int token, + @IntRange(from = 0, to = 65535) int messageRef, @SmsMessage.Format String format, byte[] pdu) throws RuntimeException { synchronized (mLock) { if (mListener == null) { @@ -419,13 +380,12 @@ public class ImsSmsImplBase { * with the {@link #STATUS_REPORT_STATUS_ERROR} result code. * @param token unique token generated by IMS providers that the platform will use to trigger * callbacks for this message. - * @param format the format of the message. Valid values are {@link SmsMessage#FORMAT_3GPP} and - * {@link SmsMessage#FORMAT_3GPP2}. + * @param format the format of the message. * @param pdu PDU representing the content of the status report. * @throws RuntimeException if called before {@link #onReady()} is triggered */ - public final void onSmsStatusReportReceived(int token, String format, byte[] pdu) - throws RuntimeException { + public final void onSmsStatusReportReceived(int token, @SmsMessage.Format String format, + byte[] pdu) throws RuntimeException { synchronized (mLock) { if (mListener == null) { throw new RuntimeException("Feature not ready."); @@ -450,13 +410,11 @@ public class ImsSmsImplBase { } /** - * Returns the SMS format. Default is {@link SmsMessage#FORMAT_3GPP} unless overridden by IMS - * Provider. + * Returns the SMS format that the ImsService expects. * - * @return the format of the message. Valid values are {@link SmsMessage#FORMAT_3GPP} and - * {@link SmsMessage#FORMAT_3GPP2}. + * @return The expected format of the SMS messages. */ - public String getSmsFormat() { + public @SmsMessage.Format String getSmsFormat() { return SmsMessage.FORMAT_3GPP; } |