数日前、DIYGOD を参考にして生活管理システムを構築しました。さまざまなプラグインのおかげで半自動化が実現しましたが、睡眠時間や歩数、さらには心拍数や血圧などのデータは手動で記録しなければならず、実際には Geek とは言えません。調べたところ、Zepp(元 Huami)には逆向きの API インターフェースが存在し、歩数などの情報が平文で保存されていることがわかりました。そこで、思い切って 小米手环 8 Pro 原神联名版 を購入しました。手に入れてみると、小米手环 8 はすでに Zepp をサポートしておらず、小米手环 7 は表面的にはサポートしていないものの、QRCode と Zepp のインストールパッケージを変更することで使用できました。しかし、小米手环 8 は完全に Zepp を非推奨にしていました。
初探 —— パケットキャプチャ#
まずは、パケットキャプチャに有用な情報があるかどうかを確認します。以前は proxifier を使ってパケットキャプチャを行っていましたが、あまり効果がありませんでした。その理由は、いくつかのソフトウェアに SSLPinning が存在するためです。そこで、今回は mitmproxy + システムレベルの証明書を使用する方法を採用しました。
ツールチェーン#
テスト方法#
長話を短くすると、まず PC に mitmproxy をインストールし、次に $HOME/.mitmproxy
ディレクトリで mitmproxy-ca-cert.cer
ファイルを取得し、通常のワークフローに従って Android デバイスにインストールします。
私のケースでは、検索で
cred
に関連する単語を探し、Credential storage
を見つけ、Install certificates from storage
がありました。これが私の通常のワークフローです。デバイスによっては異なるワークフローがあるかもしれません。
Magisk に ConscryptTrustUserCerts
をインストールし、再起動すると、ブート段階でユーザーレベルの証明書がシステムレベルの証明書ディレクトリにマウントされ、準備が整いました。
PC で mitmweb を開き、スマートフォンの Wi-Fi 設定をプロキシとして <my-pc-ip>:8080
に設定し、テストを行い、HTTPS リクエストを正常にキャプチャしました。
結論#
あまり役に立ちませんでした。すべてのリクエストは暗号化されており、signature や hash、nounce などで安全性が確保されています。apk を逆向きにする気にはなれず、断念しました。
窥见光明 —— BLE 接続#
パケットキャプチャがうまくいかないので、直接 BLE クライアントを作成し、バンドに接続してデータを取得することにしました。これは明らかに非常に合理的な方法です。また、この方法ではスマートフォンで何か操作をする必要がなく、Obsidian でスクリプトを実行し、接続してデータを取得するだけで、非常に自動化されているようです。
実装#
コードは主に wuhan005/mebeats: 💓 小米手环实时心率数据采集 - Your Soul, Your Beats! を参考にしました。ただし、彼のツールチェーンは MacOS で、私は持っていなかったので、GPT に尋ねて修正しました。
コードには auth_key
があり、公式アプリから取得する必要があります。直接 このサイト を使用して取得することもできますが、第三者を信頼しない原則に基づいて、手動で取得することにしました。
混乱が生じ、元のデータベースには存在しなくなりました。さらに、BLE は同時に一つのデバイスにしか接続できないことに気づき、公式アプリの優先度が明らかに高いため、断念しました。
逆向きにした後、前の部分に戻って少し書きます。
public final void bindDeviceToServer(lg1 lg1Var) {
Logger.i(getTAG(), "bindDeviceToServer start");
HuaMiInternalApiCaller huaMiDevice = HuaMiDeviceTool.Companion.getInstance().getHuaMiDevice(this.mac);
if (huaMiDevice == null) {
String tag = getTAG();
Logger.i(tag + "bindDeviceToServer huaMiDevice == null", new Object[0]);
if (lg1Var != null) {
lg1Var.onConnectFailure(4);
}
} else if (needCheckLockRegion() && isParallel(huaMiDevice)) {
unbindHuaMiDevice(huaMiDevice, lg1Var);
} else {
DeviceInfoExt deviceInfo = huaMiDevice.getDeviceInfo();
if (deviceInfo == null) {
String tag2 = getTAG();
Logger.i(tag2 + "bindDeviceToServer deviceInfo == null", new Object[0]);
return;
}
String sn = deviceInfo.getSn();
setMDid("huami." + sn);
setSn(deviceInfo.getSn());
BindRequestData create = BindRequestData.Companion.create(deviceInfo.getSn(), this.mac, deviceInfo.getDeviceId(), deviceInfo.getDeviceType(), deviceInfo.getDeviceSource(), deviceInfo.getAuthKey(), deviceInfo.getFirmwareVersion(), deviceInfo.getSoftwareVersion(), deviceInfo.getSystemVersion(), deviceInfo.getSystemModel(), deviceInfo.getHardwareVersion());
String tag3 = getTAG();
Logger.d(tag3 + create, new Object[0]);
getMHuaMiRequest().bindDevice(create, new HuaMiDeviceBinder$bindDeviceToServer$1(this, lg1Var), new HuaMiDeviceBinder$bindDeviceToServer$2(lg1Var, this));
}
}
deviceInfo
から取得されていることがわかりますが、これは huamiDevice
から来ています。少し遡ると、これは mac から計算されていることがわかりますが、具体的なことは見ません。興味のある方は com.xiaomi.wearable.wear.connection
というパッケージを見てください。
大道至简 —— Frida Hook#
ここまで来ると、最終的なアイデアはすでに考えています。逆向きにしてみましょう。最終的に送信されるのは暗号化されているので、暗号化されていないデータ処理のプロセスが必ずあります。逆向きにしてフックし、XPosed プラグインを書いて監視すればいいのです。
ここでは、時間が遅くなったため、frida のインストール方法を書くのにあまり時間をかけたくありません。
まず jadx-gui
には copy as frida snippets
の機能があり、かなりの手間を省けます。しかし、kotlin
のデータクラスのさまざまな奇妙な理由により、多くの場合、取得できません。私は問題を記録しながら進めなかったので、プロセスを大まかに振り返ります:
- まず、
/data/data/com.mi.health/databases
フォルダでユーザーに対応するフォルダを見つけ、その中にfitness_summary
というデータベースがあり、読み取ってみると必要なデータが存在しました。したがって、最初の検索キーワードfitness_summary
を使用して交差参照し、com.xiaomi.fit.fitness.persist.db.internal
というクラスに遡りました。 update
、insert
などの関数を見つけ、試行錯誤を繰り返しましたが、出力を見ることはできませんでした。しかし、最終的にcom.xiaomi.fit.fitness.persist.db.internal.h.getDailyRecord
という関数を見つけ、毎回リフレッシュするたびに出力がありましたが、sid
、time
などの値しか含まれていませんでした。- さらに遡り、以下のコードスニペットを利用してオーバーロードとパラメータの型を確認しました。
var insertMethodOverloads = hClass.updateAll.overloads;
for (var i = 0; i < insertMethodOverloads.length; i++) {
var overload = insertMethodOverloads[i];
console.log("Overload #" + i + " has " + overload.argumentTypes.length + " arguments.");
for (var j = 0; j < overload.argumentTypes.length; j++) {
console.log(" - Argument " + j + ": " + overload.argumentTypes[j].className);
}
}
- 突然、例外を利用して関数呼び出しスタックを確認できることに気づき、まさに雲が晴れて月が見えたような気分でした。
var callerMethodName = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new());
console.log("getTheOneDailyRecord called by: " + callerMethodName);
- 一層一層遡り、
com.xiaomi.fit.fitness.export.data.aggregation.DailyBasicReport
というクラスを見つけ、私のニーズを完璧に満たしました。
dbutilsClass.getAllDailyRecord.overload('com.xiaomi.fit.fitness.export.data.annotation.HomeDataType', 'java.lang.String', 'long', 'long', 'int').implementation = function (homeDataType, str, j, j2, i) {
console.log("getAllDailyRecord called with args: " + homeDataType + ", " + str + ", " + j + ", " + j2 + ", " + i);
var result = this.getAllDailyRecord(homeDataType, str, j, j2, i);
var entrySet = result.entrySet();
var iterator = entrySet.iterator();
while (iterator.hasNext()) {
var entry = iterator.next();
console.log("entry: " + entry);
}
var callerMethodName = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new());
console.log("getTheOneDailyRecord called by: " + callerMethodName);
return result;
}
// DailyStepReport(time=1706745600, time = 2024-02-01 08:00:00, tag='days', steps=110, distance=66, calories=3, minStartTime=1706809500, maxEndTime=1706809560, avgStep=110, avgDis=66, active=[], stepRecords=[StepRecord{time = 2024-02-02 01:30:00, steps = 110, distance = 66, calories = 3}])
- 難しい問題に直面しました。なぜなら、この
steps
はprivate
属性であり、jadx-gui
では複数のインターフェースgetSteps()
、getSourceData()
が表示されましたが、どれも使用できず、すべてnot a function
と表示されました。これはおそらく kotlin と java の処理方法の違いだと思われます。最終的にはリフレクションを使用して解決しました。
これで最終的なfrida
コードは以下のようになり、その日のsteps
データを取得でき、HomeDataType
を変更することで他のデータを取得できます。
var CommonSummaryUpdaterCompanion = Java.use("com.xiaomi.fitness.aggregation.health.updater.CommonSummaryUpdater$Companion");
var HomeDataType = Java.use("com.xiaomi.fit.fitness.export.data.annotation.HomeDataType");
var instance = CommonSummaryUpdaterCompanion.$new().getInstance();
console.log("instance: " + instance);
var step = HomeDataType.STEP;
var DailyStepReport = Java.use("com.xiaomi.fit.fitness.export.data.aggregation.DailyStepReport");
var result = instance.getReportList(step.value, 1706745600, 1706832000);
var report = result.get(0);
console.log("report: " + report + report.getClass());
var stepsField = DailyStepReport.class.getDeclaredField("steps");
stepsField.setAccessible(true);
var steps = stepsField.get(report);
console.log("Steps: " + steps);
// Steps: 110
最終 —— XPosed プラグイン#
現在のアイデアは XPosed で特定のアドレスを監視し、少しの保護を施して明文伝送を防ぐことを考えています。なぜなら、このアプリは常に起動しているため、実行可能だと思います。今の問題は、私は kotlin を書けず、XPosed も書けないことです。
幸い、kotlin のコンパイラのヒントは非常に強力であり、XPosed 自体は設定の構築以外に特別な知識を必要としないため、強力な GPT を活用して、1、2 時間考えた結果、基本的な環境を整えることができました(gradle の評価が難しく、プロキシを開かないと遅く、プロキシを開くとダウンロードできない)。
環境構築#
とにかく、Android Studio で No Activity のプロジェクトを直接開きます。誰も gradle kotlin が XPosed をどう設定するかを書いていないので、ここで簡単に説明します。主にネット上では直接 settings.gradle しか見つからず、非常に古い情報です。
// settings.gradle.kts
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url = uri("https://api.xposed.info/") }
}
}
// build.gradle.kts
dependencies {
compileOnly("de.robv.android.xposed:api:82") // この行
implementation("androidx.core:core-ktx:1.10.1")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.9.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
implementation(kotlin("reflect"))
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
}
<!-- AndroidManifest.xml、主に以下のメタデータ -->
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MiBandUploader"
tools:targetApi="31" >
<meta-data
android:name="xposedmodule"
android:value="true" />
<meta-data
android:name="xposeddescription"
android:value="Mi Fitness Data Uploader" />
<meta-data
android:name="xposedminversion"
android:value="53" />
<meta-data
android:name="xposedscope"
android:resource="@array/xposedscope" />
</application>
</manifest>
<!-- res/values/array.xml、上記の xposedscope に対応する、作用域パッケージ名 -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="xposedscope" >
<item>com.mi.health</item>
</string-array>
</resources>
その後、app/src/main/
の下に新しい assets/xposed_init
ファイルを作成し、内容にエントリクラスを記入します。
sh.ouo.miband.uploader.MainHook
これで、コンパイルして LSPosed Manager でプラグインを見ることができます。
アイデア#
HOOK ポイント#
考えてみると、バックグラウンドで起動する必要があるため、小米健康自体にはいくつかの保活と自動起動のメカニズムがあるため、MainActivity の onCreate メソッドをフックする必要はなく、自動起動のメソッドを探すだけで済みます。
Android の自動起動メソッドを少し検索したところ、BOOT_COMPLETED
ブロードキャストリスナー、AlarmManager
定期タスク、JobScheduler
作業、Service
などがあることがわかりました。jadx-gui で検索したところ、com.xiaomi.fitness.keep_alive.KeepAliveHelper
というクラスの startService
メソッドを見つけました。テストしたところ、確かに使用できました。
ここでは主にシングルトンを利用して、重複登録しないようにします。主な関数は handleLoadPackage
で、対応する LoadPackageParam
を取得し、フックしたい関数に対して XC_MethodHook
を継承すればいいのです。
以下は、CommonSummaryUpdater
のインスタンスを取得し、frida との連携に使用するコードです。
import android.util.Log
import de.robv.android.xposed.IXposedHookLoadPackage
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage
class MainHook : IXposedHookLoadPackage {
companion object {
@Volatile
var isReceiverRegistered = false
}
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
if (lpparam.packageName != "com.mi.health") return
hook(lpparam)
}
private fun hook(lpparam: XC_LoadPackage.LoadPackageParam) {
XposedHelpers.findAndHookMethod(
"com.xiaomi.fitness.keep_alive.KeepAliveHelper",
lpparam.classLoader,
"startService",
object : XC_MethodHook() {
@Throws(Throwable::class)
override fun afterHookedMethod(param: MethodHookParam) {
if ( !isReceiverRegistered ) {
Log.d("MiBand", "MiUploader Hook Startup...")
val updaterClass = XposedHelpers.findClass("com.xiaomi.fitness.aggregation.health.updater.CommonSummaryUpdater", lpparam.classLoader)
val companionInstance = XposedHelpers.getStaticObjectField(updaterClass, "Companion")
val commonSummaryUpdaterInstance = XposedHelpers.callMethod(companionInstance, "getInstance")
Log.d("MiBand","MiUploader Receiver Deployed!")
isReceiverRegistered = true
}
super.afterHookedMethod(param)
}
})
}
}
データ抽出#
基本的には frida と似ており、対応するメソッドを呼び出して解析するだけです。ここでは、少し抽象的な基底クラスを書きましたが、実際にこれを書く必要があるかはわかりません。
import android.util.Log
import de.robv.android.xposed.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam
import kotlinx.serialization.json.JsonElement
import java.time.LocalDate
import java.time.ZoneId
import java.time.format.DateTimeFormatter
abstract class DailyReportBase (
protected val lpparam: LoadPackageParam,
private val instance: Any
) {
private lateinit var enumValue: Any
protected fun setEnumValue(type: String) {
val homeDataType = XposedHelpers.findClass("com.xiaomi.fit.fitness.export.data.annotation.HomeDataType", lpparam.classLoader)
enumValue = XposedHelpers.getStaticObjectField(homeDataType, type)
}
private fun getDay(day: String?): Pair<Long, Long> {
val formatPattern = DateTimeFormatter.ofPattern("yyyy-MM-dd")
val beijingZoneId = ZoneId.of("Asia/Shanghai")
val today = if (day == null) {
LocalDate.now(beijingZoneId)
} else {
LocalDate.parse(day, formatPattern)
}
val startOfDay = today.atStartOfDay(beijingZoneId)
Log.d("MiBand", startOfDay.toString())
val startOfDayTimestamp = startOfDay.toEpochSecond()
val endOfDayTimestamp = startOfDay.plusDays(1).minusSeconds(1).toEpochSecond() // 1秒減算してその日の終了時間を取得
return Pair(startOfDayTimestamp, endOfDayTimestamp)
}
fun getDailyReport(day: String?): JsonElement {
val (j1, j2) = getDay(day)
Log.d("MiBand", "Ready to call: $instance, $enumValue, $j1, $j2")
val result = XposedHelpers.callMethod(
instance,
"getReportList",
enumValue,
j1,
j2
) as ArrayList<*>
return toJson(result)
}
abstract fun toJson(obj: ArrayList<*>): JsonElement
}
kotlin を書けないので、非常に奇妙に見えますが、基本的なアイデアは各サブクラスが setEnumValue
を呼び出して getDailyReport
の列挙値を設定し、toJson
をオーバーライドすればよいというものです。
ここでの json では多くの問題に直面しました。主にその型注釈が原因で、非常に崩れやすいです。
では、stepReport の例を見てみましょう。
import android.util.Log
import de.robv.android.xposed.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
class StepDailyReport(lpparam: XC_LoadPackage.LoadPackageParam,
instance: Any
) : DailyReportBase(lpparam, instance) {
init {
setEnumValue("STEP")
}
override fun toJson(obj: ArrayList<*>): JsonElement {
Log.d("MiBand", obj.toString())
val today = obj.getOrNull(0)
if (today != null) {
try {
return // 何を書く?
}
catch (e: Exception) {
throw e
}
}
throw NoSuchFieldException("No data fetched")
}
}
ここで問題が発生します。取得した today
は com.xiaomi.fit.fitness.export.data.aggregation.DailyStepReport
のインスタンスですが、これを json にシリアライズする方法がわかりません。型注釈では Any
と書くしかなく、どのようなオブジェクトがあるかコンパイラにはわかりません。シリアライズする方法もわかりません。さらに、オブジェクトのネストもあります。
とにかく、かなりの時間をテストし、多くの検索を行いましたが、直接的な方法は見つかりませんでした。誰かが助けてくれることを願っています。長い間試行錯誤した結果、最終的には中間データクラスを自分で作成することにしました。
@Serializable
data class SerializableDailyStepReport(
val time: Long,
val tag: String,
val steps: Int,
val distance: Int,
val calories: Int,
val minStartTime: Long?,
val maxEndTime: Long?,
val avgStep: Int,
val avgDis: Int,
val stepRecords: List<SerializableStepRecord>,
val activeStageList: List<SerializableActiveStageItem>
)
@Serializable
data class SerializableStepRecord(
val time: Long,
val steps: Int,
val distance: Int,
val calories: Int
)
@Serializable
data class SerializableActiveStageItem(
val calories: Int,
val distance: Int,
val endTime: Long,
val riseHeight: Float?,
val startTime: Long,
val steps: Int?,
val type: Int
)
private fun convertToSerializableReport(xposedReport: Any): SerializableDailyStepReport {
val stepRecordsObject = XposedHelpers.getObjectField(xposedReport, "stepRecords") as List<*>
val activeStageListObject = XposedHelpers.getObjectField(xposedReport, "activeStageList") as List<*>
val stepRecords = stepRecordsObject.mapNotNull { record ->
if (record != null) {
SerializableStepRecord(
time = XposedHelpers.getLongField(record, "time"),
steps = XposedHelpers.getIntField(record, "steps"),
distance = XposedHelpers.getIntField(record, "distance"),
calories = XposedHelpers.getIntField(record, "calories")
)
} else null
}
val activeStageList = activeStageListObject.mapNotNull { activeStageItem ->
if (activeStageItem != null) {
SerializableActiveStageItem(
calories = XposedHelpers.getIntField(activeStageItem, "calories"),
distance = XposedHelpers.getIntField(activeStageItem, "distance"),
endTime = XposedHelpers.getLongField(activeStageItem, "endTime"),
riseHeight = XposedHelpers.getObjectField(activeStageItem, "riseHeight") as? Float,
startTime = XposedHelpers.getLongField(activeStageItem, "startTime"),
steps = XposedHelpers.getObjectField(activeStageItem, "steps") as? Int,
type = XposedHelpers.getIntField(activeStageItem, "type")
)
} else null
}
return SerializableDailyStepReport(
time = XposedHelpers.getLongField(xposedReport, "time"),
tag = XposedHelpers.getObjectField(xposedReport, "tag") as String,
steps = XposedHelpers.getIntField(xposedReport, "steps"),
distance = XposedHelpers.getIntField(xposedReport, "distance"),
calories = XposedHelpers.getIntField(xposedReport, "calories"),
minStartTime = XposedHelpers.getObjectField(xposedReport, "minStartTime") as Long?,
maxEndTime = XposedHelpers.getObjectField(xposedReport, "maxEndTime") as Long?,
avgStep = XposedHelpers.callMethod(xposedReport, "getAvgStepsPerDay") as Int,
avgDis = XposedHelpers.callMethod(xposedReport, "getAvgDistancePerDay") as Int,
stepRecords = stepRecords,
activeStageList = activeStageList
)
}
}
とにかく、非常に見栄えが悪く、効率も低いと思いますが、どうすればよいかわかりませんでした。serialization
ライブラリを利用しました。
// build.gradle.kts [Module]
plugins {
...
kotlin("plugin.serialization") version "1.9.21"
}
dependencies {
...
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
}
そして、戻り値の場所では、String
を返すこともあれば、Json
を返すこともあるため、JsonElement
を使用しました。しかし、型注釈のため、次のように書く必要があります(少なくとも GPT に聞いたところでは)。
return Json.encodeToJsonElement(SerializableDailyStepReport.serializer(), convertToSerializableReport(today))
監視#
ここで本当に混乱しました。最初は BroadcastReceiver
を使用しようと思いましたが、これにはいくつかの考慮事項があります。
-
コンピュータはどのように Android にブロードキャストを送信しますか?
adb を使用して、
adb shell am broadcast -a ACTION --es "extra_key" "extra_value"
を実行します。しかし、テスト後、Android 11 以降、adb 無線デバッグのポートが変更されることがわかりました(以前は固定の 5555 でした)、WiFi を変更したり切断したりすると、開発者設定で無線デバッグを再度有効にする必要があります。方法はあります。
adb shell
でsetprop <key> <value>
を実行し、以下の値を変更すればよいのです。最初の二つはデバッグのポートで、最後の一つは無線デバッグを自動的にオフにしないようにします。service.adb.tls.port=38420 service.adb.tcp.port=38420 persist.adb.tls_server.enable=1
しかし、現在の
/system
ディレクトリは書き込み不可です。つまり、build.prop
を編集してこれらの値を永続的に変更することはできません。再起動すると元に戻ってしまいます。これは非常に面倒です(私は通常シャットダウンしないので)。もちろん方法はあります。Magisk モジュールを作成し、起動時に設定すればよいのです(笑)。
-
ブロードキャストは一方向の通信であり、コンピュータはどのようにメッセージを受信しますか?
良い方法は思いつきませんでした。現在の考えは、直接ファイルに書き込み、コンピュータ側で adb pull して読み取ることです。
したがって、これを放棄し、HTTP Restful API に移行しました。Ktor を利用してすぐに実装しました(GPT を利用)。
しかし、この時点で別の問題が発生しました。データの取得頻度は非常に低いですが、時間が不規則であるという特徴があります。したがって、安定性のために HTTP サーバーを常に起動しておく必要がありますが、HTTP サーバーは維持するものが非常に多いため、電力消費が非常に大きいです(テストはしていませんが)。
したがって、ソケットに移行しました。どちらもほぼ同じです。
class MySocketServer(
private val port: Int,
private val lpparam: LoadPackageParam,
private val instance: Any
) {
fun startServerInBackground() {
Thread {
try {
val serverSocket = ServerSocket(port)
Log.d("MiBand", "Server started on port: ${serverSocket.localPort}")
while (!Thread.currentThread().isInterrupted) {
val clientSocket = serverSocket.accept()
val clientHandler = ClientHandler(clientSocket)
Thread(clientHandler).start()
}
} catch (e: Exception) {
Log.e("MiBand", "Server Error: ${e.message}")
}
}.start()
}
そして、突然気づいたのは、Obsidian で Templater を使用して毎日の情報を取得する必要があるため、JavaScript を使用する必要がありますが、Obsidian はサンドボックスのような環境であるため、外部スクリプトを実行することはできません。JavaScript ではソケットを使用できませんか?仕方がないので、HTTP プロトコルを手動で実装しました。安全性はさておき、評価は使えるかどうかです。
override fun run() {
try {
Log.d("MiBand", "Connection: $clientSocket")
val inputStream = BufferedReader(InputStreamReader(clientSocket.getInputStream()))
val outputStream = PrintWriter(clientSocket.getOutputStream(), true)
// HTTP リクエストの最初の行を読み取る
val requestLine = inputStream.readLine()
println("Received: $requestLine")
// リクエスト行を解析する
val requestParts = requestLine?.split(" ")
if (requestParts == null || requestParts.size < 3 || requestParts[0] != "GET") {
val resp = SerializableResponse(
status = 1,
data = JsonPrimitive("Invalid request")
)
sendSuccessResponse(outputStream, resp)
return
}
val pathWithParams = requestParts[1]
val path = pathWithParams.split("?")[0]
val params = parseQueryString(pathWithParams.split("?").getOrNull(1))
when (path) {
"/getDailyReport" -> {
val type = params["type"]
val date = params["date"]
if (type == null) {
val resp = SerializableResponse(
status = 1,
data = JsonPrimitive("Missing 'type' parameter for /getDailyReport")
)
sendSuccessResponse(outputStream, resp)
} else {
// getDailyReport リクエストを処理する
var resp: SerializableResponse
try {
val report = DailyReportFactory.createDailyReport(lpparam, instance, type)
val result = report.getDailyReport(date)
resp = SerializableResponse(
status = 0,
data = result
)
}
catch (e: Exception) {
resp = SerializableResponse(
status = 1,
data = JsonPrimitive(e.message)
)
}
sendSuccessResponse(outputStream, resp)
}
}
else -> {
val resp = SerializableResponse(
status = 1,
data = JsonPrimitive("Unknown path: $path")
)
sendSuccessResponse(outputStream, resp)
}
}
inputStream.close()
outputStream.close()
clientSocket.close()
Log.d("MiBand", "Established")
} catch (e: IOException) {
e.printStackTrace()
}
}
}
private fun parseQueryString(query: String?): Map<String, String> {
val queryPairs = LinkedHashMap<String, String>()
val pairs = query?.split("&") ?: emptyList()
for (pair in pairs) {
val idx = pair.indexOf("=")
if (idx != -1) {
val key = pair.substring(0, idx)
val value = pair.substring(idx + 1)
queryPairs[key] = value
}
}
return queryPairs
}
private fun sendSuccessResponse(outputStream: PrintWriter, result: SerializableResponse) {
val jsonResponse = Json.encodeToString(result)
val response = """
HTTP/1.1 200 OK
Content-Type: application/json
Connection: close
Content-Length: ${jsonResponse.toByteArray().size}
$jsonResponse
""".trimIndent()
outputStream.println(response)
outputStream.flush()
}
ソースコードは後でアップロードしますが、今は純粋な半成品です。評価は私の睡眠データを盗むだけです。