55 * SPDX-License-Identifier: apache2
66 */
77
8+
9+ import groovy.xml.Namespace
10+
811plugins {
912 id ' com.android.application'
1013 id ' androidx.navigation.safeargs'
@@ -169,6 +172,209 @@ dependencies {
169172 implementation " com.squareup.moshi:moshi-adapters:1.15.2"
170173}
171174
172- configurations. implementation {
173- exclude group : ' org.jetbrains.kotlin' , module : ' kotlin-stdlib-jdk8'
174- }
175+
176+ tasks. register(' generatePrefsDoc' ) {
177+ def resXmlDir = file(" $projectDir /src/main/res/xml" )
178+ def stringsFile = file(" $projectDir /src/main/res/values/strings.xml" )
179+ def arraysFile = file(" $projectDir /src/main/res/values/arrays.xml" )
180+ def outputFile = file(" $projectDir /../docs/preferences.md" )
181+ def jsonOutputFileDoc = file(" $projectDir /../docs/config.json" )
182+ def jsonOutputFileResources = file(" $projectDir /src/main/res/raw/config.json" )
183+
184+ doLast {
185+ def parseStringsXml = { File xmlFile ->
186+ def parser = new XmlSlurper ()
187+ def root = parser. parse(xmlFile)
188+ def map = [:]
189+ root. string. each { s ->
190+ map[s. @name. toString()] = s. text(). trim()
191+ }
192+ return map
193+ }
194+
195+ def parseArraysXml = { File xmlFile ->
196+ if (! xmlFile. exists()) return [:]
197+ def parser = new XmlSlurper ()
198+ def root = parser. parse(xmlFile)
199+ def map = [:]
200+ root. array. each { arr ->
201+ def name = arr. @name. toString()
202+ map[name] = arr. item. collect { it. text(). trim() }
203+ }
204+ return map
205+ }
206+
207+ def stringsMap = stringsFile. exists() ? parseStringsXml(stringsFile) : [:]
208+ def arraysMap = arraysFile. exists() ? parseArraysXml(arraysFile) : [:]
209+
210+ def resolveRef = { String ref ->
211+ if (! ref) return " "
212+ if (ref. startsWith(" @string/" )) {
213+ def key = ref. substring(8 )
214+ return stringsMap. get(key, ref)
215+ } else if (ref. startsWith(" @array/" )) {
216+ def key = ref. substring(7 )
217+ def arr = arraysMap. get(key)
218+ return arr ? arr. join(" , " ) : ref
219+ }
220+ return ref
221+ }
222+
223+ def getAttrValue = { attrs , List<String > keys ->
224+ for (k in keys) {
225+ if (attrs. containsKey(k)) {
226+ return attrs[k]
227+ }
228+ }
229+ return null
230+ }
231+
232+ def md = new StringBuilder (" # Preferences Documentation\n\n " )
233+ def jsonList = [] // full JSON list to serialize
234+
235+ resXmlDir. eachFile { xmlFile ->
236+ if (xmlFile. name. toLowerCase(). startsWith(" preference" ) && xmlFile. name. endsWith(" .xml" )) {
237+ def logicalName = xmlFile. name
238+ .replaceFirst(/ ^preference_/ , ' ' )
239+ .replaceFirst(/ \. xml$/ , ' ' )
240+ .replaceAll(/ _/ , ' ' )
241+ .capitalize()
242+
243+ md. append(" ## ${ logicalName} \n\n " )
244+
245+ def xml = new XmlSlurper (false , false ). parse(xmlFile)
246+ def categories = [:]
247+
248+
249+ def processPrefs
250+ processPrefs = { node , categoryName = null ->
251+ def attrs = node. attributes()
252+ def prefType
253+ switch (node. name()) {
254+ case " SwitchPreferenceCompat" :
255+ case " SwitchPreference" :
256+ case " CheckBoxPreference" :
257+ prefType = " boolean"
258+ break
259+ case " EditTextPreference" :
260+ case " ListPreference" :
261+ prefType = " string"
262+ break
263+ case " SeekBarPreference" :
264+ prefType = " int"
265+ break
266+ case " MultiSelectListPreference" :
267+ prefType = " set"
268+ break
269+ default :
270+ prefType = " string"
271+ }
272+ if (node. name() == " PreferenceCategory" ) {
273+ def catTitleRaw = getAttrValue(attrs, [' android:title' , ' app:title' , ' title' ])
274+ def catSummaryRaw = getAttrValue(attrs, [' android:summary' , ' app:summary' , ' summary' ])
275+ def catTitle = resolveRef(catTitleRaw) ?: " Unnamed Category"
276+ def catSummary = resolveRef(catSummaryRaw) ?: " "
277+
278+ if (! categories. containsKey(catTitle)) {
279+ categories[catTitle] = [summary : catSummary, prefs : []]
280+ }
281+
282+ node. children(). each { child ->
283+ processPrefs(child, catTitle)
284+ }
285+ } else if (node. name() == " PreferenceScreen" ) {
286+ node. children(). each { child ->
287+ processPrefs(child, categoryName)
288+ }
289+ } else {
290+ def keyRaw = getAttrValue(attrs, [' android:key' , ' app:key' , ' key' ])
291+ if (keyRaw) {
292+ def titleRaw = getAttrValue(attrs, [' android:title' , ' app:title' , ' title' ])
293+ def summaryRaw = getAttrValue(attrs, [' android:summary' , ' app:summary' , ' summary' ])
294+ def defaultValueRaw = getAttrValue(attrs, [' android:defaultValue' , ' app:defaultValue' , ' defaultValue' ])
295+
296+ def title = resolveRef(titleRaw)
297+ def summary = resolveRef(summaryRaw)
298+ def defaultValue = resolveRef(defaultValueRaw)
299+
300+ if (categoryName == null ) {
301+ categoryName = " General Preferences"
302+ if (! categories. containsKey(categoryName)) {
303+ categories[categoryName] = [summary : " " , prefs : []]
304+ }
305+ }
306+
307+ categories[categoryName]. prefs << [
308+ key : keyRaw,
309+ title : title,
310+ nodeName : node. name(),
311+ summary : summary,
312+ defaultValue : defaultValue,
313+ type : prefType
314+ ]
315+ }
316+ }
317+ }
318+
319+ processPrefs(xml)
320+
321+ def jsonEntry = [
322+ setting : logicalName. toLowerCase(). replaceAll(" " , " _" ),
323+ categories : []
324+ ]
325+
326+ categories. each { catName , info ->
327+ md. append(" ### ${ catName} \n\n " )
328+ if (info. summary) {
329+ md. append(" _${ info.summary} _\n\n " )
330+ }
331+
332+ md. append(" | Key | Title | Summary | Default Value |\n " )
333+ md. append(" | --- | ----- | ------- | ------------- |\n " )
334+ info. prefs. each { p ->
335+ def escTitle = p. title?. replaceAll(" \\ |" , " \\\\ |" ) ?: " "
336+ def escSummary = p. summary?. replaceAll(" \\ |" , " \\\\ |" ) ?: " "
337+ def escDefault = p. defaultValue ? " `${ p.defaultValue.replaceAll("\\|", "\\\\|")} `" : " "
338+ md. append(" | **${ p.key} ** | ${ escTitle} | ${ escSummary} | ${ escDefault} |\n " )
339+ }
340+ md. append(" \n " )
341+ jsonEntry. categories + = info. prefs
342+ .findAll { p -> p. nodeName != " Preference" && p. key?. trim() }
343+ .groupBy { catName. replaceAll(" " , " _" ). toLowerCase() }
344+ .collect { cat , prefs ->
345+ [
346+ name : cat,
347+ preferences : prefs. collect { p -> [key : p. key, value : null , type : p. type] }
348+ ]
349+ }
350+ .findAll { it. preferences }
351+ }
352+
353+ jsonList << jsonEntry
354+ }
355+ }
356+
357+ outputFile. parentFile. mkdirs()
358+ outputFile. text = md. toString()
359+
360+ jsonList. add([
361+ metadata : [
362+ version : android. defaultConfig. versionName,
363+ code : android. defaultConfig. versionCode,
364+ gitHash : getGitHash()
365+ ]
366+ ])
367+ def json = groovy.json.JsonOutput . prettyPrint(
368+ groovy.json.JsonOutput . toJson(jsonList)
369+ )
370+
371+
372+ jsonOutputFileDoc. text = json
373+ jsonOutputFileResources. text = json
374+
375+ println " Preferences Markdown generated at: ${ outputFile.path} "
376+ println " Preferences JSON generated at: ${ jsonOutputFileDoc.path} "
377+ }
378+ }
379+
380+ preBuild. dependsOn(tasks. named(" generatePrefsDoc" ))
0 commit comments