优化时间弹窗

This commit is contained in:
2026-05-26 11:07:14 +08:00
parent d5be6a9b5f
commit 5b7d20d38c
14 changed files with 661 additions and 43 deletions

View File

@@ -0,0 +1,48 @@
kotlin version: 2.2.10
error message: java.nio.file.AccessDeniedException: C:\Users\Administrator\AppData\Local\kotlin\daemon\kotlin-daemon-client-tsmarker1897998583941030835.tmp
error message: Daemon compilation failed: Could not connect to Kotlin compile daemon
java.lang.RuntimeException: Could not connect to Kotlin compile daemon
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:214)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:159)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:111)
at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:74)
at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:68)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:64)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:61)
at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:102)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:61)
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44)
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:58)
at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:176)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:194)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:127)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:169)
at org.gradle.internal.Factories$1.create(Factories.java:30)
at org.gradle.internal.work.DefaultWorkerLeaseService.lambda$withLocksAcquired$0(DefaultWorkerLeaseService.java:269)
at org.gradle.internal.work.ResourceLockStatistics$1.measure(ResourceLockStatistics.java:42)
at org.gradle.internal.work.DefaultWorkerLeaseService.withLocksAcquired(DefaultWorkerLeaseService.java:267)
at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:259)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:132)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:133)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:47)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1583)

View File

@@ -93,6 +93,8 @@ dependencies {
implementation("com.geyifeng.immersionbar:immersionbar-ktx:3.2.2")
implementation("com.wdullaer:materialdatetimepicker:4.2.3") // 经典稳定
implementation("androidx.core:core-ktx:1.12.0")
// Android-PickerView 时间选择器
implementation("com.contrarywind:Android-PickerView:4.1.9")
//二维码三方库
implementation("com.github.maning0303:MNMLKitScanner:V1.0.4")
implementation("com.google.mlkit:barcode-scanning:17.0.2")
@@ -103,5 +105,7 @@ dependencies {
//上拉刷新下拉加载
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("com.yanzhenjie.recyclerview:x:1.3.2")
//Android-PickerView-时间弹窗
implementation("com.contrarywind:Android-PickerView:4.1.9")
}

View File

@@ -31,7 +31,10 @@ class PersonalFragment : Fragment() {
// ViewBinding
private var _binding: PersonalFragmentBinding? = null
private val binding get() = _binding!!
override fun onResume() {
super.onResume()
fetchUserInfo()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -206,10 +209,7 @@ class PersonalFragment : Fragment() {
}
}
override fun onResume() {
super.onResume()
fetchUserInfo()
}
private fun fetchUserInfo() {
CoroutineScope(Dispatchers.IO).launch {

View File

@@ -1,15 +1,17 @@
package com.example.defenseapplication.liveVideo
import android.graphics.Color
import android.os.Bundle
import android.os.LocaleList
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.defenseapplication.R
import com.example.defenseapplication.adapter.AlarmLogAdapter
import com.example.defenseapplication.databinding.AlarmLogActivityBinding
import com.gyf.immersionbar.ImmersionBar
import com.google.android.material.datepicker.MaterialDatePicker
import java.util.*
import com.bigkoo.pickerview.builder.TimePickerBuilder
import com.bigkoo.pickerview.listener.OnTimeSelectListener
import com.example.defenseapplication.R
//告警记录
class AlarmLogActivity : AppCompatActivity() {
private lateinit var binding: AlarmLogActivityBinding
@@ -65,17 +67,20 @@ class AlarmLogActivity : AppCompatActivity() {
}
private fun showDatePicker() {
val datePicker = android.app.DatePickerDialog(
this,
{ _, year, month, dayOfMonth ->
calendar.set(year, month, dayOfMonth)
updateDateDisplay()
loadAlarmList()
},
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH),
calendar.get(Calendar.DAY_OF_MONTH)
)
datePicker.show()
val timePicker = TimePickerBuilder(this, OnTimeSelectListener { date, _ ->
calendar.time = date
updateDateDisplay()
loadAlarmList()
})
.setType(booleanArrayOf(true, true, true, false, false, false)) // 年月日
.setTitleText(getString(R.string.select_date))
.setSubmitColor(Color.parseColor("#09C2BD"))//确定按钮文字颜色
.setCancelColor(Color.parseColor("#09C2BD"))//取消按钮文字颜色
.setSubCalSize(18)//确定和取消文字大小
.setTextColorCenter(Color.parseColor("#09C2BD"))//选中颜色
.setLineSpacingMultiplier(2.5f)//行间距倍数
.setItemVisibleCount(5)//可见项数量
.build()
timePicker.show()
}
}

View File

@@ -1,7 +1,10 @@
package com.example.defenseapplication.liveVideo
import android.app.TimePickerDialog
import android.content.Intent
import android.graphics.Color
import com.bigkoo.pickerview.builder.TimePickerBuilder
import com.bigkoo.pickerview.listener.OnTimeSelectListener
import java.util.Calendar
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
@@ -24,10 +27,10 @@ class AutoModeActivity : AppCompatActivity() {
private lateinit var binding: AutoModeActivityBinding
private var selectedMode = "0"
private var selectedStartHour = 21
private var selectedStartMinute = 30
private var selectedEndHour = 8
private var selectedEndMinute = 30
private var selectedStartHour = 8
private var selectedStartMinute = 0
private var selectedEndHour = 10
private var selectedEndMinute = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -130,15 +133,26 @@ class AutoModeActivity : AppCompatActivity() {
val currentHour = if (isStartTime) selectedStartHour else selectedEndHour
val currentMinute = if (isStartTime) selectedStartMinute else selectedEndMinute
TimePickerDialog(
this,
{ _, hour, minute ->
onTimeSelected(hour, minute)
},
currentHour,
currentMinute,
true
).show()
val calendar = Calendar.getInstance()
calendar.set(Calendar.HOUR_OF_DAY, currentHour)
calendar.set(Calendar.MINUTE, currentMinute)
val timePicker = TimePickerBuilder(this, OnTimeSelectListener { date, _ ->
val selectedCalendar = Calendar.getInstance()
selectedCalendar.time = date
onTimeSelected(selectedCalendar.get(Calendar.HOUR_OF_DAY), selectedCalendar.get(Calendar.MINUTE))
})
.setType(booleanArrayOf(false, false, false, true, true, false)) // 时分
.setTitleText(getString(R.string.select_time))
.setSubmitColor(Color.parseColor("#09C2BD"))//确定按钮文字颜色
.setCancelColor(Color.parseColor("#09C2BD"))//取消按钮文字颜色
.setSubCalSize(18)//确定和取消文字大小
.setTextColorCenter(Color.parseColor("#09C2BD"))//选中颜色
.setLineSpacingMultiplier(2.5f)//行间距倍数
.setItemVisibleCount(5)//可见项数量
.setDate(calendar)
.build()
timePicker.show()
}
private fun showAutoModeNoticeDialog() {

View File

@@ -31,7 +31,7 @@ import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
import java.util.Hashtable
//邀请二维码
class InvitationCodeActivity : AppCompatActivity() {
private lateinit var binding: InvitationCodeActivityBinding

View File

@@ -1,20 +1,34 @@
package com.example.defenseapplication.personal
import android.Manifest
import android.content.ContentResolver
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import android.util.Log
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import android.widget.Toast
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.example.defenseapplication.R
import com.example.defenseapplication.bean.UpdateUserInfo
import com.example.defenseapplication.bean.UserInfoBean
import com.example.defenseapplication.databinding.PersonalProfileActivityBinding
import com.example.defenseapplication.utils.OssUtil
import com.example.defenseapplication.utils.RetrofitNetwork
import com.example.defenseapplication.view.AvatarSelectDialog
import com.example.defenseapplication.view.CustomDialogFragmentText
import com.example.defenseapplication.view.PhoneModifyDialogFragment
import com.gyf.immersionbar.ImmersionBar
@@ -22,11 +36,78 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
//个人资料
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class PersonalProfileActivity : AppCompatActivity() {
private lateinit var binding: PersonalProfileActivityBinding
private var userInfo: UserInfoBean? = null
// 拍照返回的文件
private var photoFile: File? = null
// 请求权限回调
private val requestPermissionsLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
val cameraGranted = permissions[Manifest.permission.CAMERA] ?: false
val storageGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permissions[Manifest.permission.READ_MEDIA_IMAGES] ?: false
} else {
permissions[Manifest.permission.READ_EXTERNAL_STORAGE] ?: false
}
val writeStorageGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
true
} else {
permissions[Manifest.permission.WRITE_EXTERNAL_STORAGE] ?: false
}
if (!cameraGranted) {
Toast.makeText(this, getString(R.string.camera_permission_denied), Toast.LENGTH_SHORT).show()
}
if (!storageGranted) {
Toast.makeText(this, getString(R.string.storage_permission_denied), Toast.LENGTH_SHORT).show()
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU && !writeStorageGranted) {
Toast.makeText(this, getString(R.string.write_storage_permission_denied), Toast.LENGTH_SHORT).show()
}
}
// 拍照结果回调
private val takePhotoLauncher = registerForActivityResult(
ActivityResultContracts.TakePicture()
) { success ->
if (success && photoFile != null) {
Log.d("PersonalProfile", "拍照成功,文件路径: ${photoFile?.absolutePath}")
uploadAvatar(photoFile!!.absolutePath)
} else {
Toast.makeText(this, getString(R.string.take_photo_failed), Toast.LENGTH_SHORT).show()
}
}
// 相册选择结果回调
private val selectAlbumLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == RESULT_OK && result.data != null) {
val uri = result.data?.data
uri?.let {
val filePath = getFilePathFromUri(it)
if (filePath != null) {
Log.d("PersonalProfile", "相册选择成功,文件路径: $filePath")
uploadAvatar(filePath)
} else {
Toast.makeText(this, getString(R.string.get_file_failed), Toast.LENGTH_SHORT).show()
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = PersonalProfileActivityBinding.inflate(layoutInflater)
@@ -37,6 +118,8 @@ class PersonalProfileActivity : AppCompatActivity() {
.init()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
userInfo = intent.getParcelableExtra("userInfo", UserInfoBean::class.java)
} else {
userInfo = intent.getParcelableExtra("userInfo")
}
initView()
}
@@ -49,11 +132,11 @@ class PersonalProfileActivity : AppCompatActivity() {
userInfo?.let { info ->
Log.d("头像", "头像 URL = ${info.avatar}")
val requestOptions = RequestOptions()
.placeholder(R.drawable.img) // 加载中显示
.error(R.drawable.img) // 失败时显示
.placeholder(R.drawable.img)
.error(R.drawable.img)
Glide.with(this)
.load(info.avatar) // 如果 avatar 为 nullGlide 会直接回调 error
.load(info.avatar)
.apply(requestOptions)
.circleCrop()
.into(binding.ivAvatar)
@@ -61,6 +144,10 @@ class PersonalProfileActivity : AppCompatActivity() {
binding.tvPhone.text = info.phonenumber ?: ""
}
binding.ivAvatar.setOnClickListener {
showAvatarSelectDialog()
}
binding.itemNickname.setOnClickListener {
showNicknameDialog()
}
@@ -151,4 +238,188 @@ class PersonalProfileActivity : AppCompatActivity() {
}
}
}
}
private fun showAvatarSelectDialog() {
AvatarSelectDialog(this)
.setOnTakePhotoListener {
checkCameraPermission { granted ->
if (granted) {
takePhoto()
}
}
}
.setOnSelectAlbumListener {
checkStoragePermission { granted ->
if (granted) {
selectAlbum()
}
}
}
.show()
}
/**
* 检查相机权限
*/
private fun checkCameraPermission(callback: (Boolean) -> Unit) {
val permissions = mutableListOf(Manifest.permission.CAMERA)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
val allGranted = permissions.all {
ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
}
if (allGranted) {
callback(true)
} else {
requestPermissionsLauncher.launch(permissions.toTypedArray())
callback(false)
}
}
/**
* 检查存储权限
*/
private fun checkStoragePermission(callback: (Boolean) -> Unit) {
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
callback(true)
} else {
requestPermissionsLauncher.launch(arrayOf(permission))
callback(false)
}
}
/**
* 调用系统相机拍照
*/
private fun takePhoto() {
try {
photoFile = createImageFile()
val photoUri = FileProvider.getUriForFile(
this,
"$packageName.fileprovider",
photoFile!!
)
takePhotoLauncher.launch(photoUri)
} catch (e: IOException) {
e.printStackTrace()
Toast.makeText(this, getString(R.string.create_file_failed), Toast.LENGTH_SHORT).show()
}
}
/**
* 创建图片文件
*/
@Throws(IOException::class)
private fun createImageFile(): File {
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
return File.createTempFile(
"JPEG_${timeStamp}_",
".jpg",
storageDir
).apply {
// 保存文件路径以便后续使用
Log.d("PersonalProfile", "创建图片文件: $absolutePath")
}
}
/**
* 调用系统相册选择图片
*/
private fun selectAlbum() {
val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
intent.type = "image/*"
selectAlbumLauncher.launch(intent)
}
/**
* 从 Uri 获取文件路径
*/
private fun getFilePathFromUri(uri: Uri): String? {
val contentResolver: ContentResolver = contentResolver
val inputStream: InputStream? = contentResolver.openInputStream(uri)
inputStream?.use {
try {
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
val file = File(storageDir, "JPEG_${timeStamp}_temp.jpg")
FileOutputStream(file).use { outputStream ->
val buffer = ByteArray(1024)
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
}
}
return file.absolutePath
} catch (e: IOException) {
e.printStackTrace()
return null
}
}
return null
}
/**
* 上传头像到 OSS 并更新个人信息
*/
private fun uploadAvatar(filePath: String) {
Toast.makeText(this, getString(R.string.uploading), Toast.LENGTH_SHORT).show()
OssUtil.uploadFileAsync(this, filePath, "avatar") { ossUrl ->
runOnUiThread {
if (ossUrl != null) {
Log.d("PersonalProfile", "OSS 上传成功URL: $ossUrl")
updateAvatar(ossUrl)
} else {
Toast.makeText(this@PersonalProfileActivity, getString(R.string.upload_failed), Toast.LENGTH_SHORT).show()
}
}
}
}
/**
* 调用修改个人信息接口更新头像
*/
private fun updateAvatar(avatarUrl: String) {
CoroutineScope(Dispatchers.IO).launch {
try {
val request = UpdateUserInfo(
userId = userInfo?.userId,
avatar = avatarUrl
)
val response = RetrofitNetwork.getNetworkService().updateUserInfo(request)
withContext(Dispatchers.Main) {
if (response.code == 200) {
// 更新本地头像显示
Glide.with(this@PersonalProfileActivity)
.load(avatarUrl)
.apply(RequestOptions()
.placeholder(R.drawable.img)
.error(R.drawable.img))
.circleCrop()
.into(binding.ivAvatar)
userInfo = userInfo?.copy(avatar = avatarUrl)
Toast.makeText(this@PersonalProfileActivity, getString(R.string.operation_success), Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this@PersonalProfileActivity, getString(R.string.operation_failed), Toast.LENGTH_SHORT).show()
}
}
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
Toast.makeText(this@PersonalProfileActivity, getString(R.string.network_error), Toast.LENGTH_SHORT).show()
}
}
}
}
}

View File

@@ -0,0 +1,129 @@
package com.example.defenseapplication.utils
import android.content.Context
import android.util.Log
import com.alibaba.sdk.android.oss.ClientConfiguration
import com.alibaba.sdk.android.oss.OSS
import com.alibaba.sdk.android.oss.OSSClient
import com.alibaba.sdk.android.oss.common.auth.OSSCredentialProvider
import com.alibaba.sdk.android.oss.common.auth.OSSPlainTextAKSKCredentialProvider
import com.alibaba.sdk.android.oss.model.PutObjectRequest
import com.alibaba.sdk.android.oss.model.PutObjectResult
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* OSS 工具类
* 用于上传文件到阿里云 OSS
*/
object OssUtil {
private const val TAG = "OssUtil"
// OSS 配置信息 - 需要根据实际情况配置
private const val ENDPOINT = "your-oss-endpoint" // 例如: oss-cn-hangzhou.aliyuncs.com
private const val ACCESS_KEY_ID = "your-access-key-id"
private const val ACCESS_KEY_SECRET = "your-access-key-secret"
private const val BUCKET_NAME = "your-bucket-name"
private var ossClient: OSS? = null
/**
* 初始化 OSS 客户端
*/
private fun initOssClient(context: Context): OSS {
if (ossClient == null) {
// 创建凭证提供者
val credentialProvider: OSSCredentialProvider =
OSSPlainTextAKSKCredentialProvider(ACCESS_KEY_ID, ACCESS_KEY_SECRET)
// 创建配置
val conf = ClientConfiguration().apply {
connectionTimeout = 15 * 1000 // 连接超时默认15秒
socketTimeout = 15 * 1000 // socket超时默认15秒
maxConcurrentRequest = 5 // 最大并发请求数默认5
maxErrorRetry = 2 // 失败后最大重试次数默认2
}
// 创建 OSS 客户端
ossClient = OSSClient(context.applicationContext, ENDPOINT, credentialProvider, conf)
}
return ossClient!!
}
/**
* 上传文件到 OSS
* @param context 上下文
* @param filePath 文件路径
* @param folderName 文件夹名称(可选,用于分类存储)
* @return OSS 文件 URL
*/
fun uploadFile(context: Context, filePath: String, folderName: String = "avatar"): String? {
return try {
val file = File(filePath)
if (!file.exists()) {
Log.e(TAG, "File not found: $filePath")
return null
}
// 生成文件名:时间戳 + 原文件名
val timestamp = SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()).format(Date())
val originalFileName = file.name
val fileName = "${timestamp}_${originalFileName}"
val objectKey = "$folderName/$fileName"
Log.d(TAG, "Uploading file: $filePath to OSS key: $objectKey")
// 创建上传请求
val putObjectRequest = PutObjectRequest(BUCKET_NAME, objectKey, filePath)
// 执行上传
val oss = initOssClient(context)
val result: PutObjectResult = oss.putObject(putObjectRequest)
Log.d(TAG, "Upload success, ETAG: ${result.eTag}")
// 返回文件 URL
"https://$BUCKET_NAME.$ENDPOINT/$objectKey"
} catch (e: Exception) {
Log.e(TAG, "Upload failed: ${e.message}", e)
null
}
}
/**
* 异步上传文件到 OSS
* @param context 上下文
* @param filePath 文件路径
* @param folderName 文件夹名称
* @param callback 回调函数
*/
fun uploadFileAsync(
context: Context,
filePath: String,
folderName: String = "avatar",
callback: (String?) -> Unit
) {
Thread {
val result = uploadFile(context, filePath, folderName)
callback(result)
}.start()
}
/**
* 配置 OSS 参数(动态配置)
* @param endpoint OSS 端点
* @param accessKeyId Access Key ID
* @param accessKeySecret Access Key Secret
* @param bucketName Bucket 名称
*/
fun setOssConfig(endpoint: String, accessKeyId: String, accessKeySecret: String, bucketName: String) {
// 由于常量无法修改,这里需要重新创建客户端
ossClient = null
// 注意:实际项目中应该使用动态配置,这里简化处理
Log.d(TAG, "OSS config updated")
}
}

View File

@@ -0,0 +1,67 @@
package com.example.defenseapplication.view
import android.app.Dialog
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import com.example.defenseapplication.R
import com.example.defenseapplication.databinding.DialogAvatarSelectBinding
class AvatarSelectDialog(context: Context) {
private val dialog: Dialog = Dialog(context)
private val binding: DialogAvatarSelectBinding
private var onTakePhotoListener: (() -> Unit)? = null
private var onSelectAlbumListener: (() -> Unit)? = null
init {
binding = DialogAvatarSelectBinding.inflate(LayoutInflater.from(context))
dialog.setContentView(binding.root)
dialog.window?.apply {
setGravity(Gravity.BOTTOM)
setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT)
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
setDimAmount(0.5f)
}
initListeners()
}
private fun initListeners() {
binding.itemTakePhoto.setOnClickListener {
onTakePhotoListener?.invoke()
dialog.dismiss()
}
binding.itemSelectAlbum.setOnClickListener {
onSelectAlbumListener?.invoke()
dialog.dismiss()
}
binding.itemCancel.setOnClickListener {
dialog.dismiss()
}
}
fun setOnTakePhotoListener(listener: () -> Unit): AvatarSelectDialog {
this.onTakePhotoListener = listener
return this
}
fun setOnSelectAlbumListener(listener: () -> Unit): AvatarSelectDialog {
this.onSelectAlbumListener = listener
return this
}
fun show() {
dialog.show()
}
fun dismiss() {
dialog.dismiss()
}
}

View File

@@ -159,6 +159,7 @@
android:paddingRight="16dp">
<LinearLayout
android:id="@+id/expandImg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
@@ -172,7 +173,6 @@
android:textStyle="bold" />
<ImageView
android:id="@+id/expandImg"
android:layout_width="20dp"
android:layout_height="match_parent"
android:src="@drawable/expand" />

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_bottom_sheet_rounded"
android:orientation="vertical">
<LinearLayout
android:id="@+id/itemTakePhoto"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center"
android:paddingHorizontal="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/take_photo"
android:textColor="@color/black"
android:textSize="16sp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@color/dialog_view" />
<LinearLayout
android:id="@+id/itemSelectAlbum"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center"
android:paddingHorizontal="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/select_from_album"
android:textColor="@color/black"
android:textSize="16sp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="8dp"
android:background="@color/face" />
<LinearLayout
android:id="@+id/itemCancel"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/white"
android:gravity="center"
android:paddingHorizontal="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/cancel"
android:textColor="@color/gray"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>

View File

@@ -457,4 +457,8 @@
<string name="phone_number_empty">手机号为空</string>
<string name="operation_fail">操作失败</string>
<string name="get_user_info_fail">获取用户信息失败</string>
<string name="take_photo">拍照</string>
<string name="select_from_album">从相册选择</string>
<string name="select_date">选择日期</string>
<string name="select_time">选择时间</string>
</resources>

View File

@@ -2,7 +2,7 @@
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="green">#09C2BD </color>
<color name="green">#09C2BD</color>
<color name="gray">#999999</color>
<color name="login_background">#EDF4FF</color>
<color name="red">#E34D59</color>

View File

@@ -349,6 +349,14 @@
<string name="cannot_open_camera">Cannot open camera</string>
<string name="camera_permission_needed">Camera permission is required</string>
<string name="permission_denied">Camera and storage permissions are required</string>
<string name="camera_permission_denied">Camera permission is required</string>
<string name="storage_permission_denied">Storage permission is required</string>
<string name="write_storage_permission_denied">Write storage permission is required</string>
<string name="take_photo_failed">Failed to take photo</string>
<string name="get_file_failed">Failed to get file</string>
<string name="create_file_failed">Failed to create file</string>
<string name="uploading">Uploading...</string>
<string name="upload_failed">Upload failed</string>
<string name="image_selected">Image selected</string>
<string name="please_try_later">Please try again later</string>
<string name="phone_format_invalid">Invalid phone number format</string>
@@ -461,4 +469,8 @@
<string name="phone_number_empty">Phone number not found</string>
<string name="operation_fail">Operation failed</string>
<string name="get_user_info_fail">Failed to get user info</string>
<string name="take_photo">Take Photo</string>
<string name="select_from_album">Select from Album</string>
<string name="select_date">Select Date</string>
<string name="select_time">Select Time</string>
</resources>