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, it has achieved semi-automation; however, sleep time, steps, and possibly heart rate and blood pressure data still need to be recorded and filled in manually, which is not very Geek. After 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 Limited Edition. After getting 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 at the packet capture to see if there was any useful information. I originally used Proxifier for packet capture, 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 the 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, that’s my normal workflow. Different devices may have different workflows.

I installed ConscryptTrustUserCerts in Magisk and restarted, which mounted the user-level certificates to the system-level certificate directory during the boot phase, completing the preparation.

I opened mitmweb on the 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, and nonces to ensure security. I really didn't want to reverse the APK, so I gave up.

A Glimpse of Light — BLE Connection#

Since packet capture didn't work, I decided to create a BLE client to connect to the band and retrieve data, which is obviously a very reasonable thing to do. Moreover, this method doesn't require me to do anything on my phone; Obsidian runs a script, connects, retrieves data, and seems very automated.

Implementation#

The code mainly references wuhan005/mebeats: 💓 Real-time heart rate data collection for Xiaomi Mi Band - Your Soul, Your Beats!. However, his toolchain is MacOS, which I don't have, so I asked GPT to help modify it.

There is an auth_key in the code that needs to be obtained from the official app. You can directly use this website to get it, 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 obviously has a higher priority, so I gave up.

Since I reversed it later, I’ll go back and write a bit about the front.

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

You can see that it is obtained from deviceInfo, which comes from huamiDevice. Then, 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 idea: 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.
Here, since it was late, I didn't want to spend too much effort 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 can't get the data. Since I didn't record while stepping on the pit, I will roughly trace the process:

  1. First, I saw the user-specific folder in the /data/data/com.mi.health/databases directory, which contains a fitness_summary database. Upon reading, I found the desired data. Therefore, I initially searched for the keyword fitness_summary for cross-referencing, tracing 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. However, I eventually found that the com.xiaomi.fit.fitness.persist.db.internal.h.getDailyRecord function outputs values like sid, time, etc., every time it refreshes, but does not include 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 problem 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 prompted not a function. Here, I suspect it is due to the different handling of kotlin and java. Ultimately, I solved it using reflection.
    Thus, the final frida code can obtain 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 leave it for now. Since this application is always on, I think it is 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 does not require much additional knowledge besides 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 download with a proxy).

Environment Setup#

Anyway, just open a No Activity project directly 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

Now, just compile it, and you can see your plugin in LSPosed Manager.

Idea#

HOOK Points#

We think, since we need to start in the background, and Xiaomi Health itself has some keep-alive and self-start mechanisms, we don't need to hook the MainActivity's onCreate method but can find a self-start method instead.

After some search, possible Android self-start methods include BOOT_COMPLETED broadcast listener, 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 can indeed be used.

Here, we mainly use a singleton to prevent 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 used to link with what we discussed 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; I don't know 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
}

I don't know Kotlin, so it looks strange. But the general idea is that each subclass calls setEnumValue to set the enumeration value for getDailyReport, and then overrides toJson.

Here, I encountered many pitfalls with JSON, mainly due to type annotations, which can be difficult.

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?
            }
            catch (e: Exception) {
                throw e
            }
        }
        throw NoSuchFieldException("No data fetched")
    }
}

So the question arises: the today we get 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, let alone how to serialize them, 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 the efficiency is probably low, but I don't know what else to do. I utilized 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 a Json, I used JsonElement, but because of type annotations, we must write it like this (at least that's what GPT told me).

return Json.encodeToJsonElement(SerializableDailyStepReport.serializer(), convertToSerializableReport(today))

Listening#

Here, I really got confused. At first, I wanted to use BroadcastReceiver for power saving. But this brings 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 via adb changes (previously fixed at 5555), and when changing WiFi/disconnecting WiFi, you need to go to developer settings to re-enable wireless debugging.

    There are methods. By running adb shell and executing setprop <key> <value>, you can change the following values. The first two are for 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 every reboot, it will revert, which is obviously annoying (although I generally don't shut down).

    Of course, there are methods; write a Magisk Module to set it up at boot (laugh).

  2. Broadcast is one-way communication; how does the computer receive messages?

    I couldn't think of a good way. The current thought is to write to a file and then pull it from the computer using adb.

So I gave up, and then I started thinking about HTTP Restful APIs. I quickly implemented one using Ktor (with GPT's help).

image-20240203140011022

But at this point, there was another problem: the frequency of obtaining this data is very low, yet it has this characteristic: the time 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 similar.

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, and Obsidian is in a sandbox-like environment, so I can't run external scripts. JavaScript can't handle sockets, right? Well, I have to handcraft the HTTP protocol. Security 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 state

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.