A few days ago, I learned from DIYGOD to set up a life management system. With the help of various plugins, I achieved a semi-automated setup; however, sleep time, step count, and possibly heart rate and blood pressure data still need to be recorded and filled in manually, which doesn't feel very Geek. After some searching, I found out that Zepp (formerly Huami) has a reverse-engineered API interface that stores step count and other information in plain text, so I impulsively bought the Xiaomi Mi Band 8 Pro Genshin Impact Edition. Upon receiving it, I was surprised to discover that the Xiaomi Mi Band 8 no longer supports Zepp. Although the Xiaomi Mi Band 7 does not officially support it, it can still be used with modified QR codes and Zepp installation packages. However, the Xiaomi Mi Band 8 has completely deprecated Zepp.
Initial Exploration — Packet Capture#
First, of course, I looked to see if there was any useful information in the packet capture. I originally used Proxifier for packet capturing, but it didn't work well because some software has SSL pinning. So this time, I used mitmproxy + system-level certificates.
Toolchain#
Testing Method#
To make a long story short, I first installed mitmproxy on my PC, then obtained the mitmproxy-ca-cert.cer
file in the $HOME/.mitmproxy
directory and installed it on my Android device according to the normal workflow.
In my case, I searched for
cred
related terms and foundCredential storage
, which hasInstall certificates from storage
, and that is my normal workflow. Different devices may have different workflows.
I installed ConscryptTrustUserCerts
in Magisk and rebooted, which allowed user-level certificates to be mounted in the system-level certificate directory during the boot phase, completing the preparation.
I opened mitmweb on my PC, set the phone's Wi-Fi proxy to <my-pc-ip>:8080
, tested it, and successfully captured HTTPS requests.
Conclusion#
Not much use. All requests are encrypted, and there are signatures, hashes, nonces, etc., to ensure security. I really didn't want to reverse the APK, so I gave up.
A Glimpse of Hope — BLE Connection#
Since packet capturing didn't work, I decided to create a BLE client to connect to the band and retrieve data, which seems very reasonable. Moreover, this method doesn't require me to do anything on my phone; Obsidian runs a script, connects, retrieves data, and it seems very automated.
Implementation#
The code mainly references wuhan005/mebeats: 💓 Xiaomi Mi Band Real-time Heart Rate Data Collection - Your Soul, Your Beats!. However, his toolchain is MacOS, which I don't have, so I modified it with GPT's help.
The code has an auth_key
that needs to be obtained from the official app. It can be directly obtained from this website, but adhering to the principle of not trusting third parties, we still manually obtained it.
It has been obfuscated and is no longer in the original database. Plus, I suddenly realized that BLE can only connect to one device at a time, and the official app clearly has a higher priority, so I gave up.
Since I reversed it later, I'll write a bit about it here.
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));
}
}
It can be seen that it is obtained from deviceInfo
, which comes from huamiDevice
. Tracing back a bit, it can be known that this is calculated from the MAC address, but I won't look into the specifics; those interested can check the com.xiaomi.wearable.wear.connection
package.
Simplicity is the Ultimate Sophistication — Frida Hook#
At this point, I had already thought of the final approach: reverse it. Since the final output is encrypted, there must be an unencrypted data processing process. Reverse it, hook it, and write an XPosed plugin to listen to it.
At this point, since it was late, I didn't want to spend too much energy writing how to install frida.
First, jadx-gui
has a built-in copy as frida snippets
feature, which saves a lot of effort. However, due to various strange reasons with kotlin
data classes, I often couldn't get the data. Since I didn't record while stepping on the pit, I will roughly retrace the process:
- First, I saw the user's corresponding folder in the
/data/data/com.mi.health/databases
directory, where there is afitness_summary
database. After reading it, I found the desired data. Therefore, the initial search keywordfitness_summary
led me to cross-reference and trace back to thecom.xiaomi.fit.fitness.persist.db.internal
class. - I saw functions like
update
,insert
, etc., and kept trying, but I couldn't see the output. Eventually, I found that thecom.xiaomi.fit.fitness.persist.db.internal.h.getDailyRecord
function outputs values every time it refreshes, but only containssid
,time
, etc., withoutvalue
. - Continuing to trace back, I used the following code snippet to check the overloads and parameter types.
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);
}
}
- Suddenly, I thought of using exceptions to view the function call stack, and at this point, it was like seeing the moon through the clouds.
var callerMethodName = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new());
console.log("getTheOneDailyRecord called by: " + callerMethodName);
- Layer by layer, I found the
com.xiaomi.fit.fitness.export.data.aggregation.DailyBasicReport
class, which perfectly met my needs.
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}])
- I encountered a difficulty because the
steps
is aprivate
attribute. Althoughjadx-gui
showed multiple interfaces to access it, such asgetSteps()
andgetSourceData()
, none of them worked and all returnednot a function
. I suspect this is due to the different handling of Kotlin and Java. Ultimately, I resolved it using reflection.
Thus, the finalfrida
code can retrieve the day'ssteps
data; modifyingHomeDataType
allows retrieval of other data.
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
Final — XPosed Plugin#
Currently, the idea is to have XPosed listen to an address, and then do some protection against plaintext transmission and put it on hold for now. Since this application is always running, I think it's feasible. The current problem is that I don't know how to write Kotlin, let alone XPosed.
Fortunately, the Kotlin compiler's prompts are powerful enough, and XPosed itself doesn't require much extra knowledge apart from setting up the configuration. With the help of GPT, I figured out the basic environment in a couple of hours (the Gradle evaluation is difficult; it's slow without a proxy and can't be downloaded with one).
Environment Setup#
Anyway, I directly opened a No Activity project in Android Studio. No one writes how to configure XPosed with Gradle Kotlin, so I'll briefly mention it here, as most online resources are outdated and directly use 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") // This line
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, mainly the metadata below -->
<?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, corresponding to the xposedscope above, which is the scope package name -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="xposedscope" >
<item>com.mi.health</item>
</string-array>
</resources>
Then, you also need to create an assets/xposed_init
file under app/src/main/
, filling in your entry class.
sh.ouo.miband.uploader.MainHook
At this point, compiling it will allow you to see your plugin in the LSPosed Manager.
Idea#
HOOK Points#
We think, since we need to start in the background, and Xiaomi Health itself has some mechanisms for keeping alive and auto-starting, we don't need to hook the MainActivity
's onCreate
method but rather find a method for auto-starting.
After some searching, possible Android auto-start methods include BOOT_COMPLETED
broadcast listeners, AlarmManager
scheduled tasks, JobScheduler
jobs, and Service
, etc. In jadx-gui, we found the com.xiaomi.fitness.keep_alive.KeepAliveHelper
class's startService
method. After testing, it indeed works.
Here, we mainly use a singleton to avoid repeated registration. The main function is handleLoadPackage
to obtain the corresponding LoadPackageParam
, and then for the functions we want to HOOK, we inherit XC_MethodHook
.
Below is an instance of CommonSummaryUpdater
that we use to link with what we did in 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)
}
})
}
}
Data Extraction#
Basically similar to frida, we just call the corresponding method and parse it. Here, I wrote a slightly abstract base class, although I'm not sure if I need to write this base class.
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() // Subtract 1 second to get the end time of the day
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
}
Since I don't know Kotlin, my code looks quite strange. But the general idea is that each subclass calls setEnumValue
to set the enum value for getDailyReport
, and then overrides toJson
.
Here, I encountered many pitfalls with JSON, mainly due to type annotations, which can be quite frustrating.
Let's take a step report as an example.
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 // What to write here?
}
catch (e: Exception) {
throw e
}
}
throw NoSuchFieldException("No data fetched")
}
}
So the question arises: the today
we obtained is an instance of com.xiaomi.fit.fitness.export.data.aggregation.DailyStepReport
. How do I serialize it into JSON? In the type annotation, I can only write Any
, and the compiler doesn't know what objects it has, making serialization impossible, not to mention nested objects.
After testing for a long time and searching a lot, I couldn't find a direct method. I don't know if any experts can help. After much hassle, I ultimately decided to create an intermediate data class.
@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
)
}
}
It looks quite messy and probably inefficient, but I didn't know what else to do. I used the serialization
library.
// build.gradle.kts [Module]
plugins {
...
kotlin("plugin.serialization") version "1.9.21"
}
dependencies {
...
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
}
Then in the return place, since I might return either String
or Json
, I used JsonElement
, but because of type annotations, we had to write it like this (at least that's what GPT told me).
return Json.encodeToJsonElement(SerializableDailyStepReport.serializer(), convertToSerializableReport(today))
Listening#
I really got dizzy here. At first, I wanted to use BroadcastReceiver
because it's power-saving. But this brings up several considerations:
-
How does the computer send broadcasts to Android?
Using adb, run
adb shell am broadcast -a ACTION --es "extra_key" "extra_value"
. However, after testing, I found that after Android 11, the port for wireless debugging changes (previously fixed at 5555), and when changing WiFi/disconnecting WiFi, it needs to be re-enabled in the developer settings.There are methods to do this. Running
setprop <key> <value>
inadb shell
can change the following values. The first two are the debugging ports, and the last one prevents wireless debugging from automatically turning off.service.adb.tls.port=38420 service.adb.tcp.port=38420 persist.adb.tls_server.enable=1
However, the current
/system
directory is no longer writable. This means we cannot editbuild.prop
to make these values permanent. So it will revert after a reboot, which can be quite annoying (although I generally don't shut down).Of course, there are ways to do it, like writing a Magisk Module to set it up at boot (laughs).
-
Broadcasts are one-way communication; how can the computer receive messages?
I couldn't think of a good solution. My current thought is to write to a file and then use
adb pull
on the computer to read it.
So I gave up, and then I started thinking about HTTP Restful API. I quickly implemented one using Ktor (with GPT's help).
However, at this point, another problem arose: the frequency of obtaining this data is very low, but it has a characteristic: the timing is not fixed. Therefore, for stability, we must keep the HTTP server running at all times, but maintaining an HTTP server consumes a considerable amount of power (although I haven't tested it).
So I turned to the embrace of SOCKET. After all, it's pretty much the same.
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()
}
Then I suddenly realized an awkward problem. I need to use Templater in Obsidian to get daily information, which means using JavaScript, but Obsidian is in a sandbox-like environment, so I can't run external scripts. JavaScript can't handle sockets, right? So I had to handcraft the HTTP protocol. Security concerns aside, the evaluation is that it can be used.
override fun run() {
try {
Log.d("MiBand", "Connection: $clientSocket")
val inputStream = BufferedReader(InputStreamReader(clientSocket.getInputStream()))
val outputStream = PrintWriter(clientSocket.getOutputStream(), true)
// Read the first line of the HTTP request
val requestLine = inputStream.readLine()
println("Received: $requestLine")
// Parse the request line
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 {
// Handle getDailyReport request
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()
}
The source code will be uploaded later; for now, it's just a semi-finished product, and the evaluation is that it can casually steal my sleep data.