every Tech Blog

株式会社エブリーのTech Blogです。

Android アプリでの正確なタイムスタンプの運用方法を考える

はじめに

こんにちは、デリッシュキッチンでクライアントエンジニアを担当している kikuchi です。

近年は Web のサービスに限らず、アプリでもネットワーク接続を実施することが当たり前になってきていますが、皆さんはネットワーク接続をするアプリでは必須となる
タイムスタンプ について実装方法や管理方法を意識されたことはあるでしょうか?

タイムスタンプを使用するケースは多く、例えば

  • ログイン情報を保存する機能で、ログインを実施した日時を管理する
  • ワンタイムパスワードを発行する機能で、発行された日時や有効期限を管理する
  • スケジュールを管理する機能で、スケジュールが実行される日時を管理する
  • (プログラムのロジックで) 一定時間経過後に初めてポップアップを出す場合に、基準となる時間を管理する

のように多種多様な使われ方をしており、おそらく 1 つもプログラム上でタイムスタンプを使用していないアプリやサービスは無いかと思っています。
それほど当たり前のように使われているタイムスタンプですが、管理方法や発行手順を間違えると重大なバグに繋がることがあるため、今回は タイムスタンプをどう正しく管理すべきか という観点で
運用方法についてまとめてみたいと思います。

なぜタイムスタンプを正しく管理する必要があるのか

タイムスタンプが正しく運用されていない場合の問題点を考えてみたいと思います。

まずはセキュリティ (不正利用防止) の観点。
例えば毎日ログインをする度にインセンティブを付与するようなアプリで、端末の時間だけでタイムスタンプを管理していた場合、端末の設定を 1 日後にずらしてアプリを再起動を繰り返すだけで
インセンティブを簡単に取得できてしまいます。
もし金銭に関わるもの (ポイントなど) を付与していた場合、ポイントを取得して交換、そしてアプリを再インストールするといういわゆるリセマラをされると、企業としては大幅な損失に繋がる可能性が出てしまいます。

そして次に整合性の観点。
行動ログを管理 (分析) する機能にて、アプリが端末の時間で生成したタイムスタンプを正として保存する仕組みとなっており、

  • 行動 A : 現実世界の 10:00 に実施 / 端末の時間も 10:00
  • 行動 B : 現実世界の 11:00 に実施 / 端末の時間も 11:00
  • 行動 C : 現実世界の 12:00 に実施 / 端末の時間が不具合で 10:30 となっていた

という操作が行われた場合、期待としては A → B → C という順で保存されていてほしいものの、実際には A → C → B と保存されており、ログの整合性が担保されなくなります。

最後に正確性の観点。
データをタイムスタンプとともに記録するようなアプリで、タイムゾーンを考慮しておらずアプリは画面によって管理が異なる、サーバは協定世界時 (UTC) で管理していた場合、サーバで UTC 12:00 で管理されたデータを取得すると

  • 画面 A (端末の設定に依存、今回は例としてアメリカ中部標準時の CST に変換) : 6:00 と表示
  • 画面 B (日本標準時の JST に変換) : 21:00 として表示

となり画面によって表示がズレてしまい、正確性が担保されなくなります。
上記では表示するデータという比較的気づきやすい例を書きましたが、内部ロジックの場合、常に同じ人員・開発環境で開発を行っていると気づかずに予期せぬ不具合に繋がることがあります。

複数例を挙げましたが、他にもタイムスタンプを正しく運用しないことで発生する問題は多数存在するため、如何に正確に改善されることなく運用できるかがアプリやサービスの質や信頼性に繋がるものだと考えています。

タイムスタンプの管理方法

設計段階でやることとしては、 扱う時刻のタイムゾーンは何にするか を決めることが重要かと思います。
先述したタイムゾーンを考慮していない問題については、設計段階で明確に取り決めが行われなかった (サーバエンジニア、アプリエンジニアで認識がずれていた) 事が要因のため、
サービスとして取り扱うタイムスタンプのタイムゾーンは一貫してこれ、という取り決めが必要になります。
例えば

  • サービスとして記録、及び通信データとしてやり取りする全てのタイムスタンプは UTC として取り扱う
  • アプリ上で表示に使用する場合は端末で設定されたタイムゾーンに変換して取り扱う

と決めておけば、先述の正確性の問題は回避できるようになります。

そして、サーバと通信を行うアプリ・サービスであれば サーバの時刻を正とする 事が重要かと思います。
複数のトランザクションを制御する際にアプリが生成した時刻を正として信じてしまうと、先述した通り不正利用、整合性の欠如に繋がるため、時刻を管理する機能は一箇所に集約する必要があります。

上記でサーバがデータを管理するアプリについては正しくタイムスタンプを管理できますが、サーバがメンテナンスなどで使用できず、アプリ単体でタイムスタンプを管理しなければならないケースではどうでしょうか。

次の章では Android アプリ単体で正確にタイムスタンプを管理する方法を考えてみたいと思います。

Android で正確にタイムスタンプを管理するには

端末の設定で日時の自動設定が ON になっていれば一定の正確性は担保できますが、やはり端末の設定を弄られる可能性があるため確実ではありません。
このようなケースでは TrustedTime API というものを使用すると問題を回避できます。

◯公式情報 https://developers.google.com/android/reference/com/google/android/gms/time/TrustedTime
https://android-developers.googleblog.com/2025/02/trustedtime-api-introducing-reliable-approach-to-time-keeping-for-apps.html

こちらを使用するとネットワーク接続自体は必要なものの、Google が提供するインフラストラクチャにアクセスし、正確なタイムスタンプが取得できるようになります。
簡単ではありますが、図で表すとこの様になります。

毎回 Google が提供するインフラストラクチャにアクセスせず、TrustedTime のロジックで時間を算出することでネットワーク使用量を削減。
またデバイスの負荷状況などで正確な値を算出できなくなる場合は、TrustedTime API を通じてアプリ側に通知を出せる仕組みとなっています。

以降で TrustedTime API を使用してタイムスタンプを取得する実装方法をまとめてみたいと思います。

実装方法

1. app レベルの build.gradle にライブラリを追加

dependencies {
    implementation("com.google.android.gms:play-services-time:16.0.1")
}

2. TrustedTime API を実行

fun execute(context: Context) {
    val trustedTimeClient = TrustedTime.createClient(context)
    trustedTimeClient.addOnCompleteListener { task ->
        if (task.isSuccessful) {
            val client = task.result
            client.computeCurrentUnixEpochMillis()?.also { timeMillis ->
                val date = Date(timeMillis)
                val format = SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.JAPAN)
                Log.i("TestLog", "time : ${format.format(date)}")
            }
        } else {
            Log.w("TestLog", "error : ${task.exception?.message}")
        }
    }
}

TrustedTime.createClient では TrustedTimeClient のインスタンスを提供する Task が取得できます。
Task により非同期処理が開始されるため、addOnCompleteListener で結果を受け取るリスナーを定義します。
後は task.isSuccessful (タスクが成功した場合) にタイムスタンプを取り扱うのみとなります。

上記の実装例では、computeCurrentUnixEpochMillis で Unix エポックからの経過時間をミリ秒で取得しており、日本時間に変換してログ出力しています。
実際に実行し、ログを出力した結果は以下のようになります。

TrustedTime API は端末起動後に一度だけネットワーク接続が必要なものの、以降はネットワーク接続が無い場合でも内部のロジックで正確なタイムスタンプを返却してくれます。
実際に端末を機内モードにしてネットワーク接続を OFF、かつ端末の時間を数日ずらした状態でも正確なタイムスタンプが返却されました。

実装は以上となるため、非常に簡単に正確なタイムスタンプを管理する方法を実装できました。

注意点

TrustedTime API は正確なタイムスタンプを取り扱う上で有効な手段ではありますが、いくつか注意点があります。

  • 端末起動後に一度もネットワーク接続を行っていない場合はタイムスタンプを取得できない
  • 端末・Google サーバ間の通信経路が完全に安全ではない
  • Google のサーバが改ざんされている場合は不正なタイムスタンプが返却される
  • 端末がルート化され、かつ TrustedTime API の動作が改変される可能性がある
  • Android 5 (Lolipop) 以降のみで使用可能

などが挙げられます。
ただし、上記で挙げられるような問題点は TrustedTime API の話に限らず、アプリと自社のアプリサーバでやり取りするケース、別の API を使用するケースでも発生しうる問題のため
サービスとしてどこまでの品質を担保するか、どこまでの問題を許容するかを検討したうえで適切な案を採用する形が良いかと考えます。

まとめ

今回はタイムスタンプを扱う上での注意点や実装案などをまとめてみましたが、意外と細かいタイムゾーンの認識ズレ、実装ミスが発覚するケースは私自身も過去何度か遭遇しているため
設計の段階から積極的なコミュニケーションは必要不可欠だと思いました。
またタイムスタンプを正確に取り扱う手法としては、TrustedTime API は実装・学習コストが低く、非常に有効かと感じました。

本記事の情報が皆様のお役に立てれば幸いです。