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