Send data notification to Android app with Cloud Functions for Firebase

Androidアプリがフォアグラウンドでもバックグラウンドでも、通知を受け取った後にそれを表示するかどうかをアプリ内で判断させたいときは Firebase の data message を送る必要がある。

FirebaseコンソールからはNotification Composerを使って、notification messageを送ることができる。notification messageはアプリケーションがバックグラウンドのとき、システムのトレイに入るため、アプリケーション側で表示・非表示の判断をすることができない。

data messageを送るには、独自サーバーで動いているアプリケーションかCloud Functions for Firebaseを実装しなければならない。

背景

notification messageでは要件を満たせなかった。

  • 毎日18時に、当日(深夜0時から)まだアプリを開いていないユーザー送りたい
  • ユーザーが当日ログインしたかどうかは、端末のDBに保存している
  • 端末のtimezoneが日本ではない場合は通知を表示しない
  • 災害などの緊急時はユーザーがアプリを開かなくても通知を止められる
  • 通知が届く時間は、18時ちょうどである必要はない

必要なもの

  1. Androidアプリ
  2. Firebaseプロジェクト
  3. Cloud Functions for Firebaseプロジェクト

それぞれの役割

Androidアプリ
  • FCMトークンを生成し、Fire Storeに保存
  • data messageを受け取り、通知を表示する
Firebaseプロジェクト
Cloud Functionプロジェクト
  • Fire Storeに保存されたFCMトークンを取り出す
  • httpリクエストをトリガーにして、data messageを送る
  • 有料だがcronで時間をトリガーにして、送ることもできる(Schedule functions  |  Firebase)

1. Firebaseプロジェクトを作る

Add Firebase to your Android project  |  Firebase を参考にプロジェクトを作る。

2. Cloud Functions for Firebaseを作る

時間がある人は Codelab の Cloud Functions for Firebase がおすすめ。

Firebase CLIを使ってファイルを生成する。

Get started: write and deploy your first functions  |  Firebase を参考にする。

npm -g install firebase-tools
firebase login
firebase init functions

途中でFirebaseプロジェクトと言語(JavaScript | TypeScript)を選ぶ。

functioins/index.jsを編集する。

A cloud function which sends data notification to ...

ファイルを保存したら、Firebase CLIを使ってデプロイする。

firebase deploy --only functions

Deployされたら、curlコマンドで叩いてみよう。

curl https://us-central1-MY_PROJECT.cloudfunctions.net/sendNotificationApi

DeployするとFirebaseコンソール の Functionsに登録される。

f:id:bambinya:20190430182227p:plain

3. Androidアプリの作成

Set up a Firebase Cloud Messaging client app on Android  |  Firebase を手順通りに実装する。

生成されたFCMトークンをFirestoreに保存する方法は

Get started with Cloud Firestore  |  Firebase を参考にした。

上で紹介したindex.jsでとりあえず動かしたいときはコピペして使ってください。

private fun sendRegistrationToServer(token: String?) {
val tokenMap : HashMap<String, String> = HashMap()
tokenMap["fcmTokens"] = token?: ""
val db: FirebaseFirestore = FirebaseFirestore.getInstance()
db.collection("fcmTokens")
.document(token?:"")
.set(tokenMap)
.addOnSuccessListener { Log.d(tag, "token was added: ${token}") }
.addOnFailureListener { Log.d(tag, "token was NOT added") }
}

 DBには、このように保存される。

f:id:bambinya:20190430182603p:plain

 

TODO for project:

チャンネルとtopicとバックグラウンドでの使用が制限されたアプリ(Send messages to multiple devices  |  Firebase)に関しては調査が必要。

 

おしまい。

 

【和訳】Android Architecture Components ViewModel

自分で理解するために ViewModel | Android Developers (原文) を訳しました。

サンプルコードや図を全て写していないので、原文を見ながら適宜理解を深めるのに利用してください。また、理解しやすいよう所々補足を加えています。先にアクティビティの実行時の変更処理について理解しておくと読みやすくなります。

 

ViewModel

ViewModelクラスはUIに関連するデータの保持と管理をするようデザインされています。そのため画面回転などの設定変更があってもデータは保持されます。

NOTE: ViewModelをAndroidプロジェクトにインポートするには、adding components to your project を見てください。

アクティビティやフラグメントなどのAppコンポーネントAndroidフレームワークによって管理されるライフサイクルを持ちます。
Androidフレームワークは、開発者がコントロールできないユーザーのアクションや端末のイベントに従って、ActivityやFragmentを破棄または再生成するか決めます。
Activity/FragmentのオブジェクトがOSによって破棄または再生成されると、それらが保持していた全てのデータは破棄されます。
例えば、ユーザーのリスト(List<User>)をActivityが持っていて、設定変更によってActivityが再生成されると、新しく生成されたActivityはリストデータを取得し直さなければなりません。
データが単純ならActivity#onSaveInstanceState()メソッドを使って、Activity#onCreate()でBundleからデータを修復することができます。
しかしこの方法は、UIの状態など少量のデータに向いています。サイズが大きくなりそうなユーザーのリスト(List<User>)には向いていません。

他にも問題があります。activityやfragmentなどのUIコントローラーは戻るまでに時間のかかる非同期の処理を頻繁に呼ぶ必要があります。
UIコントローラーはそれらの非同期処理を管理する必要があります。非同期処理が中断されたら、メモリリークを避けるためそれらを片付ける必要があります。
これは多くの管理を必要とします。また、オブジェクトが設定変更によって再生成されて、同じ処理が呼ばれることはリソースの無駄使いです。

最後にこれも重要なことですが、これらのUIコントローラーはユーザーのアクションに反応したり、OSとのコミュニケーションに対応する必要があります。
そして自身が持つリソースも管理することになると、クラスは膨れ上がります。
そのような状態は、1つのクラスがアプリのすべての機能を他のクラスに委譲せずに自分自身でやろうとすることになります。このような状態はテストを難しくします。

Viewに必要なデータのオーナーシップをUIコントローラーのロジックから分離するのが簡単で効果的です。
ライフサイクルパッケージはViewModelと呼ばれる新しいクラスを提供します。これはUIコントローラー(Activity/Fragment)のためのヘルパークラスで、UIに使うデータを準備する役割を担います。
ViewModelは設定変更があっても自動的に保持されます。そのため、ViewModelが持つデータは再生成されたActivity/Fragmentですぐに利用できます。

前述しましたが、ユーザーのリスト(List<User>)を取得し保持するのはActivityやFragmentではなく、ViewModelの責任であるべきです。
もしActivityが再生成されても、再生成された新しいActivityは、前のActivityによって作られたものと同じViewModelのインスタンスを受け取ります。
Activityが破棄されると、AndroidフレームワークはViewModel#onCleared()を呼び、ViewModelのインスタンスは破棄されます。

NOTE: Activity/Fragmentの初期化時にもViewModelは生き残ることから、ViewModelはViewを参照するべきではありません。
またどんなクラスでもそれがActivityコンテクストの参照を持つ場合は、そのクラスを参照すべきではありません。
もしViewModelがApplicationコンテクストを必要とする場合は、提供されているAndroidViewModelを継承して、コンストラクタでApplicationを受け取ってください。(ApplicationクラスはContextを継承しています)

 

Sharing Data Between Fragments

2つ以上のフラグメントが1つのアクティビティの中で、連携を取り合うことはよくあることです。
2つのフラグメントがインタフェースを定義し、オーナーであるactivityが2つを結ぶことは、珍しいことではありません。
さらに2つのフラグメントはお互いが存在するか、visibleな状態であるか考えなければなりません。
このよくある問題は、ViewModelオブジェクトを使って解決できます。
よくあるmaster-detail fragmentsのケースを想像してください。アイテムリストを表示するfragmentからユーザーがアイテムを1つ選ぶと、選んだアイテムの詳細を表示するfragmentが出てくるパターンです。
この2つのfragmentは、Activityのスコープを使って、お互いのコミュニケーションを管理するためにViewModelを共有することができます。

gistbd67d578044e830ee00943b0e43145e7

2つのfragmentがVIewModelProviderを利用する際、getActivity()を使っていることに注意してください。これは2つのFragmentが同じactivityの範囲で、SharedViewModelのインスタンスを受け取ることを意味します。

このアプローチの利点:

  • Activityは何もする必要がありません。fragmentsのコミュニケーションについて知る必要もありません。
  • FragmentはViewModel以外、お互いを一切知る必要がありません。片方が消えても、もう片方はいつも通り動き続けます。
  • 各Fragmentはそれぞれのライフサイクルを持ちます。もう片方のライフサイクルの影響を受けることもありません。実際、Fragmentが他のFragmentに入れ替わっている間も、UIは問題なく機能します。

ViewModelの機能は、ViewModelを取得する際ViewModelProviderに渡したライフサイクルの範囲で利用できます。(activityを渡したら、activityがfinish()されるまで利用できます。)
ViewModelは、ライフサイクルが終了するまでメモリに置かれます。activityならActivity#finish()、fragmentならFragment#detached()が呼ばれるまで、メモリに残ります。

 

ViewModel vs SavedInstanceState

ViewModelは、設定変更があってもデータを保ち続けるための簡単な方法を提供します。しかし、アプリケーションがOSによってkillされるときは破棄されます。
例えば、ユーザーがアプリを離れて、数時間後に戻ってきたとき、すでにプロセスは終了され、Android OSが保存されたデータからActivityを復元しています。
すべてのframeworkコンポーネント(views, activities, fragments)は、saved instance state機能を使って状態を保存します。
ほとんどの場合、開発者は何もする必要がありません。開発者はonSavedInstanceStateコールバックを使ってデータをbundleに追加するだけです。

onSavedInstanceStateを使って保存されたデータは、システムのプロセスメモリに保持されます。そして、Android OSは開発者にとても少ない量のデータを保持することを許可しています。
そのため、実際のデータ(アプリが使うコンテンツデータ等)を保持させるのに適した場所ではありません。
開発者は、UIコンポーネントによって簡単に置き換えられないデータのためにプロセスメモリを使うのは控えましょう。
例えば、Country情報を見せるユーザーインターフェースがあっても、絶対にCountryオブジェクトをsaved instance stateに入れないでください。countryIdを入れることはできます。(ViewやFragment argumentにすでに保存されていなければ) 実際に利用するオブジェクトはデータベースに入るべきです。そしてViewModelが保存されたcountryIdを使って、オブジェクトを取得します。

Manage environmental variables safely with Android Studio.

When you wanna share an Android app's source code on GitHub(or any sharing service), you shouldn't write any secret keys inside your app.
This is a way to hide these values from entire world. :)
 
1. Create or edit ~/.gradle/gradle.properties and define secret keys.
(Pay attention to the position, it's placed in USER HOME DIRECTORY!!!!!)

~/.gradle/gradle.properties

ProductionApiKey="production-api-key"
StagingApiKey="staging-api-key"


2. Read them in application level build.gradle.
In the example below, both of ProductionApiKey and StagingApiKey are named as "API_KEY".

app/build.gradle

flavorDimensions ( "api" )
productFlavors {
    prduction {
        dimension "api"
        buildConfigField "String", "API_KEY", ProductionApiKey
    }
    staging {
        dimension "api"
        buildConfigField "String", "API_KEY", StagingApiKey
    }
}


3. Use these values in your code.
Select productionDebug build variant and run it.
You'll see the log message "production-api-key". Yay!

MainActivity.java

@Override
protected void onCreate(Bundle savedInstanceState) {
    System.out.println(BuildConfig.API_KEY); // -> production-api-key
}

ロゴの使用許可申請メールテンプレート

ロゴの使用許可申請メールを作成したので、テンプレートメモ。

カンファレンスの発表資料に他社のロゴを掲載したいとき用と他社サービスのAndroidクライアントをGoogle Play Storeに申請したいとき用。

Dear Permissions Editor,

An employee of our company is scheduled to give a presentation at AWS Summit in Tokyo on June 2, 2017.
I would like your permission to include your company's logo for non-commercial purposes in his presentation.
Also, we'd like to display a number of middleware logos as well, so that the audience understands that we're using your product with AWS.

I would greatly appreciate your consent to my request.
If you require any additional information, please do not hesitate to contact me. I can be reached at:

[メールアドレス]

Sincerely,

[名前]

 

Dear Permissions Editor,

I am in the process of developing a Redash application for Android.

I would like your permission to release this application to Google Play Store.

The project is including the following materials:

Your product’s name. (Redash)

Product’s logo. ( https://camo.githubusercontent.com/65ab82a5dbb122630e4d4d61383c48b6a1aff1f1/68747470733a2f2f7265646173682e696f2f6173736574732f696d616765732f6c6f676f2e706e67 )

Your Project’s license. ( https://raw.githubusercontent.com/getredash/redash/master/LICENSE )

The application will be used to display Redash dashboards which the user has registered to the app. Also, I would like to declare that your project is licensed under the BSD 2-clause “Simplified” License on the store product page, so the app user will understand that I’m a third party.

I would greatly appreciate your consent to my request.

If you require any additional information, please do not hesitate to contact me. I can be reached at:

[メールアドレス]

Sincerely,

[名前]

おわり。

 

10 steps to get started with Realm for Android.

This article is written for someone who just wants to give it a try anyway.
If you're interested in details, please read the official docs.
The application that you're going to make from now, inserts 1 user with 2 breads every time you start the app. :)

1. Add this line to the project level build.gradle.

classpath 'io.realm:realm-gradle-plugin:3.1.4'

2. Add this line to the top of the application level build.gradle.

apply plugin: 'realm-android'

3. Refresh gradle dependencies.

4. Configure a Realm in MyApplication.java.

5. Add MyApplication to application tag in the app's AndroidManifest.xml.

<application
      android:name=".MyApplication">
</application>

6. Create a User class.
You can mark a field as a primary key with @PrimaryKey.
Other annotation types summary is HERE.

7. Create a Bread class.

8. Write insert function in MainActivity.java.
As I mentioned before, this app inserts 1 user and 2 breads into Realm DB when onCreate() is called.
There is an auto-incrementing ID helper but it's not recommended when you:

1) Create Realm Objects in multiple processes. 2) Want to share the Realm between multiple devices at some point in the future.

Here is an example of auto-Increment ID for creating objects across processes.

Example of delete function.

User u = mRealm.where(User.class).equalTo("id", id).findFirst();
u.deleteFromRealm();

9. Copy database to somewhere accessible.
Realm provides a Mac app to edit and read your database.
Currently, this app doesn't support accessing database directly.
You have to copy it from emulator or device to somewhere accessible.

$ adb shell "run-as your.package.name chmod 666 /data/data/your.package.name/files/default.realm”

$ adb exec-out run-as your.package.name cat files/default.realm > default.realm

$ adb shell "run-as your.package.name chmod 600 /data/data/your.package.name/files/default.realm"

10. View your database in Realm Browser.
f:id:bambinya:20170509194932p:plain:w300
Choose 'Open Realm File' and you can view it.

User table
f:id:bambinya:20170509193706p:plain:w540

Bread table
f:id:bambinya:20170509193711p:plain:w540

You're done!
Now you're able to explore Realm world by yourself. Enjoy! :)

BitcoinのBlockとTransactionのデータ構造について

BitcoinP2P通信技術を利用している。Bitcoinネットワークに参加する各ノードはクライアントとしてもサーバーとしても動作し、データの生成と送受信を行う。各ノードが持つデータはほとんど同じである。(常にノード間でブロックとトランザクションをやり取りしているのでいつも全く同じデータではない)

 

新しいノードがBitcoinネットワークに参加する流れ

  1. Bitcoinのソフトウェアをインストールし、デーモンを起動する。
  2. Seedsから他のノードのIPアドレスを聞き出す。
  3. 他のノードに過去の取引データ(ブロックチェーン)を送ってもらう。送られたデータは主記憶装置に保存される。

https://github.com/bitcoin/bitcoin リポジトリは、CLI(コマンドラインインターフェース)を持っている。ソースをクローンし、デーモンを起動する。初めてネットワークに参加するノードは、他のノードのIPアドレスを知らない。そのため、Seedsというノードを利用する。Seedsと呼ばれるノードは常に稼働していることが期待されている。ネットワークに新しく参加したノードは、まずSeedsに他のノードのIPアドレスを聞く。Seedsのリストはbitcoin/src/chainparamsseeds.hに記載されている。

手に入れたIPアドレスを元に、他のノードに過去の取引データを送信してもらう。送られた取引履歴は主記憶装置に保存する。保存先は${HOME}/.bitcoin/以下である。取引履歴には、ブロックチェーン、トランザクションリスト、UTXOなどがある。2017年2月の時点で保存されるデータ量はMain Networkは100GB、Test Networkは40GB程度である。

 

ノード間でブロードキャストされるブロックとトランザクションのデータ構造

前述したように、各ノードが生成したトランザクションやブロックは他のノードにブロードキャストされ、常にデータの同期が行われている。では、トランザクションとブロックは実際どのようなデータ構造をしているのだろうか。

 

ブロックのデータ構造

ブロックは複数のトランザクションを含む。

先頭にネットワークを識別するマジックナンバーを持ち、ブロックのサイズ(1MByte以下でなければならない)、直前のブロックのhashなどを持つブロックヘッダー、内包するトランザクションの数と、最後にトランザクションデータを持つ。

f:id:bambinya:20170225222746p:plain

詳細はBitcoin Wikiに書かれている。

f:id:bambinya:20170304170353p:plain

 ( Block - Bitcoin Wiki )

 

ブロックヘッダーのデータ構造

ブロックヘッダーは6つの要素を持つ。

ブロックのバージョン、直前のブロックのヘッダーのHash(256bit)、ブロックが含むトランザクションのRoot Hash(256bit)、時間(秒)、ターゲット、Nonce(32bit)である。

f:id:bambinya:20170304195715p:plain

Block hashing algorithm - Bitcoin Wiki )

 

トランザクションのデータ構造

トランザクションは取引内容を表すデータを持つ。Bitcoinは実体がないため、どれだけの量のBitcoinがどのアドレスからどのアドレスに移動したか、という内容である。

支払い者がどのアドレスからいくらのBitcoinを受け取ったかをInputと呼ぶ。そして、どのアドレスにいくら支払うかをOutputと呼ぶ。

データ構造は下記のとおりである。先頭にバージョン番号、取引に使うInputsの数、Inputsのリスト、 取引に使うOutputsの数、Outputsのリスト、最後にlock timeを持つ。

lock timeには、取引がブロックに含まれる条件を指定することができる。詳細は

前回の記事で述べた。

f:id:bambinya:20170305100110p:plain

Transaction - Bitcoin Wiki )

BitcoinとBlockchainについて調べたこと

Bitcoin: 非中央集権型の電子通貨。非中央集権型とは、銀行などの信頼された金融機関が取引に関与しないことをさす。金融上の取引を行う際、銀行の役割は支払い者に支払いの能力があることと、取引が不正ではないことを保証することである。Bitocinは、そのような信頼される第三者を取引に必要としない。

Block: 一つ以上の取引データをまとめたものをBlockと呼ぶ。1Blockの最大容量は1MB。Bitcoinのメインネットワークでは約10分にひとつ新しいBlockが生成される。

Blockchain: Blockのつながりを指すが、Bitcoinを支える分散型台帳技術を指したり、ネットワークやソフトウェアをさすこともある。ソフトウェアはC++で開発されている。URLは下記。

GitHub - bitcoin/bitcoin: Bitcoin Core integration/staging tree

P2P(Peer to Peer): 通信技術のひとつ。対等の者同士が通信を行うモデル。通信者はサーバーにもクライアントにもなる。BlockchainはP2P通信モデルを利用している。Bitcoinのネットワークに参加したマシンは過去の全ての取引履歴(Blockchain)を主記憶装置に保存する。取引履歴のデータ容量は2017年2月の時点で、Test Networkが40GB程度、Main Networkは100GBほどある。

Blockの高さ: あるBlockが何番目に生成されたかを高さで表す。

Main/Test Network: Bitcoinには本番用とテスト用の複数のネットワークがある。やりとりする全てのメッセージの先頭4byteにMagic Valueをつけてネットワークを識別する。実際の値は下記の通り。

f:id:bambinya:20170225153554p:plain

Protocol documentation - Bitcoin Wiki )

ソースではこのあたりがネットワークのマジックナンバー。(リトルエンディアンで送られる)

https://github.com/bitcoin/bitcoin/blob/master/src/chainparams.cpp#L110

 

Node: Bitcoin Network参加者のこと。

アドレス: ビットコイン送信先。メールアドレスのようなもの。全てのノードが全ての取引履歴を見ることができるため、取引履歴を知られないために、通常は取引毎にアドレスを発行する。

BTC: Bitcoinの量を表す単位。

Satoshi: Bitcoinの量を表す単位。1 BTC = 100,000,000 Satoshi

Wallet: 発行したアドレスと、アドレスに紐づくぷ秘密鍵を管理する仕組み。秘密鍵の情報はwallet.datファイルに収められる。同様にBitcoin Networkで秘密鍵の管理とトランザクションを作成するクライアントソフトウェアを指すこともある。

Transaction: 取引ごとにユーザーが発行するもの。Txと書くこともある。Bitcoinがどのアドレスからいくら来て、どのアドレスにいくら行くのかという情報を持つ。データ構造は下記の通り。

f:id:bambinya:20170225172850p:plain

Transaction - Bitcoin Wiki )

ユーザーがどこからBitcoinを受け取ったかという情報はTxin、どこへ送るかという情報はTxoutに書き込まれる。lock_timeにはUnix Timeかブロックの高さを指定できる。時間が指定された場合は、マイナーが指定時間を過ぎないとtransactionをBlockに含めることができない。ブロックの高さも同様に、指定したブロックの高さにならないと、マイナーがTransactionをBlockに含められない。

UTXO(Unspent Tx Output): 未使用のBitcoin。ネットワークに参加するノードは基本的にネットワーク上の全てのUTXOをDBに保存している。Transactionに含まれるTxinで、まだ支払いに利用されていない(Txoutになっていない)BitcoinをUTXOという。

Fee: 送金にかかる手数料。Transactionのデータサイズは1byteあたり1Satoshiかかるのが相場。

Mining: Transactionの記録をBlockに追加し、新しいBlockの生成を試みること。生成に成功すると報酬としてBlockが含むTransactionの手数料の総額と成功報酬として12.5 bitcoin(2017年2月)を手に入れることができる。成功報酬の額は210,000 blocks置きに半額になる。

BIP(Bitcoin Improvement Proposals): Bitcoinの情報や新機能についてまとめた文書。Bitcoinに関するアイデアを話し合う手段として標準化されている。BIPは3種類に分けられる。

  • Standards Track BIPs - Bitcoin実装の多く、または全てに影響があるもの。またBitcoinを使うアプリケーションに影響があるもの。ネットワークプロトコルの変更や、ブロックとトランザクションのバリデーションルールの変更などが該当する。
  • Informational BIPs - デザインに関するもの、一般的なガイドライン。このタイプのBIPは新機能の提案でもコミュニティの合意を示すものでもない。BitcoinユーザーとBitcoinの開発者は自由にInformational BIPsを無視したり、アドバイスに従って良い。
  • Process BIPs - Bitcoinの実装以外に関する提案。何か実装を提案するかもしれないが、Bitcoinのcodebaseへの提案ではない。コミュニティの意思決定プロセスの変更、ガイドラインBitcoinの開発環境・ツールの提案がこのタイプに当てはまる。

bips/bip-0001.mediawiki at master · bitcoin/bips · GitHub )

Merkle Tree: データ構造のひとつ。複数のTransactionをひとつのBlockに追加するときに使う。下記の図ではMerkle Treeを利用して、Tx0からTx3のハッシュを作成し、Block Headerに追加している。

f:id:bambinya:20170225183527p:plain

https://bitcoin.org/bitcoin.pdf )

Target: Block生成時にMinersが目標とする256bitの数字。数字自体に意味はなく、2週間ごとに値が変わる。ブロックの生成は、hash関数に直前のBlockのhash値と、Blockに含めるTransactionのMerkle Treeのハッシュと、nonce(4-byteの意味を持たない数字)を入れて、関数の返り値がターゲットより小さくなるnonceを見つけることである。nonceはnumber used once(一度使われる数字)という意味。例えばターゲットが0x00000000FFFF000000......00000(64桁)ならば、hash関数に直前のBlockのhash値と、Blockに含めるTransactionのハッシュと、適当なnonceを入れ、返り値がターゲットより小さくなるようなnonceを見つければ、Blockの生成に成功したことになる。

Difficulty: あるネットワークでブロックを作る難しさをDifficultyで表す。値が高いほど、Blockの生成が難しい。値は最大ターゲット ÷ 現在のターゲットで求められる。Test NetworkではBlockが10分に1つ作られるよう、2週間に一度値が更新される。

 

参考資料

https://bitcoin.org/bitcoin.pdf (Satoshi Nakamotoが発表したオリジナル論文。情報は古い。シンプルなのでそんなに難しくない)

Bitcoin Wiki (少し情報が古い)