Skip to content

Commit b3ce687

Browse files
committed
feat(android): adopt Kurdistan VPN logo v1
1 parent 53afb2a commit b3ce687

11 files changed

Lines changed: 281 additions & 19 deletions

File tree

android/app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111
android:allowBackup="false"
1212
android:dataExtractionRules="@xml/data_extraction_rules"
1313
android:fullBackupContent="false"
14-
android:icon="@drawable/ic_kurdistan_vpn"
14+
android:icon="@mipmap/ic_kurdistan_vpn"
1515
android:label="@string/product_name"
1616
android:localeConfig="@xml/locales_config"
1717
android:networkSecurityConfig="@xml/network_security_config"
18-
android:roundIcon="@drawable/ic_kurdistan_vpn"
18+
android:roundIcon="@mipmap/ic_kurdistan_vpn"
1919
android:supportsRtl="true"
2020
android:theme="@style/Theme.KurdistanVPN"
2121
android:usesCleartextTraffic="false">
5.56 KB
Loading

android/app/src/main/res/drawable/ic_kurdistan_vpn.xml

Lines changed: 0 additions & 13 deletions
This file was deleted.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
3+
<background android:drawable="@color/launcher_icon_background" />
4+
<foreground android:drawable="@drawable/ic_kurdistan_vpn_foreground" />
5+
</adaptive-icon>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
3+
<background android:drawable="@color/launcher_icon_background" />
4+
<foreground android:drawable="@drawable/ic_kurdistan_vpn_foreground" />
5+
</adaptive-icon>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
3+
<background android:drawable="@color/launcher_icon_background" />
4+
<foreground android:drawable="@drawable/ic_kurdistan_vpn_foreground" />
5+
<monochrome android:drawable="@drawable/ic_kurdistan_vpn_foreground" />
6+
</adaptive-icon>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
3+
<background android:drawable="@color/launcher_icon_background" />
4+
<foreground android:drawable="@drawable/ic_kurdistan_vpn_foreground" />
5+
<monochrome android:drawable="@drawable/ic_kurdistan_vpn_foreground" />
6+
</adaptive-icon>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<resources>
3+
<color name="launcher_icon_background">#F8FAFC</color>
4+
</resources>
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package org.kurdistanvpn.app
2+
3+
import java.awt.image.BufferedImage
4+
import java.io.File
5+
import java.security.MessageDigest
6+
import javax.imageio.ImageIO
7+
import org.junit.Assert.assertEquals
8+
import org.junit.Assert.assertFalse
9+
import org.junit.Assert.assertNotNull
10+
import org.junit.Assert.assertTrue
11+
import org.junit.Test
12+
13+
class LauncherIconResourcesTest {
14+
private val repositoryRoot: File by lazy {
15+
generateSequence(File(System.getProperty("user.dir")).canonicalFile) { it.parentFile }
16+
.firstOrNull {
17+
File(it, "android/app/src/main/AndroidManifest.xml").isFile &&
18+
File(it, "brand/kurdistan-vpn/v1/compact-transparent.png").isFile
19+
}
20+
?: error("repository root containing Android and canonical brand inputs was not found")
21+
}
22+
23+
@Test
24+
fun manifestChangesOnlyLauncherIconReferences() {
25+
val manifest = source("AndroidManifest.xml").readText().replace("\r\n", "\n")
26+
27+
assertTrue(manifest.contains("android:icon=\"@mipmap/ic_kurdistan_vpn\""))
28+
assertTrue(manifest.contains("android:roundIcon=\"@mipmap/ic_kurdistan_vpn\""))
29+
30+
val normalized = manifest
31+
.replace(Regex("android:icon=\"[^\"]+\""), "android:icon=\"@ICON@\"")
32+
.replace(Regex("android:roundIcon=\"[^\"]+\""), "android:roundIcon=\"@ROUND_ICON@\"")
33+
assertEquals(
34+
"9fc330d4e6b6f392b4508290713fbeb26e75d546552664a0f1ead86fe14ed797",
35+
sha256(normalized.toByteArray(Charsets.UTF_8)),
36+
)
37+
}
38+
39+
@Test
40+
fun adaptiveResourcesUseTheCanonicalForegroundAndSystemMonochromeLayer() {
41+
for (version in listOf("mipmap-anydpi-v26", "mipmap-anydpi-v33")) {
42+
for (name in listOf("ic_kurdistan_vpn.xml", "ic_kurdistan_vpn_round.xml")) {
43+
val text = resource("$version/$name").readText()
44+
assertTrue(text.contains("<adaptive-icon"))
45+
assertTrue(text.contains("android:drawable=\"@color/launcher_icon_background\""))
46+
assertTrue(text.contains("android:drawable=\"@drawable/ic_kurdistan_vpn_foreground\""))
47+
if (version.endsWith("v33")) {
48+
assertTrue(text.contains("<monochrome"))
49+
} else {
50+
assertFalse(text.contains("<monochrome"))
51+
}
52+
}
53+
}
54+
55+
val colors = resource("values/launcher_icon_colors.xml").readText()
56+
assertTrue(colors.contains("name=\"launcher_icon_background\">#F8FAFC</color>"))
57+
assertFalse(resource("drawable/ic_kurdistan_vpn.xml").exists())
58+
}
59+
60+
@Test
61+
fun generatedForegroundIsPaddedInsideTheAdaptiveIconSafeZone() {
62+
val image: BufferedImage? = ImageIO.read(
63+
resource("drawable-nodpi/ic_kurdistan_vpn_foreground.png"),
64+
)
65+
assertNotNull(image)
66+
image ?: return
67+
68+
assertEquals(432, image.width)
69+
assertEquals(432, image.height)
70+
71+
var minX = image.width
72+
var minY = image.height
73+
var maxX = -1
74+
var maxY = -1
75+
for (y in 0 until image.height) {
76+
for (x in 0 until image.width) {
77+
if ((image.getRGB(x, y) ushr 24) != 0) {
78+
minX = minOf(minX, x)
79+
minY = minOf(minY, y)
80+
maxX = maxOf(maxX, x)
81+
maxY = maxOf(maxY, y)
82+
}
83+
}
84+
}
85+
86+
assertTrue("foreground must contain visible pixels", maxX >= minX && maxY >= minY)
87+
assertTrue("foreground exceeds the adaptive safe-zone left edge", minX >= 84)
88+
assertTrue("foreground exceeds the adaptive safe-zone top edge", minY >= 84)
89+
assertTrue("foreground exceeds the adaptive safe-zone right edge", maxX < 348)
90+
assertTrue("foreground exceeds the adaptive safe-zone bottom edge", maxY < 348)
91+
}
92+
93+
private fun source(name: String): File =
94+
File(repositoryRoot, "android/app/src/main/$name")
95+
96+
private fun resource(path: String): File =
97+
File(repositoryRoot, "android/app/src/main/res/$path")
98+
99+
private fun sha256(bytes: ByteArray): String =
100+
MessageDigest.getInstance("SHA-256")
101+
.digest(bytes)
102+
.joinToString("") { "%02x".format(it) }
103+
}

website/scripts/sync-brand-assets.mjs

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,66 @@ async function atomicRenderPng(path, svg) {
191191
}
192192
}
193193

194+
async function renderAndroidForeground(sourceRoot) {
195+
const source = resolve(sourceRoot, 'compact-transparent.png')
196+
const metadata = await sharp(source).metadata()
197+
invariant(
198+
metadata.format === 'png' && metadata.width === 512 && metadata.height === 512,
199+
'canonical Android launcher source must be a 512 by 512 PNG',
200+
)
201+
invariant(metadata.hasAlpha === true, 'canonical Android launcher source must retain alpha')
202+
203+
return sharp(source)
204+
.resize(264, 264, { fit: 'contain' })
205+
.extend({
206+
top: 84,
207+
bottom: 84,
208+
left: 84,
209+
right: 84,
210+
background: { r: 0, g: 0, b: 0, alpha: 0 },
211+
})
212+
.png({ compressionLevel: 9, palette: false })
213+
.toBuffer()
214+
}
215+
216+
function renderAdaptiveIcon({ monochrome }) {
217+
return `<?xml version="1.0" encoding="utf-8"?>
218+
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
219+
<background android:drawable="@color/launcher_icon_background" />
220+
<foreground android:drawable="@drawable/ic_kurdistan_vpn_foreground" />${
221+
monochrome
222+
? '\n <monochrome android:drawable="@drawable/ic_kurdistan_vpn_foreground" />'
223+
: ''
224+
}
225+
</adaptive-icon>
226+
`
227+
}
228+
229+
async function syncAndroid(sourceRoot) {
230+
validateSource(sourceRoot)
231+
232+
const resourceRoot = resolve(repositoryRoot, 'android/app/src/main/res')
233+
atomicWrite(
234+
resolve(resourceRoot, 'drawable-nodpi/ic_kurdistan_vpn_foreground.png'),
235+
await renderAndroidForeground(sourceRoot),
236+
)
237+
atomicWrite(
238+
resolve(resourceRoot, 'values/launcher_icon_colors.xml'),
239+
`<?xml version="1.0" encoding="utf-8"?>
240+
<resources>
241+
<color name="launcher_icon_background">#F8FAFC</color>
242+
</resources>
243+
`,
244+
)
245+
246+
for (const version of [26, 33]) {
247+
const xml = renderAdaptiveIcon({ monochrome: version >= 33 })
248+
for (const name of ['ic_kurdistan_vpn.xml', 'ic_kurdistan_vpn_round.xml']) {
249+
atomicWrite(resolve(resourceRoot, `mipmap-anydpi-v${version}/${name}`), xml)
250+
}
251+
}
252+
}
253+
194254
function renderSocialSvg() {
195255
return `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
196256
<defs>
@@ -267,7 +327,7 @@ function parseArguments(argv) {
267327
for (let index = 0; index < argv.length; index += 1) {
268328
const argument = argv[index]
269329

270-
if (argument === '--check' || argument === '--website') {
330+
if (argument === '--check' || argument === '--website' || argument === '--android') {
271331
invariant(!mode, 'exactly one brand operation is required')
272332
mode = argument.slice(2)
273333
continue
@@ -283,7 +343,10 @@ function parseArguments(argv) {
283343
throw new Error(`unknown argument: ${argument}`)
284344
}
285345

286-
invariant(mode === 'check' || mode === 'website', 'use --check or --website')
346+
invariant(
347+
mode === 'check' || mode === 'website' || mode === 'android',
348+
'use --check, --website, or --android',
349+
)
287350
return { mode, sourceRoot }
288351
}
289352

@@ -296,8 +359,14 @@ async function main() {
296359
return
297360
}
298361

299-
await syncWebsite(sourceRoot)
300-
console.log('BRAND_WEBSITE=SYNCED')
362+
if (mode === 'website') {
363+
await syncWebsite(sourceRoot)
364+
console.log('BRAND_WEBSITE=SYNCED')
365+
return
366+
}
367+
368+
await syncAndroid(sourceRoot)
369+
console.log('BRAND_ANDROID=SYNCED')
301370
}
302371

303372
main().catch((error) => {

0 commit comments

Comments
 (0)