본문 바로가기
Flutter

[Flutter] 플러터 - AOS(안드로이드) 네이티브 연동 SMS 자동완성

by s_hoonee 2024. 6. 12.
반응형

SmsBroadcastReceiver.kt

package com.test.sms

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import com.google.android.gms.auth.api.phone.SmsRetriever
import com.google.android.gms.common.api.CommonStatusCodes
import com.google.android.gms.common.api.Status
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodChannel

class SmsBroadcastReceiver : BroadcastReceiver() {
    companion object {
        private var binaryMessenger: BinaryMessenger? = null
        private const val TAG = "smsTest"

        fun setBinaryMessenger(messenger: BinaryMessenger) {
            binaryMessenger = messenger
        }
    }

    override fun onReceive(context: Context, intent: Intent) {
        Log.d(TAG, "🎁🎁 수신 성공")

        if (SmsRetriever.SMS_RETRIEVED_ACTION == intent.action) {
            val extras = intent.extras
            val status: Status? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                extras?.getParcelable(SmsRetriever.EXTRA_STATUS, Status::class.java)
            } else {
                @Suppress("DEPRECATION")
                extras?.getParcelable(SmsRetriever.EXTRA_STATUS)
            }

            Log.d(TAG, "받은: 🎁🎁 status: $status")
            Log.d(TAG, "statusCode: ${status?.statusCode}")

            when (status?.statusCode) {
                CommonStatusCodes.SUCCESS -> {
                    Log.d(TAG, "onReceive: SMS Received")
                    val message = extras?.getString(SmsRetriever.EXTRA_SMS_MESSAGE)
                    Log.d(TAG, "message: $message")

                    binaryMessenger?.let {
                        MethodChannel(it, "sms_retriever").invokeMethod("onSmsReceived", message)
                    }
                }
                // SMS Retriever API가 타임아웃되었을 때
                CommonStatusCodes.TIMEOUT -> {
                    Log.d(TAG, "onReceive: SMS Retriever timed out")
                }
                // 기타 상태 코드
                else -> {
                    Log.d(TAG, "onReceive: Unknown status code: ${status?.statusCode}")
                }
            }
        } else {
            Log.d(TAG, "🚨🚨🚨 SMS Retriever failed")
        }
    }
}

MainActivity.kt

package com.test.sms

import AppSignatureHelper
import android.util.Log
import com.google.android.gms.auth.api.phone.SmsRetriever
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

// MainActivity 클래스는 FlutterActivity를 상속받아 Flutter 앱과 Android 플랫폼 간의 통신을 담당합니다.
class MainActivity: FlutterActivity() {
    // MethodChannel을 통해 Flutter와 통신할 채널 이름을 정의합니다.
    private val CHANNEL = "sms_retriever"

    // Flutter 엔진을 구성하는 메서드입니다.
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        // MethodChannel을 설정하고, 특정 메서드 호출을 처리합니다.
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
            // Flutter에서 "startSmsRetriever" 메서드가 호출되었을 때 startSmsRetriever 메서드를 실행합니다.
            if (call.method == "startSmsRetriever") {
                startSmsRetriever(result)
                val helper = AppSignatureHelper(this)
                val hash = helper.getAppSignatures()?.get(0)

                Log.d("smsTest", "Hash: $hash")

            }
        }

        // SmsBroadcastReceiver 클래스에 Flutter 엔진의 바이너리 메신저를 설정합니다.
        SmsBroadcastReceiver.setBinaryMessenger(flutterEngine.dartExecutor.binaryMessenger)
    }

    // SMS Retriever API를 시작하는 메서드입니다.
    private fun startSmsRetriever(result: MethodChannel.Result) {
        // SmsRetriever 클라이언트를 가져옵니다.
        val client = SmsRetriever.getClient(this)
        // SMS Retriever를 시작합니다.
        client.startSmsRetriever()
            .addOnSuccessListener {
                // 성공적으로 시작되면 Flutter에 성공 메시지를 반환합니다.
                result.success("SMS Retriever started")
                Log.d("smsTest", "SMS Retriever started")
            }
            .addOnFailureListener {
                // 시작 실패 시 Flutter에 에러 메시지를 반환합니다.
                result.error("Error", "Failed to start SMS Retriever", null)
            }
    }
}

HashCheck.kt

어차피 해쉬코드는 모든 클라이언트단에서 동일하니 한 번 뽑고 안뽑아도 된다!

추출법

https://hooninha.tistory.com/114

 

[Android] AOS - SMS Retriever API 해쉬코드

굉장한 삽질 끝에 진정한 내 앱의 해쉬값을 찾을 수 있었다. 해당 코드를 사용하십시오..............  import android.content.Contextimport android.content.ContextWrapperimport android.content.pm.PackageManagerimport android.c

hooninha.tistory.com

 

import android.content.Context
import android.content.ContextWrapper
import android.content.pm.PackageManager
import android.content.pm.Signature
import android.util.Base64
import android.util.Log
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.Arrays

class AppSignatureHelper(context: Context) : ContextWrapper(context) {
    val TAG = AppSignatureHelper::class.java.simpleName
    private val HASH_TYPE = "SHA-256"
    val NUM_HASHED_BYTES = 9
    val NUM_BASE64_CHAR = 11

    init {
        getAppSignatures()
    }

    fun getAppSignatures(): ArrayList<String>? {
        val appCodes = ArrayList<String>()
        try {
            val packageName = packageName
            val packageManager = packageManager
            val signatures: Array<Signature> = packageManager.getPackageInfo(
                packageName, PackageManager.GET_SIGNATURES
            ).signatures

            for (signature in signatures) {
                val hash = hash(packageName, signature.toCharsString())
                if (hash != null) {
                    appCodes.add(String.format("%s", hash))
                }
                Log.e(TAG, "Hash $hash")
            }
        } catch (e: PackageManager.NameNotFoundException) {
            Log.e(TAG, "Unable to find package to obtain hash.", e)
        }
        return appCodes
    }

    private fun hash(packageName: String, signature: String): String? {
        val appInfo = "$packageName $signature"
        try {
            val messageDigest = MessageDigest.getInstance(HASH_TYPE)
            messageDigest.update(appInfo.toByteArray(StandardCharsets.UTF_8))
            var hashSignature = messageDigest.digest()
            hashSignature = Arrays.copyOfRange(hashSignature, 0, NUM_HASHED_BYTES)
            var base64Hash = Base64.encodeToString(hashSignature, Base64.NO_PADDING or Base64.NO_WRAP)
            base64Hash = base64Hash.substring(0, NUM_BASE64_CHAR)
            Log.e(TAG, String.format("pkg: %s -- hash: %s", packageName, base64Hash))
            return base64Hash
        } catch (e: NoSuchAlgorithmException) {
            Log.e(TAG, "hash:NoSuchAlgorithm", e)
        }
        return null
    }
}

 

 

이제 안드로이드 설정은 끝났다! 인증번호 체크해 주는 파일에 가자.

_handleSmsMessages 에서 SMS 인증번호 otp를 받아서 컨트롤러에 넣어주면 자동완성된다! (otp 필드가 다 채워지면 SMS 인증 Api 실행되게 만들자!) otp 추천 패키지로는 아래 패키지가 제일 좋았다.

https://pub.dev/packages/pinput

 

pinput | Flutter package

Pin code input (OTP) text field, iOS SMS autofill, Android SMS autofill One Time Code, Password, Passcode, Captcha, Security, Coupon, Wowcher, 2FA, Two step verification

pub.dev

 

s_verification_check.dart

(믹싱으로 가져온 VerificationTimer은 따로 안하셔도 됩니다)

class _VerificationCheckScreenState
    extends ConsumerState<VerificationCheckScreen>
    with  VerificationTimer {

  static const platform = MethodChannel('sms_retriever');
  final TextEditingController _controller = TextEditingController();

// sms 수신 대기 시작
  Future<void> _startSmsRetriever() async {
    try {
      final String result = await platform.invokeMethod('startSmsRetriever');

      if(result == 'SMS Retriever started'){
        debugPrint('🏮🏮🏮SMS Retriever started successfully: $result');
      }

    } on PlatformException catch (e) {
      debugPrint("🏮🏮🏮Failed to start SMS Retriever: '${e.message}'");
    }
  }

// 가져온 메시지 값을 otp 필드에 주입
  Future<void> _handleSmsMessages(MethodCall call) async {
    if (call.method == 'onSmsReceived') {

      debugPrint("🎀🎀SMS received: ${call.arguments}");

      final String message = call.arguments as String;
      final String code = _extractCodeFromMessage(message);
      setState(() {
        _controller.text = code;
      });
    }
  }

// 6자리 코드만 가져오깅
  String _extractCodeFromMessage(String message) {
    final RegExp codeRegExp = RegExp(r'\d{6}');
    final match = codeRegExp.firstMatch(message);
    return match?.group(0) ?? '';
  }

  @override
  void initState() {
    super.initState();
    /// SMS 자동 읽기 시작
    _startSmsRetriever();
    /// 타이머 시작
    startTimer();

    /// SMS 자동 읽기 이벤트 핸들러 등록
    platform.setMethodCallHandler(_handleSmsMessages);
  }
  
  
  @override
  Widget build(BuildContext context) {
  // ...........