diff --git a/.kotlin/errors/errors-1779756141582.log b/.kotlin/errors/errors-1779756141582.log new file mode 100644 index 0000000..add04cd --- /dev/null +++ b/.kotlin/errors/errors-1779756141582.log @@ -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) + + diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cc6fe97..6bc384b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") } \ No newline at end of file diff --git a/app/src/main/java/com/example/defenseapplication/home/PersonalFragment.kt b/app/src/main/java/com/example/defenseapplication/home/PersonalFragment.kt index 29b79a5..bda27fa 100644 --- a/app/src/main/java/com/example/defenseapplication/home/PersonalFragment.kt +++ b/app/src/main/java/com/example/defenseapplication/home/PersonalFragment.kt @@ -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 { diff --git a/app/src/main/java/com/example/defenseapplication/liveVideo/AlarmLogActivity.kt b/app/src/main/java/com/example/defenseapplication/liveVideo/AlarmLogActivity.kt index 0ca8b1d..ab5b98c 100644 --- a/app/src/main/java/com/example/defenseapplication/liveVideo/AlarmLogActivity.kt +++ b/app/src/main/java/com/example/defenseapplication/liveVideo/AlarmLogActivity.kt @@ -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() } } diff --git a/app/src/main/java/com/example/defenseapplication/liveVideo/AutoModeActivity.kt b/app/src/main/java/com/example/defenseapplication/liveVideo/AutoModeActivity.kt index 31ca28d..730cd4b 100644 --- a/app/src/main/java/com/example/defenseapplication/liveVideo/AutoModeActivity.kt +++ b/app/src/main/java/com/example/defenseapplication/liveVideo/AutoModeActivity.kt @@ -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() { diff --git a/app/src/main/java/com/example/defenseapplication/personal/InvitationCodeActivity.kt b/app/src/main/java/com/example/defenseapplication/personal/InvitationCodeActivity.kt index 47082a9..624e44e 100644 --- a/app/src/main/java/com/example/defenseapplication/personal/InvitationCodeActivity.kt +++ b/app/src/main/java/com/example/defenseapplication/personal/InvitationCodeActivity.kt @@ -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 diff --git a/app/src/main/java/com/example/defenseapplication/personal/PersonalProfileActivity.kt b/app/src/main/java/com/example/defenseapplication/personal/PersonalProfileActivity.kt index 3fe7918..35d0e06 100644 --- a/app/src/main/java/com/example/defenseapplication/personal/PersonalProfileActivity.kt +++ b/app/src/main/java/com/example/defenseapplication/personal/PersonalProfileActivity.kt @@ -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 为 null,Glide 会直接回调 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() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/defenseapplication/utils/OssUtil.kt b/app/src/main/java/com/example/defenseapplication/utils/OssUtil.kt new file mode 100644 index 0000000..e22e8b2 --- /dev/null +++ b/app/src/main/java/com/example/defenseapplication/utils/OssUtil.kt @@ -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") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/defenseapplication/view/AvatarSelectDialog.kt b/app/src/main/java/com/example/defenseapplication/view/AvatarSelectDialog.kt new file mode 100644 index 0000000..1a95209 --- /dev/null +++ b/app/src/main/java/com/example/defenseapplication/view/AvatarSelectDialog.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/alarm_log_activity.xml b/app/src/main/res/layout/alarm_log_activity.xml index 508de24..06abb5d 100644 --- a/app/src/main/res/layout/alarm_log_activity.xml +++ b/app/src/main/res/layout/alarm_log_activity.xml @@ -159,6 +159,7 @@ android:paddingRight="16dp"> @@ -172,7 +173,6 @@ android:textStyle="bold" /> diff --git a/app/src/main/res/layout/dialog_avatar_select.xml b/app/src/main/res/layout/dialog_avatar_select.xml new file mode 100644 index 0000000..3b9a118 --- /dev/null +++ b/app/src/main/res/layout/dialog_avatar_select.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index eb3dda5..04ac085 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -457,4 +457,8 @@ 手机号为空 操作失败 获取用户信息失败 + 拍照 + 从相册选择 + 选择日期 + 选择时间 \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 0488174..00eb770 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -2,7 +2,7 @@ #FF000000 #FFFFFFFF - #09C2BD + #09C2BD #999999 #EDF4FF #E34D59 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 563eb9b..89b3fd8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -349,6 +349,14 @@ Cannot open camera Camera permission is required Camera and storage permissions are required + Camera permission is required + Storage permission is required + Write storage permission is required + Failed to take photo + Failed to get file + Failed to create file + Uploading... + Upload failed Image selected Please try again later Invalid phone number format @@ -461,4 +469,8 @@ Phone number not found Operation failed Failed to get user info + Take Photo + Select from Album + Select Date + Select Time \ No newline at end of file