修改头像
This commit is contained in:
@@ -0,0 +1,13 @@
|
|||||||
|
package com.example.defenseapplication.bean
|
||||||
|
|
||||||
|
data class UploadResponse(
|
||||||
|
val code: Int,
|
||||||
|
val msg: String,
|
||||||
|
val data: UploadData?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class UploadData(
|
||||||
|
val url: String,
|
||||||
|
val fileName: String?,
|
||||||
|
val ossId: String?
|
||||||
|
)
|
||||||
@@ -4,8 +4,6 @@ import android.Manifest
|
|||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -26,16 +24,19 @@ import com.example.defenseapplication.R
|
|||||||
import com.example.defenseapplication.bean.UpdateUserInfo
|
import com.example.defenseapplication.bean.UpdateUserInfo
|
||||||
import com.example.defenseapplication.bean.UserInfoBean
|
import com.example.defenseapplication.bean.UserInfoBean
|
||||||
import com.example.defenseapplication.databinding.PersonalProfileActivityBinding
|
import com.example.defenseapplication.databinding.PersonalProfileActivityBinding
|
||||||
import com.example.defenseapplication.utils.OssUtil
|
|
||||||
import com.example.defenseapplication.utils.RetrofitNetwork
|
import com.example.defenseapplication.utils.RetrofitNetwork
|
||||||
import com.example.defenseapplication.view.AvatarSelectDialog
|
import com.example.defenseapplication.view.AvatarSelectDialog
|
||||||
import com.example.defenseapplication.view.CustomDialogFragmentText
|
import com.example.defenseapplication.view.CustomDialogFragmentText
|
||||||
|
import com.example.defenseapplication.view.LoadingDialog
|
||||||
import com.example.defenseapplication.view.PhoneModifyDialogFragment
|
import com.example.defenseapplication.view.PhoneModifyDialogFragment
|
||||||
import com.gyf.immersionbar.ImmersionBar
|
import com.gyf.immersionbar.ImmersionBar
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.MultipartBody
|
||||||
|
import okhttp3.RequestBody.Companion.asRequestBody
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -51,6 +52,9 @@ class PersonalProfileActivity : AppCompatActivity() {
|
|||||||
// 拍照返回的文件
|
// 拍照返回的文件
|
||||||
private var photoFile: File? = null
|
private var photoFile: File? = null
|
||||||
|
|
||||||
|
// 加载弹窗
|
||||||
|
private lateinit var loadingDialog: LoadingDialog
|
||||||
|
|
||||||
// 请求权限回调
|
// 请求权限回调
|
||||||
private val requestPermissionsLauncher = registerForActivityResult(
|
private val requestPermissionsLauncher = registerForActivityResult(
|
||||||
ActivityResultContracts.RequestMultiplePermissions()
|
ActivityResultContracts.RequestMultiplePermissions()
|
||||||
@@ -116,6 +120,8 @@ class PersonalProfileActivity : AppCompatActivity() {
|
|||||||
.statusBarDarkFont(true)
|
.statusBarDarkFont(true)
|
||||||
.titleBar(binding.topBar)
|
.titleBar(binding.topBar)
|
||||||
.init()
|
.init()
|
||||||
|
// 初始化加载弹窗
|
||||||
|
loadingDialog = LoadingDialog(this)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
userInfo = intent.getParcelableExtra("userInfo", UserInfoBean::class.java)
|
userInfo = intent.getParcelableExtra("userInfo", UserInfoBean::class.java)
|
||||||
} else {
|
} else {
|
||||||
@@ -327,7 +333,6 @@ class PersonalProfileActivity : AppCompatActivity() {
|
|||||||
".jpg",
|
".jpg",
|
||||||
storageDir
|
storageDir
|
||||||
).apply {
|
).apply {
|
||||||
// 保存文件路径以便后续使用
|
|
||||||
Log.d("PersonalProfile", "创建图片文件: $absolutePath")
|
Log.d("PersonalProfile", "创建图片文件: $absolutePath")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -352,7 +357,7 @@ class PersonalProfileActivity : AppCompatActivity() {
|
|||||||
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
|
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
|
||||||
val storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
|
val storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
|
||||||
val file = File(storageDir, "JPEG_${timeStamp}_temp.jpg")
|
val file = File(storageDir, "JPEG_${timeStamp}_temp.jpg")
|
||||||
|
|
||||||
FileOutputStream(file).use { outputStream ->
|
FileOutputStream(file).use { outputStream ->
|
||||||
val buffer = ByteArray(1024)
|
val buffer = ByteArray(1024)
|
||||||
var bytesRead: Int
|
var bytesRead: Int
|
||||||
@@ -370,18 +375,34 @@ class PersonalProfileActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上传头像到 OSS 并更新个人信息
|
* 上传头像到服务器并更新个人信息
|
||||||
*/
|
*/
|
||||||
private fun uploadAvatar(filePath: String) {
|
private fun uploadAvatar(filePath: String) {
|
||||||
Toast.makeText(this, getString(R.string.uploading), Toast.LENGTH_SHORT).show()
|
// 显示加载弹窗
|
||||||
|
loadingDialog.show()
|
||||||
|
|
||||||
OssUtil.uploadFileAsync(this, filePath, "avatar") { ossUrl ->
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
runOnUiThread {
|
try {
|
||||||
if (ossUrl != null) {
|
val file = File(filePath)
|
||||||
Log.d("PersonalProfile", "OSS 上传成功,URL: $ossUrl")
|
val requestBody = file.asRequestBody("image/jpeg".toMediaTypeOrNull())
|
||||||
updateAvatar(ossUrl)
|
val part = MultipartBody.Part.createFormData("file", file.name, requestBody)
|
||||||
} else {
|
|
||||||
Toast.makeText(this@PersonalProfileActivity, getString(R.string.upload_failed), Toast.LENGTH_SHORT).show()
|
val uploadResponse = RetrofitNetwork.getNetworkService().uploadFile(part)
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (uploadResponse.code == 200 && uploadResponse.data?.url != null) {
|
||||||
|
Log.d("PersonalProfile", "文件上传成功,URL: ${uploadResponse.data?.url}")
|
||||||
|
updateAvatar(uploadResponse.data!!.url)
|
||||||
|
} else {
|
||||||
|
loadingDialog.dismiss()
|
||||||
|
Toast.makeText(this@PersonalProfileActivity, getString(R.string.upload_failed), Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
loadingDialog.dismiss()
|
||||||
|
Toast.makeText(this@PersonalProfileActivity, getString(R.string.network_error), Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -399,8 +420,8 @@ class PersonalProfileActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
val response = RetrofitNetwork.getNetworkService().updateUserInfo(request)
|
val response = RetrofitNetwork.getNetworkService().updateUserInfo(request)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
loadingDialog.dismiss()
|
||||||
if (response.code == 200) {
|
if (response.code == 200) {
|
||||||
// 更新本地头像显示
|
|
||||||
Glide.with(this@PersonalProfileActivity)
|
Glide.with(this@PersonalProfileActivity)
|
||||||
.load(avatarUrl)
|
.load(avatarUrl)
|
||||||
.apply(RequestOptions()
|
.apply(RequestOptions()
|
||||||
@@ -417,6 +438,7 @@ class PersonalProfileActivity : AppCompatActivity() {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
loadingDialog.dismiss()
|
||||||
Toast.makeText(this@PersonalProfileActivity, getString(R.string.network_error), Toast.LENGTH_SHORT).show()
|
Toast.makeText(this@PersonalProfileActivity, getString(R.string.network_error), Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,10 +19,13 @@ import com.example.defenseapplication.bean.ObtainInviterInformationRequest
|
|||||||
import com.example.defenseapplication.bean.SwitchFamilyRequest
|
import com.example.defenseapplication.bean.SwitchFamilyRequest
|
||||||
import com.example.defenseapplication.bean.UpdatePassword
|
import com.example.defenseapplication.bean.UpdatePassword
|
||||||
import com.example.defenseapplication.bean.UpdateUserInfo
|
import com.example.defenseapplication.bean.UpdateUserInfo
|
||||||
|
import com.example.defenseapplication.bean.UploadResponse
|
||||||
import com.example.defenseapplication.bean.UserInfoBean
|
import com.example.defenseapplication.bean.UserInfoBean
|
||||||
import com.example.defenseapplication.bean.VerifyOptionPasswordRequest
|
import com.example.defenseapplication.bean.VerifyOptionPasswordRequest
|
||||||
|
import okhttp3.MultipartBody
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Multipart
|
||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
import retrofit2.http.PUT
|
import retrofit2.http.PUT
|
||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
@@ -206,6 +209,12 @@ interface ApiService {
|
|||||||
@POST("/app/revokeInvitation")
|
@POST("/app/revokeInvitation")
|
||||||
suspend fun revokeInvitation(@Body request: VerifyOptionPasswordRequest): GetCodeBean<Any>
|
suspend fun revokeInvitation(@Body request: VerifyOptionPasswordRequest): GetCodeBean<Any>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件接口
|
||||||
|
*/
|
||||||
|
@Multipart
|
||||||
|
@POST("/app/upload")
|
||||||
|
suspend fun uploadFile(@retrofit2.http.Part file: MultipartBody.Part): UploadResponse
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修改家庭用户状态
|
* 修改家庭用户状态
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
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,52 @@
|
|||||||
|
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.WindowManager
|
||||||
|
import com.example.defenseapplication.databinding.DialogLoadingBinding
|
||||||
|
|
||||||
|
class LoadingDialog(context: Context) {
|
||||||
|
|
||||||
|
private val dialog: Dialog = Dialog(context)
|
||||||
|
private val binding: DialogLoadingBinding
|
||||||
|
|
||||||
|
init {
|
||||||
|
binding = DialogLoadingBinding.inflate(LayoutInflater.from(context))
|
||||||
|
dialog.setContentView(binding.root)
|
||||||
|
dialog.window?.apply {
|
||||||
|
setGravity(Gravity.CENTER)
|
||||||
|
setLayout(
|
||||||
|
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||||
|
WindowManager.LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||||
|
setDimAmount(0.5f)
|
||||||
|
}
|
||||||
|
dialog.setCancelable(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMessage(message: String): LoadingDialog {
|
||||||
|
binding.tvLoading.text = message
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show() {
|
||||||
|
if (!dialog.isShowing) {
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismiss() {
|
||||||
|
if (dialog.isShowing) {
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isShowing(): Boolean {
|
||||||
|
return dialog.isShowing
|
||||||
|
}
|
||||||
|
}
|
||||||
26
app/src/main/res/layout/dialog_loading.xml
Normal file
26
app/src/main/res/layout/dialog_loading.xml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="32dp"
|
||||||
|
android:background="@drawable/bg_white_rounded">
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress_bar"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:indeterminateTint="@color/green"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_loading"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/uploading"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="16sp"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -461,4 +461,12 @@
|
|||||||
<string name="select_from_album">从相册选择</string>
|
<string name="select_from_album">从相册选择</string>
|
||||||
<string name="select_date">选择日期</string>
|
<string name="select_date">选择日期</string>
|
||||||
<string name="select_time">选择时间</string>
|
<string name="select_time">选择时间</string>
|
||||||
|
<string name="camera_permission_denied">需要相机权限</string>
|
||||||
|
<string name="storage_permission_denied">需要存储权限</string>
|
||||||
|
<string name="write_storage_permission_denied">需要写入存储权限</string>
|
||||||
|
<string name="take_photo_failed">拍照失败</string>
|
||||||
|
<string name="get_file_failed">获取文件失败</string>
|
||||||
|
<string name="create_file_failed">创建文件失败</string>
|
||||||
|
<string name="uploading">上传中...</string>
|
||||||
|
<string name="upload_failed">上传失败</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user