前幾天學著 DIYGOD 搞了一套生活管理系統。在各種插件的加持下算是做到了半自動化,然而,睡眠時間和步數,以及可能的心率血壓等數據仍然需要手動記錄手動填寫實在是不算 Geek。搜索之後得知其實 Zepp (原 Huami) 存在有逆向後的 API 接口且明文存儲步數等信息,於是便腦子一熱入了 小米手環 8 Pro 原神聯名版。拿到手後,才驚訝地發現 小米手環 8 已經不再支持 Zepp,小米手環 7 雖然表面上不支持,但也能使用修改 QRCode 和 Zepp 安裝包的方式,然而小米手環 8 已經是徹底把 Zepp 給 Deprecated 了。
初探 —— 抓包#
首先,當然是看抓包有沒有什麼有用的信息了。我原來用 proxifier 做抓包,但是效果並不好,原因是有一些軟件存在 SSLPinning,所以這次,採用了 mitmproxy + 系統級證書的方法。
工具鏈#
測試方法#
長話短說,首先在 PC 上安裝 mitmproxy,然後在 $HOME/.mitmproxy
目錄下拿到 mitmproxy-ca-cert.cer
文件,按照正常的工作流安裝在 Android 設備上。
在我的案例中,我在搜索中搜索
cred
相關字樣,就找到了Credential storage
,並且有Install certificates from storage
,這就是我的正常工作流。不同的設備可能有不同的工作流
在 Magisk 中安裝 ConscryptTrustUserCerts
,重啟,即可在 boot 階段將 用戶級證書 mount 到 系統級證書 目錄下,這就完成了準備工作。
在 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
,需要官方 APP 來獲取。倒是可以直接使用 這個網站 來獲取,但是本著信不過第三方的原則,我們還是手動獲取。
做了混淆,不在原來那個數據庫裡了。加上我突然發現 BLE 只能同時連接到一個,而官方 APP 優先級顯然更高,遂作罷。
既然後面逆了,就回來前面寫一點。
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#
到這裡,其實我已經想好最終的思路了,開逆呗。既然最終發出去是加密的,那肯定有沒加密的數據處理的過程。逆出來,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
等值,不包含value
- 繼續溯源,利用下面的代碼片段來看重載以及參數類型。
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,琢磨了一兩個小時就弄好了基本的環境(難評 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 點#
我們思考,既然需要在後台啟動,而小米健康本身就有一些保活和自啟的機制,因此我們完全沒必要 hook MainActivity 的 onCreate 方法,而是找一個自啟的方法即可。
Android 自啟的方法,經過一點搜索,可能有 BOOT_COMPLETED
廣播監聽、AlarmManager
定時任務、JobScheduler
工作以及 Service
等。在 jadx-gui 中搜索,我們找到了 com.xiaomi.fitness.keep_alive.KeepAliveHelper
這個類的 startService
方法。經過測試,確實可以使用。
在這裡我們主要利用單例,讓它不要重複註冊。其中主要的函數就是 handleLoadPackage
來獲取對應的 LoadPackageParam
,之後對於想要 HOOK 的函數,繼承 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 / 斷開 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 Module,開機的時候設置一下就好了(笑)
-
廣播是單向通信,電腦又如何接消息呢?
沒想到好辦法。目前的思考就是直接寫入文件,然後電腦端 adb pull 再讀。
於是放棄了,然後,我又開始思考 HTTP Restful API。我利用 Ktor 很快的實現了一個(利用 GPT)。
但是此時又有一個問題:我們這個數據的獲取頻次是非常低的,卻有這麼一個特點:時間不固定。因此,為了穩定性,我們必須時刻保持 HTTP 服務器的開啟,而 HTTP 服務器因為要維護的東西非常多,所以耗電量是非常可觀的(雖然我沒有測試)
於是又轉向了 SOCKET 的懷抱。倒是反正也差不多。
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()
}
源碼後面再上傳吧,現在純半成品,評價是隨便偷我的睡眠數據。