chore: initial commit
这个提交包含在:
当前提交
43cbd0f098
10
.gitignore
vendored
普通文件
10
.gitignore
vendored
普通文件
@ -0,0 +1,10 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.DS_Store
|
||||
*.class
|
||||
target/
|
||||
build/
|
||||
.gradle/
|
||||
*.iml
|
||||
.idea/
|
||||
*.log
|
||||
13
build.gradle.kts
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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
普通文件
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")
|
||||
正在加载...
在新工单中引用
屏蔽一个用户