-
Notifications
You must be signed in to change notification settings - Fork 217
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
base: master
Are you sure you want to change the base?
Add SmsOtp plugin #743
Conversation
There was a problem hiding this 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.
* 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) |
There was a problem hiding this comment.
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
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) | ||
} | ||
} |
There was a problem hiding this comment.
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
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")) | ||
} |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
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 { |
There was a problem hiding this comment.
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) {} |
There was a problem hiding this comment.
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.
/** Update candidate words from plugin */ | ||
void updateOtp(in String Otp); |
There was a problem hiding this comment.
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...
val sms = | ||
android.telephony.SmsMessage.createFromPdu( | ||
pdu as ByteArray, | ||
format | ||
) |
There was a problem hiding this comment.
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.
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 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually it's not a trival task to extract otp code from SMS messages. The logic should be more rubost like this: https://github.com/RikkaW/SmsCodeHelper/blob/d8eb66153074df0e2a62938c1ce268a7cb0a9899/app/src/main/java/rikka/smscodehelper/utils/SMSCode.java#L10
Also consider adding some tests: https://github.com/RikkaW/SmsCodeHelper/blob/d8eb66153074df0e2a62938c1ce268a7cb0a9899/app/src/test/java/rikka/smscodehelper/utils/SMSCodeTest.java#L30
@@ -0,0 +1 @@ | |||
../../../../../../../../../lib/plugin-base/src/main/res/mipmap-xxxhdpi/ic_launcher_plugin_generic.png |
There was a problem hiding this comment.
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.
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/\\-])") |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
@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/")) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are some test cases from SmsCodeHelper which definitely should be considered: https://github.com/RikkaW/SmsCodeHelper/blob/d8eb66153074df0e2a62938c1ce268a7cb0a9899/app/src/test/java/rikka/smscodehelper/utils/SMSCodeTest.java#L40-L45
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.