Answer the question directly
The recommended approach is to use the modern java.time API (available from API 26) together with a bundled copy of the latest IANA tzdata, and to detect when the device’s system zoneinfo is missing or stale so you can load your own data and inform the user.
Confirmed facts
- On Android < 26,
ZoneId.systemDefault() falls back to GMT and logs a warning when the system tzdata is absent or corrupted.
- The
java.time classes delegate to ICU, which reads the zoneinfo files under /data/misc/zoneinfo (or the OEM‑provided location).
- Device manufacturers may ship outdated or custom tzdata builds; the platform does not automatically update them.
Likely explanation (based on common Android behavior)
When the system zoneinfo is missing or outdated, the ICU library cannot find the requested zone and returns the GMT offset. This causes all date‑time conversions to be off by the actual offset, leading to incorrect local times for users.
Steps to implement the best practice
- Choose the API baseline
- If your
minSdkVersion ≥ 26, rely on java.time.ZoneId and ZonedDateTime.
- If you must support API < 26, use
java.util.TimeZone with an ICU4J fallback (or the ThreeTenABP backport) to get the same behavior.
- Bundle the latest tzdata
- Download the newest IANA tzdata zip (e.g., from
data.iana.org) and extract the zoneinfo directory.
- Place the extracted files in
src/main/assets/tzdb/ (or any subfolder you prefer).
- Detect missing/outdated system tzdata at runtime
- Read the system tzversion:
String sysVer = System.getProperty("persist.sys.timezone.version"); (available on most devices) or read /data/misc/zoneinfo/version if the property is absent.
- Compare
sysVer with the version string you bundled (e.g., embed tzdata2024a in assets/tzdb/version).
- If the system version is null, empty, or older than your bundled version, treat the zoneinfo as missing/outdated.
- Load your bundled tzdata when needed
- For API ≥ 26: use
ZoneIds.of with a custom ZoneRulesProvider that reads from your assets (see java.time.zone.ZoneRulesProvider implementation).
- For API < 26: call
TimeZone.setDefault(TimeZone.getTimeZone(customId)) after initializing ICU with your bundled data (ThreeTenABP provides helper methods).
- Inform the user
- When you detect a mismatch, show a non‑intrusive snackbar or dialog explaining that the app is using its own time‑zone data to ensure correct local times, and offer a link to the device’s system update (Google Play services → “Time zone update”).
- Keep the bundled data up‑to‑date
- Include a version check in your app’s startup; if a newer tzdata is available via a remote config (e.g., Firebase Remote Config), download and replace the assets in internal storage.
Verification steps (safe, scoped)
# 1. Simulate missing zoneinfo on a rooted device or emulator
adb shell su 0 rm -rf /data/misc/zoneinfo/*
# 2. Launch your app and log the detected tzversion
logcat -s TimeZoneHelper
# 3. Verify that the app loads the bundled tzdata (look for your version string)
# 4. Check a known future date (e.g., 2025-03-09T02:30:00 in "America/New_York")
# and confirm the offset matches the expected DST rule.
If the app shows the bundled version in the log and the conversion matches the expected offset, the fallback handling is working.
One missing diagnostic detail that could change the recommendation
Please confirm your app’s minSdkVersion (or the lowest API level you need to support). This determines whether you can rely solely on java.time or must include an ICU4J/ThreeTenABP fallback for older devices.