• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2012 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 
17 package com.android.tools.lint.checks;
18 
19 import static com.android.tools.lint.detector.api.LintConstants.DOT_XML;
20 
21 import com.android.annotations.NonNull;
22 import com.android.annotations.Nullable;
23 import com.android.tools.lint.client.api.LintClient;
24 import com.android.tools.lint.detector.api.LintUtils;
25 import com.google.common.base.Charsets;
26 import com.google.common.collect.Maps;
27 import com.google.common.io.Files;
28 import com.google.common.primitives.UnsignedBytes;
29 
30 import java.io.File;
31 import java.io.FileOutputStream;
32 import java.io.IOException;
33 import java.lang.ref.WeakReference;
34 import java.nio.ByteBuffer;
35 import java.nio.ByteOrder;
36 import java.nio.MappedByteBuffer;
37 import java.nio.channels.FileChannel.MapMode;
38 import java.util.ArrayList;
39 import java.util.Collections;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.Set;
43 
44 /**
45  * Database for API checking: Allows quick lookup of a given class, method or field
46  * to see which API level it was introduced in.
47  * <p>
48  * This class is optimized for quick bytecode lookup used in conjunction with the
49  * ASM library: It has lookup methods that take internal JVM signatures, and for a method
50  * call for example it processes the owner, name and description parameters separately
51  * the way they are provided from ASM.
52  * <p>
53  * The {@link Api} class provides access to the full Android API along with version
54  * information, initialized from an XML file. This lookup class adds a binary cache around
55  * the API to make initialization faster and to require fewer objects. It creates
56  * a binary cache data structure, which fits in a single byte array, which means that
57  * to open the database you can just read in the byte array and go. On one particular
58  * machine, this takes about 30-50 ms versus 600-800ms for the full parse. It also
59  * helps memory by placing everything in a compact byte array instead of needing separate
60  * strings (2 bytes per character in a char[] for the 25k method entries, 11k field entries
61  * and 6k class entries) - and it also avoids the same number of Map.Entry objects.
62  * When creating the memory data structure it performs a few other steps to help memory:
63  * <ul>
64  * <li> It stores the strings as single bytes, since all the JVM signatures are in ASCII
65  * <li> It strips out the method return types (which takes the binary size down from
66  *      about 4.7M to 4.0M)
67  * <li> It strips out all APIs that have since=1, since the lookup only needs to find
68  *      classes, methods and fields that have an API level *higher* than 1. This drops
69  *      the memory use down from 4.0M to 1.7M.
70  * </ul>
71  */
72 public class ApiLookup {
73     /** Relative path to the api-versions.xml database file within the Lint installation */
74     private static final String XML_FILE_PATH = "platform-tools/api/api-versions.xml"; //$NON-NLS-1$
75     private static final String FILE_HEADER = "API database used by Android lint\000";
76     private static final int BINARY_FORMAT_VERSION = 2;
77     private static final boolean DEBUG_FORCE_REGENERATE_BINARY = false;
78     private static final boolean DEBUG_SEARCH = false;
79     private static final boolean WRITE_STATS = false;
80     /** Default size to reserve for each API entry when creating byte buffer to build up data */
81     private static final int BYTES_PER_ENTRY = 40;
82 
83     private final LintClient mClient;
84     private final File mXmlFile;
85     private final File mBinaryFile;
86     private final Api mInfo;
87     private byte[] mData;
88     private int[] mIndices;
89     private int mClassCount;
90     private int mMethodCount;
91 
92     private static WeakReference<ApiLookup> sInstance =
93             new WeakReference<ApiLookup>(null);
94 
95     /**
96      * Returns an instance of the API database
97      *
98      * @param client the client to associate with this database - used only for
99      *            logging. The database object may be shared among repeated invocations,
100      *            and in that case client used will be the one originally passed in.
101      *            In other words, this parameter may be ignored if the client created
102      *            is not new.
103      * @return a (possibly shared) instance of the API database, or null
104      *         if its data can't be found
105      */
get(LintClient client)106     public static ApiLookup get(LintClient client) {
107         synchronized (ApiLookup.class) {
108             ApiLookup db = sInstance.get();
109             if (db == null) {
110                 File file = client.findResource(XML_FILE_PATH);
111                 if (file == null) {
112                     // AOSP build environment?
113                     String build = System.getenv("ANDROID_BUILD_TOP");   //$NON-NLS-1$
114                     if (build != null) {
115                         file = new File(build, "development/sdk/api-versions.xml" //$NON-NLS-1$
116                                 .replace('/', File.separatorChar));
117                     }
118                 }
119 
120                 if (file == null || !file.exists()) {
121                     client.log(null, "Fatal error: No API database found at %1$s", file);
122                     return null;
123                 } else {
124                     db = get(client, file);
125                 }
126                 sInstance = new WeakReference<ApiLookup>(db);
127             }
128 
129             return db;
130         }
131     }
132 
133     /**
134      * Returns an instance of the API database
135      *
136      * @param client the client to associate with this database - used only for
137      *            logging
138      * @param xmlFile the XML file containing configuration data to use for this
139      *            database
140      * @return a (possibly shared) instance of the API database, or null
141      *         if its data can't be found
142      */
get(LintClient client, File xmlFile)143     public static ApiLookup get(LintClient client, File xmlFile) {
144         if (!xmlFile.exists()) {
145             client.log(null, "The API database file %1$s does not exist", xmlFile);
146             return null;
147         }
148 
149         String name = xmlFile.getName();
150         if (LintUtils.endsWith(name, DOT_XML)) {
151             name = name.substring(0, name.length() - DOT_XML.length());
152         }
153         File cacheDir = client.getCacheDir(true/*create*/);
154         if (cacheDir == null) {
155             cacheDir = xmlFile.getParentFile();
156         }
157 
158         File binaryData = new File(cacheDir, name
159                 // Incorporate version number in the filename to avoid upgrade filename
160                 // conflicts on Windows (such as issue #26663)
161                 + "-" + BINARY_FORMAT_VERSION + ".bin"); //$NON-NLS-1$ //$NON-NLS-2$
162 
163         if (DEBUG_FORCE_REGENERATE_BINARY) {
164             System.err.println("\nTemporarily regenerating binary data unconditionally \nfrom "
165                     + xmlFile + "\nto " + binaryData);
166             if (!createCache(client, xmlFile, binaryData)) {
167                 return null;
168             }
169         } else if (!binaryData.exists() || binaryData.lastModified() < xmlFile.lastModified()) {
170             if (!createCache(client, xmlFile, binaryData)) {
171                 return null;
172             }
173         }
174 
175         if (!binaryData.exists()) {
176             client.log(null, "The API database file %1$s does not exist", binaryData);
177             return null;
178         }
179 
180         return new ApiLookup(client, xmlFile, binaryData, null);
181     }
182 
createCache(LintClient client, File xmlFile, File binaryData)183     private static boolean createCache(LintClient client, File xmlFile, File binaryData) {
184         long begin = 0;
185         if (WRITE_STATS) {
186             begin = System.currentTimeMillis();
187         }
188 
189         Api info = Api.parseApi(xmlFile);
190 
191         if (WRITE_STATS) {
192             long end = System.currentTimeMillis();
193             System.out.println("Reading XML data structures took " + (end - begin) + " ms)");
194         }
195 
196         if (info != null) {
197             try {
198                 writeDatabase(binaryData, info);
199                 return true;
200             } catch (IOException ioe) {
201                 client.log(ioe, "Can't write API cache file");
202             }
203         }
204 
205         return false;
206     }
207 
208     /** Use one of the {@link #get} factory methods instead */
ApiLookup( @onNull LintClient client, @NonNull File xmlFile, @Nullable File binaryFile, @Nullable Api info)209     private ApiLookup(
210             @NonNull LintClient client,
211             @NonNull File xmlFile,
212             @Nullable File binaryFile,
213             @Nullable Api info) {
214         mClient = client;
215         mXmlFile = xmlFile;
216         mBinaryFile = binaryFile;
217         mInfo = info;
218 
219         if (binaryFile != null) {
220             readData();
221         }
222     }
223 
224     /**
225      * Database format:
226      * <pre>
227      * 1. A file header, which is the exact contents of {@link FILE_HEADER} encoded
228      *     as ASCII characters. The purpose of the header is to identify what the file
229      *     is for, for anyone attempting to open the file.
230      * 2. A file version number. If the binary file does not match the reader's expected
231      *     version, it can ignore it (and regenerate the cache from XML).
232      * 3. The number of classes [1 int]
233      * 4. The number of members (across all classes) [1 int].
234      * 5. Class offset table (one integer per class, pointing to the byte offset in the
235      *      file (relative to the beginning of the file) where each class begins.
236      *      The classes are always sorted alphabetically by fully qualified name.
237      * 6. Member offset table (one integer per member, pointing to the byte offset in the
238      *      file (relative to the beginning of the file) where each member entry begins.
239      *      The members are always sorted alphabetically.
240      * 7. Class entry table. Each class entry consists of the fully qualified class name,
241      *       in JVM format (using / instead of . in package names and $ for inner classes),
242      *       followed by the byte 0 as a terminator, followed by the API version as a byte.
243      * 8. Member entry table. Each member entry consists of the class number (as a short),
244      *      followed by the JVM method/field signature, encoded as UTF-8, followed by a 0 byte
245      *      signature terminator, followed by the API level as a byte.
246      * <p>
247      * TODO: Pack the offsets: They increase by a small amount for each entry, so no need
248      * to spend 4 bytes on each. These will need to be processed when read back in anyway,
249      * so consider storing the offset -deltas- as single bytes and adding them up cumulatively
250      * in readData().
251      * </pre>
252      */
readData()253     private void readData() {
254         if (!mBinaryFile.exists()) {
255             mClient.log(null, "%1$s does not exist", mBinaryFile);
256             return;
257         }
258         long start = System.currentTimeMillis();
259         try {
260             MappedByteBuffer buffer = Files.map(mBinaryFile, MapMode.READ_ONLY);
261             assert buffer.order() == ByteOrder.BIG_ENDIAN;
262 
263             // First skip the header
264             byte[] expectedHeader = FILE_HEADER.getBytes(Charsets.US_ASCII);
265             buffer.rewind();
266             for (int offset = 0; offset < expectedHeader.length; offset++) {
267                 if (expectedHeader[offset] != buffer.get()) {
268                     mClient.log(null, "Incorrect file header: not an API database cache " +
269                             "file, or a corrupt cache file");
270                     return;
271                 }
272             }
273 
274             // Read in the format number
275             if (buffer.get() != BINARY_FORMAT_VERSION) {
276                 // Force regeneration of new binary data with up to date format
277                 if (createCache(mClient, mXmlFile, mBinaryFile)) {
278                     readData(); // Recurse
279                 }
280 
281                 return;
282             }
283 
284             mClassCount = buffer.getInt();
285             mMethodCount = buffer.getInt();
286 
287             // Read in the class table indices;
288             int count = mClassCount + mMethodCount;
289             int[] offsets = new int[count];
290 
291             // Another idea: I can just store the DELTAS in the file (and add them up
292             // when reading back in) such that it takes just ONE byte instead of four!
293 
294             for (int i = 0; i < count; i++) {
295                 offsets[i] = buffer.getInt();
296             }
297 
298             // No need to read in the rest -- we'll just keep the whole byte array in memory
299             // TODO: Make this code smarter/more efficient.
300             int size = buffer.limit();
301             byte[] b = new byte[size];
302             buffer.rewind();
303             buffer.get(b);
304             mData = b;
305             mIndices = offsets;
306 
307             // TODO: We only need to keep the data portion here since we've initialized
308             // the offset array separately.
309             // TODO: Investigate (profile) accessing the byte buffer directly instead of
310             // accessing a byte array.
311         } catch (IOException e) {
312             mClient.log(e, null);
313         }
314         if (WRITE_STATS) {
315             long end = System.currentTimeMillis();
316             System.out.println("\nRead API database in " + (end - start)
317                     + " milliseconds.");
318             System.out.println("Size of data table: " + mData.length + " bytes ("
319                     + Integer.toString(mData.length/1024) + "k)\n");
320         }
321     }
322 
323     /** See the {@link #readData()} for documentation on the data format. */
writeDatabase(File file, Api info)324     private static void writeDatabase(File file, Api info) throws IOException {
325         /*
326          * 1. A file header, which is the exact contents of {@link FILE_HEADER} encoded
327          *     as ASCII characters. The purpose of the header is to identify what the file
328          *     is for, for anyone attempting to open the file.
329          * 2. A file version number. If the binary file does not match the reader's expected
330          *     version, it can ignore it (and regenerate the cache from XML).
331          */
332         Map<String, ApiClass> classMap = info.getClasses();
333         // Write the class table
334 
335         List<String> classes = new ArrayList<String>(classMap.size());
336         Map<ApiClass, List<String>> memberMap =
337                 Maps.newHashMapWithExpectedSize(classMap.size());
338         int memberCount = 0;
339         for (Map.Entry<String, ApiClass> entry : classMap.entrySet()) {
340             String className = entry.getKey();
341             ApiClass apiClass = entry.getValue();
342 
343             Set<String> allMethods = apiClass.getAllMethods(info);
344             Set<String> allFields = apiClass.getAllFields(info);
345 
346             // Strip out all members that have been supported since version 1.
347             // This makes the database *much* leaner (down from about 4M to about
348             // 1.7M), and this just fills the table with entries that ultimately
349             // don't help the API checker since it just needs to know if something
350             // requires a version *higher* than the minimum. If in the future the
351             // database needs to answer queries about whether a method is public
352             // or not, then we'd need to put this data back in.
353             List<String> members = new ArrayList<String>(allMethods.size() + allFields.size());
354             for (String member : allMethods) {
355                 Integer since = apiClass.getMethod(member, info);
356                 assert since != null : className + ':' + member;
357                 if (since == null) {
358                     since = 1;
359                 }
360                 if (since != 1) {
361                     members.add(member);
362                 }
363             }
364 
365             // Strip out all members that have been supported since version 1.
366             // This makes the database *much* leaner (down from about 4M to about
367             // 1.7M), and this just fills the table with entries that ultimately
368             // don't help the API checker since it just needs to know if something
369             // requires a version *higher* than the minimum. If in the future the
370             // database needs to answer queries about whether a method is public
371             // or not, then we'd need to put this data back in.
372             for (String member : allFields) {
373                 Integer since = apiClass.getField(member, info);
374                 assert since != null : className + ':' + member;
375                 if (since == null) {
376                     since = 1;
377                 }
378                 if (since != 1) {
379                     members.add(member);
380                 }
381             }
382 
383             // Only include classes that have one or more members requiring version 2 or higher:
384             if (members.size() > 0) {
385                 classes.add(className);
386                 memberMap.put(apiClass, members);
387                 memberCount += members.size();
388             }
389         }
390         Collections.sort(classes);
391 
392         int entryCount = classMap.size() + memberCount;
393         int capacity = entryCount * BYTES_PER_ENTRY;
394         ByteBuffer buffer = ByteBuffer.allocate(capacity);
395         buffer.order(ByteOrder.BIG_ENDIAN);
396         //  1. A file header, which is the exact contents of {@link FILE_HEADER} encoded
397         //      as ASCII characters. The purpose of the header is to identify what the file
398         //      is for, for anyone attempting to open the file.
399 
400         buffer.put(FILE_HEADER.getBytes(Charsets.US_ASCII));
401 
402         //  2. A file version number. If the binary file does not match the reader's expected
403         //      version, it can ignore it (and regenerate the cache from XML).
404         buffer.put((byte) BINARY_FORMAT_VERSION);
405 
406 
407 
408         //  3. The number of classes [1 int]
409         buffer.putInt(classes.size());
410         //  4. The number of members (across all classes) [1 int].
411         buffer.putInt(memberCount);
412 
413         //  5. Class offset table (one integer per class, pointing to the byte offset in the
414         //       file (relative to the beginning of the file) where each class begins.
415         //       The classes are always sorted alphabetically by fully qualified name.
416         int classOffsetTable = buffer.position();
417 
418         // Reserve enough room for the offset table here: we will backfill it with pointers
419         // as we're writing out the data structures below
420         for (int i = 0, n = classes.size(); i < n; i++) {
421             buffer.putInt(0);
422         }
423 
424         //  6. Member offset table (one integer per member, pointing to the byte offset in the
425         //       file (relative to the beginning of the file) where each member entry begins.
426         //       The members are always sorted alphabetically.
427         int methodOffsetTable = buffer.position();
428         for (int i = 0, n = memberCount; i < n; i++) {
429             buffer.putInt(0);
430         }
431 
432         int nextEntry = buffer.position();
433         int nextOffset = classOffsetTable;
434 
435         // 7. Class entry table. Each class entry consists of the fully qualified class name,
436         //      in JVM format (using / instead of . in package names and $ for inner classes),
437         //      followed by the byte 0 as a terminator, followed by the API version as a byte.
438         for (String clz : classes) {
439             buffer.position(nextOffset);
440             buffer.putInt(nextEntry);
441             nextOffset = buffer.position();
442             buffer.position(nextEntry);
443             buffer.put(clz.getBytes(Charsets.UTF_8));
444             buffer.put((byte) 0);
445 
446             ApiClass apiClass = classMap.get(clz);
447             assert apiClass != null : clz;
448             int since = apiClass.getSince();
449             assert since == UnsignedBytes.toInt((byte) since) : since; // make sure it fits
450             buffer.put((byte) since);
451 
452             nextEntry = buffer.position();
453         }
454 
455         //  8. Member entry table. Each member entry consists of the class number (as a short),
456         //       followed by the JVM method/field signature, encoded as UTF-8, followed by a 0 byte
457         //       signature terminator, followed by the API level as a byte.
458         assert nextOffset == methodOffsetTable;
459 
460         for (int classNumber = 0, n = classes.size(); classNumber < n; classNumber++) {
461             String clz = classes.get(classNumber);
462             ApiClass apiClass = classMap.get(clz);
463             assert apiClass != null : clz;
464             List<String> members = memberMap.get(apiClass);
465             Collections.sort(members);
466 
467             for (String member : members) {
468                 buffer.position(nextOffset);
469                 buffer.putInt(nextEntry);
470                 nextOffset = buffer.position();
471                 buffer.position(nextEntry);
472 
473                 Integer since;
474                 if (member.indexOf('(') != -1) {
475                     since = apiClass.getMethod(member, info);
476                 } else {
477                     since = apiClass.getField(member, info);
478                 }
479                 assert since != null : clz + ':' + member;
480                 if (since == null) {
481                     since = 1;
482                 }
483 
484                 assert classNumber == (short) classNumber;
485                 buffer.putShort((short) classNumber);
486                 byte[] signature = member.getBytes(Charsets.UTF_8);
487                 for (int i = 0; i < signature.length; i++) {
488                     // Make sure all signatures are really just simple ASCII
489                     byte b = signature[i];
490                     assert b == (b & 0x7f) : member;
491                     buffer.put(b);
492                     // Skip types on methods
493                     if (b == (byte) ')') {
494                         break;
495                     }
496                 }
497                 buffer.put((byte) 0);
498                 int api = since;
499                 assert api == UnsignedBytes.toInt((byte) api);
500                 //assert api >= 1 && api < 0xFF; // max that fits in a byte
501                 buffer.put((byte) api);
502                 nextEntry = buffer.position();
503             }
504         }
505 
506         int size = buffer.position();
507         assert size <= buffer.limit();
508         buffer.mark();
509 
510         if (WRITE_STATS) {
511             System.out.println("Wrote " + classes.size() + " classes and "
512                     + memberCount + " member entries");
513             System.out.print("Actual binary size: " + size + " bytes");
514             System.out.println(String.format(" (%.1fM)", size/(1024*1024.f)));
515 
516             System.out.println("Allocated size: " + (entryCount * BYTES_PER_ENTRY) + " bytes");
517             System.out.println("Required bytes per entry: " + (size/ entryCount) + " bytes");
518         }
519 
520         // Now dump this out as a file
521         // There's probably an API to do this more efficiently; TODO: Look into this.
522         byte[] b = new byte[size];
523         buffer.rewind();
524         buffer.get(b);
525         FileOutputStream output = Files.newOutputStreamSupplier(file).getOutput();
526         output.write(b);
527         output.close();
528     }
529 
530     // For debugging only
dumpEntry(int offset)531     private String dumpEntry(int offset) {
532         if (DEBUG_SEARCH) {
533             StringBuilder sb = new StringBuilder();
534             for (int i = offset; i < mData.length; i++) {
535                 if (mData[i] == 0) {
536                     break;
537                 }
538                 char c = (char) UnsignedBytes.toInt(mData[i]);
539                 sb.append(c);
540             }
541 
542             return sb.toString();
543         } else {
544             return "<disabled>"; //$NON-NLS-1$
545         }
546     }
547 
compare(byte[] data, int offset, byte terminator, String s, int max)548     private static int compare(byte[] data, int offset, byte terminator, String s, int max) {
549         int i = offset;
550         int j = 0;
551         for (; j < max; i++, j++) {
552             byte b = data[i];
553             char c = s.charAt(j);
554             // TODO: Check somewhere that the strings are purely in the ASCII range; if not
555             // they're not a match in the database
556             byte cb = (byte) c;
557             int delta = b - cb;
558             if (delta != 0) {
559                 return delta;
560             }
561         }
562 
563         return data[i] - terminator;
564     }
565 
566     /**
567      * Quick determination whether a given class name is possibly interesting; this
568      * is a quick package prefix check to determine whether we need to consider
569      * the class at all. This let's us do less actual searching for the vast majority
570      * of APIs (in libraries, application code etc) that have nothing to do with the
571      * APIs in our packages.
572      * @param name the class name in VM format (e.g. using / instead of .)
573      * @return true if the owner is <b>possibly</b> relevant
574      */
isRelevantClass(String name)575     public boolean isRelevantClass(String name) {
576         // TODO: Add quick switching here. This is tied to the database file so if
577         // we end up with unexpected prefixes there, this could break. For that reason,
578         // for now we consider everything relevant.
579         return true;
580     }
581 
582     /**
583      * Returns the API version required by the given class reference,
584      * or -1 if this is not a known API class. Note that it may return -1
585      * for classes introduced in version 1; internally the database only
586      * stores version data for version 2 and up.
587      *
588      * @param className the internal name of the class, e.g. its
589      *            fully qualified name (as returned by Class.getName(), but with
590      *            '.' replaced by '/'.
591      * @return the minimum API version the method is supported for, or -1 if
592      *         it's unknown <b>or version 1</b>.
593      */
getClassVersion(@onNull String className)594     public int getClassVersion(@NonNull String className) {
595         if (!isRelevantClass(className)) {
596             return -1;
597         }
598 
599         if (mData != null) {
600             int classNumber = findClass(className);
601             if (classNumber != -1) {
602                 int offset = mIndices[classNumber];
603                 while (mData[offset] != 0) {
604                     offset++;
605                 }
606                 offset++;
607                 return UnsignedBytes.toInt(mData[offset]);
608             }
609         }  else {
610            ApiClass clz = mInfo.getClass(className);
611             if (clz != null) {
612                 int since = clz.getSince();
613                 if (since == Integer.MAX_VALUE) {
614                     since = -1;
615                 }
616                 return since;
617             }
618         }
619 
620         return -1;
621     }
622 
623     /**
624      * Returns the API version required by the given method call. The method is
625      * referred to by its {@code owner}, {@code name} and {@code desc} fields.
626      * If the method is unknown it returns -1. Note that it may return -1 for
627      * classes introduced in version 1; internally the database only stores
628      * version data for version 2 and up.
629      *
630      * @param owner the internal name of the method's owner class, e.g. its
631      *            fully qualified name (as returned by Class.getName(), but with
632      *            '.' replaced by '/'.
633      * @param name the method's name
634      * @param desc the method's descriptor - see {@link org.objectweb.asm.Type}
635      * @return the minimum API version the method is supported for, or -1 if
636      *         it's unknown <b>or version 1</b>.
637      */
getCallVersion( @onNull String owner, @NonNull String name, @NonNull String desc)638     public int getCallVersion(
639             @NonNull String owner,
640             @NonNull String name,
641             @NonNull String desc) {
642         if (!isRelevantClass(owner)) {
643             return -1;
644         }
645 
646         if (mData != null) {
647             int classNumber = findClass(owner);
648             if (classNumber != -1) {
649                 return findMember(classNumber, name, desc);
650             }
651         }  else {
652            ApiClass clz = mInfo.getClass(owner);
653             if (clz != null) {
654                 String signature = name + desc;
655                 int since = clz.getMethod(signature, mInfo);
656                 if (since == Integer.MAX_VALUE) {
657                     since = -1;
658                 }
659                 return since;
660             }
661         }
662 
663         return -1;
664     }
665 
666     /**
667      * Returns the API version required to access the given field, or -1 if this
668      * is not a known API method. Note that it may return -1 for classes
669      * introduced in version 1; internally the database only stores version data
670      * for version 2 and up.
671      *
672      * @param owner the internal name of the method's owner class, e.g. its
673      *            fully qualified name (as returned by Class.getName(), but with
674      *            '.' replaced by '/'.
675      * @param name the method's name
676      * @return the minimum API version the method is supported for, or -1 if
677      *         it's unknown <b>or version 1</b>
678      */
getFieldVersion( @onNull String owner, @NonNull String name)679     public int getFieldVersion(
680             @NonNull String owner,
681             @NonNull String name) {
682         if (!isRelevantClass(owner)) {
683             return -1;
684         }
685 
686         if (mData != null) {
687             int classNumber = findClass(owner);
688             if (classNumber != -1) {
689                 return findMember(classNumber, name, null);
690             }
691         }  else {
692             ApiClass clz = mInfo.getClass(owner);
693             if (clz != null) {
694                 int since = clz.getField(name, mInfo);
695                 if (since == Integer.MAX_VALUE) {
696                     since = -1;
697                 }
698                 return since;
699             }
700         }
701 
702         return -1;
703     }
704 
705     /** Returns the class number of the given class, or -1 if it is unknown */
findClass(@onNull String owner)706     private int findClass(@NonNull String owner) {
707         assert owner.indexOf('.') == -1 : "Should use / instead of . in owner: " + owner;
708 
709         // The index array contains class indexes from 0 to classCount and
710         //   member indices from classCount to mIndices.length.
711         int low = 0;
712         int high = mClassCount - 1;
713         while (low <= high) {
714             int middle = (low + high) >>> 1;
715             int offset = mIndices[middle];
716 
717             if (DEBUG_SEARCH) {
718                 System.out.println("Comparing string " + owner +" with entry at " + offset
719                         + ": " + dumpEntry(offset));
720             }
721 
722             // Compare the api info at the given index.
723             int classNameLength = owner.length();
724             int compare = compare(mData, offset, (byte) 0, owner, classNameLength);
725             if (compare == 0) {
726                 return middle;
727             }
728 
729             if (compare < 0) {
730                 low = middle + 1;
731             } else if (compare > 0) {
732                 high = middle - 1;
733             } else {
734                 assert false; // compare == 0 already handled above
735                 return -1;
736             }
737         }
738 
739         return -1;
740     }
741 
findMember(int classNumber, @NonNull String name, @NonNull String desc)742     private int findMember(int classNumber, @NonNull String name, @NonNull String desc) {
743         // The index array contains class indexes from 0 to classCount and
744         // member indices from classCount to mIndices.length.
745         int low = mClassCount;
746         int high = mIndices.length - 1;
747         while (low <= high) {
748             int middle = (low + high) >>> 1;
749             int offset = mIndices[middle];
750 
751             if (DEBUG_SEARCH) {
752                 System.out.println("Comparing string " + (name + ';' + desc) +
753                         " with entry at " + offset + ": " + dumpEntry(offset));
754             }
755 
756             // Check class number: read short. The byte data is always big endian.
757             int entryClass = (mData[offset++] & 0xFF) << 8 | (mData[offset++] & 0xFF);
758             int compare = entryClass - classNumber;
759             if (compare == 0) {
760                 if (desc != null) {
761                     // Method
762                     int nameLength = name.length();
763                     compare = compare(mData, offset, (byte) '(', name, nameLength);
764                     if (compare == 0) {
765                         offset += nameLength;
766                         int argsEnd = desc.indexOf(')');
767                         // Only compare up to the ) -- after that we have a return value in the
768                         // input description, which isn't there in the database
769                         compare = compare(mData, offset, (byte) ')', desc, argsEnd);
770                         if (compare == 0) {
771                             offset += argsEnd + 1;
772 
773                             if (mData[offset++] == 0) {
774                                 // Yes, terminated argument list: get the API level
775                                 return UnsignedBytes.toInt(mData[offset]);
776                             }
777                         }
778                     }
779                 } else {
780                     // Field
781                     int nameLength = name.length();
782                     compare = compare(mData, offset, (byte) 0, name, nameLength);
783                     if (compare == 0) {
784                         offset += nameLength;
785                         if (mData[offset++] == 0) {
786                             // Yes, terminated argument list: get the API level
787                             return UnsignedBytes.toInt(mData[offset]);
788                         }
789                     }
790                 }
791             }
792 
793             if (compare < 0) {
794                 low = middle + 1;
795             } else if (compare > 0) {
796                 high = middle - 1;
797             } else {
798                 assert false; // compare == 0 already handled above
799                 return -1;
800             }
801         }
802 
803         return -1;
804     }
805 }
806