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