Android

Sms Retriever ( 안드로이드 문자 메세지 수신 )

Machine_웅 2021. 3. 9. 11:21
728x90
반응형

Google Play에서 SMS 또는 Call Log 권한 그룹을 비롯하여

위험성이 높거나 민감한 권한의 사용을 제한하는 정책을 내놓았다.

 

여기서 다루고 싶은 내용은 기존에 사용자 편의를 위해서 제공하는 기능이였던 SMS 읽기 기능이다.

이전에는 RECEIVE_SMS권한을 이용해서 SMS내용을 읽어서 인증번호를 자동입력을 해주었지만

이제는 해당 권한을 추가할 경우 스토어에 등록이 되지 않는다.

 

기존에 등록되어있던 앱을 경우에는 예외신청을 하지 않으면 Google Play에서 삭제 대상이 된다.

 


 

RECEIVE_SMS권한을 통해서 SMS를 읽어오지는 못하지만 대신 구글에서 다른 방안을 제시하였다.

바로 SMS Retriever를 이용하여 권한허용 필요없이 문자를 읽을수있다.단 몇가지 제한사항들이 있다.

Android 단말에 Google Play Service version이 10.2 이상 설치되어 있어야한다.

그리고 SMS에도 몇가지 규칙을 준수해야한다.

1. 문자내용이 140byte를 초과하면안된다.

2. SMS 맨앞에 <#>가 반드시 포함되어야 한다.

3. SMS 맨마지막에 앱을 식별하는11글자 해시문자열을 포함해야한다.

ex) <#> 인증번호[1234] Evp3gExcg1e

 

 

1. build.gradle

// SMS 메세지 데이터 가져오기 관련
    implementation 'com.google.android.gms:play-services-auth:16.0.1'
    implementation 'com.google.android.gms:play-services-auth-api-phone:16.0.0'
    

2. AndroidManifest.xml

        <receiver
            android:name=".wUtils.Sms_receiver"
            android:exported="true">
            <intent-filter>
                <action android:name="com.google.android.gms.auth.api.phone.SMS_RETRIEVED" />
            </intent-filter>
        </receiver>

 

3. Sms_receiver.java

public class Sms_receiver extends BroadcastReceiver {
    public static final String TAG = "Woongs";

    @Override
    public void onReceive(Context context, Intent intent) {
        if (SmsRetriever.SMS_RETRIEVED_ACTION.equals(intent.getAction())) {
            Bundle extras = intent.getExtras();
            Status status = (Status) extras.get(SmsRetriever.EXTRA_STATUS);

            Log.d(TAG, "SmsReceiver : onReceiver");
            switch (status.getStatusCode()) {
                case CommonStatusCodes.SUCCESS:
                    String message = (String) extras.get(SmsRetriever.EXTRA_SMS_MESSAGE);
                    Log.d(TAG, "SmsReceiver : onReceiver(CommonStatusCodes.SUCCESS)");
                    Log.d(TAG, "message : "+message);

                    // 본인은 문자를 받았을 때 EventBus를 통해 처리해 줬다.
                    // 이 부분이 문자메시지를 받은것이니 각자 message를 가공해서 숫자를 뽑아낸 다음 세팅시켜주면 될 듯 하다.
                   // EventBus.getDefault().post(new SmsReceiverEvent(message));
                    break;
                case CommonStatusCodes.TIMEOUT:
                    Log.d(TAG, "SmsReceiver : onReceiver(CommonStatusCodes.TIMEOUT)");
                    break;
            }
        }
    }
}

 

4. MainActivity.java

    Sms_receiver smsReceiver;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_util__main);
    
        smsReceiver = new Sms_receiver();
    }

 

 @Override
    protected void onResume() {
        super.onResume();

        SmsRetrieverClient client = SmsRetriever.getClient(this);   // this = context
        Task<Void> task = client.startSmsRetriever();

        task.addOnSuccessListener(new OnSuccessListener<Void>() {
            @Override
            public void onSuccess(Void aVoid) {
                // retriever 성공
                IntentFilter intentFilter = new IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION);    // SMS_RETRIEVED_ACTION 필수입니다.
                registerReceiver(smsReceiver, intentFilter);
            }
        });

        task.addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                // retriever 실패
            }
        });

    }

 


추가

SMS 보내기

 

1. AndroidManifest.xml

    <uses-permission android:name="android.permission.SEND_SMS" />
    <uses-permission android:name="android.permission.READ_SMS" />
    <uses-permission android:name="android.permission.RECEIVE_SMS" />

2. MainActivity.java

/ -------------test code ----------------------
    public void send_Sms(){
        SmsManager sms = SmsManager.getDefault();
        String phoneNumber="받을 휴대폰 번호 ";
        String key_hash = getAppSignatures(this);
        String message ="<#> XXX 앱의 인증번호는 [123456] 입니다.메세지테스트\n"+key_hash;
        sms.sendTextMessage(phoneNumber, null, message, null, null);
    }

    public void checkPermission(){
        String[] permission_list = {
                Manifest.permission.SEND_SMS,
                Manifest.permission.READ_SMS
        };

        //현재 안드로이드 버전이 6.0미만이면 메서드를 종료한다.
        if(Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
            return;

        for(String permission : permission_list){
            //권한 허용 여부를 확인한다.
            int chk = checkCallingOrSelfPermission(permission);
            if(chk == PackageManager.PERMISSION_DENIED){
                //권한 허용을여부를 확인하는 창을 띄운다
                requestPermissions(permission_list,0);
            }
        }
    }

 

 

키해시 구하기

    // 키해시 구하기 2
    private static final String HASH_TYPE = "SHA-256";
    public static final int NUM_HASHED_BYTES = 9;
    public static final int NUM_BASE64_CHAR = 11;

    /**
     * get App Signatures
     */
    public static String getAppSignatures(Context context) {
        ArrayList<String> appCodes = new ArrayList<>();
        String hash ="";
        try {
            // Get all package signatures for the current package
            String packageName = context.getPackageName();
            PackageManager packageManager = context.getPackageManager();
            Signature[] signatures = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES).signatures;

            // For each signature create a compatible hash
            for (Signature signature : signatures) {
                hash = getHash(packageName, signature.toCharsString());
                if (hash != null) {
                    appCodes.add(String.format("%s", hash));
                }
                Log.d(TAG, String.format("이 값을 SMS 뒤에 써서 보내주면 됩니다 : %s", hash));
            }
        } catch (PackageManager.NameNotFoundException e) {
            Log.d(TAG, "Unable to find package to obtain hash. : " + e.toString());
        }
        return hash;
    }

    private static String getHash(String packageName, String signature) {
        String appInfo = packageName + " " + signature;
        try {
            MessageDigest messageDigest = MessageDigest.getInstance(HASH_TYPE);
            // minSdkVersion이 19이상이면 체크 안해도 됨
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                messageDigest.update(appInfo.getBytes(StandardCharsets.UTF_8));
            }
            byte[] hashSignature = messageDigest.digest();

            // truncated into NUM_HASHED_BYTES
            hashSignature = Arrays.copyOfRange(hashSignature, 0, NUM_HASHED_BYTES);
            // encode into Base64
            String base64Hash = Base64.encodeToString(hashSignature, Base64.NO_PADDING | Base64.NO_WRAP);
            base64Hash = base64Hash.substring(0, NUM_BASE64_CHAR);

            Log.d(TAG, String.format("\nPackage : %s\nHash : %s", packageName, base64Hash));
            return base64Hash;
        } catch (NoSuchAlgorithmException e) {
            Log.d(TAG, "hash:NoSuchAlgorithm : " + e.toString());
        }
        return null;
    }

 

 

 

출처 및 참고 : 

justbobby.tistory.com/m/18

 

Android SMS Retriever API 사용하기 - SMS 인증번호

​ Google Play에서 SMS 또는 Call Log 권한 그룹을 비롯하여 위험성이 높거나 민감한 권한의 사용을 제한하는 정책을 내놓았다. 여기서 다루고 싶은 내용은 기존에 사용자 편의를 위해서 제공하는 기

justbobby.tistory.com

 

 


* BroadcastReceiver 에 온 데이터를 Activity에 전달하기!

 

옵저버 패턴을 이용하여 액티비티에 전달을 하려고 합니다.

 

1.ObservableObject

import java.util.Observable;

public class ObservableObject extends Observable {
    // design Pattern
    private static ObservableObject instance = new ObservableObject();

    public static ObservableObject getInstance() {
        return instance;
    }

    private ObservableObject() {
    }

    public void updateValue(Object data) {
        synchronized (this) {
            setChanged();
            notifyObservers(data);
        }
    }

}

 

 

2.MainActivity.class

public class MainActivity extends AppCompatActivity implements View.OnClickListener, Observer {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

       ObservableObject.getInstance().addObserver(this);
    }
}


 @Override
 public void update(Observable observable, Object data) {
     String massage = (String)data;

     Log.d("Woongs","update : massage - "+massage);
     Toast.makeText(this, String.valueOf("activity observer " + data), Toast.LENGTH_SHORT).show();
}

 

3.Sms_receiver.class

public class Sms_receiver extends BroadcastReceiver {
    public static final String TAG = "Woongs";

    @Override
    public void onReceive(Context context, Intent intent) {
        if (SmsRetriever.SMS_RETRIEVED_ACTION.equals(intent.getAction())) {
            Bundle extras = intent.getExtras();
            Status status = (Status) extras.get(SmsRetriever.EXTRA_STATUS);

            Log.d(TAG, "SmsReceiver : onReceiver");
            switch (status.getStatusCode()) {
                case CommonStatusCodes.SUCCESS:
                    String message = (String) extras.get(SmsRetriever.EXTRA_SMS_MESSAGE);
                    Log.d(TAG, "SmsReceiver : onReceiver(CommonStatusCodes.SUCCESS)");
                    Log.d(TAG, "message : "+message);

                    // 본인은 문자를 받았을 때 EventBus를 통해 처리해 줬다.
                    // 이 부분이 문자메시지를 받은것이니 각자 message를 가공해서 숫자를 뽑아낸 다음 세팅시켜주면 될 듯 하다.
                   // EventBus.getDefault().post(new SmsReceiverEvent(message));

                    ObservableObject.getInstance().updateValue(message);

                    break;
                case CommonStatusCodes.TIMEOUT:
                    Log.d(TAG, "SmsReceiver : onReceiver(CommonStatusCodes.TIMEOUT)");
                    break;
            }
        }
    }
}

 

 


* 발생 이슈 

1 ) 메세지를 두번 읽어들이는 문제 

- 이유  Manifests 에  브로드 케스트 리시버를 등록해놓고,  동적으로 또 등록했다.. 하나만 사용할 것,

 

2 ) 처음 메세지만 수신하고,  재요청시 메세지가 오지 않는 경우 

-  추측이지만

  SmsRetrieverClient client = SmsRetriever.getClient(this);   // this = context
        Task<Void> task = client.startSmsRetriever();

에서 한번 메세지를 받은후 종료되는거 같음  그래서,  메세지 수신후 다시 초기화 해주는 방식을 선택함.

 

  public void set_module(){

        SmsRetrieverClient client = SmsRetriever.getClient(this);   // this = context
        Task<Void> task = client.startSmsRetriever();
        task.addOnSuccessListener(new OnSuccessListener<Void>() {
            @Override
            public void onSuccess(Void aVoid) {
                // retriever 성공
                // SMS_RETRIEVED_ACTION 필수입니다.
                if(smsReceiver != null){
                   unregisterReceiver(smsReceiver);
                }

                smsReceiver = new SmsReceiver();
                IntentFilter intentFilter = new IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION);    // SMS_RETRIEVED_ACTION 필수입니다.
                registerReceiver(smsReceiver, intentFilter);
            }
        });

        task.addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                // retriever 실패
            }
        });
    }

이 코드를  위에서 등록한 옵져버 패턴의 update 에다가 재 설정 해줌.

  @Override
    public void update(Observable o, Object arg) {
        String message = (String)arg;
        txt_code.setText(message);

        // 재등록
        set_module();
    }
728x90
반응형