当前位置: 首页 > news >正文

[TOTP]android kotlin实现 totp身份验证器 类似Google身份验证器

        背景:自己或者公司用一些谷歌身份验证器或者microsoft身份验证器,下载来源不明,或者有广告,使用不安全。于是自己写一个,安全放心使用。

代码已开源:shixiaotian/sxt-android-totp: android totp authenticator (github.com)

效果图

  

此身份验证器,一共1个activity,3个fragment。

实现原理,通过线程动态触发totp算法,从加密的sqlite里读取密钥信息进行加密计算。新增密钥可通过扫描二维码,或者手动添加的方式实现。

一.添加对应的包

包括,totp算法包,zxing二维码扫描包,sqlite加密包

分别为

authenticator="1.0.0"
zxing-android-embedded="4.2.0"
database-sqlcipher="4.5.0"androidx-authenticator = { group = "org.jboss.aerogear", name = "aerogear-otp-java", version.ref = "authenticator" }
androidx-zxing-android-embedded = { group = "com.journeyapps", name = "zxing-android-embedded", version.ref = "zxing-android-embedded" }
androidx-database-sqlcipher ={ group = "net.zetetic", name = "android-database-sqlcipher", version.ref = "database-sqlcipher" }

  文件

libs.versions.toml

[versions]
agp = "8.7.2"
kotlin = "1.9.24"
coreKtx = "1.10.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
appcompat = "1.6.1"
material = "1.10.0"
activity = "1.8.0"
constraintlayout = "2.1.4"
authenticator="1.0.0"
zxing-android-embedded="4.2.0"
database-sqlcipher="4.5.0"[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
androidx-authenticator = { group = "org.jboss.aerogear", name = "aerogear-otp-java", version.ref = "authenticator" }
androidx-zxing-android-embedded = { group = "com.journeyapps", name = "zxing-android-embedded", version.ref = "zxing-android-embedded" }
androidx-database-sqlcipher ={ group = "net.zetetic", name = "android-database-sqlcipher", version.ref = "database-sqlcipher" }[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

 build.gradele.kts

plugins {alias(libs.plugins.android.application)alias(libs.plugins.kotlin.android)
}android {namespace = "com.shixiaotian.totp.scan.application"compileSdk = 34defaultConfig {applicationId = "com.shixiaotian.totp.scan.application"minSdk = 24targetSdk = 34versionCode = 1versionName = "1.0"testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"}buildTypes {release {isMinifyEnabled = falseproguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"),"proguard-rules.pro")}}compileOptions {sourceCompatibility = JavaVersion.VERSION_11targetCompatibility = JavaVersion.VERSION_11}kotlinOptions {jvmTarget = "11"}
}dependencies {implementation(libs.androidx.core.ktx)implementation(libs.androidx.appcompat)implementation(libs.material)implementation(libs.androidx.activity)implementation(libs.androidx.constraintlayout)implementation(libs.androidx.authenticator)implementation(libs.androidx.espresso.core)implementation(libs.androidx.zxing.android.embedded)implementation(libs.androidx.database.sqlcipher)testImplementation(libs.junit)androidTestImplementation(libs.androidx.junit)androidTestImplementation(libs.androidx.espresso.core)}

二.项目程序文件结构为

三.全部程序文件

MyConstant

作用:常量类,添加了如下三个参数,方便自行变换,增加安全性和防止撞库

package com.shixiaotian.totp.scan.application.commonclass MyConstants {companion object {//数据库名称,记得加扰动防止重复const val dbName = "sxt.auth.code.0098675.db"// 数据库密码const val dbPassword = "jsdfjhkldsvcbuehuisudbekokmhshyebgqoondyasd"// app 首次运行标记const val firstRunTag = "sxt.auth.code.0098674.firstRunTag"}
}

DatabaseHelper

作用:数据库操作相关类,集成了数据库加密,初始化,基本插入、读取、删除等功能

package com.shixiaotian.totp.scan.application.dbimport android.content.Context
import android.database.Cursor
import androidx.core.content.contentValuesOf
import com.shixiaotian.totp.scan.application.common.MyConstants
import com.shixiaotian.totp.scan.application.vo.User
import net.sqlcipher.database.SQLiteDatabase
import net.sqlcipher.database.SQLiteOpenHelperclass DatabaseHelper(context: Context) : SQLiteOpenHelper(context, MyConstants.dbName, null, 1) {private val dbContext:Context = contextoverride fun onCreate(db: SQLiteDatabase) {}override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {// 更新数据库的时候调用}private fun openEncryptedDatabase(): SQLiteDatabase {val dbHelper = DatabaseHelper(dbContext)val db = dbHelper.getWritableDatabase(MyConstants.dbPassword)return db}// 写入数据fun insertUser(username: String, secretKey: String, issuer: String): Long {val db = this.openEncryptedDatabase()var id =db.insert("sxt_totp_users", null, contentValuesOf("username" to username, "secretKey" to secretKey, "issuer" to issuer))db.close()return id}// 删除数据fun deleteUser(id: Int) {val db = this.openEncryptedDatabase()db.delete("sxt_totp_users", "id = ?", arrayOf(id.toString()))db.close()}// 查询数据fun getUser(id: Int): User? {val db = this.openEncryptedDatabase()val cursor: Cursor = db.query("sxt_totp_users", null, "id = ?", arrayOf(id.toString()), null, null, null)var user: User? = nullif (cursor.moveToFirst()) {val id = cursor.getInt(0)val name = cursor.getString(1)val secretKey = cursor.getString(2)val issuer = cursor.getString(3)// 假设User有id和name两个字段user = User(id, name,secretKey, issuer)}cursor.close()db.close()return user}fun getAllUser(): List<User> {val db = this.openEncryptedDatabase()val items = ArrayList<User>()val cursor: Cursor  = db.query("sxt_totp_users", arrayOf("id","username","secretKey", "issuer"), null, null, null, null, null)cursor.moveToFirst()while (!cursor.isAfterLast) {val id = cursor.getInt(0)val name = cursor.getString(1)val secretKey = cursor.getString(2)val issuer = cursor.getString(3)items.add(User(id, name,secretKey, issuer))cursor.moveToNext()}cursor.close()db.close()return items}fun initDB() {// 创建表val db = this.openEncryptedDatabase()db.execSQL("CREATE TABLE sxt_totp_users (id INTEGER PRIMARY KEY, username TEXT, secretKey TEXT, issuer TEXT)")db.close()}fun init() {this.insertUser("apple","apple", "github")this.insertUser("pear","pear", "steam")this.insertUser("apricot","apricot","wiki")this.insertUser("peach","peach","TK")}
}

CodeAddFragment

作用:令牌添加页面片段,提供手动输入令牌,和触发zxing令牌扫描功能,并回收zxing扫描后的结果,进行存储,将相关id数据传输给codeShow单独展示的页面进行展示。

package com.shixiaotian.totp.scan.application.fragmentsimport android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import com.shixiaotian.totp.scan.application.db.DatabaseHelper
import com.shixiaotian.totp.scan.application.tools.EncodeTools
import com.google.zxing.integration.android.IntentIntegrator
import com.journeyapps.barcodescanner.CaptureActivity
import com.shixiaotian.totp.scan.application.R// TODO: Rename parameter arguments, choose names that match
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"/*** A simple [Fragment] subclass.* Use the [CodeAddFragment.newInstance] factory method to* create an instance of this fragment.*/
class CodeAddFragment : Fragment() {private lateinit var saveButton: Viewprivate lateinit var scanButton: Viewprivate lateinit var nameText: TextViewprivate lateinit var secretKeyText: TextViewprivate lateinit var issuerText: TextView// TODO: Rename and change types of parametersprivate var param1: String? = nullprivate var param2: String? = nulloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)arguments?.let {param1 = it.getString(ARG_PARAM1)param2 = it.getString(ARG_PARAM2)}}@SuppressLint("MissingInflatedId")override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,savedInstanceState: Bundle?): View? {// Inflate the layout for this fragmentval view = inflater.inflate(R.layout.fragment_code_add, container, false)val dbHelper = DatabaseHelper(requireContext())saveButton = view.findViewById(R.id.saveButton)saveButton.setOnClickListener {nameText = view.findViewById<TextView>(R.id.addUsernameText)val name = nameText.getText();secretKeyText = view.findViewById<TextView>(R.id.addSecretKeyText)val secretKey = secretKeyText.getText();issuerText = view.findViewById<TextView>(R.id.addIssuerText)val issuer = issuerText.getText();if(name.isEmpty()) {alertAddError("Name can't be blank")} else if(secretKey.isEmpty()) {alertAddError("SecretKey can't be blank")} else if(issuer.isEmpty()){alertAddError("issuer can't be blank")} else {var saveId = dbHelper.insertUser(name.toString(), secretKey.toString(), issuer.toString());nameText.setText("")secretKeyText.setText("")issuerText.setText("")val fragment = CodeShowFragment.newInstance(saveId.toString(), "")parentFragmentManager.beginTransaction().replace(R.id.viewPager, fragment).commit()}}scanButton = view.findViewById(R.id.cameraButton)scanButton.setOnClickListener {val integrator = IntentIntegrator.forSupportFragment(this)integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)integrator.setOrientationLocked(false)integrator.captureActivity = CaptureActivity::class.javaintegrator.setRequestCode(5766)  //_scan为自己定义的请求码integrator.initiateScan()}return view}fun alertAddError(msg : String) {println("alertAddError : " + msg)val builder = AlertDialog.Builder(context)builder.setTitle("add error")builder.setMessage(msg)builder.setPositiveButton("OK") { dialog, _ ->dialog.dismiss()}builder.create().show()}override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {super.onActivityResult(requestCode, resultCode, data)when (requestCode) {//_scan为自己定义的扫码请求码5766 -> {// 跳转扫描页面返回扫描数据var scanResult = IntentIntegrator.parseActivityResult(IntentIntegrator.REQUEST_CODE,resultCode,data);//  判断返回值是否为空if (scanResult != null) {//返回条形码数据var result = scanResult.contentsif(result == null) {parentFragmentManager.beginTransaction().replace(R.id.viewPager, this).commit()return}val user = EncodeTools.decode(result);if(user == null) {Toast.makeText(context, "Scan Fail", Toast.LENGTH_SHORT).show()parentFragmentManager.beginTransaction().replace(R.id.viewPager, this).commit()return}// 保存数据val dbHelper = DatabaseHelper(requireContext())var saveId = dbHelper.insertUser(user!!.getUsername(), user.getSecretKey(), user.getIssuer())if(saveId < 0 ) {Toast.makeText(context, "Save Data Fail", Toast.LENGTH_SHORT).show()parentFragmentManager.beginTransaction().replace(R.id.viewPager, this).commit()return}// 跳转val fragment = CodeShowFragment.newInstance(saveId.toString(), "")parentFragmentManager.beginTransaction().replace(R.id.viewPager, fragment).commit()} else {Toast.makeText(context, "Scan Fail:ERROR", Toast.LENGTH_SHORT).show()parentFragmentManager.beginTransaction().replace(R.id.viewPager, this).commit()}} else -> {val fragment = CodeAddFragment()parentFragmentManager.beginTransaction().replace(R.id.viewPager, fragment).commit()}}}companion object {/*** Use this factory method to create a new instance of* this fragment using the provided parameters.** @param param1 Parameter 1.* @param param2 Parameter 2.* @return A new instance of fragment CodeAddFragment.*/// TODO: Rename and change types and number of parameters@JvmStaticfun newInstance(param1: String, param2: String) =CodeAddFragment().apply {arguments = Bundle().apply {putString(ARG_PARAM1, param1)putString(ARG_PARAM2, param2)}}}
}

CodeListFragment

作用:令牌列表界面,提供了定时器刷新整个列表的功能,和每个令牌单独点击触发的功能

package com.shixiaotian.totp.scan.application.fragmentsimport android.annotation.SuppressLint
import android.os.Bundle
import android.os.Handler
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ListView
import android.widget.TextView
import com.shixiaotian.totp.scan.application.CodeListAdapter
import com.shixiaotian.totp.scan.application.R
import com.shixiaotian.totp.scan.application.db.DatabaseHelper
import com.shixiaotian.totp.scan.application.tools.MyTimeUtils
import com.shixiaotian.totp.scan.application.vo.User// TODO: Rename parameter arguments, choose names that match
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"/*** A simple [Fragment] subclass.* Use the [CodeListFragment.newInstance] factory method to* create an instance of this fragment.*/
class CodeListFragment : Fragment() {private var start: Long = 30private lateinit var adapter: CodeListAdapterprivate val handler = Handler()private var runnable: Runnable? = null// TODO: Rename and change types of parametersprivate var param1: String? = nullprivate var param2: String? = nullprivate var userList: List<User>? =nullprivate val data = listOf("apple","pear","apricot","peach","grape","banana","pineapple","plum","watermelon","orange","lemon","mango","strawberry","medlar","mulberry","nectarine","cherry","pomegranate","fig","persimmon")override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)arguments?.let {param1 = it.getString(ARG_PARAM1)param2 = it.getString(ARG_PARAM2)}}override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,savedInstanceState: Bundle?): View? {// 获取视图val view = inflater.inflate(R.layout.fragment_code_list, container, false)val listView = view.findViewById<ListView>(R.id.listView)var textView3 = view.findViewById<TextView>(R.id.textView3)// 查询数据库val dbHelper = DatabaseHelper(requireContext())userList = dbHelper.getAllUser();//获取当前分钟秒数// 启动定时器timer(textView3)// 创建ArrayAdapter,将数据源传递给它adapter = CodeListAdapter(requireContext(), R.layout.code_item, userList!!)// 将适配器与ListView关联listView.adapter = adapterlistView.setOnItemClickListener { parent, view, position, id ->val textView = view.findViewById<TextView>(R.id.user_id);val fragment = CodeShowFragment.newInstance(textView.text.toString(), "")// 执行跳转parentFragmentManager.beginTransaction().replace(R.id.viewPager, fragment).commit()val codeShowFragment = CodeShowFragment()switchFragment(codeShowFragment)}return view}// 刷新数据private fun refresh() {if(userList != null) {adapter.notifyDataSetChanged()}}// 定时器private fun timer(textView : TextView) {// 动态计算当前秒数start = MyTimeUtils.getCurrentSec()runnable = Runnable {val formattedNumber = String.format("%02d",start/1000)textView.setText(formattedNumber + "s")start = start -100if(start <0) {refresh()start= MyTimeUtils.getCurrentSec()}// 在这里设置下一次循环的延时时间,例如1秒handler.postDelayed(runnable!!, 100)}// 初始化计时器handler.postDelayed(runnable!!, 50) // 延时1秒后开始循环}@SuppressLint("SuspiciousIndentation")private fun switchFragment(fragment: Fragment) {val transaction = parentFragmentManager.beginTransaction()transaction.show(fragment)transaction.commit()}companion object {/*** Use this factory method to create a new instance of* this fragment using the provided parameters.** @param param1 Parameter 1.* @param param2 Parameter 2.* @return A new instance of fragment CodeListFragment.*/// TODO: Rename and change types and number of parameters@JvmStaticfun newInstance(param1: String, param2: String) =CodeListFragment().apply {arguments = Bundle().apply {putString(ARG_PARAM1, param1)putString(ARG_PARAM2, param2)}}}override fun onDestroyView() {println("onDestroyView")handler.removeCallbacks(runnable!!)super.onDestroyView()}
}

CodeShowFragment

作用:令牌单独展示页面的片段代码,用于页面操作功能处理。提供了定时器进行倒计时刷新令牌,和令牌删除功能。

package com.shixiaotian.totp.scan.application.fragmentsimport android.app.AlertDialog
import android.os.Bundle
import android.os.Handler
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ProgressBar
import android.widget.TextView
import com.shixiaotian.totp.scan.application.R
import com.shixiaotian.totp.scan.application.db.DatabaseHelper
import com.shixiaotian.totp.scan.application.tools.EncodeTools
import com.shixiaotian.totp.scan.application.tools.MyTimeUtils// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"/*** A simple [Fragment] subclass.* Use the [CodeShowFragment.newInstance] factory method to* create an instance of this fragment.*/
class CodeShowFragment : Fragment() {private var codeView: TextView? =null// TODO: Rename and change types of parametersprivate var param1: String? = nullprivate var param2: String? = nullprivate var start: Long = 30000private val handler = Handler()private var runnable: Runnable? = nullprivate var secretKey: String =""private lateinit var progressBar: ProgressBaroverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)arguments?.let {param1 = it.getString(ARG_PARAM1)param2 = it.getString(ARG_PARAM2)}}override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,savedInstanceState: Bundle?): View? {// Inflate the layout for this fragmentvar id: String? = param1if(id == null) {id = "0"}// 查询数据库val dbHelper = DatabaseHelper(requireContext())val user = dbHelper.getUser(id.toInt())val view = inflater.inflate(R.layout.fragment_code_show, container, false)// 初始化进度条progressBar = view.findViewById<ProgressBar>(R.id.progressBar)// 设置进度条的最大值progressBar.max = 30000// 设置当前进度progressBar.progress = 30000// 显示进度条progressBar.visibility = ProgressBar.VISIBLEval showIssuerTextView = view.findViewById<TextView>(R.id.showIssuerTextView)val usernameView = view.findViewById<TextView>(R.id.showUsernameTextView)codeView = view.findViewById<TextView>(R.id.showCodeView)val timeView3 = view.findViewById<TextView>(R.id.showTimeView)if(user != null) {secretKey = user!!.getSecretKey();// 开启个线程,动态计算密钥,并更新到ui界面showIssuerTextView.setText(user.getIssuer())usernameView.setText(user.getUsername())if (codeView != null) {codeView!!.setText(EncodeTools.encode(user.getSecretKey()))}timer(timeView3)}// 删除按钮val deleteButton = view.findViewById<TextView>(R.id.deleteButton)deleteButton.setOnClickListener {showDeleteConfirmationDialog(id)}return view}private fun refresh() {codeView!!.setText(EncodeTools.encode(secretKey))}private fun timer(textView : TextView) {// 动态计算当前秒数start = MyTimeUtils.getCurrentSec()runnable = Runnable {val formattedNumber = String.format("%02d",start/1000)textView.setText(formattedNumber + "s")progressBar.setProgress(start.toInt());start = start -100if(start < 0) {start= MyTimeUtils.getCurrentSec()var refreshRunnable =  Runnable {refresh()}Thread(refreshRunnable).start()}// 在这里设置下一次循环的延时时间,例如1秒handler.postDelayed(runnable!!, 100)}// 初始化计时器handler.postDelayed(runnable!!, 50) // 延时1秒后开始循环}fun showDeleteConfirmationDialog(deleteId : String) {val builder = AlertDialog.Builder(context)builder.setMessage("确定要删除吗?").setPositiveButton("Yes") { dialog, id ->// 删除操作val dbHelper = DatabaseHelper(requireContext())dbHelper.deleteUser(deleteId.toInt())val codeListFragment = CodeListFragment()parentFragmentManager.beginTransaction().replace(R.id.viewPager, codeListFragment).commit()}.setNegativeButton("No") { dialog, id ->// 取消操作,对话框不会被关闭}.setCancelable(false)val alert = builder.create()alert.show()}companion object {/*** Use this factory method to create a new instance of* this fragment using the provided parameters.** @param param1 Parameter 1.* @param param2 Parameter 2.* @return A new instance of fragment CodeShowFragment.*/// TODO: Rename and change types and number of parameters@JvmStaticfun newInstance(param1: String, param2: String) =CodeShowFragment().apply {arguments = Bundle().apply {putString(ARG_PARAM1, param1)putString(ARG_PARAM2, param2)}}}override fun onDestroyView() {if(handler!= null && runnable != null) {handler.removeCallbacks(runnable!!)}super.onDestroyView()}}

EncodeTools

作用:软件核心功能,调用jboss包的otp算法,对密钥进行运算,得出动态令牌。解析二维码扫描出的totp链接信息,转换成user实体提供软件运行

package com.shixiaotian.totp.scan.application.toolsimport com.shixiaotian.totp.scan.application.vo.User
import org.jboss.aerogear.security.otp.Totpclass EncodeTools {companion object {@JvmStaticfun encode(secretKey: String,timeStep: Long = 30,digits: Int = 6,algorithm: String = "SHA1"): String? {if (secretKey == null || secretKey.isBlank()) {return ""}try {val totp = Totp(secretKey);var result = totp.now();return result;} catch (e: Exception) {return "ERROR SK"}}@JvmStaticfun decode(uri: String): User? {if(uri.isEmpty()) {return null}if(!uri.startsWith("otpauth://totp/")) {return null}try {var uriContentIndex = uri.indexOf("otpauth://totp/");var uriContent = uri.subSequence(15, uri.length);val secContents = uriContent.split(":");var issuer = secContents.get(0);var otherContent = secContents.get(1)val secOtherContent= otherContent.split("?")var username = secOtherContent.get(0)var thOtherContent = secOtherContent.get(1)val fthOtherContent = thOtherContent.split("&")var secretKeyContent = fthOtherContent.get(0)var secretKey = secretKeyContent.split("=").get(1)var user = User(0, username, secretKey, issuer)return user}catch (e: Exception) {return null}}}}

FirstRunTools

作用:检测软件是否为第一次安装使用,该段代码不完全适用,建议使用者进行改造,或者移除

package com.shixiaotian.totp.scan.application.toolsimport android.content.Context
import android.content.SharedPreferences
import com.shixiaotian.totp.scan.application.common.MyConstantsclass FirstRunTools {companion object {@JvmStatic fun isFirstRun(context: Context): Boolean {val prefs: SharedPreferences = context.getSharedPreferences(MyConstants.firstRunTag, Context.MODE_PRIVATE)val isFirstTime = prefs.getBoolean(MyConstants.firstRunTag + "isFirstTime", true)if (isFirstTime) {val editor = prefs.edit()editor.putBoolean(MyConstants.firstRunTag + "isFirstTime", false)editor.apply()return true}return false}}
}

MyTimeUtils

作用:时间工具,因为totp是每30秒计算一次,而每次进入软件的时间不同,该功能用于纠正进入的时间差,让令牌刷新倒计时进入精确的时间区间。

package com.shixiaotian.totp.scan.application.toolsimport android.icu.util.Calendarclass MyTimeUtils {companion object {@JvmStatic fun getCurrentSec(): Long {// 获取当前时间的毫秒数val currentTimeMillis = System.currentTimeMillis()// 创建Calendar实例val calendar = Calendar.getInstance()// 设置Calendar的时间为当前时间calendar.timeInMillis = currentTimeMillis// 将秒和毫秒字段重置为0calendar.set(Calendar.SECOND, 0)calendar.set(Calendar.MILLISECOND, 0)// 当前分钟的开始时间的毫秒数val startOfCurrentMinuteMillis = calendar.timeInMillis// 已过去的毫秒数val elapsedMillis = currentTimeMillis - startOfCurrentMinuteMillis// 已过去的秒数//val elapsedSeconds = elapsedMillis / 1000if(elapsedMillis > 30000) {return 60000 - elapsedMillis;} else {return 30000 - elapsedMillis;}}}
}

User

作用:作为数据传输和存储的实体,存储用户的令牌等相关信息

package com.shixiaotian.totp.scan.application.voclass User {private var id : Int = 0private var username : String=""private var secretKey : String=""private var issuer : String=""private var code : String=""constructor(id: Int, username: String, secretKey: String, issuer: String) {this.id = idthis.username = usernamethis.secretKey = secretKeythis.issuer = issuer}fun getId() : Int {return id}fun setId(id : Int) {this.id = id}fun getUsername() : String {return username}fun setUsername(username : String) {this.username = username}fun getSecretKey() : String {return secretKey}fun setSecretKey(secretKey : String) {this.secretKey = secretKey}fun getCode() : String {return code}fun setCode(code : String) {this.code = code}fun getIssuer() : String {return issuer}fun setIssuer(issuer : String) {this.issuer = issuer}
}

CodeListAdapter

作用:适配列表内每一个数据,为令牌动态计算提供适配

package com.shixiaotian.totp.scan.applicationimport android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import com.shixiaotian.totp.scan.application.tools.EncodeTools
import com.shixiaotian.totp.scan.application.vo.User/*** 动态码列表内容适配器*/
class CodeListAdapter (context: Context, val resourceId: Int, data: List<User>) : ArrayAdapter<User>(context, resourceId, data) {override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {val view = LayoutInflater.from(context).inflate(resourceId, parent, false)val userId: TextView = view.findViewById(R.id.user_id)val issuer: TextView = view.findViewById(R.id.user_issuer)val username: TextView = view.findViewById(R.id.user_username)val userSecretKey: TextView = view.findViewById(R.id.user_secretKey)val userCode: TextView = view.findViewById(R.id.user_code)val user = getItem(position)if (user!=null){userId.text = user.getId().toString()issuer.text = user.getIssuer()username.text = user.getUsername()userSecretKey.text = user.getSecretKey()var code = EncodeTools.encode(user.getSecretKey()) as Stringuser.setCode(code)userCode.text = user.getCode()}return view}
}

MainActivity

作用:添加主页面上基本的按钮监听

package com.shixiaotian.totp.scan.applicationimport android.os.Bundle
import android.view.View
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.FragmentManager
import com.shixiaotian.totp.scan.application.fragments.CodeAddFragment
import com.shixiaotian.totp.scan.application.fragments.CodeListFragment
import com.shixiaotian.totp.scan.application.db.DatabaseHelper
import com.shixiaotian.totp.scan.application.tools.FirstRunTools
import net.sqlcipher.database.SQLiteDatabaseclass MainActivity : AppCompatActivity() {private lateinit var listButton: Viewprivate lateinit var addButton: Viewprivate lateinit var fragmentManager: FragmentManageroverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)enableEdgeToEdge()setContentView(R.layout.activity_main)// 初始化预处理init()val codeAddFragment = CodeAddFragment()val codeListFragment = CodeListFragment()fragmentManager = supportFragmentManagerfragmentManager.beginTransaction().replace(R.id.viewPager, codeListFragment).commit()// 菜单按钮监听listButton = findViewById(R.id.menuButton)listButton.setOnClickListener {fragmentManager.beginTransaction().replace(R.id.viewPager, codeListFragment).commit()}// 添加按钮监听addButton = findViewById(R.id.addButton)addButton.setOnClickListener {fragmentManager.beginTransaction().replace(R.id.viewPager, codeAddFragment).commit()}ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)insets}}private fun init() {SQLiteDatabase.loadLibs(this);println("---开始初始化")// 判断是否首次运行if(FirstRunTools.isFirstRun(this)) {println("---首次运行触发")val dbHelper = DatabaseHelper(this)// 初始化数据库dbHelper.initDB()dbHelper.init()}}}

activity_main.xml

作用:主页面布局

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"android:id="@+id/main"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#3F5CB5"tools:context=".MainActivity"><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><LinearLayoutandroid:id="@+id/mainFragment"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><FrameLayoutandroid:id="@+id/viewPager"android:layout_width="match_parent"android:layout_height="match_parent"></FrameLayout></LinearLayout><LinearLayoutandroid:layout_width="match_parent"android:layout_height="200px"android:layout_alignParentBottom="true"android:background="#ffffff"android:orientation="horizontal"><ImageViewandroid:id="@+id/menuButton"android:layout_width="wrap_content"android:layout_height="match_parent"android:layout_weight="1"android:background="#20212E"android:gravity="center_horizontal"android:src="@android:drawable/ic_menu_search"android:layout_marginRight="5px"android:textSize="30sp" /><ImageViewandroid:id="@+id/addButton"android:layout_width="wrap_content"android:layout_height="match_parent"android:layout_weight="1"android:background="#20212E"android:gravity="center_horizontal"android:src="@android:drawable/ic_menu_add"android:layout_marginLeft="5px"android:textSize="30sp" /></LinearLayout></RelativeLayout></androidx.constraintlayout.widget.ConstraintLayout>

code_item.xml

作用:令牌列表每一个令牌的布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/User"android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical" ><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical" ><TextViewandroid:id="@+id/user_id"android:layout_width="0px"android:layout_height="0px"android:layout_gravity="left"android:visibility="invisible"/><TextViewandroid:id="@+id/user_secretKey"android:layout_width="0px"android:layout_height="0px"android:layout_gravity="center_vertical"android:visibility="invisible"/><TextViewandroid:id="@+id/user_issuer"android:layout_width="wrap_content"android:layout_height="wrap_content"android:textAppearance="?android:attr/textAppearanceListItemSmall"android:gravity="center_vertical"android:paddingStart="?android:attr/listPreferredItemPaddingStart"android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"android:minHeight="?android:attr/listPreferredItemHeightSmall"android:textSize="35sp"android:text="apple"/><TextViewandroid:id="@+id/user_username"android:layout_width="wrap_content"android:layout_height="wrap_content"android:textAppearance="?android:attr/textAppearanceListItemSmall"android:gravity="center_vertical"android:paddingStart="?android:attr/listPreferredItemPaddingStart"android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"android:minHeight="?android:attr/listPreferredItemHeightSmall"android:textSize="25sp"android:text="1234567@qq.com"/><TextViewandroid:id="@+id/user_code"android:layout_width="wrap_content"android:layout_height="wrap_content"android:gravity="center_vertical"android:layout_gravity="center"android:textColor="#000000"android:text="665277"android:textSize="55sp"android:textStyle="bold"/></LinearLayout>
</LinearLayout>

fragment_code_add.xml

作用:添加令牌页面,手动添加或者,触发zxing扫码添加

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#ffffff"tools:context=".fragments.CodeAddFragment"><!-- TODO: Update blank fragment layout --><LinearLayoutandroid:layout_margin="100px"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><TextViewandroid:id="@+id/addNameView"android:layout_width="match_parent"android:layout_height="wrap_content"android:textSize="40sp"android:textStyle="bold"android:text="Username" /><EditTextandroid:id="@+id/addUsernameText"android:layout_width="match_parent"android:layout_height="wrap_content"android:maxLength="10"android:inputType="text"android:textSize="30sp"/><TextViewandroid:id="@+id/addSecretKeyView"android:layout_width="match_parent"android:layout_height="wrap_content"android:textSize="40sp"android:textStyle="bold"android:text="SecretKey" /><EditTextandroid:id="@+id/addSecretKeyText"android:layout_width="match_parent"android:layout_height="wrap_content"android:maxLength="100"android:textSize="30sp"android:inputType="text" /><TextViewandroid:id="@+id/addIssuerView"android:layout_width="match_parent"android:layout_height="wrap_content"android:textSize="40sp"android:textStyle="bold"android:text="Issuer" /><EditTextandroid:id="@+id/addIssuerText"android:layout_width="match_parent"android:layout_height="wrap_content"android:maxLength="10"android:inputType="text"android:textSize="30sp"/><TextViewandroid:id="@+id/saveButton"android:layout_width="match_parent"android:layout_height="100sp"android:background="#A62641"android:gravity="center"android:layout_marginTop="100px"android:textColor="#ffffff"android:text="Save"android:textSize="50sp" /><TextViewandroid:id="@+id/cameraButton"android:layout_width="match_parent"android:layout_height="100sp"android:layout_marginTop="100px"android:background="#20212E"android:gravity="center"android:text="Scan"android:textColor="#ffffff"android:textSize="50sp" /></LinearLayout>
</FrameLayout>

fragment_code_list.xml

作用:提供令牌快速查看列表,和选择单个令牌进行操作的功能

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:id="@+id/code_list"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".fragments.CodeListFragment"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><TextViewandroid:id="@+id/textView3"android:layout_width="match_parent"android:layout_height="101dp"android:gravity="center"android:background="#312F2F"android:text="30s"android:textColor="#ffffff"android:textColorLink="#FFFFFF"android:textSize="60sp" /><ListViewandroid:id="@+id/listView"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_weight="1"android:background="#ffffff"android:divider="#000000"android:dividerHeight="1dp" /><Viewandroid:layout_width="match_parent"android:layout_height="200px"android:background="#666666"></View></LinearLayout>
</FrameLayout>

fragment_code_show.xml

作用:动态令牌展示页面,提供令牌查看和删除功能

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:id="@+id/code_show"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#312F2F"tools:context=".fragments.CodeShowFragment"><!-- TODO: Update blank fragment layout --><LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><TextViewandroid:id="@+id/showIssuerTextView"android:layout_width="match_parent"android:layout_height="wrap_content"android:gravity="center_horizontal"android:layout_marginTop="100px"android:text="steam"android:textColor="#ffffff"android:textColorLink="#FFFFFF"android:textSize="75sp" /><TextViewandroid:id="@+id/showUsernameTextView"android:layout_width="match_parent"android:layout_height="wrap_content"android:gravity="center_horizontal"android:layout_marginTop="30px"android:text="54526322@qq.com"android:textColor="#ffffff"android:textColorLink="#FFFFFF"android:textSize="25sp" /><TextViewandroid:id="@+id/showTimeView"android:layout_width="match_parent"android:layout_height="101dp"android:gravity="center"android:background="#312F2F"android:text="30s"android:textColor="#ffffff"android:textColorLink="#FFFFFF"android:layout_marginTop="20px"android:textSize="70sp" /><ProgressBarandroid:id="@+id/progressBar"style="?android:attr/progressBarStyleHorizontal"android:layout_width="match_parent"android:layout_height="45dp"android:max="100"android:progress="10"android:progressDrawable="@drawable/progress_bar_color" /><TextViewandroid:id="@+id/showCodeView"android:layout_width="match_parent"android:layout_height="wrap_content"android:freezesText="false"android:gravity="center_horizontal"android:layout_marginTop="50px"android:text="9TXTSY"android:textColor="#ffffff"android:textColorLink="#FFFFFF"android:textSize="80sp" /><TextViewandroid:id="@+id/deleteButton"android:layout_alignParentBottom="true"android:layout_width="match_parent"android:layout_height="100sp"android:background="#20212E"android:gravity="center"android:layout_margin="50px"android:textColor="#ffffff"android:text="Delete"android:textSize="50sp" /></LinearLayout></FrameLayout>

AndroidManifest.xml

作用:添加相机权限,设定zxing相机扫码activity相机方向等相关数据

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"><uses-permission android:name="android.permission.CAMERA" /><applicationandroid:allowBackup="true"android:dataExtractionRules="@xml/data_extraction_rules"android:fullBackupContent="@xml/backup_rules"android:icon="@android:drawable/ic_lock_lock"android:label="@string/app_name"android:roundIcon="@mipmap/ic_launcher_round"android:supportsRtl="true"android:theme="@style/Theme.MyApplication"tools:targetApi="31"><activityandroid:name=".MainActivity"android:exported="true"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity><activity android:name="com.journeyapps.barcodescanner.CaptureActivity"android:screenOrientation="fullSensor"tools:replace="screenOrientation" /></application></manifest>

相关文章:

[TOTP]android kotlin实现 totp身份验证器 类似Google身份验证器

背景&#xff1a;自己或者公司用一些谷歌身份验证器或者microsoft身份验证器&#xff0c;下载来源不明&#xff0c;或者有广告&#xff0c;使用不安全。于是自己写一个&#xff0c;安全放心使用。 代码已开源&#xff1a;shixiaotian/sxt-android-totp: android totp authenti…...

IDEA+Docker一键部署项目SpringBoot项目

文章目录 1. 部署项目的传统方式2. 前置工作3. SSH配置4. 连接Docker守护进程5. 创建简单的SpringBoot应用程序6. 编写Dockerfile文件7. 配置远程部署 7.1 创建配置7.2 绑定端口7.3 添加执行前要运行的任务 8. 部署项目9. 开放防火墙的 11020 端口10. 访问项目11. 可能遇到的问…...

【发票提取明细+发票号改名】批量提取PDF电子发票明细导出Excel表格并改名技术难点,批量PDF多区域内容识别提取明细并用内容改名的小结

1、图片版的发票提取表格改名 【批量图片发票识别表格】批量图片发票的提取Excel表格和提取字段改名&#xff0c;扫描发票识别表格&#xff0c;拍照发票识别表格&#xff0c;图片发票识别改名我们在工作中很多扫描发票&#xff0c;拍照发票&#xff0c;需要整理成excel表格&am…...

pyQT + OpenCV相关练习

一、设计思路 1、思路分析与设计 本段代码是一个使用 PyQt6 和 OpenCV 创建的图像处理应用程序。其主要功能是通过一个图形界面让用户对图片进行基本的图像处理操作&#xff0c;如灰度化、翻转、旋转、亮度与对比度调整&#xff0c;以及一些滤镜效果&#xff08;模糊、锐化、边…...

石岩路边理发好去处

周末带娃去罗租公园玩&#xff0c;罗租公园旁边就是百佳华和如意豪庭小区&#xff0c;发现如意豪庭小区对面挺多路边理发摊点 理发摊点聚焦在这里的原因是刚好前面城管来了暂时避避&#xff0c;例如还有一个阿姨剪到一半就跟着过来。这里的城管只是拍了一处没有摊位的地方&…...

音视频入门基础:MPEG2-PS专题(2)——使用FFmpeg命令生成ps文件

一、错误的命令 通过FFmpeg命令可以将mp4文件转换为ps文件&#xff0c;PS文件中包含PS流数据。 由于PS流/PS文件对应的FFInputFormat结构为&#xff1a; const FFInputFormat ff_mpegps_demuxer {.p.name "mpeg",.p.long_name NULL_IF_CONFIG_SMALL…...

整合版canal ha搭建--基于1.1.4版本

开启MySql Binlog&#xff08;1&#xff09;修改MySql配置文件&#xff08;2&#xff09;重启MySql服务,查看配置是否生效&#xff08;3&#xff09;配置起效果后&#xff0c;创建canal用户&#xff0c;并赋予权限安装canal-admin&#xff08;1&#xff09;解压 canal.admin-1…...

[python SQLAlchemy数据库操作入门]-15.联合查询,跨表获取股票数据

哈喽,大家好,我是木头左! 在开始探讨如何利用SQLAlchemy实现复杂的联合查询之前,首先需要深入理解其核心组件——对象关系映射(ORM)。ORM允许开发者使用Python类来表示数据库中的表,从而以一种更直观、面向对象的方式来操作数据库。 SQLAlchemy中的JOIN操作详解 在SQLA…...

PTA数据结构作业一

6-1 链表的插入算法 本题要求实现一个插入函数&#xff0c;实现在链表llist中的元素x之后插入一个元素y的操作。 函数接口定义&#xff1a; int InsertPost_link(LinkList llist, DataType x, DataType y); 其中 llist是操作的链表&#xff0c;x是待插入元素y的前驱节点元素…...

前端(九)js介绍(2)

js介绍(2) 文章目录 js介绍(2)一、函数1.1函数的两种形式1.2函数的作用域1.3声明与提升 二、bom操作三、dom操作 一、函数 1.1函数的两种形式 //有参函数 //js中的函数只能返回一个值&#xff0c;如果要返回多个需要放在数组或对象中 function func(a,b){return ab } func(1,…...

CUTLASS:高性能 CUDA 线性代数模板库详解

CUTLASS&#xff1a;高性能 CUDA 线性代数模板库详解 引言什么是 CUTLASS&#xff1f;CUTLASS 的主要特点&#xff1a; CUTLASS 的用途如何安装 CUTLASS1. 环境准备2. 下载 CUTLASS3. 构建 CUTLASS4. 设置环境变量5. 验证安装 使用 CUTLASSCUTLASS 的优势总结 引言 在深度学习…...

关于CISP报名费用详情

CISP即“注册信息安全专业人员”&#xff0c;是中国信息安全测评中心实施的国家认证项目&#xff0c;旨在培养信息安全领域的专业人才。对于有意报考CISP的考生而言&#xff0c;了解报名考试费用是备考过程中不可或缺的一环。 CISP的报名考试费用主要包括培训费用、考试费用、…...

css 关于flex布局中子元素的属性flex

css flex布局中子元素的属性flex 1. flex 是 flex-grow、flex-shrink 和 flex-basis 的简写 语法格式&#xff1a; flex: [flex-grow] [flex-shrink] [flex-basis];各属性解析&#xff1a; flex-grow: 子元素如何按比例分配父元素的 剩余空间。 默认值&#xff1a;0&#…...

功率器件热设计基础(四)——功率半导体芯片温度和测试方法

/ 前言 / 功率半导体热设计是实现IGBT、碳化硅SiC高功率密度的基础&#xff0c;只有掌握功率半导体的热设计基础知识&#xff0c;才能完成精确热设计&#xff0c;提高功率器件的利用率&#xff0c;降低系统成本&#xff0c;并保证系统的可靠性。 功率器件热设计基础系列文章会…...

OpenStack系列第四篇:云平台基础功能与操作(Dashboard)

文章目录 1. 镜像&#xff08;Image&#xff09;添加镜像查看镜像删除镜像 2. 卷&#xff08;Volume&#xff09;创建卷查看卷删除卷 3. 网络&#xff08;虚拟网络&#xff09;创建网络查看网络删除网络 4. 实例类型创建实例类型查看实例类型删除实例类型 4. 密钥对&#xff08…...

WebSocket封装

提示:记录工作中遇到的需求及解决办法 文章目录 前言二、背景三、WebSocket3.1 什么是 WebSocket ?为什么使用他?四、封装 WebSocket4.1 Javascript 版本4.2 Typescript 版本4.3 如何使用?五、我的痛点如何处理前言 本文将介绍 WebSocket 的封装,比如:心跳机制,重连和一…...

面试题解,JVM的运行时数据区

一、请简述JVM运行时数据区的组成结构及各部分作用 总览 从线程持有的权限来看 线程私有区 虚拟机栈 虚拟机栈是一个栈结构&#xff0c;由许多个栈帧组成&#xff0c;一个方法分配一个栈帧&#xff0c;线程每执行一个方法时都会有一个栈帧入栈&#xff0c;方法执行结束后栈帧…...

【Ubuntu使用技巧】Ubuntu22.04无人值守Crontab工具实战详解

一个愿意伫立在巨人肩膀上的农民...... Crontab是Linux和类Unix操作系统下的一个任务调度工具&#xff0c;用于周期性地执行指定的任务或命令。Crontab允许用户创建和管理计划任务&#xff0c;以便在特定的时间间隔或时间点自动运行命令或脚本。这些任务可以按照分钟、小时、日…...

Caffeine Cache Java缓存组件

缓存组件Caffeine Cache 定义介绍整合springboot用法整合spring-boot-starter-cache用法 定义介绍 特性 高性能&#xff1a;基于高效并发设计和 TinyLFU 算法&#xff0c;命中率高。 丰富策略&#xff1a;支持容量限制、过期时间、异步加载、自定义清理策略。 统计监控&#x…...

电子电气架构 --- 什么是自动驾驶技术中的域控制单元(DCU)?

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 所谓鸡汤,要么蛊惑你认命,要么怂恿你拼命,但都是回避问题的根源,以现象替代逻辑,以情绪代替思考,把消极接受现实的懦弱,伪装成乐观面对不幸的…...

Redis核心技术知识点全集

Redis数据结构和常用命令 1. String字符串2. Hash哈希3. List列表4. Set集合5. Sorted Set有序集合6. Redis常用命令参考Redis事务机制...

【每日学点鸿蒙知识】文件读写、屏幕宽度亮度、扫一扫权限、编码器问题、wlan设置

1、参照文档&#xff0c;在操作文件时&#xff0c;读取不到内容或出现程序闪退&#xff1f; 参照文档&#xff0c;进行文件写入和读取时&#xff0c;出现读取不到或闪退 export function createFile() {// 获取应用文件路径let context getContext(this) as common.UIAbilit…...

后端开发-Maven

环境说明&#xff1a; windows系统&#xff1a;11版本 idea版本&#xff1a;2023.3.2 Maven 介绍 Apache Maven 是一个 Java 项目的构建管理和理解工具。Maven 使用一个项目对象模型&#xff08;POM&#xff09;&#xff0c;通过一组构建规则和约定来管理项目的构建&#xf…...

LiteFlow 流程引擎引入Spring boot项目集成pg数据库

文章目录 官网地址简要项目引入maven 所需jar包配置 PostgreSQL 数据库表使用LiteFlow配置 yml 文件通过 代码方式使用 liteflow数据库sql 数据在流程中周转 官网地址 https://liteflow.cc/ 简要 如果你要对复杂业务逻辑进行新写或者重构&#xff0c;用LiteFlow最合适不过。…...

电子电气架构 --- 汽车电子电器设计概述

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 所谓鸡汤,要么蛊惑你认命,要么怂恿你拼命,但都是回避问题的根源,以现象替代逻辑,以情绪代替思考,把消极接受现实的懦弱,伪装成乐观面对不幸的…...

API 设计:从基础到最佳实践

https://levelup.gitconnected.com/api-design-101-from-basics-to-best-practices-a0261cdf8886 在本次深入研究中&#xff0c;我们将从基础开始&#xff0c;逐步了解 API 设计&#xff0c;并逐步实现定义卓越 API 的最佳实践。 作为开发人员&#xff0c;您可能熟悉其中的许多…...

简易内存池(中)

提示&#xff1a;文章 文章目录 前言一、背景二、第二版代码用例2用例3用例4用例5 总结 前言 前期疑问&#xff1a; 本文目标&#xff1a; 一、背景 最近 二、 针对上述失败用例&#xff0c;修改代码如下 第二版代码 #include <stdbool.h> #include <stdio.h>…...

svn不能添加.a文件

解决办法 在home目录下有一个.subversion文件夹&#xff0c;文件夹内有个config文件&#xff0c;里面可以修改过滤的文件类型 在使用命令svn add的时候带上参数–no-ignore&#xff0c;这样就会不顾config中的规则&#xff0c;将指定路径的文件都添加到版本库中 rockyrocky:/e…...

PH47代码框架 24241231 重要更新

仪式感一下&#xff1a;2024年最后一天&#xff0c;发布 PH47 代码框架的一次重要更新。当然这并不是有意的&#xff0c;而是直到现在才把更新的所有工作全部做完&#xff08;希望确实如此&#xff09;。 本次更新要点&#xff1a; 1、加入多IMU支持。本次更新正式加入对 MPU65…...

小程序信息收集(小迪网络安全笔记~

免责声明&#xff1a;本文章仅用于交流学习&#xff0c;因文章内容而产生的任何违法&未授权行为&#xff0c;与文章作者无关&#xff01;&#xff01;&#xff01; 附&#xff1a;完整笔记目录~ ps&#xff1a;本人小白&#xff0c;笔记均在个人理解基础上整理&#xff0c;…...

用户界面的UML建模07

4.2 抽象表示层的行为&#xff08;Abstract Presentation Behaviour&#xff09; AbstractForm 类定义了一组如下所示的四种操作&#xff1a; showForm() &#xff0c; getData() &#xff0c; sendConfirmation() 和sendCancellation()。在该阶段的设计过程&#xff08;desig…...

LabVIEW手部运动机能实验系统

在运动科学、人机交互和康复训练等领域&#xff0c;手部运动功能的研究具有重要的应用价值。开发了一个基于LabVIEW的手部运动机能实验系统设计&#xff0c;该系统利用力量作为关键参数&#xff0c;通过实时数据采集和反馈帮助受试者完成精确的手部动作&#xff0c;同时为研究人…...

Java Map 源码解析:核心原理与应用

Java Map 源码解析&#xff1a;核心原理与应用 Java 的 Map 接口是集合框架中一个重要的组成部分&#xff0c;专门用于存储键值对。其强大的功能和灵活的实现使其在各种应用场景中得到了广泛的使用。本文面向对 Java 集合框架有一定了解的开发者&#xff0c;通过对 Map 接口及…...

基于Mosquito源码理解MQTT5.0的属性概念

MQTT 5.0协议相比之前的版本(如MQTT 3.1.1)增加了很多属性,这些属性分布于报文的可变头部(Variable Header)和有效载荷(Payload)中。这些属性大大增强了协议的可扩展性和灵活性,使其能够更好地适应现代物联网应用的复杂需求。 属性的定义在源码包mosquitto-2.0.18/inc…...

easyui textbox使用placeholder无效

easyui textbox使用placeholder无效 在easyui 的textbox控件&#xff0c;请使用data-options 设定 示例 <input type text class easyui-textbox data-options "prompt:请输入您的邮箱"/>...

java AQS

什么是AQS AQS&#xff08;AbstractQueuedSynchronizer&#xff0c;抽象队列同步器&#xff09;是 Java 中并发控制的一种机制&#xff0c;位于 java.util.concurrent.locks 包下&#xff0c;它为构建锁、信号量等同步工具提供了一个框架。AQS 通过 队列 来管理多个线程之间的…...

机器人对物体重定向操作的发展简述

物体重定向操作的发展简述 前言1、手内重定向和外部重定向2、重定向原语3、重定向状态转换网络4、连续任意姿态的重定向5、利用其他环境约束重定向总结Reference 前言 对于一些特殊的任务&#xff08;如装配和打包&#xff09;&#xff0c;对物体放置的位姿由明确的要求&#…...

数据结构与算法之动态规划: LeetCode 72. 编辑距离 (Ts版)

编辑距离 https://leetcode.cn/problems/edit-distance/description/ 描述 给你两个单词 word1 和 word2&#xff0c; 请返回将 word1 转换成 word2 所使用的最少操作数你可以对一个单词进行如下三种操作&#xff1a; 插入一个字符删除一个字符替换一个字符 示例 1 输入&…...

vue3 Teleport瞬移组件

Teleport是瞬移组件&#xff0c;也称为传送门组件 它是一个可以使元素从一个组件转到另一个组件的组件。 如对话框、自定义菜单、警告提示、徽章&#xff0c;以及许多其他需要出现在特殊位置的自定义UI组件。假设现在页面中有两个元素&#xff0c;分别为div元素和button按钮元…...

Go语言学习路线

以下是一个较为系统的Go语言学习路线&#xff1a; 一、基础阶段 环境搭建与工具链熟悉 安装Go语言开发环境。在Go官方网站&#xff08;https://golang.org/dl/&#xff09;下载适合您操作系统的安装包并完成安装。 配置Go环境变量&#xff0c;如GOPATH和GOROOT。GOROOT是Go语…...

摄像头监视脚本

摄像头监视脚本&#xff0c;若检测到摄像头画面有变化&#xff0c;保存这一段视频 一、使用方法 1.运行脚本 默认参数Threshold3, Period3, path./recordings python cam.py --threshold30 --period3 --path./recordings 2.参数说明 threshold:摄像头捕获到的画面变化量阈值…...

【Leecode】Leecode刷题之路第97天之交错字符串

题目出处 97-交错字符串-题目出处 题目描述 个人解法 思路&#xff1a; todo代码示例&#xff1a;&#xff08;Java&#xff09; todo复杂度分析 todo官方解法 97-交错字符串-官方解法 方法1&#xff1a;动态规划 思路&#xff1a; class Solution {public boolean isInte…...

MAC环境安装(卸载)软件

MAC环境安装&#xff08;卸载&#xff09;软件 jdknode安装node&#xff0c;并实现不同版本的切换背景 卸载node从node官网下载pkg安装的node卸载用 homebrew 安装的node如果你感觉删的不够干净&#xff0c;可以再细分删除验证删除结果 jdk 1.下载jdk 先去官网下载自己需要的版…...

Spring Boot + Redisson 封装分布式锁

目标&#xff1a;一行代码调用&#xff0c;简单粗暴。 基操&#xff1a;自动加锁&#xff0c;自动解锁&#xff0c;自动处理异常&#xff0c;自动处理锁超时等。 安装 redis redisson <dependency><groupId>org.springframework.boot</groupId><artifac…...

从零开发一套UWB定位系统需要多长时间?UWB超宽带定位系统源码

从零开发一套UWB定位系统需要多长时间&#xff1f; 从零开发一套UWB定位系统所需的时间会受到多种因素的影响&#xff0c;以下是详细分析&#xff1a; 一、系统复杂度 1、基本功能定位系统 如果只是开发一个简单的UWB定位系统&#xff0c;仅实现基本的定位功能&#xff0c;如在…...

DataCap 2024.4.1 版本发布:MongoDB 驱动支持、工作流引擎升级

尊敬的 DataCap 用户&#xff1a; DataCap 2024.4.1 版本现已正式发布。本次更新包含多项重要功能升级和性能优化&#xff0c;现将主要更新内容公布如下&#xff1a; 核心功能升级 数据库功能增强 (实现功能) 新增数据库管理功能&#xff1a;支持创建、删除和切换数据库完善表…...

常见端口(22、25、53、80、443、110、143、3306、6379、21)和服务的安装与配置手册

文章目录 一、系统初始设置1. 系统与工具的基础配置1.1 系统更新1.2 网络连接测试1.3 工具安装 2. 防火墙配置2.1 启用防火墙服务2.2 检查默认规则2.3 开放常用端口2.4 查看已开放端口 二、常见端口及其用途1. 端口 22&#xff08;SSH&#xff09;2. 端口 25&#xff08;SMTP&a…...

使用maven-mvnd替换maven大大提升编译打包速度

先上结论&#xff01;&#xff01;&#xff01; 多模块清理并打包提升&#xff1a;约3.5倍 多模块不清理打包提升&#xff1a;约5.5倍 单模块提升&#xff1a;约2倍 从计算结果来看&#xff0c;多模块提升的效率更高。在使用mvnd package打包多模块式&#xff0c;可在控制台…...

图像去雾 | 基于Matlab的图像去雾系统(四种方法)

图像去雾 | 基于Matlab的图像去雾系统&#xff08;四种方法&#xff09; 目录 图像去雾 | 基于Matlab的图像去雾系统&#xff08;四种方法&#xff09;效果一览基本介绍程序设计参考资料 效果一览 基本介绍 基于Matlab的图像去雾系统&#xff08;四种方法&#xff09; 关于图像…...

【Ubuntu 系统 之 开启远程桌面SSH登录】

【Ubuntu 系统 之 开启远程桌面&SSH登录】 一、开启 SSH 登录二、开启远程桌面1、更新包管理器并安装 xrdp1.1、遇到错误1.2、解决方法 2、安装桌面环境&#xff08;如果服务器上没有 GUI&#xff09;3、配置 xrdp 使用默认的 GNOME 桌面环境4、配置防火墙允许远程桌面连接…...