1 /* 2 * Copyright (C) 2022 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 android.safetycenter.lint 18 19 import android.os.Build 20 import android.os.Build.VERSION_CODES.TIRAMISU 21 import com.google.common.annotations.VisibleForTesting 22 import java.io.File 23 import java.io.InputStream 24 import java.lang.reflect.Modifier.isFinal 25 import java.lang.reflect.Modifier.isPublic 26 import java.lang.reflect.Modifier.isStatic 27 28 /** A class that allows interacting with files that are versioned by sdk. */ 29 object FileSdk { 30 /** 31 * Linter constant to limit the mocked SDK levels that will be checked. We are making an 32 * important assumption here that if new parser logic is introduced that depends on a new SDK 33 * level, we expect a new schema to exist and a new version code to have been added. 34 */ 35 private val MAX_VERSION: Int = maxOf(getMaxVersionCodesConstant(), getMaxSchemaVersion()) 36 37 /** Test only override to further limit the mocked SDK that will be checked in a test */ 38 @VisibleForTesting @Volatile @JvmStatic var maxVersionOverride: Int? = null 39 40 /** 41 * Returns the max SDK level version that should be used while linting to check for backward 42 * compatibility. 43 */ getMaxSdkVersionnull44 fun getMaxSdkVersion(): Int = maxVersionOverride ?: MAX_VERSION 45 46 /** Returns the SDK level version that a file resource belongs to. */ 47 fun getSdkQualifier(file: File): Int { 48 val directParentName = file.parentFile.name 49 val lastQualifier = directParentName.substringAfterLast("-", "") 50 if (lastQualifier.isEmpty() || lastQualifier[0] != 'v') { 51 return TIRAMISU 52 } 53 return lastQualifier.substring(1).toIntOrNull() ?: TIRAMISU 54 } 55 56 /** 57 * Returns whether the file belongs to a basic configuration. By basic, we mean either the 58 * default configuration that has no qualifier, or a configuration that is defined only by an 59 * SDK level version. 60 */ belongsToABasicConfigurationnull61 fun belongsToABasicConfiguration(file: File): Boolean { 62 val directParentName = file.parentFile.name 63 val qualifierCount = directParentName.count { it == '-' } 64 val lastQualifier = directParentName.substringAfterLast("-", "") 65 if ( 66 lastQualifier.isNotEmpty() && 67 lastQualifier[0] == 'v' && 68 lastQualifier.substring(1).toIntOrNull() != null 69 ) { 70 return qualifierCount == 1 71 } 72 return qualifierCount == 0 73 } 74 75 /** Returns the schema for the specific SDK level provided or null if it doesn't exist. */ getSchemaAsStreamnull76 fun getSchemaAsStream(sdk: Int): InputStream? = 77 FileSdk::class.java.getResourceAsStream("/safety_center_config${toQualifier(sdk)}.xsd") 78 79 private fun toQualifier(sdk: Int): String = if (sdk == TIRAMISU) "" else "-v$sdk" 80 81 private fun getMaxVersionCodesConstant(): Int = 82 Build.VERSION_CODES::class 83 .java 84 .declaredFields 85 .filter { 86 isPublic(it.modifiers) && 87 isFinal(it.modifiers) && 88 isStatic(it.modifiers) && 89 it.type == Integer.TYPE 90 } <lambda>null91 .maxOf { it.get(null) as Int } 92 getMaxSchemaVersionnull93 private fun getMaxSchemaVersion(): Int = 94 // 99 is an arbitrary high value to look for the schema with the highest SDK level. 95 // Gaps are possible which is why we cannot just stop as soon as an SDK level has no schema. 96 (TIRAMISU..99).filter { getSchemaAsStream(it) != null }.maxOrNull() ?: 0 97 } 98