From 3f42536710f47be00780697a216dcd667af9eb4f Mon Sep 17 00:00:00 2001 From: fengjignqi <15192412330@163.com> Date: Sun, 20 Sep 2026 09:18:26 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=AB=E4=B8=80=E6=89=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 8 + app/src/main/AndroidManifest.xml | 12 + .../com/juhabi/birds/device/DeviceAdapter.kt | 22 ++ .../birds/device/DeviceDetailActivity.kt | 89 +++++- .../{IndexFragment.kt => DeviceFragment.kt} | 92 +++++- .../birds/device/entity/DeviceEntity.kt | 3 +- .../juhabi/birds/device/mvp/DeviceContract.kt | 55 ++++ .../com/juhabi/birds/device/mvp/DeviceImpl.kt | 276 ++++++++++++++++++ .../birds/device/mvp/DevicePresenter.kt | 166 +++++++++++ .../com/juhabi/birds/main/MainActivity.kt | 37 +-- .../juhabi/birds/mine/MineCenterActivity.kt | 18 +- .../juhabi/birds/mine/ModifyPsdActivity.kt | 130 +++++++++ .../com/juhabi/birds/mine/mvp/MineContract.kt | 3 + .../com/juhabi/birds/mine/mvp/MineImpl.kt | 44 +++ .../juhabi/birds/mine/mvp/MinePresenter.kt | 21 ++ .../com/juhabi/birds/network/ApiService.kt | 23 ++ app/src/main/res/layout/activity_main.xml | 12 - .../main/res/layout/device_detail_layout.xml | 33 +++ app/src/main/res/layout/device_item.xml | 6 - app/src/main/res/layout/fragment_home.xml | 26 +- app/src/main/res/layout/modify_psd_layout.xml | 212 ++++++++++++++ .../main/res/layout/popup_device_action.xml | 53 ++++ .../main/res/menu/bottom_navigation_menu.xml | 10 - app/src/main/res/values/strings.xml | 1 + gradle.properties | 2 +- 25 files changed, 1258 insertions(+), 96 deletions(-) create mode 100644 app/src/main/java/com/juhabi/birds/device/DeviceAdapter.kt rename app/src/main/java/com/juhabi/birds/device/{IndexFragment.kt => DeviceFragment.kt} (64%) create mode 100644 app/src/main/java/com/juhabi/birds/device/mvp/DeviceContract.kt create mode 100644 app/src/main/java/com/juhabi/birds/device/mvp/DeviceImpl.kt create mode 100644 app/src/main/java/com/juhabi/birds/device/mvp/DevicePresenter.kt create mode 100644 app/src/main/java/com/juhabi/birds/mine/ModifyPsdActivity.kt create mode 100644 app/src/main/res/layout/device_detail_layout.xml create mode 100644 app/src/main/res/layout/modify_psd_layout.xml create mode 100644 app/src/main/res/layout/popup_device_action.xml diff --git a/app/build.gradle b/app/build.gradle index e009301..73f18ca 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -70,4 +70,12 @@ dependencies { implementation 'io.github.scwang90:refresh-layout-kernel:2.1.1' implementation 'io.github.scwang90:refresh-header-classics:2.1.1' implementation 'io.github.scwang90:refresh-footer-classics:2.1.1' + + implementation 'com.github.maning0303:MNMLKitScanner:V1.0.4' + //下面版本自己控制,barcode-scanning>=17.0.2 + implementation 'com.google.mlkit:barcode-scanning:17.0.2' + implementation "androidx.camera:camera-core:1.0.2" + implementation "androidx.camera:camera-camera2:1.0.2" + implementation "androidx.camera:camera-lifecycle:1.0.2" + implementation "androidx.camera:camera-view:1.0.0-alpha25" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9e1cb4d..158d84d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,6 +8,10 @@ + + + + + + (R.layout.device_item) { + init { + addChildClickViewIds(R.id.img_tips) + } + + override fun convert(holder: BaseViewHolder, item: DeviceEntity) { + holder.setText(R.id.tv_device_name, item.name.ifBlank { "未命名设备" }) + holder.setText(R.id.tv_device_status, item.signalText.ifBlank { "离线" }) + holder.setText(R.id.tv_device_no, "设备编号:${item.deviceNo}") + holder.setText(R.id.tv_device_imei, "IMEI:${item.imei}") + holder.setText(R.id.tv_device_detail, "型号:${item.model ?: "-"} 信号:${item.signalLevel} 头部电量:${item.hoodBattery ?: "-"} 戒指电量:${item.ringBattery ?: "-"}") + holder.setText(R.id.tv_device_location, "连接:${item.hoodConnectedText} 地址:${item.address ?: "暂无地址"}") + } +} diff --git a/app/src/main/java/com/juhabi/birds/device/DeviceDetailActivity.kt b/app/src/main/java/com/juhabi/birds/device/DeviceDetailActivity.kt index 758de85..9238a7b 100644 --- a/app/src/main/java/com/juhabi/birds/device/DeviceDetailActivity.kt +++ b/app/src/main/java/com/juhabi/birds/device/DeviceDetailActivity.kt @@ -3,8 +3,11 @@ package com.juhabi.birds.device import android.os.Bundle import android.view.LayoutInflater import android.view.View +import com.gyf.immersionbar.ImmersionBar +import com.juhabi.birds.R import com.juhabi.birds.base.BaseActivity import com.juhabi.birds.databinding.BindDeviceLayoutBinding +import com.juhabi.birds.databinding.DeviceDetailLayoutBinding import com.juhabi.birds.device.entity.DeviceEntity import com.juhabi.birds.device.mvp.DeviceContract import com.juhabi.birds.device.mvp.DeviceImpl @@ -13,28 +16,100 @@ import com.juhabi.birds.device.mvp.DevicePresenter /** * 设备详情 */ -class DeviceDetailActivity: BaseActivity() , DeviceContract.DeviceView, +class DeviceDetailActivity : + BaseActivity(), DeviceContract.DeviceView, View.OnClickListener { - override fun createBinding(inflater: LayoutInflater): BindDeviceLayoutBinding { - return BindDeviceLayoutBinding.inflate(inflater) + companion object { + const val EXTRA_DEVICE = "extra_device" + const val HOOD_COVER_ON = "0" + const val HOOD_COVER_OFF = "1" + } + + private var device: DeviceEntity? = null + override fun createBinding(inflater: LayoutInflater): DeviceDetailLayoutBinding { + return DeviceDetailLayoutBinding.inflate(inflater) } override fun createPresenter(): DevicePresenter? { return DevicePresenter(DeviceImpl()) } - override fun initView(savedInstanceState: Bundle?) { + override fun initWindows() { + ImmersionBar.with(this) + .statusBarView(binding.statusBarView) + .statusBarDarkFont(true) + .navigationBarColor(R.color.color_zhu) + .navigationBarDarkIcon(true) + .init() } + + override fun initView(savedInstanceState: Bundle?) { + device = intent.getSerializableExtra(EXTRA_DEVICE) as? DeviceEntity + binding.includedTitle.tvTitle.text = device?.name ?: "设备详情" + val deviceNo = device?.deviceNo + if (deviceNo.isNullOrBlank()) { + showError("设备信息无效") + } else { + presenter?.getDeviceDetail(deviceNo) + } + + listOf( + binding.tvBind, + binding.switchCtrl, + binding.includedTitle.imgBack, + ).forEach { button -> + button.setOnClickListener(this) + } + } + + override fun onClick(view: View) { + when(view.id){ + R.id.img_back ->{ + finish() + } + R.id.switch_ctrl ->{ + if(device?.status == "0"){ + showMessage("设备已经离线") + return; + } + val currentDevice = device ?: return + // hoodCoverOn:0 表示打开,1 表示关闭;点击后发送切换后的目标状态。 + val coverOn = currentDevice.hoodCoverOn != HOOD_COVER_ON + presenter?.controlEye(currentDevice.deviceNo, coverOn) + } + } + } + + + override fun showError(message: String) { } override fun getBindDeviceListSuccess(devices: List) { } + override fun getDeviceDetailSuccess(device: DeviceEntity) { + this.device = device + binding.includedTitle.tvTitle.text = device.name + binding.tvBind.text = "设备编号:${device.deviceNo}" + binding.switchCtrl.isChecked = device.hoodCoverOn == HOOD_COVER_ON + } + + override fun controlEyeSuccess(message: String) { + showMessage(message) + device = device?.copy( + hoodCoverOn = if (binding.switchCtrl.isChecked) { + HOOD_COVER_ON + } else { + HOOD_COVER_OFF + } + ) + } + override fun bindDeviceSuccess(message: String) { } - override fun onClick(view: View) { - } -} \ No newline at end of file + + +} diff --git a/app/src/main/java/com/juhabi/birds/device/IndexFragment.kt b/app/src/main/java/com/juhabi/birds/device/DeviceFragment.kt similarity index 64% rename from app/src/main/java/com/juhabi/birds/device/IndexFragment.kt rename to app/src/main/java/com/juhabi/birds/device/DeviceFragment.kt index 795e720..2886094 100644 --- a/app/src/main/java/com/juhabi/birds/device/IndexFragment.kt +++ b/app/src/main/java/com/juhabi/birds/device/DeviceFragment.kt @@ -1,11 +1,13 @@ package com.juhabi.birds.device +import android.Manifest +import android.content.Intent +import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import com.juhabi.birds.R -import com.juhabi.birds.base.BaseActivity import com.juhabi.birds.base.BaseFragment import com.juhabi.birds.custom.DeviceActionPopupWindow import com.juhabi.birds.custom.CustomRefreshHeader @@ -14,12 +16,12 @@ import com.juhabi.birds.device.entity.DeviceEntity import com.juhabi.birds.device.mvp.DeviceContract import com.juhabi.birds.device.mvp.DeviceImpl import com.juhabi.birds.device.mvp.DevicePresenter -import com.juhabi.birds.main.fragment.home.mvp.HomeContract -import com.juhabi.birds.main.fragment.home.mvp.HomeImpl -import com.juhabi.birds.main.fragment.home.mvp.HomePresenter +import com.juhabi.birds.util.ToastUtil +import com.maning.mlkitscanner.scan.MNScanManager +import com.maning.mlkitscanner.scan.callback.act.MNScanCallback import com.scwang.smart.refresh.footer.ClassicsFooter -class IndexFragment : BaseFragment(), +class DeviceFragment : BaseFragment(), DeviceContract.DeviceView, View.OnClickListener { private val deviceAdapter = DeviceAdapter() @@ -37,10 +39,17 @@ class IndexFragment : BaseFragment(), override fun initView(savedInstanceState: Bundle?) { listOf( binding.tvIndex, + binding.imgScan, ).forEach { button -> button.setOnClickListener(this) } binding.rvDevices.adapter = deviceAdapter + deviceAdapter.setOnItemClickListener { _, _, position -> + val device = deviceAdapter.data[position] + goTo(DeviceDetailActivity::class.java, Bundle().apply { + putSerializable(DeviceDetailActivity.EXTRA_DEVICE, device) + }) + } deviceAdapter.setOnItemChildClickListener { adapter, view, position -> when (view.id) { R.id.img_tips -> { @@ -102,6 +111,17 @@ class IndexFragment : BaseFragment(), R.id.tv_index ->{ goTo(BindDeviceActivity::class.java,null) } + + //扫一扫:先申请相机和相册权限,授权通过后再拉起扫码页。 + R.id.img_scan -> { + getMorePermissionRequst( + scanPermissions(), + "相机和相册权限", + "扫一扫功能需要使用相机及访问相册权限", + REQUEST_CAMERA_AND_ALBUM + ) + } + } } @@ -168,17 +188,67 @@ class IndexFragment : BaseFragment(), super.showError(message) } - /** 接口暂无数据时提供临时设备,方便先验收列表布局。 */ - private fun temporaryDevices(): List = listOf( - DeviceEntity("暂无地址", "BM202609160005", null, "0", "已断开", "860000000000005", null, null, null, "BM202609160005", null, 0, "离线", "1"), - DeviceEntity("暂无地址", "BM202609160006", null, "0", "已断开", "860000000000006", null, null, null, "BM202609160006", null, 0, "离线", "1"), - DeviceEntity("暂无地址", "BM202609160007", null, "0", "已断开", "860000000000007", null, null, null, "BM202609160007", null, 0, "离线", "1") - ) private companion object { const val FIRST_PAGE = 1 const val PAGE_SIZE = 10 const val REMOVE_ANIMATION_DELAY_MS = 300L + const val REQUEST_CAMERA_AND_ALBUM = 1001 + } + + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + } + + + /** 相机和相册均已授权后,自动打开扫一扫。 */ + override fun onPermissionsGranted(permissions: Array) { + startDeviceScan() + } + + override fun onPermissionsDenied(permissions: Array, permanentlyDenied: Boolean) { + super.onPermissionsDenied(permissions, permanentlyDenied) + } + + /** Android 13 及以上申请读图权限,更低版本申请存储读取权限。 */ + private fun scanPermissions(): Array { + val albumPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Manifest.permission.READ_MEDIA_IMAGES + } else { + Manifest.permission.READ_EXTERNAL_STORAGE + } + return arrayOf(Manifest.permission.CAMERA, albumPermission) + } + + /** 使用 MNMLKitScanner 默认扫码页识别二维码或条形码。 */ + private fun startDeviceScan() { + MNScanManager.startScan(mContext, object : MNScanCallback { + override fun onActivityResult(resultCode: Int, data: Intent) { + when (resultCode) { + MNScanManager.RESULT_SUCCESS -> { + val results = data.getStringArrayListExtra(MNScanManager.INTENT_KEY_RESULT_SUCCESS) + val content = results?.filter { it.isNotBlank() }?.joinToString("\n").orEmpty() + if (content.isBlank()) { + ToastUtil.showShort(mContext, "未识别到内容") + } else { + ToastUtil.showShort(mContext, content) + } + } + MNScanManager.RESULT_FAIL -> { + val resultError = data.getStringExtra(MNScanManager.INTENT_KEY_RESULT_ERROR) + ToastUtil.showShort(mContext, resultError.takeUnless { it.isNullOrBlank() } ?: "扫码失败") + } + MNScanManager.RESULT_CANCLE -> { + ToastUtil.showShort(mContext, getString(R.string.cancel_scan)) + } + } + } + }) } } diff --git a/app/src/main/java/com/juhabi/birds/device/entity/DeviceEntity.kt b/app/src/main/java/com/juhabi/birds/device/entity/DeviceEntity.kt index a6ccb1e..5802ead 100644 --- a/app/src/main/java/com/juhabi/birds/device/entity/DeviceEntity.kt +++ b/app/src/main/java/com/juhabi/birds/device/entity/DeviceEntity.kt @@ -16,5 +16,6 @@ data class DeviceEntity( val ringBattery: Int?, val signalLevel: Int, val signalText: String, - val status: String + val status: String, + val hoodCoverOn: String ) : Serializable diff --git a/app/src/main/java/com/juhabi/birds/device/mvp/DeviceContract.kt b/app/src/main/java/com/juhabi/birds/device/mvp/DeviceContract.kt new file mode 100644 index 0000000..b7dfb8f --- /dev/null +++ b/app/src/main/java/com/juhabi/birds/device/mvp/DeviceContract.kt @@ -0,0 +1,55 @@ +package com.juhabi.birds.device.mvp + +import com.juhabi.birds.base.IView +import com.juhabi.birds.device.entity.DeviceEntity +import com.juhabi.birds.login.model.UserEntity +import com.juhabi.birds.login.mvp.LoginContract +import com.juhabi.birds.network.BaseBean +import org.json.JSONObject + +interface DeviceContract { + interface Callback { + fun success(data: T) + fun fail(message: String) + } + + interface DataModel { + /** 查询已绑定的设备列表。 */ + fun getBindDeviceList(params: Map, callback: Callback>) + fun getDeviceDetail(deviceNo: String, callback: Callback) + + /** 控制设备眼罩开关。 */ + fun controlEye(deviceNo: String, coverOn: Boolean, callback: Callback>) + + /** 根据 IMEI 和设备名称绑定设备。 */ + fun bindDevice(json: JSONObject, callback: Callback>) + + /** 根据设备编号修改设备名称。 */ + fun renameDevice( + deviceNo: String, + json: JSONObject, + callback: Callback> + ) + + /** 根据设备编号解绑设备。 */ + fun unbindDevice(deviceNo: String, callback: Callback>) + + /** 取消设备模块中尚未完成的网络请求。 */ + fun cancel() + } + + interface DeviceView : IView { + fun getBindDeviceListSuccess(devices: List) + fun getDeviceDetailSuccess(device: DeviceEntity) {} + fun controlEyeSuccess(message: String) {} + fun bindDeviceSuccess(message: String) + + /** 设备重命名成功回调,不关心该事件的页面可以不实现。 */ + fun renameDeviceSuccess(message: String) { + } + + /** 设备解绑成功回调,不关心该事件的页面可以不实现。 */ + fun unbindDeviceSuccess(message: String) { + } + } +} diff --git a/app/src/main/java/com/juhabi/birds/device/mvp/DeviceImpl.kt b/app/src/main/java/com/juhabi/birds/device/mvp/DeviceImpl.kt new file mode 100644 index 0000000..e17c390 --- /dev/null +++ b/app/src/main/java/com/juhabi/birds/device/mvp/DeviceImpl.kt @@ -0,0 +1,276 @@ +package com.juhabi.birds.device.mvp + +import android.os.Handler +import android.os.Looper +import com.juhabi.birds.device.entity.DeviceEntity +import com.juhabi.birds.network.ApiClient +import com.juhabi.birds.network.BaseBean +import com.juhabi.birds.network.NetworkManager +import org.json.JSONObject +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +class DeviceImpl : DeviceContract.DataModel { + private val mainHandler = Handler(Looper.getMainLooper()) + private var deviceListCall: Call>>? = null + private var deviceDetailCall: Call>? = null + private var controlEyeCall: Call>? = null + private var bindDeviceCall: Call>? = null + private var renameDeviceCall: Call>? = null + private var unbindDeviceCall: Call>? = null + + /** 请求当前账号已绑定的设备列表。 */ + override fun getBindDeviceList(params: Map, callback: DeviceContract.Callback>) { + deviceListCall?.cancel() + deviceListCall = ApiClient.apiService + .getBindDeviceList(NetworkManager.getToken(), params) + .also { call -> + call.enqueue(object : Callback>> { + override fun onResponse( + call: Call>>, + response: Response>> + ) { + mainHandler.post { + val bean = response.body() + val devices = bean?.rows ?: bean?.data + if ( + response.isSuccessful && + (bean?.code == 0 || bean?.code == 200) && + devices != null + ) { + callback.success(devices) + } else { + callback.fail( + bean?.message?.ifBlank { null } ?: "获取设备列表失败" + ) + } + } + } + + override fun onFailure( + call: Call>>, + error: Throwable + ) { + if (!call.isCanceled) { + mainHandler.post { + callback.fail(error.message ?: "网络请求失败") + } + } + } + }) + } + } + + override fun getDeviceDetail( + deviceNo: String, + callback: DeviceContract.Callback + ) { + deviceDetailCall?.cancel() + deviceDetailCall = ApiClient.apiService + .getDeviceDetail(NetworkManager.getToken(), deviceNo) + .also { call -> + call.enqueue(object : Callback> { + override fun onResponse( + call: Call>, + response: Response> + ) { + mainHandler.post { + val bean = response.body() + val device = bean?.data + if ( + response.isSuccessful && + (bean?.code == 0 || bean?.code == 200) && + device != null + ) { + callback.success(device) + } else { + callback.fail( + bean?.message?.ifBlank { null } ?: "获取设备详情失败" + ) + } + } + } + + override fun onFailure( + call: Call>, + error: Throwable + ) { + if (!call.isCanceled) { + mainHandler.post { + callback.fail(error.message ?: "网络请求失败") + } + } + } + }) + } + } + + /** 请求服务端控制设备眼罩开关。 */ + override fun controlEye( + deviceNo: String, + coverOn: Boolean, + callback: DeviceContract.Callback> + ) { + controlEyeCall?.cancel() + val json = JSONObject().apply { + put("deviceNo", deviceNo) + put("coverOn", coverOn) + } + controlEyeCall = ApiClient.apiService + .controlEye(NetworkManager.getToken(), deviceNo, NetworkManager.toRequestBody(json)) + .also { call -> + call.enqueue(object : Callback> { + override fun onResponse( + call: Call>, + response: Response> + ) { + mainHandler.post { + val bean = response.body() + if (response.isSuccessful && (bean?.code == 0 || bean?.code == 200)) { + callback.success(bean) + } else { + callback.fail(bean?.message?.ifBlank { null } ?: "控制眼罩失败") + } + } + } + + override fun onFailure(call: Call>, error: Throwable) { + if (!call.isCanceled) { + mainHandler.post { + callback.fail(error.message ?: "网络请求失败") + } + } + } + }) + } + } + + /** 将设备 IMEI 和名称提交到服务器完成绑定。 */ + override fun bindDevice( + json: JSONObject, + callback: DeviceContract.Callback> + ) { + bindDeviceCall?.cancel() + bindDeviceCall = ApiClient.apiService + .bindDevice(NetworkManager.getToken(), NetworkManager.toRequestBody(json)) + .also { call -> + call.enqueue(object : Callback> { + override fun onResponse( + call: Call>, + response: Response> + ) { + mainHandler.post { + val bean = response.body() + if ( + response.isSuccessful && + (bean?.code == 0 || bean?.code == 200) + ) { + callback.success(bean) + } else { + callback.fail( + bean?.message?.ifBlank { null } ?: "绑定设备失败" + ) + } + } + } + + override fun onFailure(call: Call>, error: Throwable) { + if (!call.isCanceled) { + mainHandler.post { + callback.fail(error.message ?: "网络请求失败") + } + } + } + }) + } + } + + /** 将新的设备名称提交到服务器。 */ + override fun renameDevice( + deviceNo: String, + json: JSONObject, + callback: DeviceContract.Callback> + ) { + renameDeviceCall?.cancel() + renameDeviceCall = ApiClient.apiService + .deviceRename(deviceNo = deviceNo, headers = NetworkManager.getToken(), requestBody = NetworkManager.toRequestBody(json)) + .also { call -> + call.enqueue(object : Callback> { + override fun onResponse( + call: Call>, + response: Response> + ) { + mainHandler.post { + val bean = response.body() + if (response.isSuccessful && (bean?.code == 0 || bean?.code == 200)) { + callback.success(bean) + } else { + callback.fail(bean?.message?.ifBlank { null } ?: "设备重命名失败") + } + } + } + + override fun onFailure(call: Call>, error: Throwable) { + if (!call.isCanceled) { + mainHandler.post { + callback.fail(error.message ?: "网络请求失败") + } + } + } + }) + } + } + + /** 按设备编号请求服务端解绑。 */ + override fun unbindDevice( + deviceNo: String, + callback: DeviceContract.Callback> + ) { + unbindDeviceCall?.cancel() + unbindDeviceCall = ApiClient.apiService + .unbindDevice(headers = NetworkManager.getToken(), deviceNo = deviceNo) + .also { call -> + call.enqueue(object : Callback> { + override fun onResponse( + call: Call>, + response: Response> + ) { + mainHandler.post { + val bean = response.body() + if (response.isSuccessful && (bean?.code == 0 || bean?.code == 200)) { + callback.success(bean) + } else { + callback.fail(bean?.message?.ifBlank { null } ?: "设备解绑失败") + } + } + } + + override fun onFailure(call: Call>, error: Throwable) { + if (!call.isCanceled) { + mainHandler.post { + callback.fail(error.message ?: "网络请求失败") + } + } + } + }) + } + } + + /** 取消所有设备请求并清理主线程回调。 */ + override fun cancel() { + deviceListCall?.cancel() + deviceListCall = null + deviceDetailCall?.cancel() + deviceDetailCall = null + controlEyeCall?.cancel() + controlEyeCall = null + bindDeviceCall?.cancel() + bindDeviceCall = null + renameDeviceCall?.cancel() + renameDeviceCall = null + unbindDeviceCall?.cancel() + unbindDeviceCall = null + mainHandler.removeCallbacksAndMessages(null) + } +} diff --git a/app/src/main/java/com/juhabi/birds/device/mvp/DevicePresenter.kt b/app/src/main/java/com/juhabi/birds/device/mvp/DevicePresenter.kt new file mode 100644 index 0000000..e8fc9f1 --- /dev/null +++ b/app/src/main/java/com/juhabi/birds/device/mvp/DevicePresenter.kt @@ -0,0 +1,166 @@ +package com.juhabi.birds.device.mvp + +import com.juhabi.birds.base.BasePresenter +import com.juhabi.birds.device.entity.DeviceEntity +import com.juhabi.birds.network.BaseBean +import org.json.JSONObject + +class DevicePresenter( + private val deviceImpl: DeviceImpl +) : BasePresenter() { + + /** 获取当前账号已经绑定的设备列表。 */ + fun getBindDeviceList(params: Map = emptyMap()) { + val currentView = view ?: return + currentView.showLoading() + deviceImpl.getBindDeviceList(params, object : DeviceContract.Callback> { + override fun success(data: List) { + view?.apply { + hideLoading() + getBindDeviceListSuccess(data) + } + } + + override fun fail(message: String) { + view?.apply { + hideLoading() + showError(message) + } + } + } + ) + } + + /** + * 获取设备详情 + */ + fun getDeviceDetail(deviceNo: String) { + val currentView = view ?: return + currentView.showLoading() + deviceImpl.getDeviceDetail( + deviceNo, + object : DeviceContract.Callback { + override fun success(data: DeviceEntity) { + view?.apply { + hideLoading() + getDeviceDetailSuccess(data) + } + } + + override fun fail(message: String) { + view?.apply { + hideLoading() + showError(message) + } + } + } + ) + } + + /** 根据目标状态控制设备眼罩开关。 */ + fun controlEye(deviceNo: String, coverOn: Boolean) { + val currentView = view ?: return + currentView.showLoading() + deviceImpl.controlEye(deviceNo, coverOn, object : DeviceContract.Callback> { + override fun success(data: BaseBean) { + view?.apply { + hideLoading() + controlEyeSuccess(data.message.ifBlank { "眼罩状态已更新" }) + } + } + + override fun fail(message: String) { + view?.apply { + hideLoading() + showError(message) + } + } + }) + } + + /** 使用设备 IMEI 和名称发起绑定请求。 */ + fun bindDevice(imei: String, name: String) { + val currentView = view ?: return + currentView.showLoading() + val json = JSONObject().apply { + put("imei", imei) + put("name", name) + } + deviceImpl.bindDevice(json, object : DeviceContract.Callback> { + override fun success(data: BaseBean) { + view?.apply { + hideLoading() + bindDeviceSuccess(data.message.ifBlank { "设备绑定成功" }) + } + } + + override fun fail(message: String) { + view?.apply { + hideLoading() + showError(message) + } + } + }) + } + + /** 根据设备编号修改设备名称。 */ + fun renameDevice(deviceNo: String, name: String) { + val currentView = view ?: return + val trimmedName = name.trim() + if (deviceNo.isBlank() || trimmedName.isBlank()) { + currentView.showError("设备编号和名称不能为空") + return + } + currentView.showLoading() + val json = JSONObject().apply { + put("deviceNo", deviceNo) + put("name", trimmedName) + } + deviceImpl.renameDevice(deviceNo, json, object : DeviceContract.Callback> { + override fun success(data: BaseBean) { + view?.apply { + hideLoading() + renameDeviceSuccess(data.message.ifBlank { "设备重命名成功" }) + } + } + + override fun fail(message: String) { + view?.apply { + hideLoading() + showError(message) + } + } + }) + } + + /** 根据设备编号发起解绑请求。 */ + fun unbindDevice(deviceNo: String) { + val currentView = view ?: return + if (deviceNo.isBlank()) { + currentView.showError("设备编号不能为空") + return + } + currentView.showLoading() + deviceImpl.unbindDevice(deviceNo, object : DeviceContract.Callback> { + override fun success(data: BaseBean) { + view?.apply { + hideLoading() + unbindDeviceSuccess(data.message.ifBlank { "设备解绑成功" }) + } + } + + override fun fail(message: String) { + view?.apply { + hideLoading() + showError(message) + } + } + }) + } + + /** 销毁页面时取消设备模块发起的网络请求。 */ + override fun onDestroy() { + deviceImpl.cancel() + super.onDestroy() + } +} diff --git a/app/src/main/java/com/juhabi/birds/main/MainActivity.kt b/app/src/main/java/com/juhabi/birds/main/MainActivity.kt index 3d6d159..d68f1b5 100644 --- a/app/src/main/java/com/juhabi/birds/main/MainActivity.kt +++ b/app/src/main/java/com/juhabi/birds/main/MainActivity.kt @@ -2,17 +2,14 @@ package com.juhabi.birds.main import android.os.Bundle import android.view.LayoutInflater -import android.view.View import androidx.annotation.ColorRes import androidx.core.content.ContextCompat import com.juhabi.birds.R import com.juhabi.birds.base.BaseActivity import com.juhabi.birds.databinding.ActivityMainBinding import com.juhabi.birds.main.adapter.MainPagerAdapter -import com.juhabi.birds.main.fragment.HistoryFragment -import com.juhabi.birds.device.IndexFragment +import com.juhabi.birds.device.DeviceFragment import com.juhabi.birds.mine.MineFragment -import com.juhabi.birds.main.fragment.PlanFragment import com.juhabi.birds.main.model.AppVersionInfo import com.juhabi.birds.main.mvp.MainContract import com.juhabi.birds.main.mvp.MainImpl @@ -42,7 +39,7 @@ class MainActivity : BaseActivity(), override fun initView(savedInstanceState: Bundle?) { binding.viewPager.adapter = MainPagerAdapter( this, - listOf(IndexFragment(), PlanFragment(), HistoryFragment(), MineFragment()) + listOf(DeviceFragment(), MineFragment()) ) binding.viewPager.offscreenPageLimit = 1 binding.viewPager.registerOnPageChangeCallback(object : androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback() { @@ -60,8 +57,6 @@ class MainActivity : BaseActivity(), private fun onNavigationItemSelected(itemId: Int): Boolean { return when (itemId) { R.id.navigation_home -> onHomeSelected() - R.id.navigation_plan -> onPlanSelected() - R.id.navigation_history -> onHistorySelected() R.id.navigation_mine -> onMineSelected() else -> false } @@ -69,25 +64,11 @@ class MainActivity : BaseActivity(), private fun onHomeSelected(): Boolean { switchPage(PAGE_HOME) - // Add Home-specific status bar, refresh, or Fragment communication here. - return true - } - - private fun onPlanSelected(): Boolean { - switchPage(PAGE_PLAN) - // Add Plan-specific status bar, refresh, or Fragment communication here. - return true - } - - private fun onHistorySelected(): Boolean { - switchPage(PAGE_HISTORY) - // Add History-specific status bar, refresh, or Fragment communication here. return true } private fun onMineSelected(): Boolean { switchPage(PAGE_MINE) - // Add Mine-specific status bar, refresh, or Fragment communication here. return true } @@ -99,18 +80,8 @@ class MainActivity : BaseActivity(), } private fun updateTopBarColor(position: Int) { - if (position == PAGE_PLAN) { - binding.immersiveTopImageView.visibility = View.VISIBLE - binding.statusBarView.setBackgroundColor( - ContextCompat.getColor(this, android.R.color.transparent) - ) - return - } - - binding.immersiveTopImageView.visibility = View.GONE @ColorRes val colorRes = when (position) { PAGE_HOME -> R.color.color_zhu - PAGE_HISTORY -> R.color.color_E9A01C PAGE_MINE -> R.color.color_B85CE6 else -> R.color.color_zhu } @@ -131,8 +102,6 @@ class MainActivity : BaseActivity(), private companion object { const val PAGE_HOME = 0 - const val PAGE_PLAN = 1 - const val PAGE_HISTORY = 2 - const val PAGE_MINE = 3 + const val PAGE_MINE = 1 } } diff --git a/app/src/main/java/com/juhabi/birds/mine/MineCenterActivity.kt b/app/src/main/java/com/juhabi/birds/mine/MineCenterActivity.kt index 97a65a2..7b976cc 100644 --- a/app/src/main/java/com/juhabi/birds/mine/MineCenterActivity.kt +++ b/app/src/main/java/com/juhabi/birds/mine/MineCenterActivity.kt @@ -167,14 +167,6 @@ class MineCenterActivity : BaseActivity, - grantResults: IntArray - ) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - appUpdateManager.onRequestPermissionsResult(requestCode, grantResults) - } /** * 日期、时间和日期时间共用同一个组件调用入口。 @@ -211,6 +203,16 @@ class MineCenterActivity : BaseActivity, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + appUpdateManager.onRequestPermissionsResult(requestCode, grantResults) + } + + override fun onPermissionsGranted(permissions: Array) { showMessage("权限获取成功:${permissions.joinToString { it.substringAfterLast('.') }}") } diff --git a/app/src/main/java/com/juhabi/birds/mine/ModifyPsdActivity.kt b/app/src/main/java/com/juhabi/birds/mine/ModifyPsdActivity.kt new file mode 100644 index 0000000..94f2870 --- /dev/null +++ b/app/src/main/java/com/juhabi/birds/mine/ModifyPsdActivity.kt @@ -0,0 +1,130 @@ +package com.juhabi.birds.mine + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import com.gyf.immersionbar.ImmersionBar +import com.juhabi.birds.R +import com.juhabi.birds.base.BaseActivity +import com.juhabi.birds.databinding.ActivityMineCenterBinding +import com.juhabi.birds.databinding.ModifyPsdLayoutBinding +import com.juhabi.birds.main.model.AppVersionInfo +import com.juhabi.birds.mine.model.MineUserInfo +import com.juhabi.birds.mine.mvp.MineContract +import com.juhabi.birds.mine.mvp.MineImpl +import com.juhabi.birds.mine.mvp.MinePresenter +import com.juhabi.birds.util.ToastUtil + +/** + * 旧密码,修改新密码 + */ +class ModifyPsdActivity : BaseActivity(), + MineContract.MineView, View.OnClickListener { + override fun createBinding(inflater: LayoutInflater): ModifyPsdLayoutBinding { + return ModifyPsdLayoutBinding.inflate(inflater) + } + + override fun createPresenter(): MinePresenter? { + return MinePresenter(MineImpl()) + } + + override fun initWindows() { + ImmersionBar.with(this) + .statusBarView(binding.statusBarView) + .statusBarDarkFont(true) + .navigationBarColor(R.color.color_zhu) + .navigationBarDarkIcon(true) + .init() + } + + + override fun initView(savedInstanceState: Bundle?) { + listOf( + binding.oldPsdVisibilityButton, + binding.passwordVisibilityButton, + binding.confirmPasswordVisibilityButton, + binding.confirmButton, + binding.includedTitle.imgBack, + ).forEach { button -> + button.setOnClickListener(this) + } + } + + override fun onClick(view: View) { + when(view.id){ + R.id.img_back ->{ + finish() + } + + R.id.confirmButton ->{//确认修改 + val oldPassword = binding.oldPasswordEditText.text.toString() + val newPassword = binding.passwordEditText.text.toString() + val confirmPassword = binding.confirmPasswordEditText.text.toString() + + // 按顺序校验输入项,避免无效数据提交到服务端。 + if (oldPassword.isBlank()) { + ToastUtil.showShort(mContext, "当前密码不能为空") + return + } + if (newPassword.isBlank()) { + ToastUtil.showShort(mContext, "新密码不能为空") + return + } + if (confirmPassword.isBlank()) { + ToastUtil.showShort(mContext, "请再次输入新密码") + return + } + if (oldPassword == newPassword) { + ToastUtil.showShort(mContext, "新密码不能与当前密码相同") + return + } + if (newPassword != confirmPassword) { + ToastUtil.showShort(mContext, "两次输入的新密码不一致") + return + } + if (!isValidPassword(newPassword)) { + ToastUtil.showShort(mContext, "密码需为8-20位,且包含大小写字母和数字") + return + } + + presenter?.modifyPassword(oldPassword, newPassword) + + } + + + } + } + + + override fun showError(message: String) { + hideLoading() + ToastUtil.showShort(mContext, message) + } + + override fun onModifyPasswordSuccess(message: String) { + hideLoading() + ToastUtil.showShort(mContext, message.ifBlank { "密码修改成功" }) + finish() + } + + /** 8-20 位,且同时包含大写、小写字母和数字。 */ + private fun isValidPassword(password: String): Boolean { + if (password.length !in PASSWORD_MIN_LENGTH..PASSWORD_MAX_LENGTH) return false + return password.any { it.isUpperCase() } && + password.any { it.isLowerCase() } && + password.any { it.isDigit() } + } + + private companion object { + const val PASSWORD_MIN_LENGTH = 8 + const val PASSWORD_MAX_LENGTH = 20 + } + + override fun getUserInfoSuccess(userInfo: MineUserInfo) { + } + + override fun onVersionChecked(versionInfo: AppVersionInfo) { + } + + +} diff --git a/app/src/main/java/com/juhabi/birds/mine/mvp/MineContract.kt b/app/src/main/java/com/juhabi/birds/mine/mvp/MineContract.kt index 802b874..434b525 100644 --- a/app/src/main/java/com/juhabi/birds/mine/mvp/MineContract.kt +++ b/app/src/main/java/com/juhabi/birds/mine/mvp/MineContract.kt @@ -16,11 +16,14 @@ interface MineContract { params: Map, callback: Callback ) + fun modifyPassword(oldPassword: String, newPassword: String, callback: Callback) fun cancel() } interface MineView : IView { fun getUserInfoSuccess(userInfo: MineUserInfo) fun onVersionChecked(versionInfo: AppVersionInfo) + /** 修改密码成功回调,非修改密码页面无需处理。 */ + fun onModifyPasswordSuccess(message: String) {} } } diff --git a/app/src/main/java/com/juhabi/birds/mine/mvp/MineImpl.kt b/app/src/main/java/com/juhabi/birds/mine/mvp/MineImpl.kt index bfa92a3..931e0f3 100644 --- a/app/src/main/java/com/juhabi/birds/mine/mvp/MineImpl.kt +++ b/app/src/main/java/com/juhabi/birds/mine/mvp/MineImpl.kt @@ -10,11 +10,13 @@ import com.juhabi.birds.network.NetworkManager import retrofit2.Call import retrofit2.Callback import retrofit2.Response +import org.json.JSONObject class MineImpl : MineContract.DataModel { private val mainHandler = Handler(Looper.getMainLooper()) private var userInfoCall: Call>? = null private var versionCall: Call>? = null + private var modifyPasswordCall: Call>? = null override fun getUserInfo( params: Map, @@ -96,11 +98,53 @@ class MineImpl : MineContract.DataModel { } } + /** + * 通过旧密码修改原密码 + */ + override fun modifyPassword( + oldPassword: String, + newPassword: String, + callback: MineContract.Callback + ) { + modifyPasswordCall?.cancel() + val requestBody = JSONObject().apply { + put("oldPassword", oldPassword) + put("newPassword", newPassword) + } + modifyPasswordCall = ApiClient.apiService + .modifyPassword(NetworkManager.getToken(), NetworkManager.toRequestBody(requestBody)) + .also { call -> + call.enqueue(object : Callback> { + override fun onResponse( + call: Call>, + response: Response> + ) { + mainHandler.post { + val bean = response.body() + if (response.isSuccessful && bean?.code == 200) { + callback.success(bean.message.orEmpty()) + } else { + callback.fail(bean?.message?.ifBlank { null } ?: "密码修改失败") + } + } + } + + override fun onFailure(call: Call>, error: Throwable) { + if (!call.isCanceled) { + mainHandler.post { callback.fail(error.message ?: "网络请求失败") } + } + } + }) + } + } + override fun cancel() { userInfoCall?.cancel() userInfoCall = null versionCall?.cancel() versionCall = null + modifyPasswordCall?.cancel() + modifyPasswordCall = null mainHandler.removeCallbacksAndMessages(null) } } diff --git a/app/src/main/java/com/juhabi/birds/mine/mvp/MinePresenter.kt b/app/src/main/java/com/juhabi/birds/mine/mvp/MinePresenter.kt index daf793e..b383613 100644 --- a/app/src/main/java/com/juhabi/birds/mine/mvp/MinePresenter.kt +++ b/app/src/main/java/com/juhabi/birds/mine/mvp/MinePresenter.kt @@ -46,6 +46,27 @@ class MinePresenter(private val mineImpl: MineImpl) : BasePresenter { + override fun success(data: String) { + view?.apply { + hideLoading() + onModifyPasswordSuccess(data) + } + } + + override fun fail(message: String) { + view?.apply { + hideLoading() + showError(message) + } + } + }) + } + override fun onDestroy() { mineImpl.cancel() super.onDestroy() diff --git a/app/src/main/java/com/juhabi/birds/network/ApiService.kt b/app/src/main/java/com/juhabi/birds/network/ApiService.kt index b4d88c2..e661577 100644 --- a/app/src/main/java/com/juhabi/birds/network/ApiService.kt +++ b/app/src/main/java/com/juhabi/birds/network/ApiService.kt @@ -126,6 +126,15 @@ interface ApiService { fun getBindDeviceList(@HeaderMap headers: Map, @QueryMap params: Map): Call>> + /** + * 获取设备详情 + * + */ + @GET("app/device/{deviceNo}") + fun getDeviceDetail(@HeaderMap headers: Map, @Path("deviceNo") deviceNo: String): Call> + + + /** * 绑定设备 { @@ -173,6 +182,20 @@ interface ApiService { fun getMineUserInfo(@HeaderMap headers: Map, @QueryMap params: Map): Call> + + /** + * 通过旧密码修改新密码 + { + "oldPassword": "string", + "newPassword": "string" + } + */ + @PUT("app/user/password") + fun modifyPassword(@HeaderMap headers: Map, @Body requestBody: RequestBody): Call> + + + + @GET("app/v1/checkVersion") fun checkVersionCode(@HeaderMap headers: Map, @QueryMap params: Map): Call> } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index bfe34c9..b783ea9 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -4,18 +4,6 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/device_item.xml b/app/src/main/res/layout/device_item.xml index bc63ddf..925b1bf 100644 --- a/app/src/main/res/layout/device_item.xml +++ b/app/src/main/res/layout/device_item.xml @@ -20,12 +20,6 @@ android:src="@mipmap/ic_launcher" android:layout_gravity="end"/> - diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index b04312b..efda55c 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -14,13 +14,27 @@ android:id="@+id/included_title" layout="@layout/include_title_home_layout" /> - + android:orientation="horizontal"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/popup_device_action.xml b/app/src/main/res/layout/popup_device_action.xml new file mode 100644 index 0000000..9b50f95 --- /dev/null +++ b/app/src/main/res/layout/popup_device_action.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/bottom_navigation_menu.xml b/app/src/main/res/menu/bottom_navigation_menu.xml index f146994..16f21a1 100644 --- a/app/src/main/res/menu/bottom_navigation_menu.xml +++ b/app/src/main/res/menu/bottom_navigation_menu.xml @@ -4,16 +4,6 @@ android:id="@+id/navigation_home" android:icon="@drawable/ic_nav_home" android:title="@string/tab_home" /> - - 首页 重命名 解绑设备 + 取消扫码 计划 历史 我的 diff --git a/gradle.properties b/gradle.properties index 837592e..eecc11d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. For more details, visit # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects