1 /*
2  * Copyright (C) 2018 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.metalava
18 
19 import com.android.tools.metalava.model.ClassItem
20 import com.android.tools.metalava.model.Codebase
21 import com.android.tools.metalava.model.FieldItem
22 import java.io.BufferedWriter
23 import java.io.File
24 import java.io.FileWriter
25 import java.io.IOException
26 
27 // Ported from doclava1
28 
29 private const val ANDROID_VIEW_VIEW = "android.view.View"
30 private const val ANDROID_VIEW_VIEW_GROUP = "android.view.ViewGroup"
31 private const val ANDROID_VIEW_VIEW_GROUP_LAYOUT_PARAMS = "android.view.ViewGroup.LayoutParams"
32 private const val SDK_CONSTANT_ANNOTATION = "android.annotation.SdkConstant"
33 private const val SDK_CONSTANT_TYPE_ACTIVITY_ACTION =
34     "android.annotation.SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION"
35 private const val SDK_CONSTANT_TYPE_BROADCAST_ACTION =
36     "android.annotation.SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION"
37 private const val SDK_CONSTANT_TYPE_SERVICE_ACTION =
38     "android.annotation.SdkConstant.SdkConstantType.SERVICE_ACTION"
39 private const val SDK_CONSTANT_TYPE_CATEGORY = "android.annotation.SdkConstant.SdkConstantType.INTENT_CATEGORY"
40 private const val SDK_CONSTANT_TYPE_FEATURE = "android.annotation.SdkConstant.SdkConstantType.FEATURE"
41 private const val SDK_WIDGET_ANNOTATION = "android.annotation.Widget"
42 private const val SDK_LAYOUT_ANNOTATION = "android.annotation.Layout"
43 
44 private const val TYPE_NONE = 0
45 private const val TYPE_WIDGET = 1
46 private const val TYPE_LAYOUT = 2
47 private const val TYPE_LAYOUT_PARAM = 3
48 
49 /**
50  * Writes various SDK metadata files packaged with the SDK, such as
51  * {@code platforms/android-27/data/features.txt}
52  */
53 class SdkFileWriter(val codebase: Codebase, private val outputDir: File) {
54     /**
55      * Collect the values used by the Dev tools and write them in files packaged with the SDK
56      */
generatenull57     fun generate() {
58         val activityActions = mutableListOf<String>()
59         val broadcastActions = mutableListOf<String>()
60         val serviceActions = mutableListOf<String>()
61         val categories = mutableListOf<String>()
62         val features = mutableListOf<String>()
63         val layouts = mutableListOf<ClassItem>()
64         val widgets = mutableListOf<ClassItem>()
65         val layoutParams = mutableListOf<ClassItem>()
66 
67         val classes = codebase.getPackages().allClasses()
68 
69         // The topmost LayoutParams class - android.view.ViewGroup.LayoutParams
70         var topLayoutParams: ClassItem? = null
71 
72         // Go through all the fields of all the classes, looking SDK stuff.
73         for (clazz in classes) {
74             // first check constant fields for the SdkConstant annotation.
75             val fields = clazz.fields()
76             for (field in fields) {
77                 val value = field.initialValue() ?: continue
78                 val annotations = field.modifiers.annotations()
79                 for (annotation in annotations) {
80                     if (SDK_CONSTANT_ANNOTATION == annotation.qualifiedName()) {
81                         val resolved =
82                             annotation.findAttribute(null)?.leafValues()?.firstOrNull()?.resolve() as? FieldItem
83                                 ?: continue
84                         when (resolved.containingClass().qualifiedName() + "." + resolved.name()) {
85                             SDK_CONSTANT_TYPE_ACTIVITY_ACTION -> activityActions.add(value.toString())
86                             SDK_CONSTANT_TYPE_BROADCAST_ACTION -> broadcastActions.add(value.toString())
87                             SDK_CONSTANT_TYPE_SERVICE_ACTION -> serviceActions.add(value.toString())
88                             SDK_CONSTANT_TYPE_CATEGORY -> categories.add(value.toString())
89                             SDK_CONSTANT_TYPE_FEATURE -> features.add(value.toString())
90                         }
91                     }
92                 }
93             }
94 
95             // Now check the class for @Widget or if its in the android.widget package
96             // (unless the class is hidden or abstract, or non public)
97             if (!clazz.isHiddenOrRemoved() && clazz.isPublic && !clazz.modifiers.isAbstract()) {
98                 var annotated = false
99                 val annotations = clazz.modifiers.annotations()
100                 if (annotations.isNotEmpty()) {
101                     for (annotation in annotations) {
102                         if (SDK_WIDGET_ANNOTATION == annotation.qualifiedName()) {
103                             widgets.add(clazz)
104                             annotated = true
105                             break
106                         } else if (SDK_LAYOUT_ANNOTATION == annotation.qualifiedName()) {
107                             layouts.add(clazz)
108                             annotated = true
109                             break
110                         }
111                     }
112                 }
113 
114                 if (!annotated) {
115                     if (topLayoutParams == null && ANDROID_VIEW_VIEW_GROUP_LAYOUT_PARAMS == clazz.qualifiedName()) {
116                         topLayoutParams = clazz
117                     }
118                     // let's check if this is inside android.widget or android.view
119                     if (isIncludedPackage(clazz)) {
120                         // now we check what this class inherits either from android.view.ViewGroup
121                         // or android.view.View, or android.view.ViewGroup.LayoutParams
122                         when (checkInheritance(clazz)) {
123                             TYPE_WIDGET -> widgets.add(clazz)
124                             TYPE_LAYOUT -> layouts.add(clazz)
125                             TYPE_LAYOUT_PARAM -> layoutParams.add(clazz)
126                         }
127                     }
128                 }
129             }
130         }
131 
132         // now write the files, whether or not the list are empty.
133         // the SDK built requires those files to be present.
134 
135         activityActions.sort()
136         writeValues("activity_actions.txt", activityActions)
137 
138         broadcastActions.sort()
139         writeValues("broadcast_actions.txt", broadcastActions)
140 
141         serviceActions.sort()
142         writeValues("service_actions.txt", serviceActions)
143 
144         categories.sort()
145         writeValues("categories.txt", categories)
146 
147         features.sort()
148         writeValues("features.txt", features)
149 
150         // Before writing the list of classes, we do some checks, to make sure the layout params
151         // are enclosed by a layout class (and not one that has been declared as a widget)
152         var i = 0
153         while (i < layoutParams.size) {
154             var clazz: ClassItem? = layoutParams[i]
155             val containingClass = clazz?.containingClass()
156             var remove = containingClass == null || layouts.indexOf(containingClass) == -1
157             // Also ensure that super classes of the layout params are in android.widget or android.view.
158             while (!remove && clazz != null) {
159                 clazz = clazz.superClass() ?: break
160                 if (clazz == topLayoutParams) {
161                     break
162                 }
163                 remove = !isIncludedPackage(clazz)
164             }
165             if (remove) {
166                 layoutParams.removeAt(i)
167             } else {
168                 i++
169             }
170         }
171 
172         writeClasses("widgets.txt", widgets, layouts, layoutParams)
173     }
174 
175     /**
176      * Check if the clazz is in package android.view or android.widget
177      */
isIncludedPackagenull178     private fun isIncludedPackage(clazz: ClassItem): Boolean {
179         val pkgName = clazz.containingPackage().qualifiedName()
180         return "android.widget" == pkgName || "android.view" == pkgName
181     }
182 
183     /**
184      * Writes a list of values into a text files.
185      *
186      * @param name the name of the file to write in the SDK directory
187      * @param values the list of values to write.
188      */
writeValuesnull189     private fun writeValues(name: String, values: List<String>) {
190         val pathname = File(outputDir, name)
191         var fw: FileWriter? = null
192         var bw: BufferedWriter? = null
193         try {
194             fw = FileWriter(pathname, false)
195             bw = BufferedWriter(fw)
196 
197             for (value in values) {
198                 bw.append(value).append('\n')
199             }
200         } catch (e: IOException) {
201             // pass for now
202         } finally {
203             try {
204                 bw?.close()
205             } catch (e: IOException) {
206                 // pass for now
207             }
208 
209             try {
210                 fw?.close()
211             } catch (e: IOException) {
212                 // pass for now
213             }
214         }
215     }
216 
217     /**
218      * Writes the widget/layout/layout param classes into a text files.
219      *
220      * @param name the name of the output file.
221      * @param widgets the list of widget classes to write.
222      * @param layouts the list of layout classes to write.
223      * @param layoutParams the list of layout param classes to write.
224      */
writeClassesnull225     private fun writeClasses(
226         name: String,
227         widgets: List<ClassItem>,
228         layouts: List<ClassItem>,
229         layoutParams: List<ClassItem>
230     ) {
231         var fw: FileWriter? = null
232         var bw: BufferedWriter? = null
233         try {
234             val pathname = File(outputDir, name)
235             fw = FileWriter(pathname, false)
236             bw = BufferedWriter(fw)
237 
238             // write the 3 types of classes.
239             for (clazz in widgets) {
240                 writeClass(bw, clazz, 'W')
241             }
242             for (clazz in layoutParams) {
243                 writeClass(bw, clazz, 'P')
244             }
245             for (clazz in layouts) {
246                 writeClass(bw, clazz, 'L')
247             }
248         } catch (ignore: IOException) {
249         } finally {
250             bw?.close()
251             fw?.close()
252         }
253     }
254 
255     /**
256      * Writes a class name and its super class names into a [BufferedWriter].
257      *
258      * @param writer the BufferedWriter to write into
259      * @param clazz the class to write
260      * @param prefix the prefix to put at the beginning of the line.
261      * @throws IOException
262      */
263     @Throws(IOException::class)
writeClassnull264     private fun writeClass(writer: BufferedWriter, clazz: ClassItem, prefix: Char) {
265         writer.append(prefix).append(clazz.qualifiedName())
266         var superClass: ClassItem? = clazz.superClass()
267         while (superClass != null) {
268             writer.append(' ').append(superClass.qualifiedName())
269             superClass = superClass.superClass()
270         }
271         writer.append('\n')
272     }
273 
274     /**
275      * Checks the inheritance of [ClassItem] objects. This method return
276      *
277      *  * [.TYPE_LAYOUT]: if the class extends `android.view.ViewGroup`
278      *  * [.TYPE_WIDGET]: if the class extends `android.view.View`
279      *  * [.TYPE_LAYOUT_PARAM]: if the class extends
280      * `android.view.ViewGroup$LayoutParams`
281      *  * [.TYPE_NONE]: in all other cases
282      *
283      * @param clazz the [ClassItem] to check.
284      */
checkInheritancenull285     private fun checkInheritance(clazz: ClassItem): Int {
286         return when {
287             ANDROID_VIEW_VIEW_GROUP == clazz.qualifiedName() -> TYPE_LAYOUT
288             ANDROID_VIEW_VIEW == clazz.qualifiedName() -> TYPE_WIDGET
289             ANDROID_VIEW_VIEW_GROUP_LAYOUT_PARAMS == clazz.qualifiedName() -> TYPE_LAYOUT_PARAM
290             else -> {
291                 val parent = clazz.superClass()
292                 if (parent != null) {
293                     checkInheritance(parent)
294                 } else TYPE_NONE
295             }
296         }
297     }
298 }