철스토리

안드로이드 Phone Selector, SMS Retriever API 본문

안드로이드/프로그래밍

안드로이드 Phone Selector, SMS Retriever API

HyunChol 2019. 6. 3. 19:12
반응형

안드로이드에서 자신의 폰에서 자신의 번호를 가지고오는 방법이 존재했다. 

TelephonyManager를 통해서 자신의 번호를 쉽게 가지고 올 수 있었다. 

그렇지만 이것은 READ_PHONE_STATE 권한이 필요하다. 

하지만 최근 READ_PHONE_STATE와 READ_SMS 권한에 대해서 구글에서 특별관리(?)가 들어갔다.

스토어에 올리지 않을것이라면 관계 없지만 플레이스토어에 앱을 등재하기 위해서는 꼭 해당 권한이 필요하지 않는다면 사용할 수 없다.

 

앱을 서비스 할 때 스마트폰에서 폰번호를 가지고 오거나 문자메시지에 접근해서 인증번호를 가지고와서 자동으로 인증번호의 값을 세팅해주는 작업을 많이한다.

 

구글에서는 이러한경우에 READ_PHONE_STATE와  READ_SMS 권한 없이도 사용가능한 api를 제공해 주었다.

 

1. gradle에 아래의 내용을 추가

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. 폰번호를 불러오고 싶은 곳에 아래와 같이 구현

public static final int RESOLVE_HINT = 10102;
public static final String TAG = "TAG";

private void requestHint() {
    HintRequest hintRequest = new HintRequest.Builder()
            .setPhoneNumberIdentifierSupported(true)
            .build();

    GoogleApiClient apiClient = new GoogleApiClient.Builder(activity)
            .enableAutoManage(activity, 0  /* clientId */, null)
            .addApi(Auth.CREDENTIALS_API)
            .build();


    PendingIntent intent = Auth.CredentialsApi.getHintPickerIntent(apiClient, hintRequest);
    try {
        activity.startIntentSenderForResult(intent.getIntentSender(), RESOLVE_HINT, null, 0, 0, 0);
    } catch (IntentSender.SendIntentException e) {
        e.printStackTrace();
    }
}

public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == RESOLVE_HINT && resultCode == Activity.RESULT_OK) {
            Credential credential = data.getParcelableExtra(Credential.EXTRA_KEY);

		// credential.getId() : +821012345678 형태로 폰 번호를 얻을 수 있음
        Log.d(TAG, credential.getId());
    }
}

 

위의 작업을 통해 폰번호를 가지고 올 수 있게 되었다.

 

인증 문자메시지를 자동으로 불러오는 작업 자체는 복잡하지 않은데 서버에서 문자메시지에 앱의 키 해시를 담아서 보내줘야 한다. 해당 규칙은 아래와 같다.

1. 문자메시지 전체의 길이는 140 byte를 넘으면 안된다.
2. 문자의 맨 앞에는 <#> 이 무조건 붙어야 한다. ([#] 도 가능하다고 하는거 같음... 테스트 안해봄)
3. 문자의 맨 뒤에는 앱의 키 해시가 들어가야한다. (9글자 키 해시)

 

키 해시를 얻는 코드

public static final String TAG = "TAG";

public static ArrayList<String> getAppSignatures(Context context) {
    ArrayList<String> appCodes = new ArrayList<>();

    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) {
            String 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 appCodes;
}

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;
}

위의 함수 getAppSignatures를 호출하여 로그에 "이 값을 SMS 뒤에 써서 보내주면 됩니다 : " 라고 되어 있는 부분을 확인하면 된다.

 

서버쪽에서 문자를 보내주는 규칙은 되었고, 안드로이드에서 처리해줄것만 남았다.

 

1. BroadcastReceiver를 상속받은 SmsReceiver를 생성한다.

public class SmsReceiver extends BroadcastReceiver {
	public static final String TAG = "TAG";
    public static final String SMSRetrievedAction = "com.google.android.gms.auth.api.phone.SMS_RETRIEVED";

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

 

2. AndroidManifest.xml 에 receiver 등록한다.

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

 

3. SmsReceiver를 등록해준다.

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    registerSmsReceiver();
    smsRetrieverCall();
}

private void smsRetrieverCall() {
    SmsRetrieverClient client = SmsRetriever.getClient(this);

    Task<Void> task = client.startSmsRetriever();
    task.addOnSuccessListener(new OnSuccessListener<Void>() {
        @Override
        public void onSuccess(Void aVoid) {
            Log.d(TAG, "smsRetrieverCall SUCCESS");
        }
    });

    task.addOnFailureListener(new OnFailureListener() {
        @Override
        public void onFailure(@NonNull Exception e) {
            Log.e(TAG, "smsRetrieverCall FAIL");
        }
    });
}

private void registerSmsReceiver() {
    SmsReceiver smsReceiver = new SmsReceiver();

    IntentFilter filter = new IntentFilter();
    filter.addAction(SmsReceiver.SMSRetrievedAction);

    registerReceiver(smsReceiver, filter);
}

 

4. 마지막으로 문자메시지를 받아서 처리한부분인데 본인은 이벤트버스로 처리했음

@Subscribe(threadMode = ThreadMode.MAIN)
public void setSmsReceiverEvent(SmsReceiverEvent event) {
    Log.e(TAG, event.getAuthNumber());
    
	String authNumber = event.getAuthNumber();

	// 문자메시지의 내용 그대로가 넘어온다.
    // 그러기 때문에 인증번호만 뽑아내는 로직은 각자가 만들어야 함

    editSmsCode.setText(authNumber);
}

 

다음번에 까먹지 않도록 내용을 작성했다! 끝!

반응형
Comments