banner
MuElnova

NoxA

Becoming a random anime guy... Pwner@天枢Dubhe/天璇Merak | BUPT CyberSecurity | INTP-A
email
github
bilibili
steam
twitter

The idea of automatically uploading data from Xiaomi Band 8 Pro to Obsidian

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 found Credential storage, which has Install 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:

  1. First, I saw the user's corresponding folder in the /data/data/com.mi.health/databases directory, where there is a fitness_summary database. After reading it, I found the desired data. Therefore, the initial search keyword fitness_summary led me to cross-reference and trace back to the com.xiaomi.fit.fitness.persist.db.internal class.
  2. I saw functions like update, insert, etc., and kept trying, but I couldn't see the output. Eventually, I found that the com.xiaomi.fit.fitness.persist.db.internal.h.getDailyRecord function outputs values every time it refreshes, but only contains sid, time, etc., without value.
  3. 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);
	}
}
  1. 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);
  1. 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}])
  1. I encountered a difficulty because the steps is a private attribute. Although jadx-gui showed multiple interfaces to access it, such as getSteps() and getSourceData(), none of them worked and all returned not a function. I suspect this is due to the different handling of Kotlin and Java. Ultimately, I resolved it using reflection.
    Thus, the final frida code can retrieve the day's steps data; modifying HomeDataType 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:

  1. 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> in adb 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 edit build.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).

  2. 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).

image-20240203140011022

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()
    }

Very Healthy Sleep Status

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.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.