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 */
16package android.content.pm.split;
17
18import android.annotation.IntRange;
19import android.annotation.NonNull;
20import android.content.pm.PackageParser;
21import android.util.IntArray;
22import android.util.SparseArray;
23
24import libcore.util.EmptyArray;
25
26import java.util.Arrays;
27import 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 */
41public 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