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