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 foundCredential storage
, which hasInstall 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:
- First, I saw the user-specific folder in the
/data/data/com.mi.health/databases
directory, which contains afitness_summary
database. Upon reading, I found the desired data. Therefore, I initially searched for the keywordfitness_summary
for cross-referencing, tracing 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. However, I eventually found that thecom.xiaomi.fit.fitness.persist.db.internal.h.getDailyRecord
function outputs values likesid
,time
, etc., every time it refreshes, but does not includevalue
. - 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 problem because the
steps
is aprivate
attribute. Althoughjadx-gui
showed multiple interfaces to access it, such asgetSteps()
andgetSourceData()
, none of them worked and all promptednot a function
. Here, I suspect it is due to the different handling ofkotlin
andjava
. Ultimately, I solved it using reflection.
Thus, the finalfrida
code can obtain 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 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:
-
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 executingsetprop <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 editbuild.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).
-
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).
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()
}
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.