Skip to content

Add SmsOtp plugin #743

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open

Add SmsOtp plugin #743

wants to merge 4 commits into from

Conversation

benzhe
Copy link

@benzhe benzhe commented Jun 24, 2025

Add a plugin for automatic reading OTP from SMS message.

90% of the code is generated by Gemini 2.5 Pro. I'm not an Android expert, so I couldn't identify any potential flaws in it. The code has only been tested on my own device. Therefore, I'd appreciate it if you guys could help review the code.

Copy link
Member

@rocka rocka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for you contribution! But to be honest, this implemetation is far from ideal.

The plugin has permission.IPC and interact with the input method with FcitxRemoteService, which is the intended design; but the input method itself uses Broadcast which makes the whole process slow and inscure, because other applications can also register an receiver with the same name and intercept the otp code. Moreover, it's actually not a trival task to extract otp code from SMS messages, a simple regex could lead to many false-positives.

I've read through the changes and made comments accordingly. In short, this PR cannot be merged in current state and needs to be rewritten from the ground up.


Or if you're tired, maybe it's better to just use https://github.com/RikkaW/SmsCodeHelper . It still works on Android 15 with fcitx5-android. It's more secure too, because on Android 10+, only the foreground process and the input method has access to the clipboard.

Comment on lines 183 to 182
* subsequent operations can start if the prior operation is not finished (suspended),
* [postFcitxJob] ensures that operations are executed sequentially.
*/
private val otpReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val otp = intent.getStringExtra(BuildConfig.APPLICATION_ID + ".OTP_CODE") ?: return
inputView?.broadcaster?.onOtpReceived(otp)
}
}

fun postFcitxJob(block: suspend FcitxAPI.() -> Unit): Job {
val job = fcitx.lifecycleScope.launch(start = CoroutineStart.LAZY) {
fcitx.runOnReady(block)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you are adding a new member to the class, plesae do not put it between other methods and it's comments. Obviously the comment above is written for postFcitxJob but not otpReceiver

Comment on lines 110 to 116
override fun updateOtp(otp: String?) {
Timber.d("updateOtp called: $otp")
otp?.let {
val intent = Intent(BuildConfig.APPLICATION_ID + ".OTP_RECEIVED").apply {
putExtra(BuildConfig.APPLICATION_ID + ".OTP_CODE", it)
}
sendBroadcast(intent)
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this time the "otp code" is already inside the input method's process, there is no need to send a "Broadcast". Broadcast is designed for communication between different applications: https://developer.android.com/develop/background-work/background-tasks/broadcasts

Comment on lines 202 to 206
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(otpReceiver, IntentFilter(BuildConfig.APPLICATION_ID + ".OTP_RECEIVED"), RECEIVER_EXPORTED)
} else {
registerReceiver(otpReceiver, IntentFilter(BuildConfig.APPLICATION_ID + ".OTP_RECEIVED"))
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not use Broadcast here. Even if you have to use Broadcast for some reason, you should limit which application can send it with a permission, see:
https://developer.android.com/reference/android/content/Context#registerReceiver(android.content.BroadcastReceiver,%20android.content.IntentFilter,%20java.lang.String,%20android.os.Handler,%20int)

@@ -165,6 +167,7 @@ class KawaiiBarComponent : UniqueViewComponent<KawaiiBarComponent, FrameLayout>(

private fun evalIdleUiState(fromUser: Boolean = false) {
val newState = when {
otpCode != null -> IdleUi.State.Otp
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bad design: the user cannot dismiss the otp code and switch to toolbar.

Comment on lines 34 to 37
class OtpUi(override val ctx: Context, private val theme: Theme) : Ui {

private val icon = imageView {
imageDrawable = drawable(R.drawable.ic_baseline_library_books_24)!!.apply {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be fair, this file is identical with ClipboardSuggestionUi.kt, except for the class name and the icon. At least consider reuse it?

@@ -21,6 +21,8 @@ interface InputBroadcastReceiver {

fun onStartInput(info: EditorInfo, capFlags: CapabilityFlags) {}

fun onOtpReceived(otp: String) {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InputBroadcastReceiver is designed for input method events (such as input method mode, candidates, composing and selection range. etc.) and should only be used for that purpose. Consider make a singleton object like ClipboardManager, or just simply copy the otp code to clipboard and avoid all the hassle.

Comment on lines 25 to 26
/** Update candidate words from plugin */
void updateOtp(in String Otp);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment has nothing to do with the method...

Comment on lines 49 to 53
val sms =
android.telephony.SmsMessage.createFromPdu(
pdu as ByteArray,
format
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method has already been deprecated. Shoud have use Telephony.Sms.Intents.getMessagesFromIntent(Intent) instead.

Comment on lines 80 to 78
private fun extractOtp(message: String?): String? {
if (message == null) return null
// More robust regex might be needed for different OTP formats
val pattern = Pattern.compile("(?<!\\d)(\\d{4,8})(?!\\d)") // Example: 4-8 digits
val matcher = pattern.matcher(message)
return if (matcher.find()) matcher.group(1) else null
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -0,0 +1 @@
../../../../../../../../../lib/plugin-base/src/main/res/mipmap-xxxhdpi/ic_launcher_plugin_generic.png
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unnecessary to symlink the icon since the plugin depends on plugin_base module which provides the @mipmap/ic_launcher_plugin_generic icon.

@benzhe
Copy link
Author

benzhe commented Jun 25, 2025

I've revised most of the code according to the guidelines. Please review it again.

And in my situation, I don't prefer the OTP codes messing up with clipboard content.

// More robust regex might be needed for different OTP formats
val pattern = Pattern.compile("(?<![.\\d/\\-])(\\d{4,8})(?![.\\d/\\-])")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you plan to improve this in the future?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really, it's really challenging to find robust OTP match rules in multilingual, but I've tested this regex with some messages format on https://receive-smss.com/. It works well without false-positives, not work on WhatsApp's format with "-" in the middle of the OTP, like "234-567".
I removed this comment to avoid confusion.

Comment on lines 37 to 48
@Test
fun testExtractOtp_realSamples() {
assertEquals("401572", extractOtp("WhatsApp code 401-572"))
assertEquals("28843", extractOtp("28843 e o codigo de confirmacao do Facebook de Pedro Davi #fb"))
assertEquals("932033", extractOtp("Your DENT code is: 932033"))
assertEquals("050475", extractOtp("Ваш код перевірки Poe: 050475. Не повідомляйте цей код іншим."))
assertEquals("429309", extractOtp("[抖音] 429309 is your verification code, valid for 5 minutes."))
assertEquals("8650", extractOtp("<#> 8650 is your Venmo phone verification code."))
assertNull(extractOtp("OKX:USDTsuccessfully withdrawn: 132,619.89 【okxcash.com】Account: DTm888 Key: Swksf367"))
assertNull(extractOtp("You can also tap on this link to verify your phone: v.whatsapp.com/696948"))
assertNull(extractOtp("https://www.photosfromyourevent.com/4669/wauem8/"))
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benzhe benzhe requested a review from rocka July 15, 2025 06:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy