Skip to content

Commit 46b9c59

Browse files
committed
Implement kotlinx-datetime for the Android native targets
To test, run an Android emulator, and then, in the command line, ./gradlew androidNativeArm64TestBinaries && adb push core/build/bin/androidNativeArm64/debugTest/test.kexe /data/local/tmp/ && adb shell /data/local/tmp/test.kexe Change `Arm64` to another platfom (`X86`, `X64`, or `Arm32`) as needed.
1 parent c84554c commit 46b9c59

File tree

9 files changed

+165
-14
lines changed

9 files changed

+165
-14
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2019-2023 JetBrains s.r.o.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
@file:OptIn(ExperimentalForeignApi::class)
7+
package kotlinx.datetime.internal
8+
9+
import kotlinx.cinterop.*
10+
import platform.posix.*
11+
12+
internal actual val systemTzdb: TimeZoneDatabase get() = tzdb.getOrThrow()
13+
14+
private val tzdb = runCatching { TzdbBionic() }
15+
16+
internal actual fun currentSystemDefaultZone(): Pair<String, TimeZoneRules?> = memScoped {
17+
val name = readSystemProperty("persist.sys.timezone")
18+
?: throw IllegalStateException("The system property 'persist.sys.timezone' should contain the system timezone")
19+
return name to null
20+
}
21+
22+
private fun readSystemProperty(name: String): String? = memScoped {
23+
// see https://android.googlesource.com/platform/bionic/+/froyo/libc/include/sys/system_properties.h
24+
val result = allocArray<ByteVar>(92)
25+
val error = __system_property_get(name, result)
26+
if (error == 0) null else result.toKString()
27+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2019-2024 JetBrains s.r.o. and contributors.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
/*
6+
* Based on the bionic project.
7+
* Copyright (C) 2017 The Android Open Source Project
8+
*/
9+
10+
package kotlinx.datetime.internal
11+
12+
private class TzdbBionic(private val rules: Map<String, Entry>) : TimeZoneDatabase {
13+
override fun rulesForId(id: String): TimeZoneRules =
14+
rules[id]?.readRules() ?: throw IllegalStateException("Unknown time zone $id")
15+
16+
override fun availableTimeZoneIds(): Set<String> = rules.keys
17+
18+
class Entry(val file: ByteArray, val offset: Int, val length: Int) {
19+
fun readRules(): TimeZoneRules = readTzFile(file.copyOfRange(offset, offset + length)).toTimeZoneRules()
20+
}
21+
}
22+
23+
// see https://android.googlesource.com/platform/bionic/+/master/libc/tzcode/bionic.cpp for the format
24+
internal fun TzdbBionic(): TimeZoneDatabase = TzdbBionic(buildMap<String, TzdbBionic.Entry> {
25+
for (path in listOf(
26+
Path.fromString("/system/usr/share/zoneinfo/tzdata"), // immutable fallback tzdb
27+
Path.fromString("/apex/com.android.tzdata/etc/tz/tzdata"), // an up-to-date tzdb, may not exist
28+
)) {
29+
if (path.check() == null) continue // the file does not exist
30+
// be careful to only read each file a single time and keep many references to the same ByteArray in memory.
31+
val content = path.readBytes()
32+
val header = BionicTzdbHeader.parse(content)
33+
val indexSize = header.data_offset - header.index_offset
34+
check(indexSize % 52 == 0) { "Invalid index size: $indexSize (must be a multiple of 52)" }
35+
val reader = BinaryDataReader(content, header.index_offset)
36+
repeat(indexSize / 52) {
37+
val name = reader.readNullTerminatedUtf8String(40)
38+
val start = reader.readInt()
39+
val length = reader.readInt()
40+
reader.readInt() // unused
41+
// intentionally overwrite the older entries
42+
put(name, TzdbBionic.Entry(content, header.data_offset + start, length))
43+
}
44+
}
45+
})
46+
47+
// bionic_tzdata_header_t
48+
private class BionicTzdbHeader(
49+
val version: String,
50+
val index_offset: Int,
51+
val data_offset: Int,
52+
val final_offset: Int,
53+
) {
54+
override fun toString(): String =
55+
"BionicTzdbHeader(version='$version', index_offset=$index_offset, " +
56+
"data_offset=$data_offset, final_offset=$final_offset)"
57+
58+
companion object {
59+
fun parse(content: ByteArray): BionicTzdbHeader =
60+
with(BinaryDataReader(content)) {
61+
BionicTzdbHeader(
62+
version = readNullTerminatedUtf8String(12),
63+
index_offset = readInt(),
64+
data_offset = readInt(),
65+
final_offset = readInt(),
66+
)
67+
}.apply {
68+
check(version.startsWith("tzdata") && version.length < 12) { "Unknown tzdata version: $version" }
69+
check(index_offset <= data_offset) { "Invalid data and index offsets: $data_offset and $index_offset" }
70+
}
71+
}
72+
}

core/android/test/Util.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/*
2+
* Copyright 2019-2024 JetBrains s.r.o. and contributors.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package kotlinx.datetime.test
7+
8+
import kotlin.test.Ignore
9+
10+
actual typealias NoAndroid = Ignore

core/build.gradle.kts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,12 @@ kotlin {
4747
// Tier 4 (deprecated, but still in demand)
4848
target("linuxArm32Hfp")
4949
}
50-
// the following targets are not supported, as we don't have timezone database implementations for them:
51-
/*
52-
target("androidNativeArm32")
53-
target("androidNativeArm64")
54-
target("androidNativeX86")
55-
target("androidNativeX64")
56-
*/
50+
common("android") {
51+
target("androidNativeArm32")
52+
target("androidNativeArm64")
53+
target("androidNativeX86")
54+
target("androidNativeX64")
55+
}
5756
common("darwin") {
5857
common("darwinDevices") {
5958
// Tier 1
@@ -157,10 +156,11 @@ kotlin {
157156
}
158157
}
159158
}
160-
konanTarget.family == org.jetbrains.kotlin.konan.target.Family.LINUX -> {
161-
// do nothing special
162-
}
163-
konanTarget.family.isAppleFamily -> {
159+
160+
konanTarget.family == org.jetbrains.kotlin.konan.target.Family.LINUX ||
161+
konanTarget.family == org.jetbrains.kotlin.konan.target.Family.ANDROID ||
162+
konanTarget.family.isAppleFamily ->
163+
{
164164
// do nothing special
165165
}
166166
else -> {

core/common/src/internal/BinaryDataReader.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,17 @@ internal class BinaryDataReader(private val bytes: ByteArray, private var positi
4242
(bytes[position + 6].toLong() and 0xFF shl 8) or
4343
(bytes[position + 7].toLong() and 0xFF).also { position += 8 }
4444

45-
fun readUtf8String(length: Int) =
46-
bytes.decodeToString(position, position + length).also { position += length }
45+
fun readUtf8String(exactLength: Int) =
46+
bytes.decodeToString(position, position + exactLength).also { position += exactLength }
47+
48+
fun readNullTerminatedUtf8String(fieldSize: Int): String {
49+
var exactLength = 0
50+
while (position + exactLength < bytes.size && bytes[position + exactLength] != 0.toByte() && exactLength < fieldSize) {
51+
++exactLength
52+
}
53+
return bytes.decodeToString(position, position + exactLength)
54+
.also { position += fieldSize }
55+
}
4756

4857
fun readAsciiChar(): Char = readByte().toInt().toChar()
4958

core/nix/test/TimeZoneRulesCompleteTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import kotlin.test.*
1515
class TimeZoneRulesCompleteTest {
1616
@OptIn(ExperimentalEncodingApi::class)
1717
@Test
18+
@NoAndroid
1819
fun iterateOverAllTimezones() {
1920
val root = Path.fromString("/usr/share/zoneinfo")
2021
val tzdb = TzdbOnFilesystem(root)
@@ -67,7 +68,7 @@ private inline fun runUnixCommand(command: String): Sequence<String> = sequence
6768
// read line by line
6869
while (true) {
6970
val linePtr = alloc<CPointerVar<ByteVar>>()
70-
val nPtr = alloc<ULongVar>()
71+
val nPtr = alloc<size_tVar>()
7172
try {
7273
val result = getline(linePtr.ptr, nPtr.ptr, pipe)
7374
if (result != (-1).convert<ssize_t>()) {

core/nix/test/Util.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55

66
package kotlinx.datetime.test
77

8+
@OptIn(ExperimentalMultiplatform::class)
9+
@OptionalExpectation
10+
expect annotation class NoAndroid()
11+
812
// od --format=x1 --output-duplicates --address-radix=n --width=16 /usr/share/zoneinfo/Europe/Berlin |
913
// sed -e 's/\b\(\w\)/0x\1/g' -e 's/\(\w\)\b/\1,/g'
1014
// Do not remove the type annotation, otherwise the compiler slows down to a crawl for this file even more.

license/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,8 @@ may apply:
2727
https://raw.githubusercontent.com/unicode-org/cldr/master/common/supplemental/windowsZones.xml
2828
- License: Unicode ([license/thirdparty/unicode_license.txt](thirdparty/unicode_license.txt))
2929

30+
- Path: `core/android/src`
31+
- Origin: implementation is based on the bionic project.
32+
- License: BSD ([license/thirdparty/bionic_license.txt](thirdparty/bionic_license.txt))
3033

3134
[threetenbp]: thirdparty/threetenbp_license.txt

license/thirdparty/bionic_license.txt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
Copyright (C) 2017 The Android Open Source Project
2+
All rights reserved.
3+
4+
Redistribution and use in source and binary forms, with or without
5+
modification, are permitted provided that the following conditions
6+
are met:
7+
* Redistributions of source code must retain the above copyright
8+
notice, this list of conditions and the following disclaimer.
9+
* Redistributions in binary form must reproduce the above copyright
10+
notice, this list of conditions and the following disclaimer in
11+
the documentation and/or other materials provided with the
12+
distribution.
13+
14+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
16+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
17+
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
18+
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
19+
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
20+
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
21+
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
22+
AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
24+
OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25+
SUCH DAMAGE.

0 commit comments

Comments
 (0)