Home | History | Annotate | Download | only in split
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package android.content.pm.split;
     17 
     18 import android.annotation.IntRange;
     19 import android.annotation.NonNull;
     20 import android.content.pm.PackageParser;
     21 import android.util.IntArray;
     22 import android.util.SparseArray;
     23 
     24 import libcore.util.EmptyArray;
     25 
     26 import java.util.Arrays;
     27 import java.util.BitSet;
     28 
     29 /**
     30  * A helper class that implements the dependency tree traversal for splits. Callbacks
     31  * are implemented by subclasses to notify whether a split has already been constructed
     32  * and is cached, and to actually create the split requested.
     33  *
     34  * This helper is meant to be subclassed so as to reduce the number of allocations
     35  * needed to make use of it.
     36  *
     37  * All inputs and outputs are assumed to be indices into an array of splits.
     38  *
     39  * @hide
     40  */
     41 public abstract class SplitDependencyLoader<E extends Exception> {
     42     private final @NonNull SparseArray<int[]> mDependencies;
     43 
     44     /**
     45      * Construct a new SplitDependencyLoader. Meant to be called from the
     46      * subclass constructor.
     47      * @param dependencies The dependency tree of splits.
     48      */
     49     protected SplitDependencyLoader(@NonNull SparseArray<int[]> dependencies) {
     50         mDependencies = dependencies;
     51     }
     52 
     53     /**
     54      * Traverses the dependency tree and constructs any splits that are not already
     55      * cached. This routine short-circuits and skips the creation of splits closer to the
     56      * root if they are cached, as reported by the subclass implementation of
     57      * {@link #isSplitCached(int)}. The construction of splits is delegated to the subclass
     58      * implementation of {@link #constructSplit(int, int[], int)}.
     59      * @param splitIdx The index of the split to load. 0 represents the base Application.
     60      */
     61     protected void loadDependenciesForSplit(@IntRange(from = 0) int splitIdx) throws E {
     62         // Quick check before any allocations are done.
     63         if (isSplitCached(splitIdx)) {
     64             return;
     65         }
     66 
     67         // Special case the base, since it has no dependencies.
     68         if (splitIdx == 0) {
     69             final int[] configSplitIndices = collectConfigSplitIndices(0);
     70             constructSplit(0, configSplitIndices, -1);
     71             return;
     72         }
     73 
     74         // Build up the dependency hierarchy.
     75         final IntArray linearDependencies = new IntArray();
     76         linearDependencies.add(splitIdx);
     77 
     78         // Collect all the dependencies that need to be constructed.
     79         // They will be listed from leaf to root.
     80         while (true) {
     81             // Only follow the first index into the array. The others are config splits and
     82             // get loaded with the split.
     83             final int[] deps = mDependencies.get(splitIdx);
     84             if (deps != null && deps.length > 0) {
     85                 splitIdx = deps[0];
     86             } else {
     87                 splitIdx = -1;
     88             }
     89 
     90             if (splitIdx < 0 || isSplitCached(splitIdx)) {
     91                 break;
     92             }
     93 
     94             linearDependencies.add(splitIdx);
     95         }
     96 
     97         // Visit each index, from right to left (root to leaf).
     98         int parentIdx = splitIdx;
     99         for (int i = linearDependencies.size() - 1; i >= 0; i--) {
    100             final int idx = linearDependencies.get(i);
    101             final int[] configSplitIndices = collectConfigSplitIndices(idx);
    102             constructSplit(idx, configSplitIndices, parentIdx);
    103             parentIdx = idx;
    104         }
    105     }
    106 
    107     private @NonNull int[] collectConfigSplitIndices(int splitIdx) {
    108         // The config splits appear after the first element.
    109         final int[] deps = mDependencies.get(splitIdx);
    110         if (deps == null || deps.length <= 1) {
    111             return EmptyArray.INT;
    112         }
    113         return Arrays.copyOfRange(deps, 1, deps.length);
    114     }
    115 
    116     /**
    117      * Subclass to report whether the split at `splitIdx` is cached and need not be constructed.
    118      * It is assumed that if `splitIdx` is cached, any parent of `splitIdx` is also cached.
    119      * @param splitIdx The index of the split to check for in the cache.
    120      * @return true if the split is cached and does not need to be constructed.
    121      */
    122     protected abstract boolean isSplitCached(@IntRange(from = 0) int splitIdx);
    123 
    124     /**
    125      * Subclass to construct a split at index `splitIdx` with parent split `parentSplitIdx`.
    126      * The result is expected to be cached by the subclass in its own structures.
    127      * @param splitIdx The index of the split to construct. 0 represents the base Application.
    128      * @param configSplitIndices The array of configuration splits to load along with this split.
    129      *                           May be empty (length == 0) but never null.
    130      * @param parentSplitIdx The index of the parent split. -1 if there is no parent.
    131      * @throws E Subclass defined exception representing failure to construct a split.
    132      */
    133     protected abstract void constructSplit(@IntRange(from = 0) int splitIdx,
    134             @NonNull @IntRange(from = 1) int[] configSplitIndices,
    135             @IntRange(from = -1) int parentSplitIdx) throws E;
    136 
    137     public static class IllegalDependencyException extends Exception {
    138         private IllegalDependencyException(String message) {
    139             super(message);
    140         }
    141     }
    142 
    143     private static int[] append(int[] src, int elem) {
    144         if (src == null) {
    145             return new int[] { elem };
    146         }
    147         int[] dst = Arrays.copyOf(src, src.length + 1);
    148         dst[src.length] = elem;
    149         return dst;
    150     }
    151 
    152     public static @NonNull SparseArray<int[]> createDependenciesFromPackage(
    153             PackageParser.PackageLite pkg) throws IllegalDependencyException {
    154         // The data structure that holds the dependencies. In PackageParser, splits are stored
    155         // in their own array, separate from the base. We treat all paths as equals, so
    156         // we need to insert the base as index 0, and shift all other splits.
    157         final SparseArray<int[]> splitDependencies = new SparseArray<>();
    158 
    159         // The base depends on nothing.
    160         splitDependencies.put(0, new int[] {-1});
    161 
    162         // First write out the <uses-split> dependencies. These must appear first in the
    163         // array of ints, as is convention in this class.
    164         for (int splitIdx = 0; splitIdx < pkg.splitNames.length; splitIdx++) {
    165             if (!pkg.isFeatureSplits[splitIdx]) {
    166                 // Non-feature splits don't have dependencies.
    167                 continue;
    168             }
    169 
    170             // Implicit dependency on the base.
    171             final int targetIdx;
    172             final String splitDependency = pkg.usesSplitNames[splitIdx];
    173             if (splitDependency != null) {
    174                 final int depIdx = Arrays.binarySearch(pkg.splitNames, splitDependency);
    175                 if (depIdx < 0) {
    176                     throw new IllegalDependencyException("Split '" + pkg.splitNames[splitIdx]
    177                             + "' requires split '" + splitDependency + "', which is missing.");
    178                 }
    179                 targetIdx = depIdx + 1;
    180             } else {
    181                 // Implicitly depend on the base.
    182                 targetIdx = 0;
    183             }
    184             splitDependencies.put(splitIdx + 1, new int[] {targetIdx});
    185         }
    186 
    187         // Write out the configForSplit reverse-dependencies. These appear after the <uses-split>
    188         // dependencies and are considered leaves.
    189         //
    190         // At this point, all splits in splitDependencies have the first element in their array set.
    191         for (int splitIdx = 0; splitIdx < pkg.splitNames.length; splitIdx++) {
    192             if (pkg.isFeatureSplits[splitIdx]) {
    193                 // Feature splits are not configForSplits.
    194                 continue;
    195             }
    196 
    197             // Implicit feature for the base.
    198             final int targetSplitIdx;
    199             final String configForSplit = pkg.configForSplit[splitIdx];
    200             if (configForSplit != null) {
    201                 final int depIdx = Arrays.binarySearch(pkg.splitNames, configForSplit);
    202                 if (depIdx < 0) {
    203                     throw new IllegalDependencyException("Split '" + pkg.splitNames[splitIdx]
    204                             + "' targets split '" + configForSplit + "', which is missing.");
    205                 }
    206 
    207                 if (!pkg.isFeatureSplits[depIdx]) {
    208                     throw new IllegalDependencyException("Split '" + pkg.splitNames[splitIdx]
    209                             + "' declares itself as configuration split for a non-feature split '"
    210                             + pkg.splitNames[depIdx] + "'");
    211                 }
    212                 targetSplitIdx = depIdx + 1;
    213             } else {
    214                 targetSplitIdx = 0;
    215             }
    216             splitDependencies.put(targetSplitIdx,
    217                     append(splitDependencies.get(targetSplitIdx), splitIdx + 1));
    218         }
    219 
    220         // Verify that there are no cycles.
    221         final BitSet bitset = new BitSet();
    222         for (int i = 0, size = splitDependencies.size(); i < size; i++) {
    223             int splitIdx = splitDependencies.keyAt(i);
    224 
    225             bitset.clear();
    226             while (splitIdx != -1) {
    227                 // Check if this split has been visited yet.
    228                 if (bitset.get(splitIdx)) {
    229                     throw new IllegalDependencyException("Cycle detected in split dependencies.");
    230                 }
    231 
    232                 // Mark the split so that if we visit it again, we no there is a cycle.
    233                 bitset.set(splitIdx);
    234 
    235                 // Follow the first dependency only, the others are leaves by definition.
    236                 final int[] deps = splitDependencies.get(splitIdx);
    237                 splitIdx = deps != null ? deps[0] : -1;
    238             }
    239         }
    240         return splitDependencies;
    241     }
    242 }
    243