-
Notifications
You must be signed in to change notification settings - Fork 33
Expand file tree
/
Copy pathsetLabelToIcon.gradle
More file actions
496 lines (425 loc) · 16.5 KB
/
setLabelToIcon.gradle
File metadata and controls
496 lines (425 loc) · 16.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
import groovy.util.slurpersupport.GPathResult
import javax.xml.parsers.*
import javax.xml.transform.*
import javax.xml.transform.dom.*
import javax.xml.transform.stream.*
import org.w3c.dom.*
import javax.imageio.ImageIO
import java.awt.Color
import java.awt.Font
import java.awt.FontMetrics
import java.awt.Graphics2D
import java.awt.RenderingHints
import java.awt.image.BufferedImage
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import org.slf4j.Logger
task changeIcon
tasks.whenTaskAdded { Task task ->
for (int i = 0; i < IconService.BUILD_VARIANTS.size(); i++) {
String buildVariant = IconService.BUILD_VARIANTS[i]
if (task.name.contains("merge${Utils.capitalize(buildVariant)}Resources")) {
task.dependsOn(changeIcon)
changeIcon.doFirst {
new IconService(buildVariant, app.versionName, logger).change()
android.sourceSets.findByName(buildVariant).res.srcDir(
new File("${project.buildDir}/$IconService.GENERATED_PATH/$buildVariant/res")
)
}
}
}
}
/**
* Service that add label to icon for "debug" and "qa" build variants
*
* It works only with png images and Surf architecture
*
* @param buildVariant - gradle build variant
* @param versionName - application's version
*/
class IconService {
public final static String[] BUILD_VARIANTS = [DEBUG_BUILD_VARIANT, QA_BUILD_VARIANT]
public final static GENERATED_PATH = "generated/iconlauncher"
private final static BUILD_GENERATED_PATH = "build/$GENERATED_PATH"
private final static APP_DIR_NAME = "app"
private final static TEMPLATE_DIR_NAME = "template"
private final static RES_PATH = "src/main/res/"
private final static MANIFEST_PATH = "src/main/AndroidManifest.xml"
private final static APPLICATION_PROPERTY_NAME = "application"
private final static ICON_PROPERTY_NAME = "@android:icon"
private final static ROUND_ICON_PROPERTY_NAME = "@android:roundIcon"
private final static BACKGROUND_PROPERTY_NAME = "background"
private final static FOREGROUND_PROPERTY_NAME = "foreground"
private final static DRAWABLE_PROPERTY_NAME = "@android:drawable"
private final static XML_EXTENSION = "xml"
private final static OVERLAY_FILE_EXTENSION = "png"
private final static OVERLAY_FILE_NAME = "overlayicon"
private final static OVERLAY_FILE_PATH = "$OVERLAY_FILE_NAME.$OVERLAY_FILE_EXTENSION"
private final static DEBUG_BUILD_VARIANT = "debug"
private final static QA_BUILD_VARIANT = "qa"
private final static VECTOR_ICON_SUFFIX = "_ic_lnr"
private File appDir = null
private File overlayImage = null
private File variantDir = null
private final String buildVariant
private final String versionName
private final Logger logger
IconService(String buildVariant, String versionName, Logger logger) {
this.buildVariant = buildVariant
this.versionName = versionName
this.logger = logger
}
/**
* Start process to change launcher icon
*/
void change() {
appDir = getAppDir()
if (appDir == null) {
logger.debug("app directory not found")
return
}
Icon icon = getIcon()
if (icon == null) {
logger.debug("launcher icon not found")
return
}
if (icon.commonIcon != null) {
processIcon(icon.commonIcon)
} else {
logger.debug("launcher icon doesn't define in manifest file")
}
if (icon.roundIcon != null) {
processIcon(icon.roundIcon)
} else {
logger.debug("round launcher icon doesn't define in manifest file")
}
}
/**
* Find icon and start changing
* @param iconPath - launcher icon path
*/
private void processIcon(IconPath iconPath) {
File resDir = new File("${appDir.path}/${RES_PATH}")
if (!resDir.exists()) return
variantDir = new File("${appDir.path}/$BUILD_GENERATED_PATH/$buildVariant")
File variantResDir = new File("${variantDir.path}/res")
overlayImage = generateOverlayImage(variantResDir)
if (overlayImage == null) {
logger.debug("overlay image can not be create")
return
}
List<File> rowIcons = findIcons(resDir, iconPath)
if (rowIcons == null || rowIcons.empty) return
processImageIcons(findFilesByExtension(rowIcons, XML_EXTENSION, true), variantResDir)
processXmlIcons(findFilesByExtension(rowIcons, XML_EXTENSION, false), resDir, variantResDir)
}
/**
* Changing image icons
*
* @param rowIcons - icons
* @param variantResDir - res build variant directory
*/
private void processImageIcons(List<File> rowIcons, File variantResDir) {
rowIcons.forEach { icon ->
String outputPath = "${variantResDir.path}/${icon.path.split(RES_PATH).last()}"
File output = new File(outputPath)
try {
copyFile(icon, output)
changeVariantIcon(icon, output)
} catch (IOException ex) {
logger.debug(ex.toString())
}
}
}
/**
* Parse xml adaptive-icon to find background, foreground image icons
*
* @param rowIcons - icons
* @param variantResDir - res build variant directory
*/
private void processXmlIcons(List<File> rowIcons, File resDir, File variantResDir) {
List<File> icons = new ArrayList()
rowIcons.forEach { icon ->
GPathResult adaptiveIconNode = new XmlSlurper().parse(icon)
if (adaptiveIconNode == null) return
icons.addAll(getAdaptiveIcon(adaptiveIconNode, resDir, FOREGROUND_PROPERTY_NAME))
}
if (icons.empty) return
processImageIcons(findFilesByExtension(icons, XML_EXTENSION, true), variantResDir)
List<File> vectorIcons = findFilesByExtension(icons, XML_EXTENSION, false)
vectorIcons.forEach { icon ->
File variantIcon = new File("$variantResDir/${icon.parentFile.name}/${createVectorIconName(icon)}")
File layerList = new File("$variantResDir/${icon.parentFile.name}/${icon.name}")
copyFile(icon, variantIcon)
createFile(layerList)
createVectorVariantIcon(layerList, variantIcon)
}
}
/**
* Create layer-list res for adaptive icon
* @param icon - parent icon
* @param varIcon - variant icon
*/
private void createVectorVariantIcon(File layerList, File varIcon) {
Document dom
Element e = null
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance()
try {
DocumentBuilder db = dbf.newDocumentBuilder()
dom = db.newDocument()
Element root = dom.createElement("layer-list")
e = dom.createElement("item")
e.setAttributeNS("http://schemas.android.com/apk/res/android", "android:drawable", "@drawable/${varIcon.name.split("\\.")[0]}")
root.appendChild(e)
e = dom.createElement("item")
e.setAttributeNS("http://schemas.android.com/apk/res/android", "android:drawable", "@drawable/$OVERLAY_FILE_NAME")
root.appendChild(e)
dom.appendChild(root)
try {
Transformer tr = TransformerFactory.newInstance().newTransformer()
tr.setOutputProperty(OutputKeys.INDENT, "yes")
tr.setOutputProperty(OutputKeys.METHOD, "xml")
tr.setOutputProperty(OutputKeys.ENCODING, "UTF-8")
tr.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4")
tr.transform(
new DOMSource(dom),
new StreamResult(new FileOutputStream(layerList))
)
} catch (TransformerException te) {
logger.debug(te.getMessage())
} catch (IOException ioe) {
logger.debug(ioe.getMessage())
}
} catch (ParserConfigurationException pce) {
logger.debug("UsersXML: Error trying to instantiate DocumentBuilder " + pce)
}
}
/**
* Parse xml to get adaptive-icon background and foreground icons
*
* @param rootNode - root xml-element
* @param resDir - directory to find background and foreground icons
* @param property - xml's property
*
* @return icons
*/
private List<File> getAdaptiveIcon(GPathResult rootNode, File resDir, String property) {
GPathResult icon = (GPathResult) rootNode.getProperty(property)
return findIcons(
resDir,
parseIcon(String.valueOf(icon.getProperty(DRAWABLE_PROPERTY_NAME)))
)
}
/**
* Find files by extension
*
* @param files - files
* @param extension - extension
* @param isNot - not flag
*
* @return files with extension
*/
private List<File> findFilesByExtension(List<File> files, String extension, Boolean isNot) {
List<File> res = new ArrayList()
files.forEach { file ->
if (file.path.split("\\.").last() == extension) {
if (!isNot) res.add(file)
} else {
if (isNot) res.add(file)
}
}
return res
}
/**
* Combine launcher icon with overlay image
*
* @param input - input icon
* @param output - output icon
*/
private void changeVariantIcon(File input, File output) {
BufferedImage image = ImageIO.read(input)
BufferedImage overlayImage = ImageIO.read(overlayImage)
Graphics2D g2d = image.createGraphics()
g2d.drawImage(overlayImage, 0, 0, image.width, image.height, null)
g2d.dispose()
try {
ImageIO.write(image, OVERLAY_FILE_EXTENSION, output)
} catch (IOException ex) {
logger.debug(ex.toString())
}
}
/**
* Create overlay image with version label
*
* @return overlay image
*/
private File generateOverlayImage(File variantResDir) {
List<String> texts = new ArrayList()
def width = 192
def height = 192
def textWidth = 100
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
Graphics2D g2d = img.createGraphics()
Font font = new Font("Arial", Font.PLAIN, 28)
g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY)
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY)
g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE)
g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON)
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR)
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)
g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE)
g2d.setFont(font)
FontMetrics fm = g2d.getFontMetrics()
int startIndex = 0
for (int i = 0; i < versionName.length(); i++) {
if (fm.stringWidth(versionName.substring(startIndex, i)) > textWidth) {
texts.add(versionName.substring(startIndex, i - 1))
startIndex = i - 1
}
if (i == versionName.length() - 1) {
texts.add(versionName.substring(startIndex, i + 1))
}
}
int labelSize = fm.getAscent() * texts.size()
int x = ((width - textWidth) / 2).toInteger()
int y = ((height - labelSize) / 2).toInteger()
if (buildVariant == QA_BUILD_VARIANT) {
g2d.setColor(new Color(103, 86, 255, 150))
} else {
g2d.setColor(new Color(255, 66, 66, 150))
}
g2d.fillRect(x, y, textWidth, labelSize)
g2d.setColor(Color.BLACK)
for (def i = 0; i < texts.size(); i++) {
g2d.drawString(texts[i], x, y + fm.getAscent() * (i + 1))
}
g2d.dispose()
try {
File output = new File("${variantResDir.path}/drawable/$OVERLAY_FILE_PATH")
createFile(output)
ImageIO.write(img, OVERLAY_FILE_EXTENSION, output)
return output
} catch (IOException ex) {
logger.debug(ex.toString())
}
return null
}
/**
* Find app directory
*
* @return app directory
*/
static File getAppDir() {
File appInjector = new File("./${APP_DIR_NAME}")
if (appInjector.exists()) return appInjector
appInjector = new File("./${TEMPLATE_DIR_NAME}/${APP_DIR_NAME}")
if (appInjector.exists()) return appInjector
return null
}
/**
* Find launcher icon for application
*
* @return icon
*/
private Icon getIcon() {
File manifestFile = new File("${appDir.path}/$MANIFEST_PATH")
if (!manifestFile.exists()) return null
GPathResult manifestXml = new XmlSlurper().parse(manifestFile)
GPathResult applicationNode = (GPathResult) manifestXml.getProperty(APPLICATION_PROPERTY_NAME)
return new Icon(
parseIcon(String.valueOf(applicationNode.getProperty(ICON_PROPERTY_NAME))),
parseIcon(String.valueOf(applicationNode.getProperty(ROUND_ICON_PROPERTY_NAME)))
)
}
/**
* Parse string
*
* @param iconStr - string e.x. "@drawable/ic-launcher"
*
* @return - iconPath
*/
private IconPath parseIcon(String iconStr) {
if (iconStr.empty) return null
List<String> iconParts = iconStr.split("/")
if (iconParts[0] == null || iconParts[0].empty || iconParts[1] == null || iconParts[1].empty) return null
return new IconPath(iconParts[0].substring(1), iconParts[1])
}
/**
* Find icons
*
* @param folder - res directory
* @param iconPath - icon path
*
* @return icons
*/
private List<File> findIcons(File folder, IconPath iconPath) {
List<File> iconDirs = folder.listFiles(new FileFilter() {
@Override
boolean accept(File file) {
return file.name.contains(iconPath.folderName)
}
})
if (iconDirs == null || iconDirs.isEmpty()) return null
List<File> icons = new ArrayList()
iconDirs.forEach { File iconDir ->
icons.addAll(
iconDir.listFiles(new FileFilter() {
@Override
boolean accept(File file) {
if (file.file) {
if (file.name.empty) return false
String[] fileNameParts = file.name.split("\\.")
if (fileNameParts.size() != 2) return false
return fileNameParts[0] == iconPath.iconName
}
return false
}
})
)
}
if (icons.empty) return
return icons
}
private void createFile(File file) {
if (!file.parentFile.exists()) file.parentFile.mkdirs()
if (!file.exists()) file.createNewFile()
}
private void copyFile(File fileFrom, File fileTo) {
File absFileFrom = fileFrom.absoluteFile
File absFileTo = fileTo.absoluteFile
createFile(absFileTo)
Files.copy(absFileFrom.toPath(), absFileTo.toPath(), StandardCopyOption.REPLACE_EXISTING)
}
private String createVectorIconName(File icon) {
String[] iconParts = icon.name.split("\\.")
return "${iconParts[0]}$VECTOR_ICON_SUFFIX${iconParts[1]}.$XML_EXTENSION"
}
}
class Utils {
static String capitalize(String str) {
return str.substring(0, 1).toUpperCase() + str.substring(1)
}
}
/**
* Representing launcher icon
*/
class Icon {
IconPath commonIcon
IconPath roundIcon
Icon(IconPath commonIcon, IconPath roundIcon) {
this.commonIcon = commonIcon
this.roundIcon = roundIcon
}
}
/**
* Representing app-icon data
*/
class IconPath {
String folderName
String iconName
IconPath(String folderName, String iconName) {
this.folderName = folderName
this.iconName = iconName
}
}