Compare commits
5 Commits
ab0dd2fbf6
...
f154f30e2d
| Author | SHA1 | Date | |
|---|---|---|---|
| f154f30e2d | |||
| 7114e36165 | |||
| 195fb73620 | |||
| 655279a8a1 | |||
| 46385d5358 |
@@ -4,6 +4,26 @@ import android.app.Application
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.blankj.utilcode.util.Utils
|
import com.blankj.utilcode.util.Utils
|
||||||
import com.ckkj.defense_device.utils.LanguageUtil
|
import com.ckkj.defense_device.utils.LanguageUtil
|
||||||
|
// _ooOoo_
|
||||||
|
// o8888888o
|
||||||
|
// 88" . "88
|
||||||
|
// (| -_- |)
|
||||||
|
// O\ = /O
|
||||||
|
// ____/`---'\____
|
||||||
|
// . ' \\| |// `.
|
||||||
|
// / \\||| : |||// \
|
||||||
|
// / _||||| -:- |||||- \
|
||||||
|
// | | \\\ - /// | |
|
||||||
|
// | \_| ''\---/'' | |
|
||||||
|
// \ .-\__ `-` ___/-. /
|
||||||
|
// ___`. .' /--.--\ `. . __
|
||||||
|
// ."" '< `.___\_<|>_/___.' >'"".
|
||||||
|
// | | : `- \`.;`\ _ /`;.`/ - ` : | |
|
||||||
|
// \ \ `-. \_ __\ /__ _/ .-` / /
|
||||||
|
// ======`-.____`-.___\_____/___.-`____.-'======
|
||||||
|
// `=---='
|
||||||
|
//
|
||||||
|
// 佛祖保佑 永无BUG
|
||||||
|
|
||||||
class MyApplication : Application() {
|
class MyApplication : Application() {
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ data class DeviceBean(
|
|||||||
val deviceFamilyId: String? = null,
|
val deviceFamilyId: String? = null,
|
||||||
val deviceImg: String? = null,
|
val deviceImg: String? = null,
|
||||||
val deviceName: String? = null,//设备名称
|
val deviceName: String? = null,//设备名称
|
||||||
val deviceNo: String? = null,//设备编号
|
val deviceNo: String = "",//设备编号
|
||||||
val deviceSn: String? = null,//设备序列号
|
val deviceSn: String = "",//设备序列号
|
||||||
val endTime: String? = null,//自动布放结束时间
|
val endTime: String? = null,//自动布放结束时间
|
||||||
val expirationTime: String? = null,
|
val expirationTime: String? = null,
|
||||||
val fwVer: String? = null,//固件版本
|
val fwVer: String= "",//固件版本
|
||||||
val id: String? = null,
|
val id: String? = null,
|
||||||
val light: String? = null,//强光开启
|
val light: String? = null,//强光开启
|
||||||
val patternPerception: String? = null,//识别模式
|
val patternPerception: String? = null,//识别模式
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import android.view.ViewGroup
|
|||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.alibaba.sdk.android.emas.g
|
||||||
import com.ckkj.defense_device.R
|
import com.ckkj.defense_device.R
|
||||||
import com.ckkj.defense_device.adapter.DeviceAdapter
|
import com.ckkj.defense_device.adapter.DeviceAdapter
|
||||||
import com.ckkj.defense_device.adapter.FamilyAdapter
|
import com.ckkj.defense_device.adapter.FamilyAdapter
|
||||||
@@ -465,7 +466,7 @@ class HomeMenuFragment : Fragment() {
|
|||||||
|
|
||||||
isLoading = true
|
isLoading = true
|
||||||
|
|
||||||
loadingDialog = if (showLoading) LoadingDialog(requireActivity()).setMessage("加载中...").apply { show() } else null
|
loadingDialog = if (showLoading) LoadingDialog(requireActivity()).setMessage(getString(R.string.loading)).apply { show() } else null
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ import android.view.View
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.ckkj.defense_device.R
|
||||||
import com.ckkj.defense_device.adapter.MessageListAdapter
|
import com.ckkj.defense_device.adapter.MessageListAdapter
|
||||||
import com.ckkj.defense_device.adapter.MessageListItem
|
import com.ckkj.defense_device.adapter.MessageListItem
|
||||||
import com.ckkj.defense_device.databinding.MessageActivityBinding
|
import com.ckkj.defense_device.databinding.MessageActivityBinding
|
||||||
import com.ckkj.defense_device.utils.MessageStatusManager
|
import com.ckkj.defense_device.utils.MessageStatusManager
|
||||||
import com.ckkj.defense_device.utils.RetrofitNetwork
|
import com.ckkj.defense_device.utils.RetrofitNetwork
|
||||||
|
import com.ckkj.defense_device.view.LoadingDialog
|
||||||
|
import com.ckkj.defense_device.view.PullRefreshLayout
|
||||||
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
|
||||||
@@ -19,13 +22,13 @@ import kotlinx.coroutines.withContext
|
|||||||
class MessageActivity : AppCompatActivity() {
|
class MessageActivity : AppCompatActivity() {
|
||||||
private lateinit var binding: MessageActivityBinding
|
private lateinit var binding: MessageActivityBinding
|
||||||
private val messageAdapter by lazy { MessageListAdapter() }
|
private val messageAdapter by lazy { MessageListAdapter() }
|
||||||
|
private lateinit var loadingDialog: LoadingDialog
|
||||||
|
|
||||||
private var currentPage = 1
|
private var currentPage = 1
|
||||||
private val pageSize = 10
|
private val pageSize = 10
|
||||||
private var isLoading = false
|
private var isLoading = false
|
||||||
private var hasMoreData = true
|
private var hasMoreData = true
|
||||||
private val allMessages = mutableListOf<com.ckkj.defense_device.bean.MessageBean>()
|
private val allMessages = mutableListOf<com.ckkj.defense_device.bean.MessageBean>()
|
||||||
private var lastLoadedCount = 0
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -41,10 +44,19 @@ class MessageActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
|
loadingDialog = LoadingDialog(this).setMessage(getString(R.string.loading))
|
||||||
binding.ivBack.setOnClickListener { finish() }
|
binding.ivBack.setOnClickListener { finish() }
|
||||||
binding.rvMessageList.layoutManager = LinearLayoutManager(this)
|
binding.rvMessageList.layoutManager = LinearLayoutManager(this)
|
||||||
binding.rvMessageList.adapter = messageAdapter
|
binding.rvMessageList.adapter = messageAdapter
|
||||||
|
|
||||||
|
binding.swipeRefreshLayout.setOnRefreshListener(object : PullRefreshLayout.OnRefreshListener {
|
||||||
|
override fun onRefresh() {
|
||||||
|
currentPage = 1
|
||||||
|
hasMoreData = true
|
||||||
|
loadMessageList()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
binding.rvMessageList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
binding.rvMessageList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
super.onScrolled(recyclerView, dx, dy)
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
@@ -70,20 +82,21 @@ class MessageActivity : AppCompatActivity() {
|
|||||||
if (isLoading) return
|
if (isLoading) return
|
||||||
|
|
||||||
isLoading = true
|
isLoading = true
|
||||||
|
|
||||||
|
if (currentPage == 1) {
|
||||||
|
showLoading()
|
||||||
|
}
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
val messageResponse = RetrofitNetwork.getNetworkService().getMessage(
|
val messageResponse = RetrofitNetwork.getNetworkService().getMessage(
|
||||||
pageNum = currentPage,
|
pageNum = currentPage,
|
||||||
pageSize = pageSize
|
pageSize = pageSize
|
||||||
)
|
)
|
||||||
println("total: ${messageResponse.total}")
|
|
||||||
println("rows size: ${messageResponse.rows.size}")
|
|
||||||
println("当前页:$currentPage, 加载条数:${messageResponse.rows.size}")
|
|
||||||
|
|
||||||
val newMessages = messageResponse.rows
|
val newMessages = messageResponse.rows
|
||||||
val loadedCount = newMessages.size
|
|
||||||
|
|
||||||
if (loadedCount < pageSize) {
|
if (newMessages.size < pageSize) {
|
||||||
hasMoreData = false
|
hasMoreData = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,11 +111,11 @@ class MessageActivity : AppCompatActivity() {
|
|||||||
allMessages.clear()
|
allMessages.clear()
|
||||||
}
|
}
|
||||||
allMessages.addAll(newMessages)
|
allMessages.addAll(newMessages)
|
||||||
lastLoadedCount = loadedCount
|
|
||||||
|
|
||||||
val items = convertToMessageListItems(allMessages)
|
val items = convertToMessageListItems(allMessages)
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
hideLoading()
|
||||||
hideEmptyState()
|
hideEmptyState()
|
||||||
messageAdapter.submitList(items)
|
messageAdapter.submitList(items)
|
||||||
|
|
||||||
@@ -116,12 +129,10 @@ class MessageActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (allMessages.isEmpty()) {
|
if (allMessages.isEmpty()) {
|
||||||
println("items is empty, showing empty state")
|
|
||||||
showEmptyState()
|
showEmptyState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("Exception: ${e.message}")
|
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (allMessages.isEmpty()) {
|
if (allMessages.isEmpty()) {
|
||||||
@@ -130,6 +141,12 @@ class MessageActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isLoading = false
|
isLoading = false
|
||||||
|
if (currentPage == 1) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
hideLoading()
|
||||||
|
binding.swipeRefreshLayout.finishRefresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,19 +158,31 @@ class MessageActivity : AppCompatActivity() {
|
|||||||
loadMessageList()
|
loadMessageList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showLoading() {
|
||||||
|
binding.swipeRefreshLayout.visibility = View.GONE
|
||||||
|
binding.emptyLayout.visibility = View.GONE
|
||||||
|
binding.layoutNoNetwork.visibility = View.GONE
|
||||||
|
loadingDialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideLoading() {
|
||||||
|
binding.swipeRefreshLayout.visibility = View.VISIBLE
|
||||||
|
loadingDialog.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
private fun showEmptyState() {
|
private fun showEmptyState() {
|
||||||
binding.rvMessageList.visibility = View.GONE
|
binding.swipeRefreshLayout.visibility = View.GONE
|
||||||
binding.emptyLayout.visibility = View.VISIBLE
|
binding.emptyLayout.visibility = View.VISIBLE
|
||||||
binding.tvNoMoreData.visibility = View.GONE
|
binding.tvNoMoreData.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hideEmptyState() {
|
private fun hideEmptyState() {
|
||||||
binding.rvMessageList.visibility = View.VISIBLE
|
binding.swipeRefreshLayout.visibility = View.VISIBLE
|
||||||
binding.emptyLayout.visibility = View.GONE
|
binding.emptyLayout.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun noNetwork() {
|
private fun noNetwork() {
|
||||||
binding.rvMessageList.visibility = View.GONE
|
binding.swipeRefreshLayout.visibility = View.GONE
|
||||||
binding.layoutNoNetwork.visibility = View.VISIBLE
|
binding.layoutNoNetwork.visibility = View.VISIBLE
|
||||||
binding.tvNoMoreData.visibility = View.GONE
|
binding.tvNoMoreData.visibility = View.GONE
|
||||||
}
|
}
|
||||||
@@ -171,9 +200,6 @@ class MessageActivity : AppCompatActivity() {
|
|||||||
val groupedMessages = messages.groupBy { extractDate(it.createTime ?: "") }
|
val groupedMessages = messages.groupBy { extractDate(it.createTime ?: "") }
|
||||||
groupedMessages.forEach { (date, messageList) ->
|
groupedMessages.forEach { (date, messageList) ->
|
||||||
val headerTitle = formatDate(date)
|
val headerTitle = formatDate(date)
|
||||||
messageList.forEachIndexed { index, message ->
|
|
||||||
println(" 消息${index + 1}: id=${message.id}, categoryName=${message.categoryName}, createTime=${message.createTime}")
|
|
||||||
}
|
|
||||||
items.add(MessageListItem.Header(headerTitle))
|
items.add(MessageListItem.Header(headerTitle))
|
||||||
messageList.forEach { message ->
|
messageList.forEach { message ->
|
||||||
items.add(
|
items.add(
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import com.ckkj.defense_device.utils.ActivityManager
|
|||||||
import com.ckkj.defense_device.view.CustomDialogFragmentText
|
import com.ckkj.defense_device.view.CustomDialogFragmentText
|
||||||
import com.ckkj.defense_device.view.FamilySwitchDialog
|
import com.ckkj.defense_device.view.FamilySwitchDialog
|
||||||
import com.ckkj.defense_device.view.LanguageDialog
|
import com.ckkj.defense_device.view.LanguageDialog
|
||||||
|
import com.ckkj.defense_device.view.LoadingDialog
|
||||||
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
|
||||||
@@ -35,6 +36,7 @@ class PersonalFragment : Fragment() {
|
|||||||
// ViewBinding
|
// ViewBinding
|
||||||
private var _binding: PersonalFragmentBinding? = null
|
private var _binding: PersonalFragmentBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
private var loadingDialog: LoadingDialog? = null
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
fetchUserInfo()
|
fetchUserInfo()
|
||||||
@@ -234,6 +236,7 @@ class PersonalFragment : Fragment() {
|
|||||||
|
|
||||||
|
|
||||||
private fun fetchUserInfo() {
|
private fun fetchUserInfo() {
|
||||||
|
loadingDialog = LoadingDialog(requireContext()).setMessage(getString(R.string.loading)).apply { show() }
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
val response = RetrofitNetwork.getNetworkService().getUserInfo()
|
val response = RetrofitNetwork.getNetworkService().getUserInfo()
|
||||||
@@ -244,6 +247,11 @@ class PersonalFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
} finally {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
loadingDialog?.dismiss()
|
||||||
|
loadingDialog = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ class DeviceInfoActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateDeviceInfo(device: com.ckkj.defense_device.bean.DeviceBean) {
|
private fun updateDeviceInfo(device: com.ckkj.defense_device.bean.DeviceBean) {
|
||||||
binding.tvDeviceModelValue.text = device.deviceNo.toString()
|
binding.tvDeviceModelValue.text = device.deviceNo?.toString()?: ""
|
||||||
binding.tvDeviceNoValue.text = device.deviceSn.toString()
|
binding.tvDeviceNoValue.text = device.deviceSn?.toString() ?: ""
|
||||||
binding.tvFwVersionValue.text = device.fwVer.toString()
|
binding.tvFwVersionValue.text = device.fwVer?.toString()?: ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,6 +24,7 @@ import com.ckkj.defense_device.utils.DeviceIdEvent
|
|||||||
import com.ckkj.defense_device.utils.DeviceUserUpdateEvent
|
import com.ckkj.defense_device.utils.DeviceUserUpdateEvent
|
||||||
import com.ckkj.defense_device.utils.RetrofitNetwork
|
import com.ckkj.defense_device.utils.RetrofitNetwork
|
||||||
import com.ckkj.defense_device.view.CustomDialogFragmentText
|
import com.ckkj.defense_device.view.CustomDialogFragmentText
|
||||||
|
import com.ckkj.defense_device.view.PullRefreshLayout
|
||||||
import com.gyf.immersionbar.ImmersionBar
|
import com.gyf.immersionbar.ImmersionBar
|
||||||
import com.yanzhenjie.recyclerview.SwipeMenuItem
|
import com.yanzhenjie.recyclerview.SwipeMenuItem
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -95,9 +96,11 @@ class LinkedUserActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
(binding.rvLinkedUser as RecyclerView).layoutManager = LinearLayoutManager(this)
|
(binding.rvLinkedUser as RecyclerView).layoutManager = LinearLayoutManager(this)
|
||||||
|
|
||||||
binding.swipeRefresh.setOnRefreshListener {
|
binding.swipeRefresh.setOnRefreshListener(object : PullRefreshLayout.OnRefreshListener {
|
||||||
|
override fun onRefresh() {
|
||||||
fetchUserList()
|
fetchUserList()
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
binding.rvLinkedUser.setSwipeMenuCreator { leftMenu, rightMenu, position ->
|
binding.rvLinkedUser.setSwipeMenuCreator { leftMenu, rightMenu, position ->
|
||||||
if (mode == MODE_SETTINGS) {
|
if (mode == MODE_SETTINGS) {
|
||||||
@@ -229,7 +232,6 @@ class LinkedUserActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchUserList() {
|
private fun fetchUserList() {
|
||||||
binding.swipeRefresh.isRefreshing = true
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
var deviceId: String? = null
|
var deviceId: String? = null
|
||||||
@@ -238,7 +240,7 @@ class LinkedUserActivity : AppCompatActivity() {
|
|||||||
if (deviceId.isNullOrEmpty()) {
|
if (deviceId.isNullOrEmpty()) {
|
||||||
Toast.makeText(this@LinkedUserActivity, "设备 ID 为空", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this@LinkedUserActivity, "设备 ID 为空", Toast.LENGTH_SHORT).show()
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
binding.swipeRefresh.isRefreshing = false
|
binding.swipeRefresh.finishRefresh()
|
||||||
}
|
}
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
@@ -249,7 +251,7 @@ class LinkedUserActivity : AppCompatActivity() {
|
|||||||
RetrofitNetwork.getNetworkService().getFamilyMembers()
|
RetrofitNetwork.getNetworkService().getFamilyMembers()
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
binding.swipeRefresh.isRefreshing = false
|
binding.swipeRefresh.finishRefresh()
|
||||||
if (response.code == 200 && response.data != null) {
|
if (response.code == 200 && response.data != null) {
|
||||||
if (mode == MODE_SETTINGS) {
|
if (mode == MODE_SETTINGS) {
|
||||||
updateDeviceUserList(response.data as List<DeviceUserBean>)
|
updateDeviceUserList(response.data as List<DeviceUserBean>)
|
||||||
@@ -264,7 +266,7 @@ class LinkedUserActivity : AppCompatActivity() {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
binding.swipeRefresh.isRefreshing = false
|
binding.swipeRefresh.finishRefresh()
|
||||||
noNetwork()
|
noNetwork()
|
||||||
Toast.makeText(this@LinkedUserActivity, R.string.network_request_failed, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this@LinkedUserActivity, R.string.network_request_failed, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class UserDetailsActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateUI(data: com.ckkj.defense_device.bean.ObtainInviterInformationBean) {
|
private fun updateUI(data: com.ckkj.defense_device.bean.ObtainInviterInformationBean) {
|
||||||
binding.tvContactName.text = data.nickName ?: "未知用户"
|
binding.tvContactName.text = data.nickName ?: ""
|
||||||
binding.tvPhone.text = data.phonenumber ?: ""
|
binding.tvPhone.text = data.phonenumber ?: ""
|
||||||
binding.tvIdCard.text = data.idCard ?: ""
|
binding.tvIdCard.text = data.idCard ?: ""
|
||||||
binding.tvMacAddress.text = data.mac ?: ""
|
binding.tvMacAddress.text = data.mac ?: ""
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import androidx.activity.enableEdgeToEdge
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.ckkj.defense_device.databinding.SettingsMenusActivityBinding
|
import com.ckkj.defense_device.databinding.SettingsMenusActivityBinding
|
||||||
import com.gyf.immersionbar.ImmersionBar
|
import com.gyf.immersionbar.ImmersionBar
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class SettingsMenusActivity : AppCompatActivity() {
|
class SettingsMenusActivity : AppCompatActivity() {
|
||||||
private lateinit var binding: SettingsMenusActivityBinding
|
private lateinit var binding: SettingsMenusActivityBinding
|
||||||
@@ -25,6 +26,8 @@ class SettingsMenusActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
val versionName = packageManager.getPackageInfo(packageName, 0).versionName
|
val versionName = packageManager.getPackageInfo(packageName, 0).versionName
|
||||||
binding.tvVersion.text = versionName
|
binding.tvVersion.text = versionName
|
||||||
|
// 显示缓存大小
|
||||||
|
displayCacheSize()
|
||||||
|
|
||||||
setupClickListeners()
|
setupClickListeners()
|
||||||
}
|
}
|
||||||
@@ -42,8 +45,9 @@ class SettingsMenusActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
// 清除缓存
|
// 清除缓存
|
||||||
binding.itemClearCache.setOnClickListener {
|
binding.itemClearCache.setOnClickListener {
|
||||||
|
clearCache()
|
||||||
|
displayCacheSize() // 更新缓存大小显示
|
||||||
Toast.makeText(this, "缓存已清除", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, "缓存已清除", Toast.LENGTH_SHORT).show()
|
||||||
binding.tvCacheSize.text = "0 KB"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户协议
|
// 用户协议
|
||||||
@@ -62,4 +66,46 @@ class SettingsMenusActivity : AppCompatActivity() {
|
|||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//缓存方法
|
||||||
|
private fun displayCacheSize() {
|
||||||
|
val cacheSize = getCacheSize()
|
||||||
|
val sizeString = when {
|
||||||
|
cacheSize >= 1024 * 1024 -> String.format("%.2f MB", cacheSize / (1024.0 * 1024.0))
|
||||||
|
else -> String.format("%d KB", cacheSize / 1024)
|
||||||
|
}
|
||||||
|
binding.tvCacheSize.text = sizeString
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCacheSize(): Long {
|
||||||
|
val cacheDir = cacheDir
|
||||||
|
return getFolderSize(cacheDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFolderSize(dir: File?): Long {
|
||||||
|
var size = 0L
|
||||||
|
if (dir != null && dir.isDirectory) {
|
||||||
|
dir.listFiles()?.forEach { file ->
|
||||||
|
size += if (file.isFile) {
|
||||||
|
file.length()
|
||||||
|
} else {
|
||||||
|
getFolderSize(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearCache() {
|
||||||
|
val cacheDir = cacheDir
|
||||||
|
deleteDir(cacheDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteDir(dir: File?): Boolean {
|
||||||
|
if (dir != null && dir.isDirectory) {
|
||||||
|
dir.listFiles()?.forEach { file ->
|
||||||
|
deleteDir(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dir?.delete() ?: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -39,6 +39,25 @@ import retrofit2.http.POST
|
|||||||
import retrofit2.http.PUT
|
import retrofit2.http.PUT
|
||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
|
|
||||||
|
// ┏┓ ┏┓
|
||||||
|
//┏┛┻━━━┛┻┓
|
||||||
|
//┃ ┃
|
||||||
|
//┃ ━ ┃
|
||||||
|
//┃ ┳┛ ┗┳ ┃
|
||||||
|
//┃ ┃
|
||||||
|
//┃ ┻ ┃
|
||||||
|
//┃ ┃
|
||||||
|
//┗━┓ ┏━┛
|
||||||
|
// ┃ ┃
|
||||||
|
// ┃ ┃
|
||||||
|
// ┃ ┗━━━┓
|
||||||
|
// ┃ ┣┓
|
||||||
|
// ┃ ┏┛
|
||||||
|
// ┗┓┓┏━┳┓┏┛
|
||||||
|
// ┃┫┫ ┃┫┫
|
||||||
|
// ┗┻┛ ┗┻┛
|
||||||
|
//神兽护体,代码零Bug,需求不改,一次编译通过
|
||||||
|
|
||||||
interface ApiService {
|
interface ApiService {
|
||||||
/**
|
/**
|
||||||
* 获取登录验证码
|
* 获取登录验证码
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.ckkj.defense_device.utils
|
package com.ckkj.defense_device.utils
|
||||||
|
|
||||||
|
import com.ckkj.defense_device.utils.crypto.CryptoInterceptor
|
||||||
|
import com.ckkj.defense_device.utils.crypto.CryptoKeyManager
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
@@ -65,12 +67,39 @@ object RetrofitNetwork {
|
|||||||
.readTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
|
.readTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
|
||||||
.writeTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
|
.writeTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
|
||||||
.addInterceptor(ApiConfig.createHeadersInterceptor())
|
.addInterceptor(ApiConfig.createHeadersInterceptor())
|
||||||
.addInterceptor(ApiConfig.getUnifiedInterceptor()) // 使用 ApiConfig 中的统一拦截器
|
.addInterceptor(ApiConfig.getUnifiedInterceptor())
|
||||||
|
|
||||||
|
// 加密拦截器(密钥就绪即启用,按开关自动判断)
|
||||||
|
if (CryptoKeyManager.isCryptoEnabled && CryptoKeyManager.isKeyConfigured()) {
|
||||||
|
builder.addInterceptor(getCryptoInterceptor())
|
||||||
|
}
|
||||||
|
|
||||||
if (enableLogging) {
|
if (enableLogging) {
|
||||||
builder.addInterceptor(ApiConfig.getLoggingInterceptor()) // 使用 ApiConfig 中的日志拦截器
|
builder.addInterceptor(ApiConfig.getLoggingInterceptor())
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取加密拦截器实例
|
||||||
|
* 每次都重新创建,确保使用最新密钥
|
||||||
|
*/
|
||||||
|
private fun getCryptoInterceptor(): CryptoInterceptor {
|
||||||
|
// 清除旧实例,确保使用最新密钥
|
||||||
|
CryptoInterceptor.clearInstance()
|
||||||
|
return CryptoInterceptor.getInstance(
|
||||||
|
publicKey = CryptoKeyManager.requestPublicKey,
|
||||||
|
privateKey = CryptoKeyManager.responsePrivateKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新初始化网络客户端(密钥更新后调用)
|
||||||
|
*/
|
||||||
|
fun resetClient() {
|
||||||
|
retrofit = null
|
||||||
|
// 强制重新创建 Retrofit
|
||||||
|
getNetworkService()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package com.ckkj.defense_device.utils.crypto
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AES 加密工具类
|
||||||
|
*
|
||||||
|
* 算法:AES/ECB/PKCS5Padding (Android) 与 AES/ECB/PKCS7Padding (iOS) 兼容
|
||||||
|
*/
|
||||||
|
object AESUtil {
|
||||||
|
|
||||||
|
private const val ALGORITHM = "AES"
|
||||||
|
private const val TRANSFORMATION = "AES/ECB/PKCS5Padding"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AES ECB 模式加密
|
||||||
|
*
|
||||||
|
* @param key 32字节 ASCII 字符串密钥
|
||||||
|
* @param data UTF-8 编码的明文字节数组
|
||||||
|
* @return Base64 编码的密文字节数组
|
||||||
|
*/
|
||||||
|
fun encrypt(key: String, data: ByteArray): ByteArray {
|
||||||
|
val keySpec = SecretKeySpec(key.toByteArray(Charsets.UTF_8), ALGORITHM)
|
||||||
|
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, keySpec)
|
||||||
|
return cipher.doFinal(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AES ECB 模式加密,返回 Base64 字符串
|
||||||
|
*
|
||||||
|
* @param key 32字节 ASCII 字符串密钥
|
||||||
|
* @param plainText UTF-8 明文字符串
|
||||||
|
* @return Base64 编码的密文字符串
|
||||||
|
*/
|
||||||
|
fun encrypt(key: String, plainText: String): String {
|
||||||
|
val encrypted = encrypt(key, plainText.toByteArray(Charsets.UTF_8))
|
||||||
|
return Base64.encodeToString(encrypted, Base64.NO_WRAP)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AES ECB 模式解密
|
||||||
|
*
|
||||||
|
* @param key 32字节 ASCII 字符串密钥
|
||||||
|
* @param encryptedData Base64 编码的密文字节数组
|
||||||
|
* @return UTF-8 解密后的明文字节数组
|
||||||
|
*/
|
||||||
|
fun decrypt(key: String, encryptedData: ByteArray): ByteArray {
|
||||||
|
val keySpec = SecretKeySpec(key.toByteArray(Charsets.UTF_8), ALGORITHM)
|
||||||
|
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, keySpec)
|
||||||
|
return cipher.doFinal(encryptedData)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AES ECB 模式解密,返回 UTF-8 字符串
|
||||||
|
*
|
||||||
|
* @param key 32字节 ASCII 字符串密钥
|
||||||
|
* @param encryptedText Base64 编码的密文字符串
|
||||||
|
* @return UTF-8 解密后的明文字符串
|
||||||
|
*/
|
||||||
|
fun decrypt(key: String, encryptedText: String): String {
|
||||||
|
val encryptedData = Base64.decode(encryptedText, Base64.NO_WRAP)
|
||||||
|
val decrypted = decrypt(key, encryptedData)
|
||||||
|
return String(decrypted, Charsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机 AES 密钥
|
||||||
|
*
|
||||||
|
* @param length 密钥长度,默认 32 字节
|
||||||
|
* @return 随机 ASCII 字符串密钥
|
||||||
|
*/
|
||||||
|
fun generateKey(length: Int = 32): String {
|
||||||
|
val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
return (1..length)
|
||||||
|
.map { chars.random() }
|
||||||
|
.joinToString("")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base64 编码
|
||||||
|
*/
|
||||||
|
fun base64Encode(data: ByteArray): String {
|
||||||
|
return Base64.encodeToString(data, Base64.NO_WRAP)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base64 解码
|
||||||
|
*/
|
||||||
|
fun base64Decode(encoded: String): ByteArray {
|
||||||
|
return Base64.decode(encoded, Base64.NO_WRAP)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
package com.ckkj.defense_device.utils.crypto
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
import android.util.Log
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加密拦截器
|
||||||
|
*
|
||||||
|
* 实现 App 请求加密和服务端响应解密
|
||||||
|
*
|
||||||
|
* 加密流程:
|
||||||
|
* 1. 生成随机 AES 密钥 (32字节 ASCII)
|
||||||
|
* 2. 用 RSA 公钥加密 AES 密钥
|
||||||
|
* 3. 用 AES 加密请求体
|
||||||
|
* 4. 设置请求头 encrypt-key 为 RSA 加密后的 AES 密钥
|
||||||
|
* 5. 请求体为 AES 加密后的 Base64 字符串
|
||||||
|
*
|
||||||
|
* 解密流程(仅需要对响应解密的接口):
|
||||||
|
* 1. 从响应头获取 encrypt-key
|
||||||
|
* 2. 用 RSA 私钥解密得到 AES 密钥
|
||||||
|
* 3. 用 AES 解密响应体
|
||||||
|
*/
|
||||||
|
class CryptoInterceptor private constructor(
|
||||||
|
private val publicKey: String,
|
||||||
|
private val privateKey: String
|
||||||
|
) : Interceptor {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "CryptoInterceptor"
|
||||||
|
private const val HEADER_ENCRYPT_KEY = "encrypt-key"
|
||||||
|
private const val HEADER_IS_ENCRYPT = "isEncrypt"
|
||||||
|
private const val CONTENT_TYPE_JSON = "application/json; charset=utf-8"
|
||||||
|
|
||||||
|
// 需要加密的接口列表(POST/PUT 请求)
|
||||||
|
private val ENCRYPT_ENDPOINTS = setOf(
|
||||||
|
"app/login",
|
||||||
|
"app/register",
|
||||||
|
"app/forgot",
|
||||||
|
"app/switchFamily",
|
||||||
|
"app/addDevice",
|
||||||
|
"app/updateDeviceInfo",
|
||||||
|
"app/deleteDevice",
|
||||||
|
"app/addDeviceUser",
|
||||||
|
"app/updateUserInfo",
|
||||||
|
"app/forgotPassword",
|
||||||
|
"app/retrievePassword",
|
||||||
|
"app/verifyOptionPassword",
|
||||||
|
"app/obtainInviterInformation",
|
||||||
|
"app/selectRecommendationCode",
|
||||||
|
"app/generateRecommendationCode",
|
||||||
|
"app/revokeInvitation",
|
||||||
|
"app/updateFamilyUser",
|
||||||
|
"app/selectInviterInfo",
|
||||||
|
"app/joinTheFamily",
|
||||||
|
"app/stream/start"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 需要解密响应的接口列表(默认与加密列表对称 —— 加密请求的接口响应也会被加密)
|
||||||
|
// 额外加上 GET 接口(GET 无 body 不加密请求,但后端仍可能加密响应)
|
||||||
|
private val DECRYPT_RESPONSE_ENDPOINTS: Set<String> = ENCRYPT_ENDPOINTS + setOf(
|
||||||
|
"app/selectFamily", // 获取家庭列表(GET)
|
||||||
|
"app/getdeviceList", // 获取设备列表(GET)
|
||||||
|
"app/deviceInfo", // 获取设备信息(GET)
|
||||||
|
"app/appMessage/list", // 消息列表(GET)
|
||||||
|
"app/appMessageInfo", // 告警消息详情(GET)
|
||||||
|
"app/appMessage/deviceMeslist", // 设备告警日志(GET)
|
||||||
|
"app/familyMembers/list", // 家庭成员列表(GET)
|
||||||
|
"app/deviceUser/list", // 设备关联用户列表(GET)
|
||||||
|
"app/userInfo", // 当前用户信息(GET)
|
||||||
|
"app/deleteMessage" // 删除消息(DELETE)
|
||||||
|
)
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var instance: CryptoInterceptor? = null
|
||||||
|
|
||||||
|
fun getInstance(publicKey: String, privateKey: String): CryptoInterceptor {
|
||||||
|
return instance ?: synchronized(this) {
|
||||||
|
instance ?: CryptoInterceptor(publicKey, privateKey).also {
|
||||||
|
instance = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearInstance() {
|
||||||
|
instance = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isEncryptEndpoint(url: String): Boolean {
|
||||||
|
return ENCRYPT_ENDPOINTS.any { url.contains(it, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isDecryptEndpoint(url: String): Boolean {
|
||||||
|
return DECRYPT_RESPONSE_ENDPOINTS.any { url.contains(it, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val request = chain.request()
|
||||||
|
val url = request.url.toString()
|
||||||
|
|
||||||
|
// 检查是否需要加密
|
||||||
|
val needsEncrypt = isEncryptEndpoint(url)
|
||||||
|
val needsDecrypt = isDecryptEndpoint(url)
|
||||||
|
|
||||||
|
if (!needsEncrypt && !needsDecrypt) {
|
||||||
|
return chain.proceed(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调试日志
|
||||||
|
Log.d(TAG, "=== CryptoInterceptor Debug ===")
|
||||||
|
Log.d(TAG, "URL: $url")
|
||||||
|
Log.d(TAG, "needsEncrypt: $needsEncrypt, needsDecrypt: $needsDecrypt")
|
||||||
|
Log.d(TAG, "PublicKey available: ${publicKey.isNotBlank()}, length: ${publicKey.length}")
|
||||||
|
Log.d(TAG, "PrivateKey available: ${privateKey.isNotBlank()}, length: ${privateKey.length}")
|
||||||
|
|
||||||
|
// 加密请求
|
||||||
|
val encryptedRequest = if (needsEncrypt) {
|
||||||
|
encryptRequest(request)
|
||||||
|
} else {
|
||||||
|
request
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发起请求
|
||||||
|
val response = try {
|
||||||
|
chain.proceed(encryptedRequest)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Request failed", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解密响应
|
||||||
|
return if (needsDecrypt) {
|
||||||
|
decryptResponse(response, request)
|
||||||
|
} else {
|
||||||
|
response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加密请求
|
||||||
|
*/
|
||||||
|
private fun encryptRequest(request: Request): Request {
|
||||||
|
val requestBody = request.body ?: return request
|
||||||
|
|
||||||
|
// 只处理 JSON 请求
|
||||||
|
val contentType = requestBody.contentType()?.toString() ?: ""
|
||||||
|
if (!contentType.contains("application/json")) {
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只处理 POST 和 PUT
|
||||||
|
val method = request.method
|
||||||
|
if (method != "POST" && method != "PUT") {
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取原始请求体
|
||||||
|
val buffer = okio.Buffer()
|
||||||
|
requestBody.writeTo(buffer)
|
||||||
|
val originalBody = buffer.readString(Charset.defaultCharset())
|
||||||
|
Log.d(TAG, "Original request body: $originalBody")
|
||||||
|
|
||||||
|
// 生成随机 AES 密钥
|
||||||
|
val aesKey = AESUtil.generateKey(32)
|
||||||
|
Log.d(TAG, "Generated AES key: $aesKey")
|
||||||
|
|
||||||
|
// 1. keyBase64 = Base64(UTF8(aesKey))
|
||||||
|
val keyBase64 = AESUtil.base64Encode(aesKey.toByteArray(Charsets.UTF_8))
|
||||||
|
Log.d(TAG, "keyBase64: $keyBase64")
|
||||||
|
|
||||||
|
// 2. encrypt-key = RSA-PKCS1-Encrypt(请求公钥, UTF8(keyBase64))
|
||||||
|
// encryptByPublicKey(String, String) 已返回 Base64 编码结果
|
||||||
|
val encryptedKeyBase64 = RSAUtil.encryptByPublicKey(publicKey, keyBase64)
|
||||||
|
Log.d(TAG, "Encrypted key (Base64): $encryptedKeyBase64")
|
||||||
|
|
||||||
|
// 3. body = AES-ECB-PKCS5-Encrypt(aesKey, UTF8(JSON))
|
||||||
|
// encrypt(String, String) 已返回 Base64 编码结果
|
||||||
|
val encryptedBodyBase64 = AESUtil.encrypt(aesKey, originalBody)
|
||||||
|
Log.d(TAG, "Encrypted body (Base64): $encryptedBodyBase64")
|
||||||
|
|
||||||
|
// 构建新请求(根据原方法选择 POST 或 PUT)
|
||||||
|
val newRequestBuilder = request.newBuilder()
|
||||||
|
.header(HEADER_ENCRYPT_KEY, encryptedKeyBase64)
|
||||||
|
.header(HEADER_IS_ENCRYPT, "true")
|
||||||
|
|
||||||
|
return if (method == "PUT") {
|
||||||
|
newRequestBuilder.put(encryptedBodyBase64.toRequestBody(CONTENT_TYPE_JSON.toMediaTypeOrNull()))
|
||||||
|
} else {
|
||||||
|
newRequestBuilder.post(encryptedBodyBase64.toRequestBody(CONTENT_TYPE_JSON.toMediaTypeOrNull()))
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解密响应
|
||||||
|
*
|
||||||
|
* 兼容三种后端响应形态:
|
||||||
|
* 1. 普通 JSON(无 encrypt-key 头)→ 原样放行
|
||||||
|
* 2. 整 body 加密(响应头有 encrypt-key 且 body 不是合法 JSON)→ 整体 AES 解密
|
||||||
|
* 3. 嵌套加密(响应头有 encrypt-key,body 是 {code,msg,data:"加密串"})
|
||||||
|
* → 只解 data 字段,保持外层 JSON 结构(与 iOS 端一致)
|
||||||
|
*/
|
||||||
|
private fun decryptResponse(response: Response, originalRequest: Request): Response {
|
||||||
|
val responseBody = response.body ?: return response
|
||||||
|
|
||||||
|
val encryptedKeyBase64 = response.header(HEADER_ENCRYPT_KEY)
|
||||||
|
if (encryptedKeyBase64.isNullOrBlank()) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
val rawBody: String = try {
|
||||||
|
responseBody.source().apply { request(Long.MAX_VALUE) }
|
||||||
|
.buffer.clone().readString(Charset.defaultCharset())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Read response body failed", e)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先拿到 AES 密钥(RSA 私钥解密 encrypt-key 头)
|
||||||
|
val aesKey = try {
|
||||||
|
val aesKeyBase64 = RSAUtil.decryptByPrivateKeyBase64(privateKey, encryptedKeyBase64)
|
||||||
|
String(Base64.decode(aesKeyBase64, Base64.NO_WRAP), Charsets.UTF_8)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "RSA decrypt encrypt-key failed", e)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
// 形态 3:嵌套加密 —— 响应是 JSON,data 字段是加密串
|
||||||
|
val trimmed = rawBody.trim()
|
||||||
|
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
||||||
|
try {
|
||||||
|
val jsonElement = JsonParser().parse(trimmed)
|
||||||
|
if (jsonElement.isJsonObject) {
|
||||||
|
val obj = jsonElement.asJsonObject
|
||||||
|
if (obj.has("data") && obj.get("data").isJsonPrimitive
|
||||||
|
&& obj.get("data").asJsonPrimitive.isString
|
||||||
|
) {
|
||||||
|
val encryptedData = obj.get("data").asString
|
||||||
|
val decryptedData = AESUtil.decrypt(aesKey, encryptedData)
|
||||||
|
// 把 data 字段替换为解密后的 JSON 字符串(让 Gson 当成对象继续解析)
|
||||||
|
obj.add("data", parseDataAsJsonElement(decryptedData))
|
||||||
|
val newBody = obj.toString().toResponseBody(CONTENT_TYPE_JSON.toMediaTypeOrNull())
|
||||||
|
return response.newBuilder()
|
||||||
|
.body(newBody)
|
||||||
|
.removeHeader(HEADER_ENCRYPT_KEY)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Nested decrypt failed, fallback to whole-body decrypt", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 形态 2:整 body 加密
|
||||||
|
return try {
|
||||||
|
val decryptedBody = AESUtil.decrypt(aesKey, rawBody)
|
||||||
|
response.newBuilder()
|
||||||
|
.body(decryptedBody.toResponseBody(CONTENT_TYPE_JSON.toMediaTypeOrNull()))
|
||||||
|
.removeHeader(HEADER_ENCRYPT_KEY)
|
||||||
|
.build()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Whole-body decrypt failed, return raw response", e)
|
||||||
|
response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把解密后的字符串解析成 JsonElement(如果是合法 JSON);否则当成普通字符串返回
|
||||||
|
*/
|
||||||
|
private fun parseDataAsJsonElement(text: String): com.google.gson.JsonElement {
|
||||||
|
val s = text.trim()
|
||||||
|
return try {
|
||||||
|
if ((s.startsWith("{") && s.endsWith("}")) ||
|
||||||
|
(s.startsWith("[") && s.endsWith("]"))
|
||||||
|
) {
|
||||||
|
JsonParser().parse(s)
|
||||||
|
} else {
|
||||||
|
com.google.gson.JsonPrimitive(s)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
com.google.gson.JsonPrimitive(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扩展函数:String 转 RequestBody
|
||||||
|
private fun String.toRequestBody(mediaType: okhttp3.MediaType?): okhttp3.RequestBody {
|
||||||
|
return okhttp3.RequestBody.create(mediaType, this)
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
package com.ckkj.defense_device.utils.crypto
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import com.blankj.utilcode.util.SPUtils
|
||||||
|
import com.ckkj.defense_device.utils.UserinfoUtil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密钥管理类
|
||||||
|
*
|
||||||
|
* 管理 RSA 加密所需的公钥和私钥
|
||||||
|
*
|
||||||
|
* 密钥关系:
|
||||||
|
* ┌─────────────┬──────────────────┬──────────────────┐
|
||||||
|
* │ 方向 │ App 持有 │ 服务端持有 │
|
||||||
|
* ├─────────────┼──────────────────┼──────────────────┤
|
||||||
|
* │ App请求服务端 │ 请求公钥 (RSA-P) │ 请求私钥 (RSA-S) │
|
||||||
|
* ├─────────────┼──────────────────┼──────────────────┤
|
||||||
|
* │ 服务端响应App │ 响应私钥 (RSA-S) │ 响应公钥 (RSA-P) │
|
||||||
|
* └─────────────┴──────────────────┴──────────────────┘
|
||||||
|
*
|
||||||
|
* 需要生成两套独立 RSA 2048/3072 密钥对
|
||||||
|
*/
|
||||||
|
object CryptoKeyManager {
|
||||||
|
|
||||||
|
private const val TAG = "CryptoKeyManager"
|
||||||
|
|
||||||
|
// SPUtils 用于持久化密钥配置
|
||||||
|
private val spUtils: SPUtils by lazy {
|
||||||
|
SPUtils.getInstance("crypto_keys")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置项 Keys
|
||||||
|
private const val KEY_REQUEST_PUBLIC_KEY = "request_public_key"
|
||||||
|
private const val KEY_RESPONSE_PRIVATE_KEY = "response_private_key"
|
||||||
|
private const val KEY_CRYPTO_ENABLED = "crypto_enabled"
|
||||||
|
|
||||||
|
// 配置项默认值
|
||||||
|
// 注意:这些密钥必须与后端配置配对使用
|
||||||
|
// App端公钥 = 后端私钥(App加密请求,后端解密)
|
||||||
|
// App端私钥 = 后端公钥(后端加密响应,App解密)
|
||||||
|
private const val DEFAULT_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtVIQm+1AWMfkT1uVOyugKQwBeHtgpulABOrVfaL7Xzc1/rKSdLdjYZLZnnOU+rdE1jqz20MXDBozK22spH/j53d9nFLftHkMCRUlfc0MWEqtSla+2MdzElIHEbq4CSizZOcT8QXCRw7V2OuDqnC7XRQmnpA5Ks1WaKH5V0aoKWztjsqZV6vVWPqyDC94mvu/Q/9e6KVfkqAzR+afuo9OoBFewDGQ6jfqyu1TxoGZrfSr5lXOVeADf0CVfWCDze5y4f7er4JjPHWA2+yv8Scd9bqVJ4Q+/yR2Gd516Mi11OgBKc8GdkuA4/kfmXc7n91SMjgQP6DlTVuPc/6KnB/pyQIDAQAB"
|
||||||
|
private const val DEFAULT_PRIVATE_KEY = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDYg1dC9XDMRtKJ5IFj+Hd3bq7+r2YL1y8FM9HD111PN/YVy4K6O67cRA/TLG7+tl6eYiA73f5lzPgv1riOwBPSCmvVopxq2KiJs30uEnug0qGp6l2cwQOmjitFAQF8dOtAl4m6EM/jdXCZYbQrthJ6n3tR+vGj2IrGSJEjPrj3VRljK9I7Zw0Be0rhJn/vZw2CA4GoVfE/3gqa5bpl+3SFxMP7TwF5uZueCNqLaPnd7jK5jB1SUghI7+pq897Ng4EG6kdOGsiQe5kbHWkSPTmPEObMwgLJ18i/XCVg+LUkTDJOC/MAfB+WGcKryNHrhcbhB6+XstpoGT8JlOPOgh0BAgMBAAECggEBALZG7MH8bDguL7XTYHNPjRRJZJ4aAGlbgcR5edHMkEvPKyfyK16qPG5IBqKcN93Mnx/sMIL2Q+RkiVWNCdd0MbuU9m/m9JSnTkyPhYWyHc6pRV0NaD496Nrhud/gFuY2cI/yhArXeI1gI6mdrddW83u0pFfCUojEFyETmsz3UzU1LMkVbwCXNbZ1b3GfcVnONMn8T/7CMlRPlKyOuE0OPnTut4wddAybe5BKDQYnGzuosUdO+Q+2K3hqaTD6Bb33D4ATHjvR3Vt9itE07BiaBFjZkE2odP4x3fSsnc7Cw3A7zHQivpfgqcoBcGUaMcN1FdgpaF2mbe9/eHn/lYCpevkCgYEA4FoKq3fPOITAtA6bjwCHXvIQUjv4vQ0BZtt7OX7zwpRwqOhJfN0ZRkd7NMUqYnB10N9NgPRz0kKC6qff5duN+zkYIzjHrlm5lzdIEAouovRlrGXST4n8gAYteh5D5Or9wRXj7USQoaNyiWKB9A/07ortJvul09sPjJU7IVfzzXMCgYEA9w45Gh2bAvYvm2bWieKZRhcV6Pwy9R8587qmKLC7V/0gfVojBHx7xTmTEfKCGrt3fK4jB5v/2Dar8gDF+RM7g01u0YuFJt17jeFr/4o5KMimfXhoEulDbgjfvhxQOrtkrF5iHpKxiEXqIPiH+B7155PAnE43011AAJCBSylmTrsCgYEAkxr3PA9HFKwXHvklDtMt8BeQlBs2sd9BOAxZ9A1GECP86wPEi9b9p5NfOe6+J+XNmrOQwimHeCqcZPjGWpVnt35sUUv9wlia1Igu/DVw9vCBalUpXXYA1oE2eIg3xHZBBMYxuXXnz9S4WVT6GOoNlAwMDC+dQBi3TVrcdrSQ6/kCgYBA254ICh+ovmKvJGdMGY3thZ/94z+pdEIthyGZ6xOzvMMrxV8ODXQcycmfW4/mXrK2q6yMkdqvs2KejK9sfS0RgmGGZ19UXa7TB4vnsSziVRLIO7TuyggmufOrIBm74XhDfB+8MPykbt5RO43OiKo72mElZ69mMMPdohIfXkX6gwKBgH4L9qJRhNunWSfHO4/8dST+2meSV+Cpqf0b/9cV6v5FMS22m+szzW9XoBXnI9dbAqiEDlCHCLw34pRlC3OzBNlbT6HzNmYXLko2Ttg9thiDq7WS5tV+EHJRP6kLabYGq0Aghtq+6LIwwogbHzhRi8Q52o3CNmM9lThNWZIkzaBe"
|
||||||
|
/**
|
||||||
|
* 是否启用加密
|
||||||
|
*/
|
||||||
|
var isCryptoEnabled: Boolean
|
||||||
|
get() {
|
||||||
|
val stored = spUtils.getBoolean(KEY_CRYPTO_ENABLED, false)
|
||||||
|
// 如果存储值为 false 但密钥已配置,也返回 true
|
||||||
|
return stored || (DEFAULT_PUBLIC_KEY.isNotBlank() && DEFAULT_PRIVATE_KEY.isNotBlank())
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
spUtils.put(KEY_CRYPTO_ENABLED, value)
|
||||||
|
Log.d(TAG, "Crypto enabled: $value")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求公钥(用于 App 加密请求)
|
||||||
|
* App 使用此公钥加密 AES 密钥,服务端使用对应的私钥解密
|
||||||
|
*/
|
||||||
|
var requestPublicKey: String
|
||||||
|
get() {
|
||||||
|
val stored = spUtils.getString(KEY_REQUEST_PUBLIC_KEY, "")
|
||||||
|
return if (stored.isNotBlank()) stored else DEFAULT_PUBLIC_KEY
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
spUtils.put(KEY_REQUEST_PUBLIC_KEY, value)
|
||||||
|
Log.d(TAG, "Request public key updated: ${value.take(20)}...")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应私钥(用于 App 解密响应)
|
||||||
|
* 服务端使用对应的响应公钥加密响应,App 使用此私钥解密
|
||||||
|
*/
|
||||||
|
var responsePrivateKey: String
|
||||||
|
get() {
|
||||||
|
val stored = spUtils.getString(KEY_RESPONSE_PRIVATE_KEY, "")
|
||||||
|
return if (stored.isNotBlank()) stored else DEFAULT_PRIVATE_KEY
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
spUtils.put(KEY_RESPONSE_PRIVATE_KEY, value)
|
||||||
|
Log.d(TAG, "Response private key updated: ${value.take(20)}...")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否已配置密钥
|
||||||
|
*/
|
||||||
|
fun isKeyConfigured(): Boolean {
|
||||||
|
// 检查密钥是否有内容(默认值或用户配置)
|
||||||
|
val hasPublicKey = requestPublicKey.isNotBlank()
|
||||||
|
val hasPrivateKey = responsePrivateKey.isNotBlank()
|
||||||
|
Log.d(TAG, "isKeyConfigured: publicKey=$hasPublicKey (length=${requestPublicKey.length}), privateKey=$hasPrivateKey (length=${responsePrivateKey.length})")
|
||||||
|
return hasPublicKey && hasPrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从服务端配置初始化密钥
|
||||||
|
*
|
||||||
|
* 配置格式(可从服务端接口或配置中心获取):
|
||||||
|
* api-decrypt:
|
||||||
|
* enabled: true
|
||||||
|
* headerFlag: encrypt-key
|
||||||
|
* publicKey: ${APP_RESPONSE_PUBLIC_KEY}
|
||||||
|
* privateKey: ${APP_REQUEST_PRIVATE_KEY}
|
||||||
|
*
|
||||||
|
* @param enabled 是否启用加密
|
||||||
|
* @param publicKey 服务端响应公钥(用于 App 加密请求)
|
||||||
|
* @param privateKey 服务端请求私钥(用于 App 解密响应)
|
||||||
|
*/
|
||||||
|
fun initialize(enabled: Boolean, publicKey: String, privateKey: String) {
|
||||||
|
isCryptoEnabled = enabled
|
||||||
|
requestPublicKey = publicKey
|
||||||
|
responsePrivateKey = privateKey
|
||||||
|
|
||||||
|
Log.i(TAG, "CryptoKeyManager initialized: enabled=$enabled, keyConfigured=${isKeyConfigured()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从用户登录信息初始化密钥
|
||||||
|
* 适用于密钥存储在用户账户中的场景
|
||||||
|
*
|
||||||
|
* @param publicKey 公钥
|
||||||
|
* @param privateKey 私钥
|
||||||
|
*/
|
||||||
|
fun initializeFromLogin(publicKey: String, privateKey: String) {
|
||||||
|
initialize(
|
||||||
|
enabled = publicKey.isNotBlank() && privateKey.isNotBlank(),
|
||||||
|
publicKey = publicKey,
|
||||||
|
privateKey = privateKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除密钥配置
|
||||||
|
*/
|
||||||
|
fun clear() {
|
||||||
|
spUtils.clear()
|
||||||
|
Log.i(TAG, "CryptoKeyManager cleared")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取密钥信息摘要(用于调试)
|
||||||
|
*/
|
||||||
|
fun getKeySummary(): String {
|
||||||
|
return buildString {
|
||||||
|
appendLine("CryptoKeyManager Summary:")
|
||||||
|
appendLine(" Enabled: $isCryptoEnabled")
|
||||||
|
appendLine(" RequestPublicKey: ${if (requestPublicKey.isNotBlank()) "${requestPublicKey.take(20)}..." else "NOT SET"}")
|
||||||
|
appendLine(" ResponsePrivateKey: ${if (responsePrivateKey.isNotBlank()) "${responsePrivateKey.take(20)}..." else "NOT SET"}")
|
||||||
|
appendLine(" Configured: ${isKeyConfigured()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证密钥格式
|
||||||
|
*
|
||||||
|
* @return true 如果密钥是有效的 Base64 编码
|
||||||
|
*/
|
||||||
|
fun validateKeys(): Pair<Boolean, String> {
|
||||||
|
val publicKeyValid = validateBase64Key(requestPublicKey)
|
||||||
|
val privateKeyValid = validateBase64Key(responsePrivateKey)
|
||||||
|
|
||||||
|
return when {
|
||||||
|
!publicKeyValid && !privateKeyValid -> false to "Both keys are invalid"
|
||||||
|
!publicKeyValid -> false to "Request public key is invalid"
|
||||||
|
!privateKeyValid -> false to "Response private key is invalid"
|
||||||
|
else -> true to "Keys are valid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateBase64Key(key: String): Boolean {
|
||||||
|
if (key.isBlank()) return false
|
||||||
|
return try {
|
||||||
|
android.util.Base64.decode(key, android.util.Base64.NO_WRAP)
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package com.ckkj.defense_device.utils.crypto
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
import java.security.KeyFactory
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec
|
||||||
|
import java.security.spec.RSAPrivateKeySpec
|
||||||
|
import java.security.spec.X509EncodedKeySpec
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RSA 加密工具类
|
||||||
|
*
|
||||||
|
* 算法:RSA/ECB/PKCS1Padding
|
||||||
|
* 支持 RSA 2048/3072 位密钥
|
||||||
|
*/
|
||||||
|
object RSAUtil {
|
||||||
|
|
||||||
|
private const val ALGORITHM = "RSA"
|
||||||
|
private const val TRANSFORMATION = "RSA/ECB/PKCS1Padding"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用公钥加密(用于 App 加密请求)
|
||||||
|
*
|
||||||
|
* @param publicKeyStr Base64 编码的公钥字符串
|
||||||
|
* @param data UTF-8 编码的明文字节数组
|
||||||
|
* @return 密文字节数组
|
||||||
|
*/
|
||||||
|
fun encryptByPublicKey(publicKeyStr: String, data: ByteArray): ByteArray {
|
||||||
|
val publicKey = parsePublicKey(publicKeyStr)
|
||||||
|
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
|
||||||
|
return cipher.doFinal(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用公钥加密,返回 Base64 字符串
|
||||||
|
*
|
||||||
|
* @param publicKeyStr Base64 编码的公钥字符串
|
||||||
|
* @param plainText UTF-8 明文字符串
|
||||||
|
* @return Base64 编码的密文字符串
|
||||||
|
*/
|
||||||
|
fun encryptByPublicKey(publicKeyStr: String, plainText: String): String {
|
||||||
|
val encrypted = encryptByPublicKey(publicKeyStr, plainText.toByteArray(Charsets.UTF_8))
|
||||||
|
return Base64.encodeToString(encrypted, Base64.NO_WRAP)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用私钥解密(用于 App 解密响应)
|
||||||
|
*
|
||||||
|
* @param privateKeySpec RSA 私钥规格
|
||||||
|
* @param encryptedData 密文字节数组
|
||||||
|
* @return UTF-8 解密后的明文字节数组
|
||||||
|
*/
|
||||||
|
fun decryptByPrivateKey(privateKeySpec: RSAPrivateKeySpec, encryptedData: ByteArray): ByteArray {
|
||||||
|
val keyFactory = KeyFactory.getInstance(ALGORITHM)
|
||||||
|
val privateKey = keyFactory.generatePrivate(privateKeySpec)
|
||||||
|
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, privateKey)
|
||||||
|
return cipher.doFinal(encryptedData)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用私钥解密,返回 UTF-8 字符串
|
||||||
|
*
|
||||||
|
* @param privateKeySpec RSA 私钥规格
|
||||||
|
* @param encryptedText Base64 编码的密文字符串
|
||||||
|
* @return UTF-8 解密后的明文字符串
|
||||||
|
*/
|
||||||
|
fun decryptByPrivateKey(privateKeySpec: RSAPrivateKeySpec, encryptedText: String): String {
|
||||||
|
val encryptedData = Base64.decode(encryptedText, Base64.NO_WRAP)
|
||||||
|
val decrypted = decryptByPrivateKey(privateKeySpec, encryptedData)
|
||||||
|
return String(decrypted, Charsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 Base64 编码的 PKCS#8 私钥字符串解密
|
||||||
|
*
|
||||||
|
* @param privateKeyBase64 Base64 编码的 PKCS#8 私钥
|
||||||
|
* @param encryptedText Base64 编码的密文字符串
|
||||||
|
* @return UTF-8 解密后的明文字符串
|
||||||
|
*/
|
||||||
|
fun decryptByPrivateKeyBase64(privateKeyBase64: String, encryptedText: String): String {
|
||||||
|
val encryptedData = Base64.decode(encryptedText, Base64.NO_WRAP)
|
||||||
|
val keyFactory = KeyFactory.getInstance(ALGORITHM)
|
||||||
|
val privateKeySpec = PKCS8EncodedKeySpec(Base64.decode(privateKeyBase64, Base64.NO_WRAP))
|
||||||
|
val privateKey = keyFactory.generatePrivate(privateKeySpec)
|
||||||
|
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, privateKey)
|
||||||
|
val decrypted = cipher.doFinal(encryptedData)
|
||||||
|
return String(decrypted, Charsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析公钥
|
||||||
|
*
|
||||||
|
* @param publicKeyStr Base64 编码的 X.509 公钥
|
||||||
|
* @return PublicKey 对象
|
||||||
|
*/
|
||||||
|
fun parsePublicKey(publicKeyStr: String): PublicKey {
|
||||||
|
val keyBytes = Base64.decode(publicKeyStr, Base64.NO_WRAP)
|
||||||
|
val keySpec = X509EncodedKeySpec(keyBytes)
|
||||||
|
val keyFactory = KeyFactory.getInstance(ALGORITHM)
|
||||||
|
return keyFactory.generatePublic(keySpec)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Base64 编码的 PKCS#8 私钥创建私钥规格
|
||||||
|
*
|
||||||
|
* @param privateKeyBase64 Base64 编码的 PKCS#8 私钥
|
||||||
|
* @return RSAPrivateKeySpec
|
||||||
|
*/
|
||||||
|
fun createPrivateKeySpecFromBase64(privateKeyBase64: String): RSAPrivateKeySpec {
|
||||||
|
val keyBytes = Base64.decode(privateKeyBase64, Base64.NO_WRAP)
|
||||||
|
val keyFactory = KeyFactory.getInstance(ALGORITHM)
|
||||||
|
val keySpec = keyFactory.getKeySpec(
|
||||||
|
keyFactory.generatePrivate(PKCS8EncodedKeySpec(keyBytes)),
|
||||||
|
RSAPrivateKeySpec::class.java
|
||||||
|
)
|
||||||
|
return keySpec
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
<!-- 列表区域 -->
|
<!-- 列表区域 -->
|
||||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
<com.ckkj.defense_device.view.PullRefreshLayout
|
||||||
android:id="@+id/swipeRefresh"
|
android:id="@+id/swipeRefresh"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
@@ -84,8 +84,10 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:paddingHorizontal="16dp"
|
android:paddingHorizontal="16dp"
|
||||||
android:paddingTop="8dp"
|
android:paddingTop="8dp"
|
||||||
android:paddingBottom="16dp"/>
|
android:paddingBottom="16dp"
|
||||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
android:clipToPadding="false"
|
||||||
|
android:overScrollMode="never"/>
|
||||||
|
</com.ckkj.defense_device.view.PullRefreshLayout>
|
||||||
|
|
||||||
<!-- 底部按钮 -->
|
<!-- 底部按钮 -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_marginStart="30dp"
|
android:layout_marginStart="20dp"
|
||||||
android:layout_marginEnd="30dp"
|
android:layout_marginEnd="20dp"
|
||||||
android:layout_marginTop="20dp"
|
android:layout_marginTop="10dp"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="40dp"
|
android:layout_height="30dp"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
android:text="@string/cancel"
|
android:text="@string/cancel"
|
||||||
android:textColor="@color/gray"
|
android:textColor="@color/gray"
|
||||||
android:textSize="14dp"
|
android:textSize="14dp"
|
||||||
|
android:textStyle="bold"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true" />
|
android:focusable="true" />
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
android:text="@string/confirm"
|
android:text="@string/confirm"
|
||||||
android:textColor="@color/green"
|
android:textColor="@color/green"
|
||||||
android:textSize="14dp"
|
android:textSize="14dp"
|
||||||
|
android:textStyle="bold"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true" />
|
android:focusable="true" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -45,6 +47,7 @@
|
|||||||
android:id="@+id/rvMessageList"
|
android:id="@+id/rvMessageList"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:maxHeight="150dp"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:paddingStart="14dp"
|
android:paddingStart="14dp"
|
||||||
android:paddingTop="12dp"
|
android:paddingTop="12dp"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:background="@mipmap/home_back"
|
android:background="@mipmap/home_back"
|
||||||
tools:context=".home.HomeMenuFragment">
|
tools:context=".home.HomeMenuFragment">
|
||||||
|
|
||||||
@@ -37,14 +38,12 @@
|
|||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tvCurrentFamily"
|
android:id="@+id/tvCurrentFamily"
|
||||||
android:layout_width="0dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="@string/my_family_home"
|
android:text="@string/my_family_home"
|
||||||
android:textColor="@color/black"
|
android:textColor="@color/black"
|
||||||
android:textSize="20dp"
|
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:ellipsize="end"
|
android:textSize="20sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
@@ -55,11 +54,6 @@
|
|||||||
android:src="@mipmap/triangle" />
|
android:src="@mipmap/triangle" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"/>
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|||||||
@@ -3,16 +3,17 @@
|
|||||||
android:id="@+id/itemFamily1"
|
android:id="@+id/itemFamily1"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:layout_height="30dp">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
android:paddingVertical="10dp"
|
||||||
android:id="@+id/tvFamilyName"
|
android:id="@+id/tvFamilyName"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/my_family"
|
android:text="@string/my_family"
|
||||||
android:textColor="@color/black"
|
android:textColor="@color/black"
|
||||||
android:textSize="16dp" />
|
android:textSize="14dp" />
|
||||||
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
android:layout_weight="1">
|
android:layout_weight="1">
|
||||||
|
|
||||||
<!-- 列表 -->
|
<!-- 列表 -->
|
||||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
<com.ckkj.defense_device.view.PullRefreshLayout
|
||||||
android:id="@+id/swipeRefresh"
|
android:id="@+id/swipeRefresh"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
@@ -93,8 +93,11 @@
|
|||||||
<com.yanzhenjie.recyclerview.SwipeRecyclerView
|
<com.yanzhenjie.recyclerview.SwipeRecyclerView
|
||||||
android:id="@+id/rvLinkedUser"
|
android:id="@+id/rvLinkedUser"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"/>
|
android:layout_height="match_parent"
|
||||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
android:clipToPadding="false"
|
||||||
|
android:overScrollMode="never"
|
||||||
|
android:paddingBottom="16dp"/>
|
||||||
|
</com.ckkj.defense_device.view.PullRefreshLayout>
|
||||||
|
|
||||||
<!-- 空页面 -->
|
<!-- 空页面 -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|||||||
@@ -39,14 +39,24 @@
|
|||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<com.ckkj.defense_device.view.PullRefreshLayout
|
||||||
|
android:id="@+id/swipeRefreshLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_marginStart="20dp"
|
||||||
|
android:layout_marginEnd="20dp"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/rvMessageList"
|
android:id="@+id/rvMessageList"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_marginStart="20dp"
|
android:clipToPadding="false"
|
||||||
android:layout_marginEnd="20dp"
|
android:overScrollMode="never"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
tools:listitem="@layout/item_message_notice" />
|
tools:listitem="@layout/item_message_notice" />
|
||||||
|
|
||||||
|
</com.ckkj.defense_device.view.PullRefreshLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/emptyLayout"
|
android:id="@+id/emptyLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -99,7 +99,7 @@
|
|||||||
android:id="@+id/tv_cache_size"
|
android:id="@+id/tv_cache_size"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="11Kb"
|
android:text=""
|
||||||
android:textColor="#999999"
|
android:textColor="#999999"
|
||||||
android:textSize="13sp" />
|
android:textSize="13sp" />
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user