[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身份验证器
背景:自己或者公司用一些谷歌身份验证器或者microsoft身份验证器,下载来源不明,或者有广告,使用不安全。于是自己写一个,安全放心使用。 代码已开源:shixiaotian/sxt-android-totp: android totp authenti…...

2025决战智驾:从中阶卷到L3,车企需要抓好一个数据闭环
作者 |王博 编辑 |德新 全国都能开之后,智驾继续走向哪里? 2024年末,大部分主流车企已经实现了无(高精度)图全国都能开。而第一梯队的玩家,从以规则为主的算法框架,向神经网络模型为主的新架构…...

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

SpringAI从入门到熟练
学习SpringAI的记录情况 文章目录 前言 因公司需要故而学习SpringAI文档,故将自己所见所想写成文章,供大佬们参考 主要是为什么这么写呢,为何不抽出来呢,还是希望可以用的时候更加方便一点,如果大家有需求可以自行去…...

[算法] [leetcode-20] 有效的括号
20 有效的括号 给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。 有效字符串需满足: 左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合…...

R语言入门笔记:第一节,快速了解R语言——文件与基础操作
关于 R 语言的简单介绍 上一期 R 语言入门笔记里面我简单介绍了 R 语言的安装和使用方法,以及各项避免踩坑的注意事项。我想把这个系列的笔记持续写下去。 这份笔记只是我的 R 语言入门学习笔记,而不是一套 R 语言教程。换句话说:这份笔记不…...

【亚马逊云】基于Amazon EC2实例部署 NextCloud 云网盘并使用 Docker-compose 搭建 ONLYOFFICE 企业在线办公应用软件
文章目录 1. 部署EC2实例2. 安装 Docker 服务3. 安装docker-compose4. 创建Docker-compose文件5. 创建nginx.conf文件6. 运行docker-compose命令开始部署7. 访问ONLYOFFICE插件8. 访问NextCloud云盘9. 下载并启用ONLYOFFICE插件10. 上传文件测试11. 所遇问题12. 参考链接 1. 部…...

java Redisson 实现限流每秒/分钟/小时限制N个
1.引入maven包: <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.redisson</groupId><artifactId>red…...
【漫话机器学习系列】029.累积分布函数(Cumulative Distribution Function)
累积分布函数(Cumulative Distribution Function, CDF) 累积分布函数(CDF)是概率论和统计学中的一个基本概念,用于描述随机变量取值的累积概率分布情况。它在理论研究和实际应用中广泛使用。 定义 给定随机变量 X&am…...

设计模式之访问者模式:一楼千面 各有玄机
~犬📰余~ “我欲贱而贵,愚而智,贫而富,可乎? 曰:其唯学乎” 一、访问者模式概述 \quad 江湖中有一个传说:在遥远的东方,有一座神秘的玉楼。每当武林中人来访,楼中的各个房…...

AI 编程的世界:用Cursor编写评分项目
AI 编程的世界:用Cursor编写评分项目 今天是2024年的最后一天,祝大家在新的一年,健康开心快乐! 岁末之际,星辰为伴,灯火长明,我终于在 2024 年的最后一天成功上线了 AI 编程项目。回首这一年&am…...

Cesium教程(二十三):Cesium实现下雨场景
文章目录 实现效果代码引入js文件创建容器创建视图定义下雨场景完整代码下载实现效果 代码 在 Cesium 中利用PostProcessStageLibrary实现下雪场景,你可以按照以下步骤进行: 创建一个 PostProcessStage:首先,你需要创建一个PostProcessStage对象,它将用于定义下雪效果的渲…...

SpringCloudAlibaba技术栈-Higress
1、什么是Higress? 云原生网关,干啥的?用通俗易懂的话来说,微服务架构下Higress 就像是一个智能的“交通警察”,它站在你的网络世界里,负责指挥和调度所有进出的“车辆”(也就是数据流量)。它的…...

uniapp 微信小程序开发使用高德地图、腾讯地图
一、高德地图 1.注册高德地图开放平台账号 (1)创建应用 这个key 第3步骤,配置到项目中locationGps.js 2.下载高德地图微信小程序插件 (1)下载地址 高德地图API | 微信小程序插件 (2)引入项目…...

Springboot:后端接收数组形式参数
1、接收端写法 PermissionAnnotation(permissionName "",isCheckToken true)PostMapping("/batchDeleteByIds")public ReturnBean webPageSelf( NotNull(message "请选择要删除的单据!") Long[] ids) {for (Long string : ids) {l…...

Postman[2] 入门——界面介绍
可参考官方 文档 Postman 导航 | Postman 官方帮助文档中文版Postman 拥有各种工具、视图和控件,帮助你管理 API 项目。本指南是对 Postman 主要界面区域的高级概述:https://postman.xiniushu.com/docs/getting-started/navigating-postman 1. Header&a…...

1月第四讲:Java Web学生自习管理系统
一、项目背景与需求分析 随着网络技术的不断发展和学校规模的扩大,学生自习管理系统的需求日益增加。传统的自习管理方式存在效率低下、资源浪费等问题,因此,开发一个智能化的学生自习管理系统显得尤为重要。该系统旨在提高自习室的利用率和…...

【Redis】Redis 典型应用 - 缓存 (cache)
目录 1. 什么是缓存 2. 使用 Redis 作为缓存 3. 缓存的更新策略 3.1 定期生成 3.2 实时生成 4. 缓存的淘汰策略 5. 缓存预热, 缓存穿透, 缓存雪崩 和 缓存击穿 关于缓存预热 (Cache preheating) 关于缓存穿透 (Cache penetration) 关于缓存雪崩 (Cache avalanche) 关…...

HTML——38.Span标签和字符实体
<!DOCTYPE html> <html><head><meta charset"UTF-8"><title>span标签和字符实体</title><style type"text/css">h1{text-align: center;}p{text-indent: 2em;}span{color: red;}</style></head><…...

ROS2+OpenCV综合应用--10. AprilTag标签码追踪
1. 简介 apriltag标签码追踪是在apriltag标签码识别的基础上,增加了小车摄像头云台运动的功能,摄像头会保持标签码在视觉中间而运动,根据这一特性,从而实现标签码追踪功能。 2. 启动 2.1 程序启动前的准备 本次apriltag标签码使…...

python Celery 是一个基于分布式消息传递的异步任务队列系统
Celery 是一个基于分布式消息传递的异步任务队列系统,主要用于处理耗时任务、定时任务和周期性任务。它能够将任务分配到多个工作节点(Worker)上执行,从而提高应用程序的性能和可扩展性。Celery 是 Python 生态中最流行的任务队列…...

嵌入式硬件杂谈(七)IGBT MOS管 三极管应用场景与区别
引言:在现代嵌入式硬件设计中,开关元件作为电路中的重要组成部分,起着至关重要的作用。三种主要的开关元件——IGBT(绝缘栅双极型晶体管)、MOSFET(金属氧化物半导体场效应晶体管)和三极管&#…...

麒麟信安云在长沙某银行的应用入选“云建设与应用领航计划(2024)”,打造湖湘金融云化升级优质范本
12月26日,2024云计算产业和标准应用大会在北京成功召开。大会汇集政产学研用各方专家学者,共同探讨云计算产业发展方向和未来机遇,展示云计算标准化工作重要成果。 会上,云建设与应用领航计划(2024)建云用…...

好用的随机生成图片的网站
官网: Lorem Picsum 获取自定义大小的随机图像 https://picsum.photos/200/300 获取正方形图像 https://picsum.photos/200 获取特定类型的图像 通过添加到 /id/{image} url 的开头来获取特定图像。 https://picsum.photos/id/237/200/300 获取静态随机图像…...

添加 env 配置,解决import路径问题
添加 env 配置,解决import路径问题 { // 使用 IntelliSense 了解相关属性。 // 悬停以查看现有属性的描述。 // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid830387 “version”: “0.2.0”, “configurations”: [ {"name&q…...

Go work stealing 机制
Go语言的Work Stealing(工作窃取)机制是一种用于调度Goroutines(协程)的策略,其核心目的是最大化CPU使用率,减少任务调度的开销,并提高并发性能和吞吐量。以下是Go Work Stealing机制的详细解释…...

基础数据结构--二叉树
一、二叉树的定义 二叉树是 n( n > 0 ) 个结点组成的有限集合,这个集合要么是空集(当 n 等于 0 时),要么是由一个根结点和两棵互不相交的二叉树组成。其中这两棵互不相交的二叉树被称为根结点的左子树和右子树。 如图所示&am…...

《C++设计模式》策略模式
文章目录 1、引言1.1 什么是策略模式1.2 策略模式的应用场景1.3 本文结构概览 2、策略模式的基本概念2.1 定义与结构2.2 核心角色解析2.2.1 策略接口(Strategy)2.2.2 具体策略实现(ConcreteStrategy)2.2.3 上下文(Cont…...

JavaScript学习记录6
第一节 算数运算符 1. 概述 JavaScript 共提供10个算术运算符,用来完成基本的算术运算。 加法运算符x y减法运算符 x - y乘法运算符 x * y除法运算符x / y指数运算符x ** y余数运算符x % y自增运算符x 、x自减运算符--x 、x--数值运算符 x负数值运算符-x 减法、…...

如何在没有 iCloud 的情况下将数据从 iPhone 传输到 iPhone
概括 您可能会遇到将数据从 iPhone 转移到 iPhone 的情况,尤其是当您获得新的 iPhone 15/14 时,您会很兴奋并希望将数据转移到它。 使用iCloud最终可以做到这一点,但它的缺点也不容忽视,阻碍了你选择它。例如,您需要…...