chore: initial commit

这个提交包含在:
XuqmGroup 2026-04-21 22:07:29 +08:00
当前提交 43cbd0f098
共有 30 个文件被更改,包括 974 次插入0 次删除

10
.gitignore vendored 普通文件
查看文件

@ -0,0 +1,10 @@
node_modules/
dist/
.DS_Store
*.class
target/
build/
.gradle/
*.iml
.idea/
*.log

13
build.gradle.kts 普通文件
查看文件

@ -0,0 +1,13 @@
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.kotlin.serialization) apply false
}
group = "com.xuqm"
version = providers.gradleProperty("PUBLISH_VERSION").getOrElse("0.1.0-SNAPSHOT")
ext["nexusUrl"] = "https://nexus.xuqinmin.com/repository/android-hosted/"
ext["nexusUser"] = providers.gradleProperty("NEXUS_USER").getOrElse("")
ext["nexusPassword"] = providers.gradleProperty("NEXUS_PASSWORD").getOrElse("")

5
gradle.properties 普通文件
查看文件

@ -0,0 +1,5 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
kotlin.code.style=official
android.nonTransitiveRClass=true
PUBLISH_VERSION=0.1.0-SNAPSHOT

77
gradle/libs.versions.toml 普通文件
查看文件

@ -0,0 +1,77 @@
[versions]
agp = "9.1.0"
kotlin = "2.3.10"
compileSdk = "36"
targetSdk = "36"
minSdk = "24"
coreKtx = "1.18.0"
lifecycle = "2.10.0"
activityCompose = "1.13.0"
activityKtx = "1.13.0"
composeBom = "2026.03.00"
coroutines = "1.10.2"
datastore = "1.1.7"
retrofit = "3.0.0"
okhttp = "5.3.2"
gson = "2.13.2"
jserialization = "1.9.0"
webkit = "1.14.0"
coil = "2.7.0"
junit4 = "4.13.2"
androidxJunit = "1.3.0"
espresso = "3.7.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "jserialization" }
androidx-webkit = { group = "androidx.webkit", name = "webkit", version.ref = "webkit" }
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }
junit4 = { group = "junit", name = "junit", version.ref = "junit4" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidxJunit" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso" }
[bundles]
compose = [
"androidx-ui",
"androidx-ui-graphics",
"androidx-ui-tooling-preview",
"androidx-material3",
"androidx-material-icons-extended"
]
compose-debug = [
"androidx-ui-tooling",
"androidx-ui-test-manifest"
]
network = [
"retrofit",
"retrofit-converter-gson",
"okhttp",
"okhttp-logging",
"gson"
]
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

25
gradle/publish.gradle.kts 普通文件
查看文件

@ -0,0 +1,25 @@
apply(plugin = "maven-publish")
afterEvaluate {
(extensions.findByType(com.android.build.gradle.LibraryExtension::class.java))?.let {
extensions.configure<PublishingExtension> {
publications {
register<MavenPublication>("release") {
from(components["release"])
groupId = rootProject.group.toString()
artifactId = project.name
version = rootProject.version.toString()
}
}
repositories {
maven {
url = uri(rootProject.ext["nexusUrl"] as String)
credentials {
username = rootProject.ext["nexusUser"] as String
password = rootProject.ext["nexusPassword"] as String
}
}
}
}
}
}

44
sample-app/build.gradle.kts 普通文件
查看文件

@ -0,0 +1,44 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
id("org.jetbrains.kotlin.android") version "2.3.10"
}
android {
namespace = "com.xuqm.sdk.sample"
compileSdk = libs.versions.compileSdk.get().toInt()
defaultConfig {
applicationId = "com.xuqm.sdk.sample"
minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = 1
versionName = "1.0.0"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions { jvmTarget = "11" }
buildFeatures { compose = true }
}
dependencies {
implementation(project(":sdk-core"))
implementation(project(":sdk-im"))
implementation(project(":sdk-push"))
implementation(project(":sdk-update"))
implementation(platform(libs.androidx.compose.bom))
implementation(libs.bundles.compose)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.lifecycle.runtime.ktx)
debugImplementation(libs.bundles.compose.debug)
}

查看文件

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:allowBackup="true"
android:label="XuqmSDK Demo"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

查看文件

@ -0,0 +1,123 @@
package com.xuqm.sdk.sample
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.xuqm.sdk.XuqmSDK
import com.xuqm.sdk.im.ImSDK
import com.xuqm.sdk.im.listener.ImEventListener
import com.xuqm.sdk.im.model.ChatType
import com.xuqm.sdk.im.model.ImMessage
import com.xuqm.sdk.im.model.MsgType
import com.xuqm.sdk.update.UpdateSDK
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
XuqmSDK.init(
context = this,
appKey = "ak_your_app_key",
appSecret = "your_app_secret",
apiBaseUrl = "http://10.0.2.2:8082",
imBaseUrl = "ws://10.0.2.2:8082/ws/im",
debug = true,
)
setContent {
MaterialTheme {
Surface(modifier = Modifier.fillMaxSize()) {
SdkDemoScreen()
}
}
}
}
}
@Composable
fun SdkDemoScreen() {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val messages = remember { mutableStateListOf<String>() }
var msgInput by remember { mutableStateOf("") }
var userId by remember { mutableStateOf("user_001") }
var connected by remember { mutableStateOf(false) }
var updateInfo by remember { mutableStateOf("") }
val listener = remember {
object : ImEventListener {
override fun onConnected() { messages.add("[IM] 已连接"); connected = true }
override fun onDisconnected(reason: String?) { messages.add("[IM] 断开: $reason"); connected = false }
override fun onMessage(message: ImMessage) { messages.add("[消息] ${message.fromUserId}: ${message.content}") }
}
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text("XuqmSDK Demo", style = MaterialTheme.typography.headlineSmall)
Card {
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("IM 测试", style = MaterialTheme.typography.titleMedium)
OutlinedTextField(value = userId, onValueChange = { userId = it }, label = { Text("UserId") }, modifier = Modifier.fillMaxWidth())
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = {
ImSDK.addListener(listener)
ImSDK.login("your_app_id", userId)
}) { Text("连接") }
Button(onClick = { ImSDK.disconnect(); connected = false },
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)) {
Text("断开")
}
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(value = msgInput, onValueChange = { msgInput = it },
label = { Text("消息内容") }, modifier = Modifier.weight(1f))
Button(onClick = {
if (msgInput.isNotBlank()) {
ImSDK.sendMessage("user_002", ChatType.SINGLE, MsgType.TEXT, msgInput)
msgInput = ""
}
}) { Text("发送") }
}
}
}
Card {
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("版本更新", style = MaterialTheme.typography.titleMedium)
Button(onClick = {
scope.launch {
val info = UpdateSDK.checkUpdate(context, "your_app_id")
updateInfo = if (info?.needsUpdate == true)
"发现新版本: ${info.versionName}" else "已是最新版本"
}
}) { Text("检查更新") }
if (updateInfo.isNotBlank()) Text(updateInfo)
}
}
Card {
Column(Modifier.padding(12.dp)) {
Text("消息日志", style = MaterialTheme.typography.titleMedium)
Spacer(Modifier.height(8.dp))
messages.forEach { msg -> Text(msg, style = MaterialTheme.typography.bodySmall) }
}
}
}
}

查看文件

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-files-path name="apk_downloads" path="." />
</paths>

29
sdk-core/build.gradle.kts 普通文件
查看文件

@ -0,0 +1,29 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.serialization)
id("org.jetbrains.kotlin.android") version "2.3.10"
}
android {
namespace = "com.xuqm.sdk.core"
compileSdk = libs.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()
consumerProguardFiles("consumer-rules.pro")
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions { jvmTarget = "11" }
}
dependencies {
api(libs.bundles.network)
api(libs.kotlinx.coroutines.android)
api(libs.kotlinx.serialization.json)
api(libs.androidx.datastore.preferences)
api(libs.androidx.core.ktx)
}

查看文件

@ -0,0 +1,35 @@
package com.xuqm.sdk
import android.content.Context
import com.xuqm.sdk.auth.TokenStore
import com.xuqm.sdk.core.SDKConfig
import com.xuqm.sdk.network.ApiClient
object XuqmSDK {
lateinit var config: SDKConfig
private set
lateinit var tokenStore: TokenStore
private set
private var initialized = false
fun init(
context: Context,
appKey: String,
appSecret: String,
apiBaseUrl: String = "https://api.xuqm.com",
imBaseUrl: String = "wss://im.xuqm.com",
debug: Boolean = false,
) {
config = SDKConfig(appKey, appSecret, apiBaseUrl, imBaseUrl, debug)
tokenStore = TokenStore(context.applicationContext)
ApiClient.init(config, tokenStore)
initialized = true
}
fun requireInit() {
check(initialized) { "XuqmSDK is not initialized. Call XuqmSDK.init() first." }
}
}

查看文件

@ -0,0 +1,27 @@
package com.xuqm.sdk.auth
import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
private val Context.dataStore by preferencesDataStore(name = "xuqm_sdk_prefs")
class TokenStore(private val context: Context) {
private val TOKEN_KEY = stringPreferencesKey("access_token")
fun getToken(): String? = runBlocking {
context.dataStore.data.first()[TOKEN_KEY]
}
suspend fun saveToken(token: String) {
context.dataStore.edit { prefs -> prefs[TOKEN_KEY] = token }
}
suspend fun clear() {
context.dataStore.edit { prefs -> prefs.remove(TOKEN_KEY) }
}
}

查看文件

@ -0,0 +1,9 @@
package com.xuqm.sdk.core
data class SDKConfig(
val appKey: String,
val appSecret: String,
val apiBaseUrl: String = "https://api.xuqm.com",
val imBaseUrl: String = "wss://im.xuqm.com",
val debug: Boolean = false,
)

查看文件

@ -0,0 +1,51 @@
package com.xuqm.sdk.network
import com.xuqm.sdk.auth.TokenStore
import com.xuqm.sdk.core.SDKConfig
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
object ApiClient {
private lateinit var config: SDKConfig
private var tokenStore: TokenStore? = null
lateinit var retrofit: Retrofit
private set
fun init(cfg: SDKConfig, store: TokenStore) {
config = cfg
tokenStore = store
val logging = HttpLoggingInterceptor().apply {
level = if (cfg.debug) HttpLoggingInterceptor.Level.BODY
else HttpLoggingInterceptor.Level.NONE
}
val okhttp = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(logging)
.addInterceptor { chain ->
val token = store.getToken()
val req: Request = if (token != null) {
chain.request().newBuilder()
.header("Authorization", "Bearer $token")
.build()
} else chain.request()
chain.proceed(req)
}
.build()
retrofit = Retrofit.Builder()
.baseUrl(cfg.apiBaseUrl.trimEnd('/') + "/")
.client(okhttp)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
inline fun <reified T> create(): T = retrofit.create(T::class.java)
}

查看文件

@ -0,0 +1,14 @@
package com.xuqm.sdk.network
sealed class ApiResult<out T> {
data class Success<T>(val data: T) : ApiResult<T>()
data class Error(val code: Int, val message: String, val cause: Throwable? = null) : ApiResult<Nothing>()
}
suspend fun <T> safeApiCall(block: suspend () -> T): ApiResult<T> = try {
ApiResult.Success(block())
} catch (e: retrofit2.HttpException) {
ApiResult.Error(e.code(), e.message(), e)
} catch (e: Exception) {
ApiResult.Error(-1, e.message ?: "Unknown error", e)
}

查看文件

@ -0,0 +1,24 @@
package com.xuqm.sdk.utils
import android.content.Context
import android.os.Build
import android.provider.Settings
object DeviceUtils {
fun getDeviceId(context: Context): String =
Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
?: Build.SERIAL
fun getDeviceModel(): String = "${Build.MANUFACTURER} ${Build.MODEL}"
fun getOsVersion(): String = "Android ${Build.VERSION.RELEASE}"
fun getVendor(): String = when (Build.MANUFACTURER.lowercase()) {
"huawei", "honor" -> if (Build.MANUFACTURER.lowercase() == "honor") "HONOR" else "HUAWEI"
"xiaomi", "redmi" -> "XIAOMI"
"oppo", "realme", "oneplus" -> "OPPO"
"vivo", "iqoo" -> "VIVO"
else -> "FCM"
}
}

25
sdk-im/build.gradle.kts 普通文件
查看文件

@ -0,0 +1,25 @@
plugins {
alias(libs.plugins.android.library)
id("org.jetbrains.kotlin.android") version "2.3.10"
}
android {
namespace = "com.xuqm.sdk.im"
compileSdk = libs.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()
consumerProguardFiles("consumer-rules.pro")
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions { jvmTarget = "11" }
}
dependencies {
api(project(":sdk-core"))
implementation(libs.kotlinx.coroutines.android)
}

查看文件

@ -0,0 +1,84 @@
package com.xuqm.sdk.im
import com.google.gson.Gson
import com.xuqm.sdk.im.listener.ImEventListener
import com.xuqm.sdk.im.model.ChatType
import com.xuqm.sdk.im.model.ImMessage
import com.xuqm.sdk.im.model.MsgType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit
class ImClient(
private val wsUrl: String,
private val token: String,
private val appId: String,
) {
private var webSocket: WebSocket? = null
private val listeners = CopyOnWriteArrayList<ImEventListener>()
private val scope = CoroutineScope(Dispatchers.IO + Job())
private val gson = Gson()
private val okhttp = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(0, TimeUnit.SECONDS)
.build()
fun connect() {
val request = Request.Builder()
.url(wsUrl)
.header("Authorization", "Bearer $token")
.build()
webSocket = okhttp.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(ws: WebSocket, response: Response) {
listeners.forEach { it.onConnected() }
}
override fun onMessage(ws: WebSocket, text: String) {
try {
val msg = gson.fromJson(text, ImMessage::class.java)
if (msg.chatType == ChatType.GROUP) {
listeners.forEach { it.onGroupMessage(msg) }
} else {
listeners.forEach { it.onMessage(msg) }
}
} catch (e: Exception) {
listeners.forEach { it.onError("Parse error: ${e.message}") }
}
}
override fun onFailure(ws: WebSocket, t: Throwable, response: Response?) {
listeners.forEach { it.onDisconnected(t.message) }
}
override fun onClosed(ws: WebSocket, code: Int, reason: String) {
listeners.forEach { it.onDisconnected(reason) }
}
})
}
fun sendMessage(toId: String, chatType: ChatType, msgType: MsgType, content: String) {
val payload = mapOf(
"appId" to appId, "toId" to toId,
"chatType" to chatType.name, "msgType" to msgType.name,
"content" to content,
)
webSocket?.send(gson.toJson(mapOf("type" to "chat.send", "data" to payload)))
}
fun addListener(listener: ImEventListener) = listeners.add(listener)
fun removeListener(listener: ImEventListener) = listeners.remove(listener)
fun disconnect() {
webSocket?.close(1000, "User disconnect")
webSocket = null
}
}

查看文件

@ -0,0 +1,39 @@
package com.xuqm.sdk.im
import com.xuqm.sdk.XuqmSDK
import com.xuqm.sdk.im.api.ImApi
import com.xuqm.sdk.im.listener.ImEventListener
import com.xuqm.sdk.im.model.ChatType
import com.xuqm.sdk.im.model.MsgType
import com.xuqm.sdk.network.ApiClient
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
object ImSDK {
private var client: ImClient? = null
private val api: ImApi by lazy { ApiClient.create() }
private val scope = CoroutineScope(Dispatchers.IO)
fun login(appId: String, userId: String, nickname: String? = null, avatar: String? = null) {
XuqmSDK.requireInit()
scope.launch {
val res = api.login(appId, userId, nickname, avatar)
res.data?.token?.let { token ->
XuqmSDK.tokenStore.saveToken(token)
val wsUrl = XuqmSDK.config.imBaseUrl
client = ImClient(wsUrl, token, appId)
client?.connect()
}
}
}
fun sendMessage(toId: String, chatType: ChatType, msgType: MsgType, content: String) {
client?.sendMessage(toId, chatType, msgType, content)
}
fun addListener(listener: ImEventListener) = client?.addListener(listener)
fun removeListener(listener: ImEventListener) = client?.removeListener(listener)
fun disconnect() = client?.disconnect()
}

查看文件

@ -0,0 +1,26 @@
package com.xuqm.sdk.im.api
import com.xuqm.sdk.im.model.ImMessage
import retrofit2.http.POST
import retrofit2.http.GET
import retrofit2.http.Query
data class ApiResponse<T>(val code: Int, val status: String, val data: T?, val message: String)
data class LoginResponse(val token: String)
data class SendMessageRequest(
val toId: String,
val chatType: String,
val msgType: String,
val content: String,
val mentionedUserIds: String? = null,
)
interface ImApi {
@POST("api/im/auth/login")
suspend fun login(
@Query("appId") appId: String,
@Query("userId") userId: String,
@Query("nickname") nickname: String? = null,
@Query("avatar") avatar: String? = null,
): ApiResponse<LoginResponse>
}

查看文件

@ -0,0 +1,11 @@
package com.xuqm.sdk.im.listener
import com.xuqm.sdk.im.model.ImMessage
interface ImEventListener {
fun onConnected() {}
fun onDisconnected(reason: String?) {}
fun onMessage(message: ImMessage) {}
fun onGroupMessage(message: ImMessage) {}
fun onError(error: String) {}
}

查看文件

@ -0,0 +1,23 @@
package com.xuqm.sdk.im.model
data class ImMessage(
val id: String,
val appId: String,
val fromUserId: String,
val toId: String,
val chatType: ChatType,
val msgType: MsgType,
val content: String,
val status: MsgStatus,
val mentionedUserIds: String?,
val createdAt: String,
)
enum class ChatType { SINGLE, GROUP }
enum class MsgType {
TEXT, IMAGE, VIDEO, AUDIO, FILE, CUSTOM, LOCATION, NOTIFY,
RICH_TEXT, CALL_AUDIO, CALL_VIDEO, REVOKED, FORWARD
}
enum class MsgStatus { SENT, DELIVERED, READ, REVOKED }

24
sdk-push/build.gradle.kts 普通文件
查看文件

@ -0,0 +1,24 @@
plugins {
alias(libs.plugins.android.library)
id("org.jetbrains.kotlin.android") version "2.3.10"
}
android {
namespace = "com.xuqm.sdk.push"
compileSdk = libs.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()
consumerProguardFiles("consumer-rules.pro")
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions { jvmTarget = "11" }
}
dependencies {
api(project(":sdk-core"))
}

查看文件

@ -0,0 +1,35 @@
package com.xuqm.sdk.push
import android.content.Context
import com.xuqm.sdk.XuqmSDK
import com.xuqm.sdk.network.ApiClient
import com.xuqm.sdk.push.api.PushApi
import com.xuqm.sdk.utils.DeviceUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
object PushSDK {
private val api: PushApi by lazy { ApiClient.create() }
private val scope = CoroutineScope(Dispatchers.IO)
fun registerToken(context: Context, appId: String, userId: String, token: String) {
XuqmSDK.requireInit()
val vendor = DeviceUtils.getVendor()
scope.launch {
runCatching {
api.registerToken(appId, userId, vendor, token)
}
}
}
fun unregisterToken(appId: String, userId: String) {
XuqmSDK.requireInit()
scope.launch {
runCatching {
api.unregisterToken(appId, userId)
}
}
}
}

查看文件

@ -0,0 +1,21 @@
package com.xuqm.sdk.push.api
import retrofit2.http.DELETE
import retrofit2.http.POST
import retrofit2.http.Query
interface PushApi {
@POST("api/push/register")
suspend fun registerToken(
@Query("appId") appId: String,
@Query("userId") userId: String,
@Query("vendor") vendor: String,
@Query("token") token: String,
)
@DELETE("api/push/unregister")
suspend fun unregisterToken(
@Query("appId") appId: String,
@Query("userId") userId: String,
)
}

25
sdk-update/build.gradle.kts 普通文件
查看文件

@ -0,0 +1,25 @@
plugins {
alias(libs.plugins.android.library)
id("org.jetbrains.kotlin.android") version "2.3.10"
}
android {
namespace = "com.xuqm.sdk.update"
compileSdk = libs.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()
consumerProguardFiles("consumer-rules.pro")
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions { jvmTarget = "11" }
}
dependencies {
api(project(":sdk-core"))
implementation(libs.kotlinx.coroutines.android)
}

查看文件

@ -0,0 +1,71 @@
package com.xuqm.sdk.update
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import androidx.core.content.FileProvider
import com.xuqm.sdk.XuqmSDK
import com.xuqm.sdk.network.ApiClient
import com.xuqm.sdk.update.api.UpdateApi
import com.xuqm.sdk.update.model.RnUpdateInfo
import com.xuqm.sdk.update.model.UpdateInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.net.URL
object UpdateSDK {
private val api: UpdateApi by lazy { ApiClient.create() }
suspend fun checkUpdate(context: Context, appId: String): UpdateInfo? = withContext(Dispatchers.IO) {
XuqmSDK.requireInit()
val versionCode = context.packageManager
.getPackageInfo(context.packageName, 0).longVersionCode.toInt()
runCatching { api.checkUpdate(appId, "ANDROID", versionCode).data }.getOrNull()
}
suspend fun downloadAndInstall(
context: Context,
downloadUrl: String,
onProgress: (Int) -> Unit = {},
) = withContext(Dispatchers.IO) {
val apkFile = File(context.getExternalFilesDir(null), "update.apk")
val url = URL(downloadUrl)
val connection = url.openConnection()
connection.connect()
val totalSize = connection.contentLengthLong
connection.getInputStream().use { input ->
apkFile.outputStream().use { output ->
val buffer = ByteArray(8192)
var downloaded = 0L
var read: Int
while (input.read(buffer).also { read = it } != -1) {
output.write(buffer, 0, read)
downloaded += read
if (totalSize > 0) onProgress((downloaded * 100 / totalSize).toInt())
}
}
}
withContext(Dispatchers.Main) {
installApk(context, apkFile)
}
}
private fun installApk(context: Context, apkFile: File) {
val intent = Intent(Intent.ACTION_VIEW).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", apkFile)
setDataAndType(uri, "application/vnd.android.package-archive")
}
context.startActivity(intent)
}
suspend fun checkRnUpdate(appId: String, moduleId: String, currentVersion: String): RnUpdateInfo? =
withContext(Dispatchers.IO) {
runCatching { api.checkRnUpdate(appId, moduleId, "ANDROID", currentVersion).data }.getOrNull()
}
}

查看文件

@ -0,0 +1,25 @@
package com.xuqm.sdk.update.api
import com.xuqm.sdk.update.model.UpdateInfo
import com.xuqm.sdk.update.model.RnUpdateInfo
import retrofit2.http.GET
import retrofit2.http.Query
data class ApiResponse<T>(val code: Int, val data: T?, val message: String)
interface UpdateApi {
@GET("api/v1/updates/app/check")
suspend fun checkUpdate(
@Query("appId") appId: String,
@Query("platform") platform: String,
@Query("currentVersionCode") currentVersionCode: Int,
): ApiResponse<UpdateInfo>
@GET("api/v1/rn/update/check")
suspend fun checkRnUpdate(
@Query("appId") appId: String,
@Query("moduleId") moduleId: String,
@Query("platform") platform: String,
@Query("currentVersion") currentVersion: String,
): ApiResponse<RnUpdateInfo>
}

查看文件

@ -0,0 +1,21 @@
package com.xuqm.sdk.update.model
data class UpdateInfo(
val needsUpdate: Boolean,
val versionName: String = "",
val versionCode: Int = 0,
val downloadUrl: String = "",
val changeLog: String = "",
val forceUpdate: Boolean = false,
val appStoreUrl: String = "",
val marketUrl: String = "",
)
data class RnUpdateInfo(
val needsUpdate: Boolean,
val latestVersion: String = "",
val downloadUrl: String = "",
val md5: String = "",
val minCommonVersion: String = "0.0.0",
val note: String = "",
)

25
settings.gradle.kts 普通文件
查看文件

@ -0,0 +1,25 @@
pluginManagement {
repositories {
maven(url = "https://nexus.xuqinmin.com/repository/android/")
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
maven(url = "https://nexus.xuqinmin.com/repository/android/")
google()
mavenCentral()
}
}
rootProject.name = "XuqmGroupAndroidSDK"
include(":sdk-core")
include(":sdk-im")
include(":sdk-push")
include(":sdk-update")
include(":sample-app")