扫一扫

This commit is contained in:
fengjignqi
2026-09-20 09:18:26 +08:00
parent 981cfae71c
commit 3f42536710
25 changed files with 1258 additions and 96 deletions

View File

@@ -70,4 +70,12 @@ dependencies {
implementation 'io.github.scwang90:refresh-layout-kernel:2.1.1' 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-header-classics:2.1.1'
implementation 'io.github.scwang90:refresh-footer-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"
} }

View File

@@ -8,6 +8,10 @@
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<application <application
android:name=".MyApplication" android:name=".MyApplication"
@@ -69,6 +73,14 @@
android:name=".device.BindDeviceActivity" android:name=".device.BindDeviceActivity"
android:exported="false" /> android:exported="false" />
<activity
android:name=".device.DeviceDetailActivity"
android:exported="false" />
<activity
android:name=".mine.ModifyPsdActivity"
android:exported="false" />
<provider <provider

View File

@@ -0,0 +1,22 @@
package com.juhabi.birds.device
import com.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.viewholder.BaseViewHolder
import com.juhabi.birds.R
import com.juhabi.birds.device.entity.DeviceEntity
/** 临时设备列表适配器,后续可根据设计稿替换 item 展示。 */
class DeviceAdapter : BaseQuickAdapter<DeviceEntity, BaseViewHolder>(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 ?: "暂无地址"}")
}
}

View File

@@ -3,8 +3,11 @@ package com.juhabi.birds.device
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import com.gyf.immersionbar.ImmersionBar
import com.juhabi.birds.R
import com.juhabi.birds.base.BaseActivity import com.juhabi.birds.base.BaseActivity
import com.juhabi.birds.databinding.BindDeviceLayoutBinding import com.juhabi.birds.databinding.BindDeviceLayoutBinding
import com.juhabi.birds.databinding.DeviceDetailLayoutBinding
import com.juhabi.birds.device.entity.DeviceEntity import com.juhabi.birds.device.entity.DeviceEntity
import com.juhabi.birds.device.mvp.DeviceContract import com.juhabi.birds.device.mvp.DeviceContract
import com.juhabi.birds.device.mvp.DeviceImpl import com.juhabi.birds.device.mvp.DeviceImpl
@@ -13,28 +16,100 @@ import com.juhabi.birds.device.mvp.DevicePresenter
/** /**
* 设备详情 * 设备详情
*/ */
class DeviceDetailActivity: BaseActivity<BindDeviceLayoutBinding, DevicePresenter>() , DeviceContract.DeviceView, class DeviceDetailActivity :
BaseActivity<DeviceDetailLayoutBinding, DevicePresenter>(), DeviceContract.DeviceView,
View.OnClickListener { View.OnClickListener {
override fun createBinding(inflater: LayoutInflater): BindDeviceLayoutBinding { companion object {
return BindDeviceLayoutBinding.inflate(inflater) 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? { override fun createPresenter(): DevicePresenter? {
return DevicePresenter(DeviceImpl()) 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 showError(message: String) {
} }
override fun getBindDeviceListSuccess(devices: List<DeviceEntity>) { override fun getBindDeviceListSuccess(devices: List<DeviceEntity>) {
} }
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 bindDeviceSuccess(message: String) {
} }
override fun onClick(view: View) {
}
} }

View File

@@ -1,11 +1,13 @@
package com.juhabi.birds.device package com.juhabi.birds.device
import android.Manifest
import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.juhabi.birds.R import com.juhabi.birds.R
import com.juhabi.birds.base.BaseActivity
import com.juhabi.birds.base.BaseFragment import com.juhabi.birds.base.BaseFragment
import com.juhabi.birds.custom.DeviceActionPopupWindow import com.juhabi.birds.custom.DeviceActionPopupWindow
import com.juhabi.birds.custom.CustomRefreshHeader 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.DeviceContract
import com.juhabi.birds.device.mvp.DeviceImpl import com.juhabi.birds.device.mvp.DeviceImpl
import com.juhabi.birds.device.mvp.DevicePresenter import com.juhabi.birds.device.mvp.DevicePresenter
import com.juhabi.birds.main.fragment.home.mvp.HomeContract import com.juhabi.birds.util.ToastUtil
import com.juhabi.birds.main.fragment.home.mvp.HomeImpl import com.maning.mlkitscanner.scan.MNScanManager
import com.juhabi.birds.main.fragment.home.mvp.HomePresenter import com.maning.mlkitscanner.scan.callback.act.MNScanCallback
import com.scwang.smart.refresh.footer.ClassicsFooter import com.scwang.smart.refresh.footer.ClassicsFooter
class IndexFragment : BaseFragment<FragmentHomeBinding, DevicePresenter>(), class DeviceFragment : BaseFragment<FragmentHomeBinding, DevicePresenter>(),
DeviceContract.DeviceView, View.OnClickListener { DeviceContract.DeviceView, View.OnClickListener {
private val deviceAdapter = DeviceAdapter() private val deviceAdapter = DeviceAdapter()
@@ -37,10 +39,17 @@ class IndexFragment : BaseFragment<FragmentHomeBinding, DevicePresenter>(),
override fun initView(savedInstanceState: Bundle?) { override fun initView(savedInstanceState: Bundle?) {
listOf( listOf(
binding.tvIndex, binding.tvIndex,
binding.imgScan,
).forEach { button -> ).forEach { button ->
button.setOnClickListener(this) button.setOnClickListener(this)
} }
binding.rvDevices.adapter = deviceAdapter 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 -> deviceAdapter.setOnItemChildClickListener { adapter, view, position ->
when (view.id) { when (view.id) {
R.id.img_tips -> { R.id.img_tips -> {
@@ -102,6 +111,17 @@ class IndexFragment : BaseFragment<FragmentHomeBinding, DevicePresenter>(),
R.id.tv_index ->{ R.id.tv_index ->{
goTo(BindDeviceActivity::class.java,null) goTo(BindDeviceActivity::class.java,null)
} }
//扫一扫:先申请相机和相册权限,授权通过后再拉起扫码页。
R.id.img_scan -> {
getMorePermissionRequst(
scanPermissions(),
"相机和相册权限",
"扫一扫功能需要使用相机及访问相册权限",
REQUEST_CAMERA_AND_ALBUM
)
}
} }
} }
@@ -168,17 +188,67 @@ class IndexFragment : BaseFragment<FragmentHomeBinding, DevicePresenter>(),
super.showError(message) super.showError(message)
} }
/** 接口暂无数据时提供临时设备,方便先验收列表布局。 */
private fun temporaryDevices(): List<DeviceEntity> = 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 { private companion object {
const val FIRST_PAGE = 1 const val FIRST_PAGE = 1
const val PAGE_SIZE = 10 const val PAGE_SIZE = 10
const val REMOVE_ANIMATION_DELAY_MS = 300L const val REMOVE_ANIMATION_DELAY_MS = 300L
const val REQUEST_CAMERA_AND_ALBUM = 1001
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
/** 相机和相册均已授权后,自动打开扫一扫。 */
override fun onPermissionsGranted(permissions: Array<String>) {
startDeviceScan()
}
override fun onPermissionsDenied(permissions: Array<String>, permanentlyDenied: Boolean) {
super.onPermissionsDenied(permissions, permanentlyDenied)
}
/** Android 13 及以上申请读图权限,更低版本申请存储读取权限。 */
private fun scanPermissions(): Array<String> {
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))
}
}
}
})
} }
} }

View File

@@ -16,5 +16,6 @@ data class DeviceEntity(
val ringBattery: Int?, val ringBattery: Int?,
val signalLevel: Int, val signalLevel: Int,
val signalText: String, val signalText: String,
val status: String val status: String,
val hoodCoverOn: String
) : Serializable ) : Serializable

View File

@@ -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<T> {
fun success(data: T)
fun fail(message: String)
}
interface DataModel {
/** 查询已绑定的设备列表。 */
fun getBindDeviceList(params: Map<String, String>, callback: Callback<List<DeviceEntity>>)
fun getDeviceDetail(deviceNo: String, callback: Callback<DeviceEntity>)
/** 控制设备眼罩开关。 */
fun controlEye(deviceNo: String, coverOn: Boolean, callback: Callback<BaseBean<Any>>)
/** 根据 IMEI 和设备名称绑定设备。 */
fun bindDevice(json: JSONObject, callback: Callback<BaseBean<Any>>)
/** 根据设备编号修改设备名称。 */
fun renameDevice(
deviceNo: String,
json: JSONObject,
callback: Callback<BaseBean<Any>>
)
/** 根据设备编号解绑设备。 */
fun unbindDevice(deviceNo: String, callback: Callback<BaseBean<Any>>)
/** 取消设备模块中尚未完成的网络请求。 */
fun cancel()
}
interface DeviceView : IView {
fun getBindDeviceListSuccess(devices: List<DeviceEntity>)
fun getDeviceDetailSuccess(device: DeviceEntity) {}
fun controlEyeSuccess(message: String) {}
fun bindDeviceSuccess(message: String)
/** 设备重命名成功回调,不关心该事件的页面可以不实现。 */
fun renameDeviceSuccess(message: String) {
}
/** 设备解绑成功回调,不关心该事件的页面可以不实现。 */
fun unbindDeviceSuccess(message: String) {
}
}
}

View File

@@ -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<BaseBean<List<DeviceEntity>>>? = null
private var deviceDetailCall: Call<BaseBean<DeviceEntity>>? = null
private var controlEyeCall: Call<BaseBean<Any>>? = null
private var bindDeviceCall: Call<BaseBean<Any>>? = null
private var renameDeviceCall: Call<BaseBean<Any>>? = null
private var unbindDeviceCall: Call<BaseBean<Any>>? = null
/** 请求当前账号已绑定的设备列表。 */
override fun getBindDeviceList(params: Map<String, String>, callback: DeviceContract.Callback<List<DeviceEntity>>) {
deviceListCall?.cancel()
deviceListCall = ApiClient.apiService
.getBindDeviceList(NetworkManager.getToken(), params)
.also { call ->
call.enqueue(object : Callback<BaseBean<List<DeviceEntity>>> {
override fun onResponse(
call: Call<BaseBean<List<DeviceEntity>>>,
response: Response<BaseBean<List<DeviceEntity>>>
) {
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<BaseBean<List<DeviceEntity>>>,
error: Throwable
) {
if (!call.isCanceled) {
mainHandler.post {
callback.fail(error.message ?: "网络请求失败")
}
}
}
})
}
}
override fun getDeviceDetail(
deviceNo: String,
callback: DeviceContract.Callback<DeviceEntity>
) {
deviceDetailCall?.cancel()
deviceDetailCall = ApiClient.apiService
.getDeviceDetail(NetworkManager.getToken(), deviceNo)
.also { call ->
call.enqueue(object : Callback<BaseBean<DeviceEntity>> {
override fun onResponse(
call: Call<BaseBean<DeviceEntity>>,
response: Response<BaseBean<DeviceEntity>>
) {
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<BaseBean<DeviceEntity>>,
error: Throwable
) {
if (!call.isCanceled) {
mainHandler.post {
callback.fail(error.message ?: "网络请求失败")
}
}
}
})
}
}
/** 请求服务端控制设备眼罩开关。 */
override fun controlEye(
deviceNo: String,
coverOn: Boolean,
callback: DeviceContract.Callback<BaseBean<Any>>
) {
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<BaseBean<Any>> {
override fun onResponse(
call: Call<BaseBean<Any>>,
response: Response<BaseBean<Any>>
) {
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<BaseBean<Any>>, error: Throwable) {
if (!call.isCanceled) {
mainHandler.post {
callback.fail(error.message ?: "网络请求失败")
}
}
}
})
}
}
/** 将设备 IMEI 和名称提交到服务器完成绑定。 */
override fun bindDevice(
json: JSONObject,
callback: DeviceContract.Callback<BaseBean<Any>>
) {
bindDeviceCall?.cancel()
bindDeviceCall = ApiClient.apiService
.bindDevice(NetworkManager.getToken(), NetworkManager.toRequestBody(json))
.also { call ->
call.enqueue(object : Callback<BaseBean<Any>> {
override fun onResponse(
call: Call<BaseBean<Any>>,
response: Response<BaseBean<Any>>
) {
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<BaseBean<Any>>, error: Throwable) {
if (!call.isCanceled) {
mainHandler.post {
callback.fail(error.message ?: "网络请求失败")
}
}
}
})
}
}
/** 将新的设备名称提交到服务器。 */
override fun renameDevice(
deviceNo: String,
json: JSONObject,
callback: DeviceContract.Callback<BaseBean<Any>>
) {
renameDeviceCall?.cancel()
renameDeviceCall = ApiClient.apiService
.deviceRename(deviceNo = deviceNo, headers = NetworkManager.getToken(), requestBody = NetworkManager.toRequestBody(json))
.also { call ->
call.enqueue(object : Callback<BaseBean<Any>> {
override fun onResponse(
call: Call<BaseBean<Any>>,
response: Response<BaseBean<Any>>
) {
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<BaseBean<Any>>, error: Throwable) {
if (!call.isCanceled) {
mainHandler.post {
callback.fail(error.message ?: "网络请求失败")
}
}
}
})
}
}
/** 按设备编号请求服务端解绑。 */
override fun unbindDevice(
deviceNo: String,
callback: DeviceContract.Callback<BaseBean<Any>>
) {
unbindDeviceCall?.cancel()
unbindDeviceCall = ApiClient.apiService
.unbindDevice(headers = NetworkManager.getToken(), deviceNo = deviceNo)
.also { call ->
call.enqueue(object : Callback<BaseBean<Any>> {
override fun onResponse(
call: Call<BaseBean<Any>>,
response: Response<BaseBean<Any>>
) {
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<BaseBean<Any>>, 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)
}
}

View File

@@ -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<DeviceContract.DeviceView>() {
/** 获取当前账号已经绑定的设备列表。 */
fun getBindDeviceList(params: Map<String, String> = emptyMap()) {
val currentView = view ?: return
currentView.showLoading()
deviceImpl.getBindDeviceList(params, object : DeviceContract.Callback<List<DeviceEntity>> {
override fun success(data: List<DeviceEntity>) {
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<DeviceEntity> {
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<BaseBean<Any>> {
override fun success(data: BaseBean<Any>) {
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<BaseBean<Any>> {
override fun success(data: BaseBean<Any>) {
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<BaseBean<Any>> {
override fun success(data: BaseBean<Any>) {
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<BaseBean<Any>> {
override fun success(data: BaseBean<Any>) {
view?.apply {
hideLoading()
unbindDeviceSuccess(data.message.ifBlank { "设备解绑成功" })
}
}
override fun fail(message: String) {
view?.apply {
hideLoading()
showError(message)
}
}
})
}
/** 销毁页面时取消设备模块发起的网络请求。 */
override fun onDestroy() {
deviceImpl.cancel()
super.onDestroy()
}
}

View File

@@ -2,17 +2,14 @@ package com.juhabi.birds.main
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.juhabi.birds.R import com.juhabi.birds.R
import com.juhabi.birds.base.BaseActivity import com.juhabi.birds.base.BaseActivity
import com.juhabi.birds.databinding.ActivityMainBinding import com.juhabi.birds.databinding.ActivityMainBinding
import com.juhabi.birds.main.adapter.MainPagerAdapter import com.juhabi.birds.main.adapter.MainPagerAdapter
import com.juhabi.birds.main.fragment.HistoryFragment import com.juhabi.birds.device.DeviceFragment
import com.juhabi.birds.device.IndexFragment
import com.juhabi.birds.mine.MineFragment 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.model.AppVersionInfo
import com.juhabi.birds.main.mvp.MainContract import com.juhabi.birds.main.mvp.MainContract
import com.juhabi.birds.main.mvp.MainImpl import com.juhabi.birds.main.mvp.MainImpl
@@ -42,7 +39,7 @@ class MainActivity : BaseActivity<ActivityMainBinding, MainPresenter>(),
override fun initView(savedInstanceState: Bundle?) { override fun initView(savedInstanceState: Bundle?) {
binding.viewPager.adapter = MainPagerAdapter( binding.viewPager.adapter = MainPagerAdapter(
this, this,
listOf(IndexFragment(), PlanFragment(), HistoryFragment(), MineFragment()) listOf(DeviceFragment(), MineFragment())
) )
binding.viewPager.offscreenPageLimit = 1 binding.viewPager.offscreenPageLimit = 1
binding.viewPager.registerOnPageChangeCallback(object : androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback() { binding.viewPager.registerOnPageChangeCallback(object : androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback() {
@@ -60,8 +57,6 @@ class MainActivity : BaseActivity<ActivityMainBinding, MainPresenter>(),
private fun onNavigationItemSelected(itemId: Int): Boolean { private fun onNavigationItemSelected(itemId: Int): Boolean {
return when (itemId) { return when (itemId) {
R.id.navigation_home -> onHomeSelected() R.id.navigation_home -> onHomeSelected()
R.id.navigation_plan -> onPlanSelected()
R.id.navigation_history -> onHistorySelected()
R.id.navigation_mine -> onMineSelected() R.id.navigation_mine -> onMineSelected()
else -> false else -> false
} }
@@ -69,25 +64,11 @@ class MainActivity : BaseActivity<ActivityMainBinding, MainPresenter>(),
private fun onHomeSelected(): Boolean { private fun onHomeSelected(): Boolean {
switchPage(PAGE_HOME) 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 return true
} }
private fun onMineSelected(): Boolean { private fun onMineSelected(): Boolean {
switchPage(PAGE_MINE) switchPage(PAGE_MINE)
// Add Mine-specific status bar, refresh, or Fragment communication here.
return true return true
} }
@@ -99,18 +80,8 @@ class MainActivity : BaseActivity<ActivityMainBinding, MainPresenter>(),
} }
private fun updateTopBarColor(position: Int) { 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) { @ColorRes val colorRes = when (position) {
PAGE_HOME -> R.color.color_zhu PAGE_HOME -> R.color.color_zhu
PAGE_HISTORY -> R.color.color_E9A01C
PAGE_MINE -> R.color.color_B85CE6 PAGE_MINE -> R.color.color_B85CE6
else -> R.color.color_zhu else -> R.color.color_zhu
} }
@@ -131,8 +102,6 @@ class MainActivity : BaseActivity<ActivityMainBinding, MainPresenter>(),
private companion object { private companion object {
const val PAGE_HOME = 0 const val PAGE_HOME = 0
const val PAGE_PLAN = 1 const val PAGE_MINE = 1
const val PAGE_HISTORY = 2
const val PAGE_MINE = 3
} }
} }

View File

@@ -167,14 +167,6 @@ class MineCenterActivity : BaseActivity<ActivityMineCenterBinding, MinePresenter
appUpdateManager.checkAndUpdate(currentVersion, versionInfo) appUpdateManager.checkAndUpdate(currentVersion, versionInfo)
} }
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
appUpdateManager.onRequestPermissionsResult(requestCode, grantResults)
}
/** /**
* 日期、时间和日期时间共用同一个组件调用入口。 * 日期、时间和日期时间共用同一个组件调用入口。
@@ -211,6 +203,16 @@ class MineCenterActivity : BaseActivity<ActivityMineCenterBinding, MinePresenter
showMessage(message) showMessage(message)
} }
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
appUpdateManager.onRequestPermissionsResult(requestCode, grantResults)
}
override fun onPermissionsGranted(permissions: Array<String>) { override fun onPermissionsGranted(permissions: Array<String>) {
showMessage("权限获取成功:${permissions.joinToString { it.substringAfterLast('.') }}") showMessage("权限获取成功:${permissions.joinToString { it.substringAfterLast('.') }}")
} }

View File

@@ -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<ModifyPsdLayoutBinding, MinePresenter>(),
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) {
}
}

View File

@@ -16,11 +16,14 @@ interface MineContract {
params: Map<String, String>, params: Map<String, String>,
callback: Callback<AppVersionInfo> callback: Callback<AppVersionInfo>
) )
fun modifyPassword(oldPassword: String, newPassword: String, callback: Callback<String>)
fun cancel() fun cancel()
} }
interface MineView : IView { interface MineView : IView {
fun getUserInfoSuccess(userInfo: MineUserInfo) fun getUserInfoSuccess(userInfo: MineUserInfo)
fun onVersionChecked(versionInfo: AppVersionInfo) fun onVersionChecked(versionInfo: AppVersionInfo)
/** 修改密码成功回调,非修改密码页面无需处理。 */
fun onModifyPasswordSuccess(message: String) {}
} }
} }

View File

@@ -10,11 +10,13 @@ import com.juhabi.birds.network.NetworkManager
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import org.json.JSONObject
class MineImpl : MineContract.DataModel { class MineImpl : MineContract.DataModel {
private val mainHandler = Handler(Looper.getMainLooper()) private val mainHandler = Handler(Looper.getMainLooper())
private var userInfoCall: Call<BaseBean<MineUserInfo>>? = null private var userInfoCall: Call<BaseBean<MineUserInfo>>? = null
private var versionCall: Call<BaseBean<AppVersionInfo>>? = null private var versionCall: Call<BaseBean<AppVersionInfo>>? = null
private var modifyPasswordCall: Call<BaseBean<Any>>? = null
override fun getUserInfo( override fun getUserInfo(
params: Map<String, String>, params: Map<String, String>,
@@ -96,11 +98,53 @@ class MineImpl : MineContract.DataModel {
} }
} }
/**
* 通过旧密码修改原密码
*/
override fun modifyPassword(
oldPassword: String,
newPassword: String,
callback: MineContract.Callback<String>
) {
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<BaseBean<Any>> {
override fun onResponse(
call: Call<BaseBean<Any>>,
response: Response<BaseBean<Any>>
) {
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<BaseBean<Any>>, error: Throwable) {
if (!call.isCanceled) {
mainHandler.post { callback.fail(error.message ?: "网络请求失败") }
}
}
})
}
}
override fun cancel() { override fun cancel() {
userInfoCall?.cancel() userInfoCall?.cancel()
userInfoCall = null userInfoCall = null
versionCall?.cancel() versionCall?.cancel()
versionCall = null versionCall = null
modifyPasswordCall?.cancel()
modifyPasswordCall = null
mainHandler.removeCallbacksAndMessages(null) mainHandler.removeCallbacksAndMessages(null)
} }
} }

View File

@@ -46,6 +46,27 @@ class MinePresenter(private val mineImpl: MineImpl) : BasePresenter<MineContract
) )
} }
/** 提交修改密码请求,统一处理加载状态和接口结果。 */
fun modifyPassword(oldPassword: String, newPassword: String) {
val currentView = view ?: return
currentView.showLoading()
mineImpl.modifyPassword(oldPassword, newPassword, object : MineContract.Callback<String> {
override fun success(data: String) {
view?.apply {
hideLoading()
onModifyPasswordSuccess(data)
}
}
override fun fail(message: String) {
view?.apply {
hideLoading()
showError(message)
}
}
})
}
override fun onDestroy() { override fun onDestroy() {
mineImpl.cancel() mineImpl.cancel()
super.onDestroy() super.onDestroy()

View File

@@ -126,6 +126,15 @@ interface ApiService {
fun getBindDeviceList(@HeaderMap headers: Map<String, String>, @QueryMap params: Map<String, String>): Call<BaseBean<List<DeviceEntity>>> fun getBindDeviceList(@HeaderMap headers: Map<String, String>, @QueryMap params: Map<String, String>): Call<BaseBean<List<DeviceEntity>>>
/**
* 获取设备详情
*
*/
@GET("app/device/{deviceNo}")
fun getDeviceDetail(@HeaderMap headers: Map<String, String>, @Path("deviceNo") deviceNo: String): Call<BaseBean<DeviceEntity>>
/** /**
* 绑定设备 * 绑定设备
{ {
@@ -173,6 +182,20 @@ interface ApiService {
fun getMineUserInfo(@HeaderMap headers: Map<String, String>, @QueryMap params: Map<String, String>): Call<BaseBean<MineUserInfo>> fun getMineUserInfo(@HeaderMap headers: Map<String, String>, @QueryMap params: Map<String, String>): Call<BaseBean<MineUserInfo>>
/**
* 通过旧密码修改新密码
{
"oldPassword": "string",
"newPassword": "string"
}
*/
@PUT("app/user/password")
fun modifyPassword(@HeaderMap headers: Map<String, String>, @Body requestBody: RequestBody): Call<BaseBean<Any>>
@GET("app/v1/checkVersion") @GET("app/v1/checkVersion")
fun checkVersionCode(@HeaderMap headers: Map<String, String>, @QueryMap params: Map<String, String>): Call<BaseBean<AppVersionInfo>> fun checkVersionCode(@HeaderMap headers: Map<String, String>, @QueryMap params: Map<String, String>): Call<BaseBean<AppVersionInfo>>
} }

View File

@@ -4,18 +4,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<ImageView
android:id="@+id/immersiveTopImageView"
android:layout_width="0dp"
android:layout_height="320dp"
android:contentDescription="@string/plan_top_image_description"
android:scaleType="centerCrop"
android:src="@drawable/img_top"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View <View
android:id="@+id/statusBarView" android:id="@+id/statusBarView"
android:layout_width="0dp" android:layout_width="0dp"

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/statusBarView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@color/color_zhu" />
<RelativeLayout
android:id="@+id/rl_title"
android:layout_width="match_parent"
android:layout_height="48dp">
<include
android:id="@+id/included_title"
layout="@layout/include_activity_back_gray_layout" />
</RelativeLayout>
<TextView
android:id="@+id/tv_bind"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:textSize="15sp"
android:text="绑定"
android:background="@color/color_zhu"/>
<Switch
android:id="@+id/switch_ctrl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>

View File

@@ -20,12 +20,6 @@
android:src="@mipmap/ic_launcher" android:src="@mipmap/ic_launcher"
android:layout_gravity="end"/> android:layout_gravity="end"/>
<ImageView
android:id="@+id/control_eye"
android:layout_width="30dp"
android:layout_height="30dp"
android:src="@mipmap/icon_login_show"
android:layout_gravity="end"/>
</LinearLayout> </LinearLayout>

View File

@@ -14,13 +14,27 @@
android:id="@+id/included_title" android:id="@+id/included_title"
layout="@layout/include_title_home_layout" /> layout="@layout/include_title_home_layout" />
</RelativeLayout> </RelativeLayout>
<TextView
android:id="@+id/tv_index" <LinearLayout
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/home_page_title" android:orientation="horizontal">
android:textColor="@color/black" <TextView
android:textSize="24sp" /> android:id="@+id/tv_index"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/home_page_title"
android:textColor="@color/black"
android:textSize="24sp" />
<ImageView
android:id="@+id/img_scan"
android:layout_width="30dp"
android:layout_height="30dp"
android:src="@mipmap/ic_launcher"
android:layout_marginEnd="15dp"/>
</LinearLayout>
<com.scwang.smart.refresh.layout.SmartRefreshLayout <com.scwang.smart.refresh.layout.SmartRefreshLayout
android:id="@+id/refresh" android:id="@+id/refresh"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -0,0 +1,212 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/color_zhu">
<View
android:id="@+id/statusBarView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@color/color_zhu" />
<RelativeLayout
android:id="@+id/rl_title"
android:layout_width="match_parent"
android:layout_height="48dp">
<include
android:id="@+id/included_title"
layout="@layout/include_activity_back_gray_layout" />
</RelativeLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:paddingBottom="32dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginTop="20dp"
android:gravity="center_vertical"
android:background="@drawable/shape_login_field_bg"
android:paddingStart="44dp"
android:paddingEnd="16dp">
<EditText
android:id="@+id/oldPasswordEditText"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@null"
android:hint="请输入当前密码"
android:inputType="textPassword"
android:textColorHint="@color/color_999999"
android:textSize="16sp" />
<ImageView
android:id="@+id/oldPsdVisibilityButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@mipmap/icon_login_show" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginTop="20dp"
android:gravity="center_vertical"
android:background="@drawable/shape_login_field_bg"
android:paddingStart="44dp"
android:paddingEnd="16dp">
<EditText
android:id="@+id/passwordEditText"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@null"
android:hint="请输入8-20位密码"
android:inputType="textPassword"
android:textColorHint="@color/color_999999"
android:textSize="16sp" />
<ImageView
android:id="@+id/passwordVisibilityButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@mipmap/icon_login_show"
android:contentDescription="显示密码" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginTop="20dp"
android:gravity="center_vertical"
android:background="@drawable/shape_login_field_bg"
android:paddingStart="44dp"
android:paddingEnd="16dp">
<EditText
android:id="@+id/confirmPasswordEditText"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@null"
android:hint="请再次输入新密码"
android:inputType="textPassword"
android:textColorHint="@color/color_999999"
android:textSize="16sp" />
<ImageView
android:id="@+id/confirmPasswordVisibilityButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@mipmap/icon_login_show"
android:contentDescription="显示密码" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="26dp"
android:layout_marginEnd="26dp"
android:layout_marginTop="36dp"
android:orientation="vertical"
android:paddingStart="18dp"
android:paddingVertical="10dp"
android:background="@drawable/shape_register_rule_bg">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="密码规则"
android:textColor="@color/color_white"
android:textSize="18sp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center">
<ImageView
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_marginVertical="5dp"
android:src="@mipmap/icon_login_select"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:text="长度在8-20个字符之间"
android:textColor="@color/white"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center">
<ImageView
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_marginVertical="5dp"
android:src="@mipmap/icon_login_select"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:text="包含大小写字母和数字"
android:textColor="@color/white"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center">
<ImageView
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_marginVertical="5dp"
android:src="@mipmap/icon_login_tips"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:text="不能与当前密码相同"
android:textColor="@color/white"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/confirmButton"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:layout_marginTop="84dp"
android:gravity="center"
android:background="@drawable/shape_login_field_bg"
android:backgroundTint="@null"
android:stateListAnimator="@null"
android:text="确定"
android:textAllCaps="false"
android:textColor="@color/color_zhu"
android:textSize="17sp" />
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="6dp">
<LinearLayout
android:id="@+id/action_rename"
android:layout_width="130dp"
android:layout_height="42dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="10dp">
<ImageView
android:layout_width="22dp"
android:layout_height="22dp"
android:src="@mipmap/ic_launcher"
android:contentDescription="@string/device_action_rename" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="重命名"
android:textColor="#FFFFFF"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/action_unbind"
android:layout_width="130dp"
android:layout_height="42dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="10dp">
<ImageView
android:layout_width="22dp"
android:layout_height="22dp"
android:src="@mipmap/ic_launcher"
android:contentDescription="@string/device_action_unbind" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="解绑设备"
android:textColor="#FFFFFF"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>

View File

@@ -4,16 +4,6 @@
android:id="@+id/navigation_home" android:id="@+id/navigation_home"
android:icon="@drawable/ic_nav_home" android:icon="@drawable/ic_nav_home"
android:title="@string/tab_home" /> android:title="@string/tab_home" />
<item
android:id="@+id/navigation_plan"
android:icon="@drawable/ic_nav_plan"
android:title="@string/tab_plan"
android:visible="false"/>
<item
android:id="@+id/navigation_history"
android:icon="@drawable/ic_nav_history"
android:title="@string/tab_history"
android:visible="false"/>
<item <item
android:id="@+id/navigation_mine" android:id="@+id/navigation_mine"
android:icon="@drawable/ic_nav_mine" android:icon="@drawable/ic_nav_mine"

View File

@@ -67,6 +67,7 @@
<string name="home_page_title">首页</string> <string name="home_page_title">首页</string>
<string name="device_action_rename">重命名</string> <string name="device_action_rename">重命名</string>
<string name="device_action_unbind">解绑设备</string> <string name="device_action_unbind">解绑设备</string>
<string name="cancel_scan">取消扫码</string>
<string name="plan_page_title">计划</string> <string name="plan_page_title">计划</string>
<string name="history_page_title">历史</string> <string name="history_page_title">历史</string>
<string name="mine_page_title">我的</string> <string name="mine_page_title">我的</string>

View File

@@ -6,7 +6,7 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html # http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process. # Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings. # 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. # When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit # This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects