summaryrefslogtreecommitdiff
path: root/core/java/android/content/pm/split/SplitDependencyLoader.java
blob: 3e681327e0c0a73cf21c0c62f651bafd71485ef4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
/*
 * 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.parsing.PackageLite;
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;
    }

    /**
     * Build the split dependency tree by the given package
     *
     * @param pkg The package to retrieve the dependency tree
     * @return The dependency tree of splits
     * @throws IllegalDependencyException if the requires split is missing, targets split is
     *         missing, it declares itself as configuration split for a non-feature split, or
     *         cycle detected in split dependencies.
     */
    public static @NonNull SparseArray<int[]> createDependenciesFromPackage(PackageLite pkg)
            throws IllegalDependencyException {
        // The data structure that holds the dependencies. In ParsingPackageUtils, 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.getSplitNames().length; splitIdx++) {
            if (!pkg.getIsFeatureSplits()[splitIdx]) {
                // Non-feature splits don't have dependencies.
                continue;
            }

            // Implicit dependency on the base.
            final int targetIdx;
            final String splitDependency = pkg.getUsesSplitNames()[splitIdx];
            if (splitDependency != null) {
                final int depIdx = Arrays.binarySearch(pkg.getSplitNames(), splitDependency);
                if (depIdx < 0) {
                    throw new IllegalDependencyException("Split '" + pkg.getSplitNames()[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, size = pkg.getSplitNames().length; splitIdx < size; splitIdx++) {
            if (pkg.getIsFeatureSplits()[splitIdx]) {
                // Feature splits are not configForSplits.
                continue;
            }

            // Implicit feature for the base.
            final int targetSplitIdx;
            final String configForSplit = pkg.getConfigForSplit()[splitIdx];
            if (configForSplit != null) {
                final int depIdx = Arrays.binarySearch(pkg.getSplitNames(), configForSplit);
                if (depIdx < 0) {
                    throw new IllegalDependencyException("Split '" + pkg.getSplitNames()[splitIdx]
                            + "' targets split '" + configForSplit + "', which is missing.");
                }

                if (!pkg.getIsFeatureSplits()[depIdx]) {
                    throw new IllegalDependencyException("Split '" + pkg.getSplitNames()[splitIdx]
                            + "' declares itself as configuration split for a non-feature split '"
                            + pkg.getSplitNames()[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;
    }
}