diff options
author | Adam Lesinski <adamlesinski@google.com> | 2017-03-10 14:46:57 -0800 |
---|---|---|
committer | Adam Lesinski <adamlesinski@google.com> | 2017-03-16 15:45:10 -0700 |
commit | 1665d0f028e3a225cb117d3e227bef5c5dace2d4 (patch) | |
tree | 176d355b285d5d210ea8238f2dc59444a1a7964a | |
parent | f18cec265d471d7b6f3a738bc478b66b149692d7 (diff) |
Add support for configForSplit
Applications with the android:isolatedSplits="true" attribute in
their AndroidManifest.xml would have their Split APKs loaded in
isolation of each other, based on a set of dependencies.
Configuration Splits generated for a Feature split would not be properly
loaded before, so this change, along with a tools change, fixes this
issue and completes support for isolatedSplits.
Bug: 30999713
Test: CTS test coming (depends on some tool changes)
Change-Id: Ia4e7b0e69168a9d6637867558e306f7031720fb3
-rw-r--r-- | api/current.txt | 1 | ||||
-rw-r--r-- | api/system-current.txt | 1 | ||||
-rw-r--r-- | api/test-current.txt | 1 | ||||
-rw-r--r-- | cmds/pm/src/com/android/commands/pm/Pm.java | 3 | ||||
-rw-r--r-- | core/java/android/app/ContextImpl.java | 86 | ||||
-rw-r--r-- | core/java/android/app/LoadedApk.java | 114 | ||||
-rw-r--r-- | core/java/android/content/pm/ApplicationInfo.java | 12 | ||||
-rw-r--r-- | core/java/android/content/pm/InstrumentationInfo.java | 10 | ||||
-rw-r--r-- | core/java/android/content/pm/PackageParser.java | 102 | ||||
-rw-r--r-- | core/java/android/content/pm/split/SplitAssetDependencyLoader.java | 84 | ||||
-rw-r--r-- | core/java/android/content/pm/split/SplitDependencyLoader.java | 242 | ||||
-rw-r--r-- | core/java/android/content/pm/split/SplitDependencyLoaderHelper.java | 149 | ||||
-rw-r--r-- | core/res/res/values/attrs_manifest.xml | 10 | ||||
-rw-r--r-- | core/res/res/values/public.xml | 1 | ||||
-rw-r--r-- | services/core/java/com/android/server/pm/PackageInstallerSession.java | 2 | ||||
-rw-r--r-- | services/core/java/com/android/server/pm/PackageManagerShellCommand.java | 3 |
16 files changed, 445 insertions, 376 deletions
diff --git a/api/current.txt b/api/current.txt index 9557ee5586f1..ebadfdfe136e 100644 --- a/api/current.txt +++ b/api/current.txt @@ -746,6 +746,7 @@ package android { field public static final int isAsciiCapable = 16843753; // 0x10103e9 field public static final int isAuxiliary = 16843647; // 0x101037f field public static final int isDefault = 16843297; // 0x1010221 + field public static final int isFeatureSplit = 16844126; // 0x101055e field public static final int isGame = 16843764; // 0x10103f4 field public static final int isIndicator = 16843079; // 0x1010147 field public static final int isModifier = 16843334; // 0x1010246 diff --git a/api/system-current.txt b/api/system-current.txt index 6d01c6b0c27f..60e0e4cb8bf8 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -859,6 +859,7 @@ package android { field public static final int isAsciiCapable = 16843753; // 0x10103e9 field public static final int isAuxiliary = 16843647; // 0x101037f field public static final int isDefault = 16843297; // 0x1010221 + field public static final int isFeatureSplit = 16844126; // 0x101055e field public static final int isGame = 16843764; // 0x10103f4 field public static final int isIndicator = 16843079; // 0x1010147 field public static final int isModifier = 16843334; // 0x1010246 diff --git a/api/test-current.txt b/api/test-current.txt index 1d0a289832a6..9c65c6dfbc7c 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -746,6 +746,7 @@ package android { field public static final int isAsciiCapable = 16843753; // 0x10103e9 field public static final int isAuxiliary = 16843647; // 0x101037f field public static final int isDefault = 16843297; // 0x1010221 + field public static final int isFeatureSplit = 16844126; // 0x101055e field public static final int isGame = 16843764; // 0x10103f4 field public static final int isIndicator = 16843079; // 0x1010147 field public static final int isModifier = 16843334; // 0x1010246 diff --git a/cmds/pm/src/com/android/commands/pm/Pm.java b/cmds/pm/src/com/android/commands/pm/Pm.java index 91520f1a7d8e..827a77f24ee9 100644 --- a/cmds/pm/src/com/android/commands/pm/Pm.java +++ b/cmds/pm/src/com/android/commands/pm/Pm.java @@ -411,7 +411,8 @@ public final class Pm { if (file.isFile()) { try { ApkLite baseApk = PackageParser.parseApkLite(file, 0); - PackageLite pkgLite = new PackageLite(null, baseApk, null, null, null, null); + PackageLite pkgLite = new PackageLite(null, baseApk, null, null, null, null, + null, null); params.sessionParams.setSize( PackageHelper.calculateInstalledSize(pkgLite, false, params.sessionParams.abiOverride)); diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index ede9281264b3..8a3d9b197117 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -156,13 +156,13 @@ class ContextImpl extends Context { @GuardedBy("ContextImpl.class") private ArrayMap<String, File> mSharedPrefsPaths; - final ActivityThread mMainThread; - final LoadedApk mPackageInfo; - private ClassLoader mClassLoader; + final @NonNull ActivityThread mMainThread; + final @NonNull LoadedApk mPackageInfo; + private @Nullable ClassLoader mClassLoader; - private final IBinder mActivityToken; + private final @Nullable IBinder mActivityToken; - private final UserHandle mUser; + private final @Nullable UserHandle mUser; private final ApplicationContentResolver mContentResolver; @@ -181,6 +181,9 @@ class ContextImpl extends Context { private PackageManager mPackageManager; private Context mReceiverRestrictedContext = null; + // The name of the split this Context is representing. May be null. + private @Nullable String mSplitName = null; + private final Object mSync = new Object(); @GuardedBy("mSync") @@ -1914,17 +1917,25 @@ class ContextImpl extends Context { } } - private static Resources createResources(IBinder activityToken, LoadedApk pi, int displayId, - Configuration overrideConfig, CompatibilityInfo compatInfo) { + private static Resources createResources(IBinder activityToken, LoadedApk pi, String splitName, + int displayId, Configuration overrideConfig, CompatibilityInfo compatInfo) { + final String[] splitResDirs; + final ClassLoader classLoader; + try { + splitResDirs = pi.getSplitPaths(splitName); + classLoader = pi.getSplitClassLoader(splitName); + } catch (NameNotFoundException e) { + throw new RuntimeException(e); + } return ResourcesManager.getInstance().getResources(activityToken, pi.getResDir(), - pi.getSplitResDirs(), + splitResDirs, pi.getOverlayDirs(), pi.getApplicationInfo().sharedLibraryFiles, displayId, overrideConfig, compatInfo, - pi.getClassLoader()); + classLoader); } @Override @@ -1933,14 +1944,13 @@ class ContextImpl extends Context { LoadedApk pi = mMainThread.getPackageInfo(application, mResources.getCompatibilityInfo(), flags | CONTEXT_REGISTER_PACKAGE); if (pi != null) { - ContextImpl c = new ContextImpl(this, mMainThread, pi, mActivityToken, - new UserHandle(UserHandle.getUserId(application.uid)), flags, - null); + ContextImpl c = new ContextImpl(this, mMainThread, pi, null, mActivityToken, + new UserHandle(UserHandle.getUserId(application.uid)), flags, null); final int displayId = mDisplay != null ? mDisplay.getDisplayId() : Display.DEFAULT_DISPLAY; - c.setResources(createResources(mActivityToken, pi, displayId, null, + c.setResources(createResources(mActivityToken, pi, null, displayId, null, getDisplayAdjustments(displayId).getCompatibilityInfo())); if (c.mResources != null) { return c; @@ -1964,20 +1974,20 @@ class ContextImpl extends Context { if (packageName.equals("system") || packageName.equals("android")) { // The system resources are loaded in every application, so we can safely copy // the context without reloading Resources. - return new ContextImpl(this, mMainThread, mPackageInfo, mActivityToken, user, flags, - null); + return new ContextImpl(this, mMainThread, mPackageInfo, null, mActivityToken, user, + flags, null); } LoadedApk pi = mMainThread.getPackageInfo(packageName, mResources.getCompatibilityInfo(), flags | CONTEXT_REGISTER_PACKAGE, user.getIdentifier()); if (pi != null) { - ContextImpl c = new ContextImpl(this, mMainThread, pi, mActivityToken, user, flags, - null); + ContextImpl c = new ContextImpl(this, mMainThread, pi, null, mActivityToken, user, + flags, null); final int displayId = mDisplay != null ? mDisplay.getDisplayId() : Display.DEFAULT_DISPLAY; - c.setResources(createResources(mActivityToken, pi, displayId, null, + c.setResources(createResources(mActivityToken, pi, null, displayId, null, getDisplayAdjustments(displayId).getCompatibilityInfo())); if (c.mResources != null) { return c; @@ -1999,7 +2009,7 @@ class ContextImpl extends Context { final ClassLoader classLoader = mPackageInfo.getSplitClassLoader(splitName); final String[] paths = mPackageInfo.getSplitPaths(splitName); - final ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, + final ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, splitName, mActivityToken, mUser, mFlags, classLoader); final int displayId = mDisplay != null @@ -2024,11 +2034,11 @@ class ContextImpl extends Context { throw new IllegalArgumentException("overrideConfiguration must not be null"); } - ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, mActivityToken, - mUser, mFlags, mClassLoader); + ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, mSplitName, + mActivityToken, mUser, mFlags, mClassLoader); final int displayId = mDisplay != null ? mDisplay.getDisplayId() : Display.DEFAULT_DISPLAY; - context.setResources(createResources(mActivityToken, mPackageInfo, displayId, + context.setResources(createResources(mActivityToken, mPackageInfo, mSplitName, displayId, overrideConfiguration, getDisplayAdjustments(displayId).getCompatibilityInfo())); return context; } @@ -2039,12 +2049,12 @@ class ContextImpl extends Context { throw new IllegalArgumentException("display must not be null"); } - ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, mActivityToken, - mUser, mFlags, mClassLoader); + ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, mSplitName, + mActivityToken, mUser, mFlags, mClassLoader); final int displayId = display.getDisplayId(); - context.setResources(createResources(mActivityToken, mPackageInfo, displayId, null, - getDisplayAdjustments(displayId).getCompatibilityInfo())); + context.setResources(createResources(mActivityToken, mPackageInfo, mSplitName, displayId, + null, getDisplayAdjustments(displayId).getCompatibilityInfo())); context.mDisplay = display; return context; } @@ -2053,16 +2063,16 @@ class ContextImpl extends Context { public Context createDeviceProtectedStorageContext() { final int flags = (mFlags & ~Context.CONTEXT_CREDENTIAL_PROTECTED_STORAGE) | Context.CONTEXT_DEVICE_PROTECTED_STORAGE; - return new ContextImpl(this, mMainThread, mPackageInfo, mActivityToken, mUser, flags, - mClassLoader); + return new ContextImpl(this, mMainThread, mPackageInfo, mSplitName, mActivityToken, mUser, + flags, mClassLoader); } @Override public Context createCredentialProtectedStorageContext() { final int flags = (mFlags & ~Context.CONTEXT_DEVICE_PROTECTED_STORAGE) | Context.CONTEXT_CREDENTIAL_PROTECTED_STORAGE; - return new ContextImpl(this, mMainThread, mPackageInfo, mActivityToken, mUser, flags, - mClassLoader); + return new ContextImpl(this, mMainThread, mPackageInfo, mSplitName, mActivityToken, mUser, + flags, mClassLoader); } @Override @@ -2149,7 +2159,7 @@ class ContextImpl extends Context { static ContextImpl createSystemContext(ActivityThread mainThread) { LoadedApk packageInfo = new LoadedApk(mainThread); - ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, 0, + ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0, null); context.setResources(packageInfo.getResources()); context.mResources.updateConfiguration(context.mResourcesManager.getConfiguration(), @@ -2159,7 +2169,7 @@ class ContextImpl extends Context { static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) { if (packageInfo == null) throw new IllegalArgumentException("packageInfo"); - ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, 0, + ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0, null); context.setResources(packageInfo.getResources()); return context; @@ -2186,8 +2196,8 @@ class ContextImpl extends Context { } } - ContextImpl context = new ContextImpl(null, mainThread, packageInfo, activityToken, null, - 0, classLoader); + ContextImpl context = new ContextImpl(null, mainThread, packageInfo, activityInfo.splitName, + activityToken, null, 0, classLoader); // Clamp display ID to DEFAULT_DISPLAY if it is INVALID_DISPLAY. displayId = (displayId != Display.INVALID_DISPLAY) ? displayId : Display.DEFAULT_DISPLAY; @@ -2214,9 +2224,10 @@ class ContextImpl extends Context { return context; } - private ContextImpl(ContextImpl container, ActivityThread mainThread, - LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags, - ClassLoader classLoader) { + private ContextImpl(@Nullable ContextImpl container, @NonNull ActivityThread mainThread, + @NonNull LoadedApk packageInfo, @Nullable String splitName, + @Nullable IBinder activityToken, @Nullable UserHandle user, int flags, + @Nullable ClassLoader classLoader) { mOuterContext = this; // If creator didn't specify which storage to use, use the default @@ -2241,6 +2252,7 @@ class ContextImpl extends Context { mUser = user; mPackageInfo = packageInfo; + mSplitName = splitName; mClassLoader = classLoader; mResourcesManager = ResourcesManager.getInstance(); diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java index cf41e4e5dc9c..dbed1beb4cf7 100644 --- a/core/java/android/app/LoadedApk.java +++ b/core/java/android/app/LoadedApk.java @@ -27,7 +27,8 @@ import android.content.ServiceConnection; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; -import android.content.pm.split.SplitDependencyLoaderHelper; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.split.SplitDependencyLoader; import android.content.res.AssetManager; import android.content.res.CompatibilityInfo; import android.content.res.Resources; @@ -49,7 +50,6 @@ import android.util.ArrayMap; import android.util.Log; import android.util.Slog; import android.util.SparseArray; -import android.util.SparseIntArray; import android.view.Display; import android.view.DisplayAdjustments; @@ -304,7 +304,7 @@ public final class LoadedApk { final String[] splitPaths; try { splitPaths = getSplitPaths(null); - } catch (PackageManager.NameNotFoundException e) { + } catch (NameNotFoundException e) { // This should NEVER fail. throw new AssertionError("null split not found"); } @@ -336,7 +336,7 @@ public final class LoadedApk { mSplitResDirs = aInfo.uid == myUid ? aInfo.splitSourceDirs : aInfo.splitPublicSourceDirs; if (aInfo.requestsIsolatedSplitLoading() && !ArrayUtils.isEmpty(mSplitNames)) { - mSplitLoader = new SplitDependencyLoader(aInfo.splitDependencies); + mSplitLoader = new SplitDependencyLoaderImpl(aInfo.splitDependencies); } } @@ -465,110 +465,88 @@ public final class LoadedApk { } } - private class SplitDependencyLoader - extends SplitDependencyLoaderHelper<PackageManager.NameNotFoundException> { - private String[] mCachedBaseResourcePath; + /* + * All indices received by the super class should be shifted by 1 when accessing mSplitNames, + * etc. The super class assumes the base APK is index 0, while the PackageManager APIs don't + * include the base APK in the list of splits. + */ + private class SplitDependencyLoaderImpl extends SplitDependencyLoader<NameNotFoundException> { private final String[][] mCachedResourcePaths; - private final ClassLoader[] mCachedSplitClassLoaders; + private final ClassLoader[] mCachedClassLoaders; - SplitDependencyLoader(SparseIntArray dependencies) { + SplitDependencyLoaderImpl(@NonNull SparseArray<int[]> dependencies) { super(dependencies); - mCachedResourcePaths = new String[mSplitNames.length][]; - mCachedSplitClassLoaders = new ClassLoader[mSplitNames.length]; + mCachedResourcePaths = new String[mSplitNames.length + 1][]; + mCachedClassLoaders = new ClassLoader[mSplitNames.length + 1]; } @Override protected boolean isSplitCached(int splitIdx) { - if (splitIdx != -1) { - return mCachedSplitClassLoaders[splitIdx] != null; - } - return mClassLoader != null && mCachedBaseResourcePath != null; - } - - private void addAllConfigSplits(String splitName, ArrayList<String> outAssetPaths) { - for (int i = 0; i < mSplitNames.length; i++) { - if (isConfigurationSplitOf(mSplitNames[i], splitName)) { - outAssetPaths.add(mSplitResDirs[i]); - } - } + return mCachedClassLoaders[splitIdx] != null; } @Override - protected void constructSplit(int splitIdx, int parentSplitIdx) throws - PackageManager.NameNotFoundException { + protected void constructSplit(int splitIdx, @NonNull int[] configSplitIndices, + int parentSplitIdx) throws NameNotFoundException { final ArrayList<String> splitPaths = new ArrayList<>(); - if (splitIdx == -1) { + if (splitIdx == 0) { createOrUpdateClassLoaderLocked(null); - addAllConfigSplits(null, splitPaths); - mCachedBaseResourcePath = splitPaths.toArray(new String[splitPaths.size()]); - return; - } + mCachedClassLoaders[0] = mClassLoader; - final ClassLoader parent; - if (parentSplitIdx == -1) { - // The parent is the base APK, so use its ClassLoader as parent - // and its configuration splits as part of our own too. - parent = mClassLoader; - Collections.addAll(splitPaths, mCachedBaseResourcePath); - } else { - parent = mCachedSplitClassLoaders[parentSplitIdx]; - Collections.addAll(splitPaths, mCachedResourcePaths[parentSplitIdx]); + // Never add the base resources here, they always get added no matter what. + for (int configSplitIdx : configSplitIndices) { + splitPaths.add(mSplitResDirs[configSplitIdx - 1]); + } + mCachedResourcePaths[0] = splitPaths.toArray(new String[splitPaths.size()]); + return; } - mCachedSplitClassLoaders[splitIdx] = ApplicationLoaders.getDefault().getClassLoader( - mSplitAppDirs[splitIdx], getTargetSdkVersion(), false, null, null, parent); + // Since we handled the special base case above, parentSplitIdx is always valid. + final ClassLoader parent = mCachedClassLoaders[parentSplitIdx]; + mCachedClassLoaders[splitIdx] = ApplicationLoaders.getDefault().getClassLoader( + mSplitAppDirs[splitIdx - 1], getTargetSdkVersion(), false, null, null, parent); - splitPaths.add(mSplitResDirs[splitIdx]); - addAllConfigSplits(mSplitNames[splitIdx], splitPaths); + Collections.addAll(splitPaths, mCachedResourcePaths[parentSplitIdx]); + splitPaths.add(mSplitResDirs[splitIdx - 1]); + for (int configSplitIdx : configSplitIndices) { + splitPaths.add(mSplitResDirs[configSplitIdx - 1]); + } mCachedResourcePaths[splitIdx] = splitPaths.toArray(new String[splitPaths.size()]); } - private int ensureSplitLoaded(String splitName) - throws PackageManager.NameNotFoundException { - final int idx; - if (splitName == null) { - idx = -1; - } else { + private int ensureSplitLoaded(String splitName) throws NameNotFoundException { + int idx = 0; + if (splitName != null) { idx = Arrays.binarySearch(mSplitNames, splitName); if (idx < 0) { throw new PackageManager.NameNotFoundException( "Split name '" + splitName + "' is not installed"); } + idx += 1; } - loadDependenciesForSplit(idx); return idx; } - ClassLoader getClassLoaderForSplit(String splitName) - throws PackageManager.NameNotFoundException { - final int idx = ensureSplitLoaded(splitName); - if (idx < 0) { - return mClassLoader; - } - return mCachedSplitClassLoaders[idx]; + ClassLoader getClassLoaderForSplit(String splitName) throws NameNotFoundException { + return mCachedClassLoaders[ensureSplitLoaded(splitName)]; } - String[] getSplitPathsForSplit(String splitName) - throws PackageManager.NameNotFoundException { - final int idx = ensureSplitLoaded(splitName); - if (idx < 0) { - return mCachedBaseResourcePath; - } - return mCachedResourcePaths[idx]; + String[] getSplitPathsForSplit(String splitName) throws NameNotFoundException { + return mCachedResourcePaths[ensureSplitLoaded(splitName)]; } } - private SplitDependencyLoader mSplitLoader; + private SplitDependencyLoaderImpl mSplitLoader; - ClassLoader getSplitClassLoader(String splitName) throws PackageManager.NameNotFoundException { + ClassLoader getSplitClassLoader(String splitName) throws NameNotFoundException { if (mSplitLoader == null) { return mClassLoader; } return mSplitLoader.getClassLoaderForSplit(splitName); } - String[] getSplitPaths(String splitName) throws PackageManager.NameNotFoundException { + String[] getSplitPaths(String splitName) throws NameNotFoundException { if (mSplitLoader == null) { return mSplitResDirs; } @@ -925,7 +903,7 @@ public final class LoadedApk { final String[] splitPaths; try { splitPaths = getSplitPaths(null); - } catch (PackageManager.NameNotFoundException e) { + } catch (NameNotFoundException e) { // This should never fail. throw new AssertionError("null split not found"); } diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index b4d77a0fe54a..0b3742f27ddf 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -31,7 +31,7 @@ import android.os.Parcelable; import android.os.UserHandle; import android.text.TextUtils; import android.util.Printer; -import android.util.SparseIntArray; +import android.util.SparseArray; import com.android.internal.util.ArrayUtils; @@ -656,13 +656,15 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * * The keys and values are all indices into the {@link #splitNames}, {@link #splitSourceDirs}, * and {@link #splitPublicSourceDirs} arrays. - * Each key represents a split and its value is its parent split. + * Each key represents a split and its value is an array of splits. The first element of this + * array is the parent split, and the rest are configuration splits. These configuration splits + * have no dependencies themselves. * Cycles do not exist because they are illegal and screened for during installation. * * May be null if no splits are installed, or if no dependencies exist between them. * @hide */ - public SparseIntArray splitDependencies; + public SparseArray<int[]> splitDependencies; /** * Full paths to the locations of extra resource packages (runtime overlays) @@ -1182,7 +1184,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { dest.writeStringArray(splitNames); dest.writeStringArray(splitSourceDirs); dest.writeStringArray(splitPublicSourceDirs); - dest.writeSparseIntArray(splitDependencies); + dest.writeSparseArray((SparseArray) splitDependencies); dest.writeString(nativeLibraryDir); dest.writeString(secondaryNativeLibraryDir); dest.writeString(nativeLibraryRootDir); @@ -1244,7 +1246,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { splitNames = source.readStringArray(); splitSourceDirs = source.readStringArray(); splitPublicSourceDirs = source.readStringArray(); - splitDependencies = source.readSparseIntArray(); + splitDependencies = source.readSparseArray(null); nativeLibraryDir = source.readString(); secondaryNativeLibraryDir = source.readString(); nativeLibraryRootDir = source.readString(); diff --git a/core/java/android/content/pm/InstrumentationInfo.java b/core/java/android/content/pm/InstrumentationInfo.java index 59c530730170..f6f1be63baee 100644 --- a/core/java/android/content/pm/InstrumentationInfo.java +++ b/core/java/android/content/pm/InstrumentationInfo.java @@ -82,13 +82,15 @@ public class InstrumentationInfo extends PackageItemInfo implements Parcelable { * * The keys and values are all indices into the {@link #splitNames}, {@link #splitSourceDirs}, * and {@link #splitPublicSourceDirs} arrays. - * Each key represents a split and its value is its parent split. + * Each key represents a split and its value is an array of splits. The first element of this + * array is the parent split, and the rest are configuration splits. These configuration splits + * have no dependencies themselves. * Cycles do not exist because they are illegal and screened for during installation. * * May be null if no splits are installed, or if no dependencies exist between them. * @hide */ - public SparseIntArray splitDependencies; + public SparseArray<int[]> splitDependencies; /** * Full path to a directory assigned to the package for its persistent data. @@ -155,7 +157,7 @@ public class InstrumentationInfo extends PackageItemInfo implements Parcelable { dest.writeStringArray(splitNames); dest.writeStringArray(splitSourceDirs); dest.writeStringArray(splitPublicSourceDirs); - dest.writeSparseIntArray(splitDependencies); + dest.writeSparseArray((SparseArray) splitDependencies); dest.writeString(dataDir); dest.writeString(deviceProtectedDataDir); dest.writeString(credentialProtectedDataDir); @@ -185,7 +187,7 @@ public class InstrumentationInfo extends PackageItemInfo implements Parcelable { splitNames = source.readStringArray(); splitSourceDirs = source.readStringArray(); splitPublicSourceDirs = source.readStringArray(); - splitDependencies = source.readSparseIntArray(); + splitDependencies = source.readSparseArray(null); dataDir = source.readString(); deviceProtectedDataDir = source.readString(); credentialProtectedDataDir = source.readString(); diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index 5a28e87ac65f..805c8239229d 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -76,7 +76,7 @@ import android.util.DisplayMetrics; import android.util.Log; import android.util.Pair; import android.util.Slog; -import android.util.SparseIntArray; +import android.util.SparseArray; import android.util.TypedValue; import android.util.apk.ApkSignatureSchemeV2Verifier; import android.util.jar.StrictJarFile; @@ -109,7 +109,6 @@ import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; import java.util.ArrayList; import java.util.Arrays; -import java.util.BitSet; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; @@ -368,8 +367,12 @@ public class PackageParser { /** Names of any split APKs, ordered by parsed splitName */ public final String[] splitNames; + /** Names of any split APKs that are features. Ordered by splitName */ + public final boolean[] isFeatureSplits; + /** Dependencies of any split APKs, ordered by parsed splitName */ public final String[] usesSplitNames; + public final String[] configForSplit; /** * Path where this package was found on disk. For monolithic packages @@ -396,14 +399,16 @@ public class PackageParser { public final boolean isolatedSplits; public PackageLite(String codePath, ApkLite baseApk, String[] splitNames, - String[] usesSplitNames, String[] splitCodePaths, - int[] splitRevisionCodes) { + boolean[] isFeatureSplits, String[] usesSplitNames, String[] configForSplit, + String[] splitCodePaths, int[] splitRevisionCodes) { this.packageName = baseApk.packageName; this.versionCode = baseApk.versionCode; this.installLocation = baseApk.installLocation; this.verifiers = baseApk.verifiers; this.splitNames = splitNames; + this.isFeatureSplits = isFeatureSplits; this.usesSplitNames = usesSplitNames; + this.configForSplit = configForSplit; this.codePath = codePath; this.baseCodePath = baseApk.codePath; this.splitCodePaths = splitCodePaths; @@ -434,6 +439,8 @@ public class PackageParser { public final String codePath; public final String packageName; public final String splitName; + public boolean isFeatureSplit; + public final String configForSplit; public final String usesSplitName; public final int versionCode; public final int revisionCode; @@ -448,14 +455,17 @@ public class PackageParser { public final boolean extractNativeLibs; public final boolean isolatedSplits; - public ApkLite(String codePath, String packageName, String splitName, String usesSplitName, - int versionCode, int revisionCode, int installLocation, List<VerifierInfo> verifiers, - Signature[] signatures, Certificate[][] certificates, boolean coreApp, - boolean debuggable, boolean multiArch, boolean use32bitAbi, - boolean extractNativeLibs, boolean isolatedSplits) { + public ApkLite(String codePath, String packageName, String splitName, boolean isFeatureSplit, + String configForSplit, String usesSplitName, int versionCode, int revisionCode, + int installLocation, List<VerifierInfo> verifiers, Signature[] signatures, + Certificate[][] certificates, boolean coreApp, boolean debuggable, + boolean multiArch, boolean use32bitAbi, boolean extractNativeLibs, + boolean isolatedSplits) { this.codePath = codePath; this.packageName = packageName; this.splitName = splitName; + this.isFeatureSplit = isFeatureSplit; + this.configForSplit = configForSplit; this.usesSplitName = usesSplitName; this.versionCode = versionCode; this.revisionCode = revisionCode; @@ -811,10 +821,10 @@ public class PackageParser { final ApkLite baseApk = parseApkLite(packageFile, flags); final String packagePath = packageFile.getAbsolutePath(); Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); - return new PackageLite(packagePath, baseApk, null, null, null, null); + return new PackageLite(packagePath, baseApk, null, null, null, null, null, null); } - private static PackageLite parseClusterPackageLite(File packageDir, int flags) + static PackageLite parseClusterPackageLite(File packageDir, int flags) throws PackageParserException { final File[] files = packageDir.listFiles(); if (ArrayUtils.isEmpty(files)) { @@ -869,12 +879,16 @@ public class PackageParser { final int size = apks.size(); String[] splitNames = null; + boolean[] isFeatureSplits = null; String[] usesSplitNames = null; + String[] configForSplits = null; String[] splitCodePaths = null; int[] splitRevisionCodes = null; if (size > 0) { splitNames = new String[size]; + isFeatureSplits = new boolean[size]; usesSplitNames = new String[size]; + configForSplits = new String[size]; splitCodePaths = new String[size]; splitRevisionCodes = new int[size]; @@ -884,14 +898,16 @@ public class PackageParser { for (int i = 0; i < size; i++) { final ApkLite apk = apks.get(splitNames[i]); usesSplitNames[i] = apk.usesSplitName; + isFeatureSplits[i] = apk.isFeatureSplit; + configForSplits[i] = apk.configForSplit; splitCodePaths[i] = apk.codePath; splitRevisionCodes[i] = apk.revisionCode; } } final String codePath = packageDir.getAbsolutePath(); - return new PackageLite(codePath, baseApk, splitNames, usesSplitNames, splitCodePaths, - splitRevisionCodes); + return new PackageLite(codePath, baseApk, splitNames, isFeatureSplits, usesSplitNames, + configForSplits, splitCodePaths, splitRevisionCodes); } /** @@ -1069,42 +1085,6 @@ public class PackageParser { } } - private static SparseIntArray buildSplitDependencyTree(PackageLite pkg) - throws PackageParserException { - SparseIntArray splitDependencies = new SparseIntArray(); - for (int splitIdx = 0; splitIdx < pkg.splitNames.length; splitIdx++) { - final String splitDependency = pkg.usesSplitNames[splitIdx]; - if (splitDependency != null) { - final int depIdx = Arrays.binarySearch(pkg.splitNames, splitDependency); - if (depIdx < 0) { - throw new PackageParserException( - PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST, - "Split '" + pkg.splitNames[splitIdx] + "' requires split '" - + splitDependency + "', which is missing."); - } - splitDependencies.put(splitIdx, depIdx); - } - } - - // Verify that there are no cycles. - final BitSet bitset = new BitSet(); - for (int i = 0; i < splitDependencies.size(); i++) { - int splitIdx = splitDependencies.keyAt(i); - - bitset.clear(); - while (splitIdx != -1) { - if (bitset.get(splitIdx)) { - throw new PackageParserException( - PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST, - "Cycle detected in split dependencies."); - } - bitset.set(splitIdx); - splitIdx = splitDependencies.get(splitIdx, -1); - } - } - return splitDependencies.size() != 0 ? splitDependencies : null; - } - /** * Parse all APKs contained in the given directory, treating them as a * single package. This also performs sanity checking, such as requiring @@ -1122,11 +1102,15 @@ public class PackageParser { } // Build the split dependency tree. - SparseIntArray splitDependencies = null; + SparseArray<int[]> splitDependencies = null; final SplitAssetLoader assetLoader; if (lite.isolatedSplits && !ArrayUtils.isEmpty(lite.splitNames)) { - splitDependencies = buildSplitDependencyTree(lite); - assetLoader = new SplitAssetDependencyLoader(lite, splitDependencies, flags); + try { + splitDependencies = SplitAssetDependencyLoader.createDependenciesFromPackage(lite); + assetLoader = new SplitAssetDependencyLoader(lite, splitDependencies, flags); + } catch (SplitAssetDependencyLoader.IllegalDependencyException e) { + throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST, e.getMessage()); + } } else { assetLoader = new DefaultSplitAssetLoader(lite, flags); } @@ -1762,6 +1746,8 @@ public class PackageParser { boolean use32bitAbi = false; boolean extractNativeLibs = true; boolean isolatedSplits = false; + boolean isFeatureSplit = false; + String configForSplit = null; String usesSplitName = null; for (int i = 0; i < attrs.getAttributeCount(); i++) { @@ -1777,6 +1763,10 @@ public class PackageParser { coreApp = attrs.getAttributeBooleanValue(i, false); } else if (attr.equals("isolatedSplits")) { isolatedSplits = attrs.getAttributeBooleanValue(i, false); + } else if (attr.equals("configForSplit")) { + configForSplit = attrs.getAttributeValue(i); + } else if (attr.equals("isFeatureSplit")) { + isFeatureSplit = attrs.getAttributeBooleanValue(i, false); } } @@ -1831,10 +1821,10 @@ public class PackageParser { } } - return new ApkLite(codePath, packageSplit.first, packageSplit.second, usesSplitName, - versionCode, revisionCode, installLocation, verifiers, signatures, - certificates, coreApp, debuggable, multiArch, use32bitAbi, extractNativeLibs, - isolatedSplits); + return new ApkLite(codePath, packageSplit.first, packageSplit.second, isFeatureSplit, + configForSplit, usesSplitName, versionCode, revisionCode, installLocation, + verifiers, signatures, certificates, coreApp, debuggable, multiArch, use32bitAbi, + extractNativeLibs, isolatedSplits); } /** diff --git a/core/java/android/content/pm/split/SplitAssetDependencyLoader.java b/core/java/android/content/pm/split/SplitAssetDependencyLoader.java index 4df90eb9f53e..16023f0d9d97 100644 --- a/core/java/android/content/pm/split/SplitAssetDependencyLoader.java +++ b/core/java/android/content/pm/split/SplitAssetDependencyLoader.java @@ -18,10 +18,11 @@ package android.content.pm.split; import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST; import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_NOT_APK; +import android.annotation.NonNull; import android.content.pm.PackageParser; import android.content.res.AssetManager; import android.os.Build; -import android.util.SparseIntArray; +import android.util.SparseArray; import libcore.io.IoUtils; @@ -34,49 +35,31 @@ import java.util.Collections; * @hide */ public class SplitAssetDependencyLoader - extends SplitDependencyLoaderHelper<PackageParser.PackageParserException> + extends SplitDependencyLoader<PackageParser.PackageParserException> implements SplitAssetLoader { - private static final int BASE_ASSET_PATH_IDX = -1; - private final String mBasePath; - private final String[] mSplitNames; private final String[] mSplitPaths; private final int mFlags; - private String[] mCachedBasePaths; - private AssetManager mCachedBaseAssetManager; - - private String[][] mCachedSplitPaths; + private String[][] mCachedPaths; private AssetManager[] mCachedAssetManagers; - public SplitAssetDependencyLoader(PackageParser.PackageLite pkg, SparseIntArray dependencies, - int flags) { + public SplitAssetDependencyLoader(PackageParser.PackageLite pkg, + SparseArray<int[]> dependencies, int flags) { super(dependencies); - mBasePath = pkg.baseCodePath; - mSplitNames = pkg.splitNames; - mSplitPaths = pkg.splitCodePaths; + + // The base is inserted into index 0, so we need to shift all the splits by 1. + mSplitPaths = new String[pkg.splitCodePaths.length + 1]; + mSplitPaths[0] = pkg.baseCodePath; + System.arraycopy(pkg.splitCodePaths, 0, mSplitPaths, 1, pkg.splitCodePaths.length); + mFlags = flags; - mCachedBasePaths = null; - mCachedBaseAssetManager = null; - mCachedSplitPaths = new String[mSplitNames.length][]; - mCachedAssetManagers = new AssetManager[mSplitNames.length]; + mCachedPaths = new String[mSplitPaths.length][]; + mCachedAssetManagers = new AssetManager[mSplitPaths.length]; } @Override protected boolean isSplitCached(int splitIdx) { - if (splitIdx != -1) { - return mCachedAssetManagers[splitIdx] != null; - } - return mCachedBaseAssetManager != null; - } - - // Adds all non-code configuration splits for this split name. The split name is expected - // to represent a feature split. - private void addAllConfigSplits(String splitName, ArrayList<String> outAssetPaths) { - for (int i = 0; i < mSplitNames.length; i++) { - if (isConfigurationSplitOf(mSplitNames[i], splitName)) { - outAssetPaths.add(mSplitPaths[i]); - } - } + return mCachedAssetManagers[splitIdx] != null; } private static AssetManager createAssetManagerWithPaths(String[] assetPaths, int flags) @@ -107,45 +90,38 @@ public class SplitAssetDependencyLoader } @Override - protected void constructSplit(int splitIdx, int parentSplitIdx) throws - PackageParser.PackageParserException { + protected void constructSplit(int splitIdx, @NonNull int[] configSplitIndices, + int parentSplitIdx) throws PackageParser.PackageParserException { final ArrayList<String> assetPaths = new ArrayList<>(); - if (splitIdx == BASE_ASSET_PATH_IDX) { - assetPaths.add(mBasePath); - addAllConfigSplits(null, assetPaths); - mCachedBasePaths = assetPaths.toArray(new String[assetPaths.size()]); - mCachedBaseAssetManager = createAssetManagerWithPaths(mCachedBasePaths, mFlags); - return; - } - - if (parentSplitIdx == BASE_ASSET_PATH_IDX) { - Collections.addAll(assetPaths, mCachedBasePaths); - } else { - Collections.addAll(assetPaths, mCachedSplitPaths[parentSplitIdx]); + if (parentSplitIdx >= 0) { + Collections.addAll(assetPaths, mCachedPaths[parentSplitIdx]); } assetPaths.add(mSplitPaths[splitIdx]); - addAllConfigSplits(mSplitNames[splitIdx], assetPaths); - mCachedSplitPaths[splitIdx] = assetPaths.toArray(new String[assetPaths.size()]); - mCachedAssetManagers[splitIdx] = createAssetManagerWithPaths(mCachedSplitPaths[splitIdx], + for (int configSplitIdx : configSplitIndices) { + assetPaths.add(mSplitPaths[configSplitIdx]); + } + mCachedPaths[splitIdx] = assetPaths.toArray(new String[assetPaths.size()]); + mCachedAssetManagers[splitIdx] = createAssetManagerWithPaths(mCachedPaths[splitIdx], mFlags); } @Override public AssetManager getBaseAssetManager() throws PackageParser.PackageParserException { - loadDependenciesForSplit(BASE_ASSET_PATH_IDX); - return mCachedBaseAssetManager; + loadDependenciesForSplit(0); + return mCachedAssetManagers[0]; } @Override public AssetManager getSplitAssetManager(int idx) throws PackageParser.PackageParserException { - loadDependenciesForSplit(idx); - return mCachedAssetManagers[idx]; + // Since we insert the base at position 0, and PackageParser keeps splits separate from + // the base, we need to adjust the index. + loadDependenciesForSplit(idx + 1); + return mCachedAssetManagers[idx + 1]; } @Override public void close() throws Exception { - IoUtils.closeQuietly(mCachedBaseAssetManager); for (AssetManager assets : mCachedAssetManagers) { IoUtils.closeQuietly(assets); } diff --git a/core/java/android/content/pm/split/SplitDependencyLoader.java b/core/java/android/content/pm/split/SplitDependencyLoader.java new file mode 100644 index 000000000000..358654692c9e --- /dev/null +++ b/core/java/android/content/pm/split/SplitDependencyLoader.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2016 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.pm.split; + +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.content.pm.PackageParser; +import android.util.IntArray; +import android.util.SparseArray; + +import libcore.util.EmptyArray; + +import java.util.Arrays; +import java.util.BitSet; + +/** + * A helper class that implements the dependency tree traversal for splits. Callbacks + * are implemented by subclasses to notify whether a split has already been constructed + * and is cached, and to actually create the split requested. + * + * This helper is meant to be subclassed so as to reduce the number of allocations + * needed to make use of it. + * + * All inputs and outputs are assumed to be indices into an array of splits. + * + * @hide + */ +public abstract class SplitDependencyLoader<E extends Exception> { + private final @NonNull SparseArray<int[]> mDependencies; + + /** + * Construct a new SplitDependencyLoader. Meant to be called from the + * subclass constructor. + * @param dependencies The dependency tree of splits. + */ + protected SplitDependencyLoader(@NonNull SparseArray<int[]> dependencies) { + mDependencies = dependencies; + } + + /** + * Traverses the dependency tree and constructs any splits that are not already + * cached. This routine short-circuits and skips the creation of splits closer to the + * root if they are cached, as reported by the subclass implementation of + * {@link #isSplitCached(int)}. The construction of splits is delegated to the subclass + * implementation of {@link #constructSplit(int, int[], int)}. + * @param splitIdx The index of the split to load. 0 represents the base Application. + */ + protected void loadDependenciesForSplit(@IntRange(from = 0) int splitIdx) throws E { + // Quick check before any allocations are done. + if (isSplitCached(splitIdx)) { + return; + } + + // Special case the base, since it has no dependencies. + if (splitIdx == 0) { + final int[] configSplitIndices = collectConfigSplitIndices(0); + constructSplit(0, configSplitIndices, -1); + return; + } + + // Build up the dependency hierarchy. + final IntArray linearDependencies = new IntArray(); + linearDependencies.add(splitIdx); + + // Collect all the dependencies that need to be constructed. + // They will be listed from leaf to root. + while (true) { + // Only follow the first index into the array. The others are config splits and + // get loaded with the split. + final int[] deps = mDependencies.get(splitIdx); + if (deps != null && deps.length > 0) { + splitIdx = deps[0]; + } else { + splitIdx = -1; + } + + if (splitIdx < 0 || isSplitCached(splitIdx)) { + break; + } + + linearDependencies.add(splitIdx); + } + + // Visit each index, from right to left (root to leaf). + int parentIdx = splitIdx; + for (int i = linearDependencies.size() - 1; i >= 0; i--) { + final int idx = linearDependencies.get(i); + final int[] configSplitIndices = collectConfigSplitIndices(idx); + constructSplit(idx, configSplitIndices, parentIdx); + parentIdx = idx; + } + } + + private @NonNull int[] collectConfigSplitIndices(int splitIdx) { + // The config splits appear after the first element. + final int[] deps = mDependencies.get(splitIdx); + if (deps == null || deps.length <= 1) { + return EmptyArray.INT; + } + return Arrays.copyOfRange(deps, 1, deps.length); + } + + /** + * Subclass to report whether the split at `splitIdx` is cached and need not be constructed. + * It is assumed that if `splitIdx` is cached, any parent of `splitIdx` is also cached. + * @param splitIdx The index of the split to check for in the cache. + * @return true if the split is cached and does not need to be constructed. + */ + protected abstract boolean isSplitCached(@IntRange(from = 0) int splitIdx); + + /** + * Subclass to construct a split at index `splitIdx` with parent split `parentSplitIdx`. + * The result is expected to be cached by the subclass in its own structures. + * @param splitIdx The index of the split to construct. 0 represents the base Application. + * @param configSplitIndices The array of configuration splits to load along with this split. + * May be empty (length == 0) but never null. + * @param parentSplitIdx The index of the parent split. -1 if there is no parent. + * @throws E Subclass defined exception representing failure to construct a split. + */ + protected abstract void constructSplit(@IntRange(from = 0) int splitIdx, + @NonNull @IntRange(from = 1) int[] configSplitIndices, + @IntRange(from = -1) int parentSplitIdx) throws E; + + public static class IllegalDependencyException extends Exception { + private IllegalDependencyException(String message) { + super(message); + } + } + + private static int[] append(int[] src, int elem) { + if (src == null) { + return new int[] { elem }; + } + int[] dst = Arrays.copyOf(src, src.length + 1); + dst[src.length] = elem; + return dst; + } + + public static @NonNull SparseArray<int[]> createDependenciesFromPackage( + PackageParser.PackageLite pkg) throws IllegalDependencyException { + // The data structure that holds the dependencies. In PackageParser, splits are stored + // in their own array, separate from the base. We treat all paths as equals, so + // we need to insert the base as index 0, and shift all other splits. + final SparseArray<int[]> splitDependencies = new SparseArray<>(); + + // The base depends on nothing. + splitDependencies.put(0, new int[] {-1}); + + // First write out the <uses-split> dependencies. These must appear first in the + // array of ints, as is convention in this class. + for (int splitIdx = 0; splitIdx < pkg.splitNames.length; splitIdx++) { + if (!pkg.isFeatureSplits[splitIdx]) { + // Non-feature splits don't have dependencies. + continue; + } + + // Implicit dependency on the base. + final int targetIdx; + final String splitDependency = pkg.usesSplitNames[splitIdx]; + if (splitDependency != null) { + final int depIdx = Arrays.binarySearch(pkg.splitNames, splitDependency); + if (depIdx < 0) { + throw new IllegalDependencyException("Split '" + pkg.splitNames[splitIdx] + + "' requires split '" + splitDependency + "', which is missing."); + } + targetIdx = depIdx + 1; + } else { + // Implicitly depend on the base. + targetIdx = 0; + } + splitDependencies.put(splitIdx + 1, new int[] {targetIdx}); + } + + // Write out the configForSplit reverse-dependencies. These appear after the <uses-split> + // dependencies and are considered leaves. + // + // At this point, all splits in splitDependencies have the first element in their array set. + for (int splitIdx = 0; splitIdx < pkg.splitNames.length; splitIdx++) { + if (pkg.isFeatureSplits[splitIdx]) { + // Feature splits are not configForSplits. + continue; + } + + // Implicit feature for the base. + final int targetSplitIdx; + final String configForSplit = pkg.configForSplit[splitIdx]; + if (configForSplit != null) { + final int depIdx = Arrays.binarySearch(pkg.splitNames, configForSplit); + if (depIdx < 0) { + throw new IllegalDependencyException("Split '" + pkg.splitNames[splitIdx] + + "' targets split '" + configForSplit + "', which is missing."); + } + + if (!pkg.isFeatureSplits[depIdx]) { + throw new IllegalDependencyException("Split '" + pkg.splitNames[splitIdx] + + "' declares itself as configuration split for a non-feature split '" + + pkg.splitNames[depIdx] + "'"); + } + targetSplitIdx = depIdx + 1; + } else { + targetSplitIdx = 0; + } + splitDependencies.put(targetSplitIdx, + append(splitDependencies.get(targetSplitIdx), splitIdx + 1)); + } + + // Verify that there are no cycles. + final BitSet bitset = new BitSet(); + for (int i = 0, size = splitDependencies.size(); i < size; i++) { + int splitIdx = splitDependencies.keyAt(i); + + bitset.clear(); + while (splitIdx != -1) { + // Check if this split has been visited yet. + if (bitset.get(splitIdx)) { + throw new IllegalDependencyException("Cycle detected in split dependencies."); + } + + // Mark the split so that if we visit it again, we no there is a cycle. + bitset.set(splitIdx); + + // Follow the first dependency only, the others are leaves by definition. + final int[] deps = splitDependencies.get(splitIdx); + splitIdx = deps != null ? deps[0] : -1; + } + } + return splitDependencies; + } +} diff --git a/core/java/android/content/pm/split/SplitDependencyLoaderHelper.java b/core/java/android/content/pm/split/SplitDependencyLoaderHelper.java deleted file mode 100644 index b49348000449..000000000000 --- a/core/java/android/content/pm/split/SplitDependencyLoaderHelper.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright (C) 2016 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.pm.split; - -import android.annotation.Nullable; -import android.util.IntArray; -import android.util.SparseIntArray; - -/** - * A helper class that implements the dependency tree traversal for splits. Callbacks - * are implemented by subclasses to notify whether a split has already been constructed - * and is cached, and to actually create the split requested. - * - * This helper is meant to be subclassed so as to reduce the number of allocations - * needed to make use of it. - * - * All inputs and outputs are assumed to be indices into an array of splits. - * - * @hide - */ -public abstract class SplitDependencyLoaderHelper<E extends Exception> { - @Nullable private final SparseIntArray mDependencies; - - /** - * Construct a new SplitDependencyLoaderHelper. Meant to be called from the - * subclass constructor. - * @param dependencies The dependency tree of splits. Can be null, which leads to - * just the implicit dependency of all splits on the base. - */ - protected SplitDependencyLoaderHelper(@Nullable SparseIntArray dependencies) { - mDependencies = dependencies; - } - - /** - * Traverses the dependency tree and constructs any splits that are not already - * cached. This routine short-circuits and skips the creation of splits closer to the - * root if they are cached, as reported by the subclass implementation of - * {@link #isSplitCached(int)}. The construction of splits is delegated to the subclass - * implementation of {@link #constructSplit(int, int)}. - * @param splitIdx The index of the split to load. Can be -1, which represents the - * base Application. - */ - protected void loadDependenciesForSplit(int splitIdx) throws E { - // Quick check before any allocations are done. - if (isSplitCached(splitIdx)) { - return; - } - - final IntArray linearDependencies = new IntArray(); - linearDependencies.add(splitIdx); - - // Collect all the dependencies that need to be constructed. - // They will be listed from leaf to root. - while (splitIdx >= 0) { - splitIdx = mDependencies != null ? mDependencies.get(splitIdx, -1) : -1; - if (isSplitCached(splitIdx)) { - break; - } - linearDependencies.add(splitIdx); - } - - // Visit each index, from right to left (root to leaf). - int parentIdx = splitIdx; - for (int i = linearDependencies.size() - 1; i >= 0; i--) { - final int idx = linearDependencies.get(i); - constructSplit(idx, parentIdx); - parentIdx = idx; - } - } - - /** - * Subclass to report whether the split at `splitIdx` is cached and need not be constructed. - * It is assumed that if `splitIdx` is cached, any parent of `splitIdx` is also cached. - * @param splitIdx The index of the split to check for in the cache. - * @return true if the split is cached and does not need to be constructed. - */ - protected abstract boolean isSplitCached(int splitIdx); - - /** - * Subclass to construct a split at index `splitIdx` with parent split `parentSplitIdx`. - * The result is expected to be cached by the subclass in its own structures. - * @param splitIdx The index of the split to construct. Can be -1, which represents the - * base Application. - * @param parentSplitIdx The index of the parent split. Can be -1, which represents the - * base Application. - * @throws E - */ - protected abstract void constructSplit(int splitIdx, int parentSplitIdx) throws E; - - /** - * Returns true if `splitName` represents a Configuration split of `featureSplitName`. - * - * A Configuration split's name is prefixed with the associated Feature split's name - * or the empty string if the split is for the base Application APK. It is then followed by the - * dollar sign character "$" and some unique string that should represent the configurations - * the split contains. - * - * Example: - * <table> - * <tr> - * <th>Feature split name</th> - * <th>Configuration split name: xhdpi</th> - * <th>Configuration split name: fr-rFR</th> - * </tr> - * <tr> - * <td>(base APK)</td> - * <td><code>$xhdpi</code></td> - * <td><code>$fr-rFR</code></td> - * </tr> - * <tr> - * <td><code>Extras</code></td> - * <td><code>Extras$xhdpi</code></td> - * <td><code>Extras$fr-rFR</code></td> - * </tr> - * </table> - * - * @param splitName The name of the split to check. - * @param featureSplitName The name of the Feature split. May be null or "" if checking - * the base Application APK. - * @return true if the splitName represents a Configuration split of featureSplitName. - */ - protected static boolean isConfigurationSplitOf(String splitName, String featureSplitName) { - if (featureSplitName == null || featureSplitName.length() == 0) { - // We are looking for configuration splits of the base, which have some legacy support. - if (splitName.startsWith("config_")) { - return true; - } else if (splitName.startsWith("$")) { - return true; - } else { - return false; - } - } else { - return splitName.startsWith(featureSplitName + "$"); - } - } -} diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index 67050f751f5e..cf6bd9e0d22f 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -1014,6 +1014,15 @@ <p>The default value of this attribute is <code>false</code>. --> <attr name="isolatedSplits" format="boolean" /> + <!-- If set to <code>true</code>, indicates to the platform that this APK is + a 'feature' split and that it implicitly depends on the base APK. This distinguishes + this split APK from a 'configuration' split, which provides resource overrides + for a particular 'feature' split. Only useful when the base APK specifies + <code>android:isolatedSplits="true"</code>. + + <p>The default value of this attribute is <code>false</code>. --> + <attr name="isFeatureSplit" format="boolean" /> + <!-- Extra options for an activity's UI. Applies to either the {@code <activity>} or {@code <application>} tag. If specified on the {@code <application>} tag these will be considered defaults for all activities in the @@ -1286,6 +1295,7 @@ <attr name="sharedUserLabel" /> <attr name="installLocation" /> <attr name="isolatedSplits" /> + <attr name="isFeatureSplit" /> <attr name="targetSandboxVersion" /> </declare-styleable> diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index 2897c62a8aa5..f965c6954948 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -2807,6 +2807,7 @@ <public name="importantForAutofill" /> <public name="recycleEnabled"/> <public name="isStatic" /> + <public name="isFeatureSplit" /> </public-group> <public-group type="style" first-id="0x010302e0"> diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 0ec85aa993cb..2e4a3a3f7968 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -915,7 +915,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { // This is kind of hacky; we're creating a half-parsed package that is // straddled between the inherited and staged APKs. - final PackageLite pkg = new PackageLite(null, baseApk, null, null, + final PackageLite pkg = new PackageLite(null, baseApk, null, null, null, null, splitPaths.toArray(new String[splitPaths.size()]), null); final boolean isForwardLocked = (params.installFlags & PackageManager.INSTALL_FORWARD_LOCK) != 0; diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index a7349fc8454c..728b406e16d5 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -169,7 +169,8 @@ class PackageManagerShellCommand extends ShellCommand { if (file.isFile()) { try { ApkLite baseApk = PackageParser.parseApkLite(file, 0); - PackageLite pkgLite = new PackageLite(null, baseApk, null, null, null, null); + PackageLite pkgLite = new PackageLite(null, baseApk, null, null, null, null, + null, null); params.sessionParams.setSize(PackageHelper.calculateInstalledSize( pkgLite, false, params.sessionParams.abiOverride)); } catch (PackageParserException | IOException e) { |