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
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index ede9281..8a3d9b1 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -156,13 +156,13 @@
@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 @@
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 @@
}
}
- 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
}
- 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 @@
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 @@
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 cf41e4e..dbed1be 100644
--- a/core/java/android/app/LoadedApk.java
+++ b/core/java/android/app/LoadedApk.java
@@ -27,7 +27,8 @@
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.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 @@
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 @@
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 @@
}
}
- 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()]);
+ mCachedClassLoaders[0] = mClassLoader;
+
+ // 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;
}
- 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]);
+ // 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);
+
+ Collections.addAll(splitPaths, mCachedResourcePaths[parentSplitIdx]);
+ splitPaths.add(mSplitResDirs[splitIdx - 1]);
+ for (int configSplitIdx : configSplitIndices) {
+ splitPaths.add(mSplitResDirs[configSplitIdx - 1]);
}
-
- mCachedSplitClassLoaders[splitIdx] = ApplicationLoaders.getDefault().getClassLoader(
- mSplitAppDirs[splitIdx], getTargetSdkVersion(), false, null, null, parent);
-
- splitPaths.add(mSplitResDirs[splitIdx]);
- addAllConfigSplits(mSplitNames[splitIdx], splitPaths);
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 @@
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 b4d77a0..0b3742f 100644
--- a/core/java/android/content/pm/ApplicationInfo.java
+++ b/core/java/android/content/pm/ApplicationInfo.java
@@ -31,7 +31,7 @@
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 @@
*
* 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 @@
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 @@
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 59c5307..f6f1be6 100644
--- a/core/java/android/content/pm/InstrumentationInfo.java
+++ b/core/java/android/content/pm/InstrumentationInfo.java
@@ -82,13 +82,15 @@
*
* 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 @@
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 @@
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 5a28e87..805c823 100644
--- a/core/java/android/content/pm/PackageParser.java
+++ b/core/java/android/content/pm/PackageParser.java
@@ -76,7 +76,7 @@
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.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 @@
/** 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 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 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 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 @@
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 @@
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 @@
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 @@
}
}
- 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 @@
}
// 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 @@
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 @@
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 @@
}
}
- 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 4df90eb..16023f0 100644
--- a/core/java/android/content/pm/split/SplitAssetDependencyLoader.java
+++ b/core/java/android/content/pm/split/SplitAssetDependencyLoader.java
@@ -18,10 +18,11 @@
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 @@
* @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 @@
}
@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 0000000..3586546
--- /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 b493480..0000000
--- 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 67050f7..cf6bd9e 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 2897c62..f965c69 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">