This commit is contained in:
Administrator 2025-07-09 09:39:06 +08:00
parent fc62906c0e
commit ce0933ee94
10 changed files with 397 additions and 0 deletions

View File

@ -0,0 +1,70 @@
package com.android.grape.net
import android.util.Log
import com.android.grape.pad.Pad
import com.android.grape.pad.PadTask
import com.android.grape.pad.TaskDetail
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import org.json.JSONArray
import org.json.JSONObject
object Api {
private const val TAG = "Api"
const val UPDATE_PAD: String = "/openapi/open/pad/updatePadProperties"
const val PAD_DETAIL: String = "/openapi/open/pad/padDetails"
const val PAD_TASK: String = "/task-center/open/task/padTaskDetail"
fun padDetail(): ApiResponse<Pad>{
val param = Gson().toJson(mapOf(
"page" to 1,
"rows" to 10,
"padCode" to listOf("ACP250702PWJCTLF")
))
val (response, code) = HttpUtils.postSync(PAD_DETAIL, param)
if (code == 200) {
Log.d("padDetail", response.toString())
var apiResponse: ApiResponse<Pad> = Gson().fromJson(response, object : TypeToken<ApiResponse<Pad>>() {}.type)
return apiResponse
} else {
Log.d("padDetail", "error: $code")
return ApiResponse(code, "error", 0, null)
}
}
fun updatePad(params: String): ApiResponseList<PadTask>{
val (response, code) = HttpUtils.postSync(UPDATE_PAD, params)
if (code == 200) {
Log.d("updatePad", response.toString())
var apiResponse: ApiResponseList<PadTask> = Gson().fromJson(response, object : TypeToken<ApiResponseList<PadTask>>() {}.type)
return apiResponse
} else {
Log.d("updatePad", "error: $code")
return ApiResponseList(code, "error", 0, emptyList())
}
}
fun padTaskDetail(taskId: Int): Int{
val jsonString = JSONObject().apply {
put("taskIds", JSONArray().apply {
put(taskId)
})
}.toString()
Log.d(TAG, "padTaskDetail: $jsonString")
val (response, code) = HttpUtils.postSync(PAD_TASK, jsonString)
if (code == 200) {
Log.d("padTaskDetail", response.toString())
var apiResponse: ApiResponseList<TaskDetail> = Gson().fromJson(response, object : TypeToken<ApiResponseList<TaskDetail>>() {}.type)
val taskList = apiResponse.data
if (apiResponse.isSuccess() && taskList!= null && taskList.isNotEmpty()){
val task = taskList[0]
return task.taskStatus
}
return -1
}else {
Log.d("padTaskDetail", "error: $code")
return -1
}
}
}

View File

@ -0,0 +1,23 @@
package com.android.grape.net
data class ApiResponse<T>(
val code: Int,
val message: String,
val ts: Long,
var data: T? = null
){
fun isSuccess(): Boolean {
return code == 200
}
}
data class ApiResponseList<T>(
val code: Int,
val message: String,
val ts: Long,
var data: List<T>? = null
){
fun isSuccess(): Boolean {
return code == 200
}
}

View File

@ -0,0 +1,39 @@
package com.android.grape.net
import android.util.Log
import java.nio.charset.StandardCharsets
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
object ArmCloudSignatureV2 {
const val ALGORITHM: String = "HmacSHA256"
const val SECRET_KEY: String = "gz8f1u0t63byzdu6ozbx8r5qs3e5lipt"
const val SECRET_ID: String = "3yc8c8bg1dym0zaiwjh867al"
@Throws(Exception::class)
fun calculateSignature(
timestamp: String?,
path: String,
body: String?,
secretKey: String = SECRET_ID
): String {
val stringToSign = timestamp + path + (body ?: "")
Log.d("TAG", "calculateSignature: $stringToSign")
val hmacSha256 = Mac.getInstance(ALGORITHM)
val secretKeySpec = SecretKeySpec(secretKey.toByteArray(StandardCharsets.UTF_8), ALGORITHM)
hmacSha256.init(secretKeySpec)
val hash = hmacSha256.doFinal(stringToSign.toByteArray(StandardCharsets.UTF_8))
return bytesToHex(hash)
}
private fun bytesToHex(bytes: ByteArray): String {
val hexString = StringBuilder()
for (b in bytes) {
val hex = Integer.toHexString(0xff and b.toInt())
if (hex.length == 1) hexString.append('0')
hexString.append(hex)
}
return hexString.toString()
}
}

View File

@ -0,0 +1,14 @@
package com.android.grape.pad
import com.google.gson.annotations.SerializedName
data class DcInfo(
var area: String = "",
var dcCode: String = "",
var dcName: String = "",
var ossEndpoint: String = "",
var ossEndpointInternal: String = "",
var ossFileEndpoint: String = "",
var ossScreenshotEndpoint: String = ""
)

View File

@ -0,0 +1,13 @@
package com.android.grape.pad
import com.google.gson.annotations.SerializedName
data class Pad(
var page: Int = 0,
var pageData: List<PageData> = listOf(),
var rows: Int = 0,
var size: Int = 0,
var total: Int = 0,
var totalPage: Int = 0
)

View File

@ -0,0 +1,10 @@
package com.android.grape.pad
import com.google.gson.annotations.SerializedName
data class PadTask(
var padCode: String = "",
var taskId: Int = 0,
var vmStatus: Int = 0
)

View File

@ -0,0 +1,17 @@
package com.android.grape.pad
import com.google.gson.annotations.SerializedName
data class PageData(
var dataSize: Long = 0,
var dataSizeUsed: Long = 0,
var dcInfo: DcInfo = DcInfo(),
var deviceLevel: String = "",
var deviceStatus: Int = 0,
var imageId: String = "",
var online: Int = 0,
var padCode: String = "",
var padStatus: Int = 0,
var streamStatus: Int = 0
)

View File

@ -0,0 +1,14 @@
package com.android.grape.pad
import com.google.gson.annotations.SerializedName
data class TaskDetail(
var endTime: Long = 0,
var errorMsg: String = "",
var padCode: String = "",
var taskContent: Any? = Any(),
var taskId: Int = 0,
var taskResult: Any? = Any(),
var taskStatus: Int = 0
)

View File

@ -0,0 +1,128 @@
package com.android.grape.util
import android.content.Context
import android.os.Environment
import androidx.core.content.ContextCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
class AndroidFileDownloader(private val context: Context) {
private val client = OkHttpClient()
/**
* 下载文件到指定路径
*
* @param url 文件下载URL
* @param relativePath 相对于外部存储目录的路径 "Downloads/MyApp/file.zip"
* @param fileName 文件名可选如未提供则从URL提取
* @param progressCallback 进度回调
* @param completionCallback 完成回调
*/
fun downloadFile(
url: String,
relativePath: String,
fileName: String? = null,
): Boolean {
try {
// 检查存储权限
if (!hasStoragePermission()) {
throw IOException("缺少存储权限")
}
// 获取目标文件
val file = getOutputFile(relativePath, fileName ?: extractFileName(url))
// 确保目录存在
file.parentFile?.let { parentDir ->
if (!parentDir.exists()) {
val dirsCreated = parentDir.mkdirs()
if (!dirsCreated) {
throw IOException("无法创建目录: ${parentDir.absolutePath}")
}
}
}
val request = Request.Builder()
.url(url)
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
throw IOException("下载失败: ${response.code} ${response.message}")
}
val body = response.body ?: throw IOException("响应体为空")
val totalBytes = body.contentLength()
// 写入文件
body.byteStream().use { inputStream ->
FileOutputStream(file).use { outputStream ->
copyStreamWithProgress(inputStream, outputStream, totalBytes)
}
}
return true
}
} catch (e: Exception) {
e.printStackTrace()
return false
}
}
/**
* 检查存储权限
*/
private fun hasStoragePermission(): Boolean {
return ContextCompat.checkSelfPermission(
context,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
) == android.content.pm.PackageManager.PERMISSION_GRANTED
}
/**
* 获取输出文件路径
*/
private fun getOutputFile(relativePath: String, fileName: String): File {
val baseDir = context.getExternalFilesDir(null)
?: Environment.getExternalStorageDirectory()
?: throw IOException("无法访问外部存储")
return File(baseDir, "$relativePath/$fileName")
}
/**
* 从URL提取文件名
*/
private fun extractFileName(url: String): String {
return url.substringAfterLast('/').takeIf { it.isNotBlank() } ?: "downloaded_file"
}
/**
* 带进度回调的流复制
*/
private fun copyStreamWithProgress(
inputStream: InputStream,
outputStream: FileOutputStream,
totalBytes: Long?,
progressCallback: ((Long, Long?) -> Unit)? = null
) {
val buffer = ByteArray(4096)
var bytesCopied = 0L
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
bytesCopied += bytesRead
progressCallback?.invoke(bytesCopied, totalBytes)
}
outputStream.flush()
}
}

View File

@ -0,0 +1,69 @@
package com.android.grape.util
import android.content.Context
import java.util.*
import kotlin.math.abs
object TimeZoneUtils {
/**
* 从偏移时间获取最匹配的时区ID
*
* @param offsetMillis 时区偏移毫秒
* @param useDaylight 是否考虑夏令时
*/
fun getBestMatchTimeZoneId(offsetMillis: Int, useDaylight: Boolean = false): String {
val availableIds = TimeZone.getAvailableIDs()
// 首选:完全匹配的时区
availableIds.firstOrNull { id ->
val tz = TimeZone.getTimeZone(id)
if (useDaylight) {
tz.getOffset(System.currentTimeMillis()) == offsetMillis
} else {
tz.rawOffset == offsetMillis
}
}?.let { return it }
// 次选:最接近的时区
return availableIds.minByOrNull { id ->
val tz = TimeZone.getTimeZone(id)
val diff = if (useDaylight) {
abs(tz.getOffset(System.currentTimeMillis()) - offsetMillis)
} else {
abs(tz.rawOffset - offsetMillis)
}
diff
} ?: TimeZone.getDefault().id
}
/**
* 获取时区偏移的格式化字符串
*/
fun formatTimeZoneOffset(offsetMillis: Int): String {
val hours = offsetMillis / (1000 * 60 * 60)
val minutes = abs(offsetMillis) / (1000 * 60) % 60
return when {
hours > 0 -> String.format("UTC+%d:%02d", hours, minutes)
hours < 0 -> String.format("UTC%d:%02d", hours, minutes) // 自动显示负号
else -> "UTC"
}
}
/**
* 从设备获取当前时区偏移
*/
fun getCurrentTimeZoneOffset(): Int {
return TimeZone.getDefault().getOffset(System.currentTimeMillis())
}
/**
* 将时间转换为指定时区偏移的时间
*/
fun convertTimeToOffset(time: Date, targetOffsetMillis: Int): Date {
val currentOffset = TimeZone.getDefault().getOffset(time.time)
val diff = targetOffsetMillis - currentOffset
return Date(time.time + diff)
}
}