This commit is contained in:
parent
fc62906c0e
commit
ce0933ee94
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 = ""
|
||||||
|
)
|
|
@ -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
|
||||||
|
)
|
|
@ -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
|
||||||
|
)
|
|
@ -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
|
||||||
|
)
|
|
@ -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
|
||||||
|
)
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue