封装二维码扫描业务

This commit is contained in:
qiji4215 2023-06-21 17:51:49 +08:00
parent d5fbeec02b
commit d0b85d649a
19 changed files with 616 additions and 6 deletions

View File

@ -115,6 +115,18 @@ dependencies {
//kotlin反射
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.7.0"
implementation "org.jetbrains.kotlin:kotlin-reflect:1.7.0"
implementation 'com.permissionx.guolindev:permissionx:1.4.0'
def camerax_version = "1.1.0-alpha04"
// The following line is optional, as the core library is included indirectly by camera-camera2
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
// If you want to additionally use the CameraX Lifecycle library
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
// If you want to additionally use the CameraX View class
implementation "androidx.camera:camera-view:1.0.0-alpha24"
implementation 'com.google.mlkit:barcode-scanning:16.1.1'
}
//
kapt {

View File

@ -4,6 +4,11 @@
android:versionCode="3"
android:versionName="1.4"
package="com.navinfo.omqs">
<!-- 这个权限用于相机权限-->
<uses-feature android:name="android.hardware.camera.any" />
<uses-feature
android:name="android.hardware.camera"
android:required="true" />
<!-- 这个权限用于进行网络定位-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- 这个权限用于访问GPS定位-->
@ -31,6 +36,9 @@
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<!-- 读取缓存数据 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- 相机权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- 音频权限 -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!--android:largeHeap="true" 大内存 128M -->
<application
@ -63,6 +71,14 @@
android:launchMode="singleTask"
android:screenOrientation="landscape"
android:theme="@style/Theme.OMQualityInspection" />
<activity
android:name=".ui.activity.scan.QRCodeActivity"
android:screenOrientation="portrait" />
<activity
android:name=".ui.activity.scan.QRCodeResultActivity"
android:screenOrientation="portrait" />
<meta-data
android:name="ScopedStorage"
android:value="true" />

View File

@ -45,6 +45,11 @@ class Constant {
*/
lateinit var DOWNLOAD_PATH: String
/**
* 室内整理工具IP
*/
lateinit var INDOOR_IPS: String
const val DEBUG = true
var IS_VIDEO_SPEED by kotlin.properties.Delegates.notNull<Boolean>()

View File

@ -0,0 +1,6 @@
package com.navinfo.omqs.bean
data class QRCodeBean(
var errcode: Int = -1,
var msg: String = ""
)

View File

@ -3,6 +3,7 @@ package com.navinfo.omqs.http
import com.navinfo.omqs.bean.OfflineMapCityBean
import com.navinfo.collect.library.data.entity.TaskBean
import com.navinfo.omqs.bean.LoginUserBean
import com.navinfo.omqs.bean.QRCodeBean
import com.navinfo.omqs.bean.SysUserBean
import okhttp3.ResponseBody
import retrofit2.Response
@ -15,14 +16,20 @@ interface NetworkService {
/**
* 获取离线地图城市列表
*/
suspend fun getOfflineMapCityList():NetResult<List<OfflineMapCityBean>>
suspend fun getOfflineMapCityList(): NetResult<List<OfflineMapCityBean>>
/**
* 获取任务列表
*/
suspend fun getTaskList(evaluatorNo:String): NetResult<DefaultResponse<List<TaskBean>>>
suspend fun getTaskList(evaluatorNo: String): NetResult<DefaultResponse<List<TaskBean>>>
/**
* 登录接口
*/
suspend fun loginUser(loginUserBean: LoginUserBean): NetResult<DefaultResponse<SysUserBean>>
/**
* 连接室内整理工具
*/
suspend fun connectIndoorTools(url: String): NetResult<DefaultResponse<QRCodeBean>>
}

View File

@ -3,6 +3,7 @@ package com.navinfo.omqs.http
import com.navinfo.omqs.bean.OfflineMapCityBean
import com.navinfo.collect.library.data.entity.TaskBean
import com.navinfo.omqs.bean.LoginUserBean
import com.navinfo.omqs.bean.QRCodeBean
import com.navinfo.omqs.bean.SysUserBean
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -75,4 +76,23 @@ class NetworkServiceImpl @Inject constructor(
NetResult.Error<Any>(e)
}
}
override suspend fun connectIndoorTools(url: String): NetResult<DefaultResponse<QRCodeBean>> =
//在IO线程中运行
withContext(Dispatchers.IO) {
return@withContext try {
val result = netApi.retrofitConnectIndoorTools(url)
if (result.isSuccessful) {
if (result.code() == 200) {
NetResult.Success(result.body())
} else {
NetResult.Failure<Any>(result.code(), result.message())
}
} else {
NetResult.Failure<Any>(result.code(), result.message())
}
} catch (e: Exception) {
NetResult.Error<Any>(e)
}
}
}

View File

@ -4,6 +4,7 @@ import com.navinfo.omqs.bean.EvaluationInfo
import com.navinfo.omqs.bean.OfflineMapCityBean
import com.navinfo.collect.library.data.entity.TaskBean
import com.navinfo.omqs.bean.LoginUserBean
import com.navinfo.omqs.bean.QRCodeBean
import com.navinfo.omqs.bean.SysUserBean
import okhttp3.ResponseBody
import retrofit2.Response
@ -64,6 +65,14 @@ interface RetrofitNetworkServiceAPI {
@Query("evaluatorNo") evaluatorNo: String,
): Response<DefaultResponse<List<TaskBean>>>
/**
* 获取离线地图城市列表
*/
@GET("/drdc/MapDownload/maplist")
suspend fun retrofitConnectIndoorTools(@Url url: String): Response<DefaultResponse<QRCodeBean>>
@Headers("Content-Type: application/json")
@POST("/devcp/uploadSceneProblem")
suspend fun postRequest(@Body listEvaluationInfo: List<EvaluationInfo>?): Response<DefaultResponse<*>>

View File

@ -37,7 +37,8 @@ public class CheckPermissionsActivity extends BaseActivity {
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.RECORD_AUDIO
Manifest.permission.RECORD_AUDIO,
Manifest.permission.CAMERA,
};
private static final int PERMISSON_REQUESTCODE = 0;
@ -53,6 +54,7 @@ public class CheckPermissionsActivity extends BaseActivity {
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.CAMERA,
BACKGROUND_LOCATION_PERMISSION
};
}

View File

@ -123,8 +123,6 @@ class MainActivity : BaseActivity() {
checkIntent.action = TextToSpeech.Engine.ACTION_CHECK_TTS_DATA
someActivityResultLauncher.launch(checkIntent)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
//初始化地图

View File

@ -0,0 +1,125 @@
package com.navinfo.omqs.ui.activity.scan
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Rect
import android.graphics.RectF
import android.os.Bundle
import android.util.Log
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.ImageCapture
import androidx.camera.view.LifecycleCameraController
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.viewModelScope
import com.navinfo.omqs.R
import com.navinfo.omqs.databinding.ActivityQrCodeBinding
import com.navinfo.omqs.ui.activity.login.LoginViewModel
import com.navinfo.omqs.ui.listener.QRCodeAnalyser
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
/**
* date:2021/6/18
* author:zhangteng
* description:二维码扫描
*/
class QRCodeActivity : AppCompatActivity() {
private lateinit var binding: ActivityQrCodeBinding
private lateinit var lifecycleCameraController: LifecycleCameraController
private lateinit var cameraExecutor: ExecutorService
private val viewModel by viewModels<QRCodeViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_qr_code)
binding.qrCodeModel = viewModel
binding.lifecycleOwner = this
binding.activity = this
initController()
}
@SuppressLint("ClickableViewAccessibility", "UnsafeOptInUsageError")
private fun initController() {
cameraExecutor = Executors.newSingleThreadExecutor()
lifecycleCameraController = LifecycleCameraController(this)
lifecycleCameraController.bindToLifecycle(this)
lifecycleCameraController.imageCaptureFlashMode = ImageCapture.FLASH_MODE_AUTO
lifecycleCameraController.setImageAnalysisAnalyzer(
cameraExecutor,
QRCodeAnalyser { barcodes, imageWidth, imageHeight ->
if (barcodes.isEmpty()) {
return@QRCodeAnalyser
}
initScale(imageWidth, imageHeight)
val list = ArrayList<RectF>()
val strList = ArrayList<String>()
barcodes.forEach { barcode ->
barcode.boundingBox?.let { rect ->
val translateRect = translateRect(rect)
list.add(translateRect)
Log.e(
"ztzt", "left${translateRect.left} +" +
" top${translateRect.top} + right${translateRect.right}" +
" + bottom${translateRect.bottom}"
)
Log.e("ztzt", "barcode.rawValue${barcode.rawValue}")
strList.add(barcode.rawValue ?: "No Value")
}
}
judgeIntent(strList)
binding.scanView.setRectList(list)
})
binding.previewView.controller = lifecycleCameraController
}
fun judgeIntent(list: ArrayList<String>) {
val sb = StringBuilder()
list.forEach {
sb.append(it)
sb.append("\n")
}
intentToResult(sb.toString())
}
private fun intentToResult(result: String) {
viewModel.connect(this, result)
Log.e("qj", "QRCodeActivity === $result")
/* val intent = Intent(this, QRCodeResultActivity::class.java)
intent.putExtra(QRCodeResultActivity.RESULT_KEY, result)
startActivity(intent)
finish()*/
}
private var scaleX = 0f
private var scaleY = 0f
private fun translateX(x: Float): Float = x * scaleX
private fun translateY(y: Float): Float = y * scaleY
//将扫描的矩形换算为当前屏幕大小
private fun translateRect(rect: Rect) = RectF(
translateX(rect.left.toFloat()),
translateY(rect.top.toFloat()),
translateX(rect.right.toFloat()),
translateY(rect.bottom.toFloat())
)
//初始化缩放比例
private fun initScale(imageWidth: Int, imageHeight: Int) {
Log.e("ztzt", "imageWidth${imageWidth} + imageHeight${imageHeight}")
scaleY = binding.scanView.height.toFloat() / imageWidth.toFloat()
scaleX = binding.scanView.width.toFloat() / imageHeight.toFloat()
Log.e("ztzt", "scaleX${scaleX} + scaleY${scaleY}")
}
}

View File

@ -0,0 +1,30 @@
package com.navinfo.omqs.ui.activity.scan
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.navinfo.omqs.databinding.ActivityResultBinding
/**
* date:2021/6/18
* author:zhangteng
* description:
*/
class QRCodeResultActivity : AppCompatActivity() {
private lateinit var binding: ActivityResultBinding
companion object {
const val RESULT_KEY = "result_key";
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityResultBinding.inflate(layoutInflater)
setContentView(binding.root)
val intent = intent
binding.text.text = intent.getStringExtra(RESULT_KEY)
binding.button.setOnClickListener {
finish()
}
}
}

View File

@ -0,0 +1,151 @@
package com.navinfo.omqs.ui.activity.scan
import android.content.Context
import android.text.TextUtils
import android.widget.Toast
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.navinfo.omqs.bean.QRCodeBean
import com.navinfo.omqs.bean.SysUserBean
import com.navinfo.omqs.http.DefaultResponse
import com.navinfo.omqs.http.NetResult
import com.navinfo.omqs.http.NetworkService
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.IOException
import javax.inject.Inject
enum class QRCodeStatus {
/**
* 访问服务器登陆中
*/
LOGIN_STATUS_NET_LOADING,
/**
* 访问离线地图列表
*/
LOGIN_STATUS_NET_OFFLINE_MAP,
/**
* 初始化文件夹
*/
LOGIN_STATUS_FOLDER_INIT,
/**
* 创建文件夹失败
*/
LOGIN_STATUS_FOLDER_FAILURE,
/**
* 网络访问失败
*/
LOGIN_STATUS_NET_FAILURE,
/**
* 成功
*/
LOGIN_STATUS_SUCCESS,
/**
* 取消
*/
LOGIN_STATUS_CANCEL,
}
@HiltViewModel
class QRCodeViewModel @Inject constructor(
private val networkService: NetworkService
) : ViewModel() {
//用户信息
val qrCodeBean: MutableLiveData<QRCodeBean> = MutableLiveData()
//是不是连接成功
val qrCodeStatus: MutableLiveData<QRCodeStatus> = MutableLiveData()
var jobQRCodeStatus: Job? = null;
init {
qrCodeBean.value = QRCodeBean()
}
/**
* 扫一扫按钮
*/
fun connect(context: Context, ips: String) {
if (TextUtils.isEmpty(ips)) {
Toast.makeText(context, "获取ip失败", Toast.LENGTH_LONG).show()
return
}
val ipArray = ips.split(";".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
//测试代码
//final String[] ipArray = new String[]{"172.21.2.137"};
if (ipArray.isEmpty()) {
Toast.makeText(context, "获取ip失败", Toast.LENGTH_SHORT).show()
return
}
ipArray.forEach { ip ->
if (!TextUtils.isEmpty(ip)) {
viewModelScope.launch(Dispatchers.Default) {
val ipTemp: String = ip
val url = "http://$ipTemp:8080/sensor/service/keepalive"
when (val result = networkService.connectIndoorTools(url)) {
is NetResult.Success<*> -> {
if (result.data != null) {
try {
val defaultUserResponse =
result.data as DefaultResponse<SysUserBean>
if (defaultUserResponse.success) {
} else {
withContext(Dispatchers.Main) {
Toast.makeText(
context,
"${defaultUserResponse.msg}",
Toast.LENGTH_SHORT
)
.show()
}
}
} catch (e: IOException) {
}
}
}
is NetResult.Error<*> -> {
withContext(Dispatchers.Main) {
Toast.makeText(
context,
"${result.exception.message}",
Toast.LENGTH_SHORT
)
.show()
}
}
is NetResult.Failure<*> -> {
withContext(Dispatchers.Main) {
Toast.makeText(
context,
"${result.code}:${result.msg}",
Toast.LENGTH_SHORT
)
.show()
}
}
else -> {}
}
}
}
}
}
}

View File

@ -1,13 +1,14 @@
package com.navinfo.omqs.ui.fragment.personalcenter
import android.Manifest
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.blankj.utilcode.util.ToastUtils
import com.blankj.utilcode.util.UriUtils
@ -21,6 +22,8 @@ import com.navinfo.omqs.db.ImportOMDBHelper
import com.navinfo.omqs.hilt.ImportOMDBHiltFactory
import com.navinfo.omqs.tools.CoroutineUtils
import com.navinfo.omqs.ui.fragment.BaseFragment
import com.navinfo.omqs.ui.activity.scan.QRCodeActivity
import com.permissionx.guolindev.PermissionX
import dagger.hilt.android.AndroidEntryPoint
import org.oscim.core.GeoPoint
import javax.inject.Inject
@ -123,6 +126,10 @@ class PersonalCenterFragment(private var backListener: (() -> Unit?)? = null) :
R.id.personal_center_menu_layer_manager -> { // 图层管理
findNavController().navigate(R.id.QsLayerManagerFragment)
}
R.id.personal_center_menu_scan_qr_code -> {
//跳转二维码扫描界面
checkPermission()
}
}
true
}
@ -134,6 +141,11 @@ class PersonalCenterFragment(private var backListener: (() -> Unit?)? = null) :
fileChooser.setCallbacks(this@PersonalCenterFragment)
}
private fun intentTOQRCode() {
var intent = Intent(context, QRCodeActivity::class.java);
startActivity(intent)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
@ -147,4 +159,18 @@ class PersonalCenterFragment(private var backListener: (() -> Unit?)? = null) :
super.onActivityResult(requestCode, resultCode, data)
fileChooser.onActivityResult(requestCode, resultCode, data)
}
private fun checkPermission() {
PermissionX.init(this)
.permissions(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
.request { allGranted, grantedList, deniedList ->
if (allGranted) {
//所有权限已经授权
Toast.makeText(context,"授权成功",Toast.LENGTH_LONG).show()
intentTOQRCode()
} else {
Toast.makeText(context, "拒绝权限: $deniedList", Toast.LENGTH_LONG).show()
}
}
}
}

View File

@ -0,0 +1,43 @@
package com.navinfo.omqs.ui.listener
import android.annotation.SuppressLint
import android.util.Log
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import com.google.mlkit.vision.barcode.Barcode
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.common.InputImage
class QRCodeAnalyser(private val listener: (List<Barcode>, Int, Int) -> Unit) :
ImageAnalysis.Analyzer {
//配置当前扫码格式
private val options = BarcodeScannerOptions.Builder()
.setBarcodeFormats(
Barcode.FORMAT_QR_CODE,
Barcode.FORMAT_AZTEC
).build()
//获取解析器
private val detector = BarcodeScanning.getClient(options)
@SuppressLint("UnsafeExperimentalUsageError", "UnsafeOptInUsageError")
override fun analyze(imageProxy: ImageProxy) {
val mediaImage = imageProxy.image ?: kotlin.run {
imageProxy.close()
return
}
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
detector.process(image)
.addOnSuccessListener { barCodes ->
Log.e("ztzt", "barCodes: ${barCodes.size}")
if (barCodes.size > 0) {
listener.invoke(barCodes, imageProxy.width, imageProxy.height)
//接收到结果后,就关闭解析
detector.close()
}
}
.addOnFailureListener { Log.e("ztzt", "Error: ${it.message}") }
.addOnCompleteListener { imageProxy.close() }
}
}

View File

@ -0,0 +1,88 @@
package com.navinfo.omqs.ui.widget
import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import androidx.core.content.ContextCompat
import com.navinfo.omqs.R
/**
* Author:zhangteng
* description:
* date2021/6/19
*/
class ScanView(context: Context, attrs: AttributeSet) : View(context, attrs) {
private val circlePaint = Paint() //二维码圆圈画笔
private var rectList: ArrayList<RectF>? = null //二维码数组
private var scanLine: Bitmap//横线
private var isShowLine = true//是否显示扫描线
private var animator: ObjectAnimator? = null
private var floatYFraction = 0f
set(value) {
field = value
invalidate()
}
init {
circlePaint.apply {
this.style = Paint.Style.FILL
this.color = ContextCompat.getColor(
context, android.R.color.holo_green_dark
)
}
scanLine = BitmapFactory.decodeResource(resources, R.drawable.scan_light)
getAnimator().start()
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
parseResult(canvas)
if (isShowLine) {
canvas?.drawBitmap(scanLine, (width - scanLine.width) / 2f, height * floatYFraction, circlePaint)
}
}
private fun getAnimator(): ObjectAnimator {
if (animator == null) {
animator = ObjectAnimator.ofFloat(
this,
"floatYFraction",
0f,
1f
)
animator?.duration = 5000
animator?.repeatCount = -1 //-1代表无限循环
}
return animator!!
}
private fun parseResult(canvas: Canvas?) {
rectList?.let { list ->
if (list.isEmpty()) {
return
}
list.forEach {
canvas?.drawCircle(
it.left + (it.right - it.left) / 2f,
it.top + (it.bottom - it.top) / 2f,
50f,
circlePaint
)
}
}
}
fun setRectList(list: ArrayList<RectF>?) {
rectList = list
rectList?.let {
if (it.isNotEmpty()) {
isShowLine = false
getAnimator().cancel()
invalidate()
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".ui.activity.scan.QRCodeActivity">
<data>
<import type="android.view.View" />
<variable
name="activity"
type="com.navinfo.omqs.ui.activity.scan.QRCodeActivity" />
<variable
name="qrCodeModel"
type="com.navinfo.omqs.ui.activity.scan.QRCodeViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.helper.widget.Layer
android:layout_width="match_parent"
android:layout_height="match_parent"
app:constraint_referenced_ids="previewView,scanView"
tools:ignore="MissingConstraints">
</androidx.constraintlayout.helper.widget.Layer>
<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.navinfo.omqs.ui.widget.ScanView
android:id="@+id/scanView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@ -0,0 +1,19 @@
<?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">
<TextView
android:id="@+id/text"
android:textColor="@color/black"
android:textSize="20sp"
android:layout_width="match_parent"
android:layout_height="200dp" />
<Button
android:id="@+id/button"
android:text="返回"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>

View File

@ -22,6 +22,10 @@
android:id="@+id/personal_center_menu_import_yuan_data"
android:icon="@drawable/ic_baseline_scatter_plot_24"
android:title="导入元数据" />
<item
android:id="@+id/personal_center_menu_scan_qr_code"
android:icon="@drawable/ic_baseline_scatter_plot_24"
android:title="扫一扫" />
<item
android:icon="@drawable/ic_baseline_sim_card_download_24"
android:title="备份数据" />