diff --git a/app/src/main/java/com/android/grape/net/Api.kt b/app/src/main/java/com/android/grape/net/Api.kt new file mode 100644 index 0000000..6e481ca --- /dev/null +++ b/app/src/main/java/com/android/grape/net/Api.kt @@ -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{ + 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 = Gson().fromJson(response, object : TypeToken>() {}.type) + return apiResponse + } else { + Log.d("padDetail", "error: $code") + return ApiResponse(code, "error", 0, null) + } + } + + fun updatePad(params: String): ApiResponseList{ + val (response, code) = HttpUtils.postSync(UPDATE_PAD, params) + if (code == 200) { + Log.d("updatePad", response.toString()) + var apiResponse: ApiResponseList = Gson().fromJson(response, object : TypeToken>() {}.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 = Gson().fromJson(response, object : TypeToken>() {}.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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/net/ApiResponse.kt b/app/src/main/java/com/android/grape/net/ApiResponse.kt new file mode 100644 index 0000000..b8dd473 --- /dev/null +++ b/app/src/main/java/com/android/grape/net/ApiResponse.kt @@ -0,0 +1,23 @@ +package com.android.grape.net + +data class ApiResponse( + val code: Int, + val message: String, + val ts: Long, + var data: T? = null +){ + fun isSuccess(): Boolean { + return code == 200 + } +} + +data class ApiResponseList( + val code: Int, + val message: String, + val ts: Long, + var data: List? = null +){ + fun isSuccess(): Boolean { + return code == 200 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/net/ArmCloudSignatureV2.kt b/app/src/main/java/com/android/grape/net/ArmCloudSignatureV2.kt new file mode 100644 index 0000000..143829f --- /dev/null +++ b/app/src/main/java/com/android/grape/net/ArmCloudSignatureV2.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/pad/DcInfo.kt b/app/src/main/java/com/android/grape/pad/DcInfo.kt new file mode 100644 index 0000000..83c9c52 --- /dev/null +++ b/app/src/main/java/com/android/grape/pad/DcInfo.kt @@ -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 = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/pad/Pad.kt b/app/src/main/java/com/android/grape/pad/Pad.kt new file mode 100644 index 0000000..d89c811 --- /dev/null +++ b/app/src/main/java/com/android/grape/pad/Pad.kt @@ -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 = listOf(), + var rows: Int = 0, + var size: Int = 0, + var total: Int = 0, + var totalPage: Int = 0 +) \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/pad/PadTask.kt b/app/src/main/java/com/android/grape/pad/PadTask.kt new file mode 100644 index 0000000..2c20f61 --- /dev/null +++ b/app/src/main/java/com/android/grape/pad/PadTask.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/pad/PageData.kt b/app/src/main/java/com/android/grape/pad/PageData.kt new file mode 100644 index 0000000..21d1fac --- /dev/null +++ b/app/src/main/java/com/android/grape/pad/PageData.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/pad/TaskDetail.kt b/app/src/main/java/com/android/grape/pad/TaskDetail.kt new file mode 100644 index 0000000..7a62ce6 --- /dev/null +++ b/app/src/main/java/com/android/grape/pad/TaskDetail.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/util/AndroidFileDownloader.kt b/app/src/main/java/com/android/grape/util/AndroidFileDownloader.kt new file mode 100644 index 0000000..17375cb --- /dev/null +++ b/app/src/main/java/com/android/grape/util/AndroidFileDownloader.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/grape/util/TimeZoneUtils.kt b/app/src/main/java/com/android/grape/util/TimeZoneUtils.kt new file mode 100644 index 0000000..8d2166d --- /dev/null +++ b/app/src/main/java/com/android/grape/util/TimeZoneUtils.kt @@ -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) + } +} \ No newline at end of file