优化时间弹窗
This commit is contained in:
@@ -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")
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
64
app/src/main/res/layout/dialog_avatar_select.xml
Normal file
64
app/src/main/res/layout/dialog_avatar_select.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user