투케이2K

304. (AndroidStudio/android/java) 지문 (finger) 인증 수행 클래스 파일 정의 및 사용 방법 정의 실시 본문

Android

304. (AndroidStudio/android/java) 지문 (finger) 인증 수행 클래스 파일 정의 및 사용 방법 정의 실시

투케이2K 2022. 6. 24. 20:52

[개발 환경 설정]

개발 툴 : AndroidStudio

개발 언어 : java

 

[A_Finger : 소스 코드]

 

import static android.content.Context.FINGERPRINT_SERVICE;
import static android.content.Context.KEYGUARD_SERVICE;

import android.Manifest;
import android.app.AlertDialog;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.os.CancellationSignal;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyPermanentlyInvalidatedException;
import android.security.keystore.KeyProperties;
import android.util.Log;
import android.view.View;
import android.widget.Toast;

import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;

import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;

public class A_Finger {


    /**
     * // -----------------------------------------
     * TODO [클래스 설명]
     * // -----------------------------------------
     * 1. 지문 인증 수행 클래스
     * // -----------------------------------------
     * 2. 필요 퍼미션 :
     *  <uses-permission android:name="android.permission.USE_FINGERPRINT" />
     *  <uses-permission android:name="android.permission.USE_BIOMETRIC"/>
     * // -----------------------------------------
     * 3. 제약 조건 :
     *  안드로이드 디바이스 기기내 지문인증 기능을 사용하기 위해서는 마시멜로 버전 이상이어야합니다
     *  지문인증 기능을 사용하기 위해서는 안드로이드 시스템 설정 내에 보안 설정 >> 잠금 설정 >> 지문 설정이되어야합니다
     * // -----------------------------------------
     * */





    /**
     * // -----------------------------------------
     * TODO [호출 및 사용 방법]
     * // -----------------------------------------
     * 1. 브로드 캐스트 알림 채널 등록 : onCreate
     *
     *   try {
     *        IntentFilter filter = new IntentFilter(); // [인텐트 필터 선언]
     *        filter.addAction(A_Finger.FINGER_AUTH_CHANNER); // [푸시 알림 받기 위함]
     *        LocalBroadcastManager.getInstance(getActivity()).registerReceiver(mMessageReceiver, filter); // [알림을 받는 리시버 지정]
     *   }
     *   catch (Exception e){
     *        e.printStackTrace();
     *   }
     * // -----------------------------------------
     * 2. 브로드 캐스트 알림 채널 해제 : onDestroy
     *
     *   try {
     *        LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(mMessageReceiver);
     *   }
     *   catch (Exception e){
     *        e.printStackTrace();
     *   }
     * // -----------------------------------------
     * 3. 실시간 지문 인증 성공 상태 알림 리시버 등록
     *
     *   private BroadcastReceiver mMessageReceiver = new BroadcastReceiver() {
     *         @Override
     *         public void onReceive(Context context, Intent intent) {
     *
     *             // [특정 채널에서 푸시 알림을 받은 경우]
     *             if(A_Finger.FINGER_AUTH_CHANNER.equals(String.valueOf(intent.getAction()))) {
     *
     *                 // [인텐트로 전달 받은 메시지 확인]
     *                 String message = String.valueOf(intent.getStringExtra("message"));
     *
     *                 // [지문 인증 성공 인지 확인 실시]
     *                 if (A_Finger.SUCCESS_FINGER.equals(message)){ // [지문 인증 성공한 경우]
     *
     *                 }
     *             }
     *         }
     *     };
     * // -----------------------------------------
     * 4. 지문 인증 기능 호출 실시 :
     *
     *   new A_Finger().FingerStart(getActivity());
     * // -----------------------------------------
     * */




    // TODO [지문 인증에 필요한 객체 선언 실시]
    private static final String KEY_NAME = "simpleAuth_twok_key";
    private FingerprintManager fingerprintManager;
    private KeyguardManager keyguardManager;
    private KeyStore keyStore;
    private KeyGenerator keyGenerator;
    public static Cipher cipher;
    private FingerprintManager.CryptoObject cryptoObject;





    // TODO [팝업창 사용을 위해 객체 정의 및 알림 표시 내용 정의]
    AlertDialog.Builder builder;
    AlertDialog alertDialog;
    private static final String AL_TITLE = "알 림";
    private static final String AL_OK = "확 인";
    private static final String AL_NO = "취 소";





    // TODO [전역 변수 선언 실시]
    private static final String USE_NO_DEVICE = "지문을 사용할 수 없는 디바이스 입니다.";
    private static final String PERMISSON_IS_NO = "지문 인증 사용을 허용해 주세요.";
    private static final String DEVICE_LOCK_NO = "지문 인증을 사용하기 위해서는 디바이스 잠금 화면을 설정해 주세요.";
    private static final String FINGER_REG_NO = "잠금 설정에 등록된 지문이 없습니다. 지문을 먼저 등록해주세요.";
    private static final String FINGER_START_MSG = "손가락을 지문인식 센서에 대 주세요.";
    private static final String VER_NO_DEVICE = "지문을 사용할 수 없는 하위 버전의 디바이스 입니다.";
    private static final String FAIL_FINGER = "지문 인증 실패 ... 다시 시도해주세요.";
    public static final String SUCCESS_FINGER = "지문 인증에 성공했습니다.";

    public static final String FINGER_AUTH_CHANNER = "FINGER_AUTH_CHANNER"; // [브로드 캐스트 알림 채널]





    // TODO [모바일 버전 확인 및 지문인증 수행 메소드]
    public void FingerStart(Context mContext){
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){ // TODO [안드로이드 마시멜로우 부터 사용 가능]

            //TODO [Manifest에 Fingerprint 퍼미션을 추가해 워야 사용가능]
            fingerprintManager = (FingerprintManager) mContext.getSystemService(FINGERPRINT_SERVICE);
            keyguardManager = (KeyguardManager) mContext.getSystemService(KEYGUARD_SERVICE);

            //TODO [지문을 사용할 수 없는 디바이스인 경우]
            if(!fingerprintManager.isHardwareDetected()){
                Log.i("---","---");
                Log.e("//===========//","================================================");
                Log.i("","\n"+"[A_Finger >> FingerStart :: 모바일 버전 확인 및 지문 인증 수행 실시]");
                Log.i("","\n"+"[결 과 :: "+String.valueOf(USE_NO_DEVICE)+"]");
                Log.e("//===========//","================================================");
                Log.i("---","---");

                // [Alert 팝업창 알림 실시]
                showMessageAlert(
                        mContext,
                        AL_TITLE,
                        USE_NO_DEVICE,
                        AL_OK,
                        ""
                );
            }
            //TODO [지문 인증 사용을 거부한 경우]
            else if(ContextCompat.checkSelfPermission(mContext,
                    Manifest.permission.USE_FINGERPRINT) != PackageManager.PERMISSION_GRANTED){
                Log.i("---","---");
                Log.e("//===========//","================================================");
                Log.i("","\n"+"[A_Finger >> FingerStart :: 모바일 버전 확인 및 지문 인증 수행 실시]");
                Log.i("","\n"+"[결 과 :: "+String.valueOf(PERMISSON_IS_NO)+"]");
                Log.e("//===========//","================================================");
                Log.i("---","---");

                // [Alert 팝업창 알림 실시]
                showMessageAlert(
                        mContext,
                        AL_TITLE,
                        PERMISSON_IS_NO,
                        AL_OK,
                        ""
                );
            }
            //TODO [잠금 설정에 등록된 지문이 없는 경우]
            else if(!keyguardManager.isKeyguardSecure()){
                Log.i("---","---");
                Log.e("//===========//","================================================");
                Log.i("","\n"+"[A_Finger >> FingerStart :: 모바일 버전 확인 및 지문 인증 수행 실시]");
                Log.i("","\n"+"[결 과 :: "+String.valueOf(DEVICE_LOCK_NO)+"]");
                Log.e("//===========//","================================================");
                Log.i("---","---");

                // [Alert 팝업창 알림 실시]
                showMessageAlert(
                        mContext,
                        AL_TITLE,
                        DEVICE_LOCK_NO,
                        AL_OK,
                        ""
                );
            }
            //TODO [잠금 설정에 등록된 지문이 없는 경우]
            else if(!fingerprintManager.hasEnrolledFingerprints()){
                Log.i("---","---");
                Log.e("//===========//","================================================");
                Log.i("","\n"+"[A_Finger >> FingerStart :: 모바일 버전 확인 및 지문 인증 수행 실시]");
                Log.i("","\n"+"[결 과 :: "+String.valueOf(FINGER_REG_NO)+"]");
                Log.e("//===========//","================================================");
                Log.i("---","---");

                // [Alert 팝업창 알림 실시]
                showMessageAlert(
                        mContext,
                        AL_TITLE,
                        FINGER_REG_NO,
                        AL_OK,
                        ""
                );
            }
            //TODO [모든 관문을 성공적으로 통과 (지문인식을 지원하고 지문 사용이 허용되어 있고 잠금화면이 설정되었고 지문이 등록되어 있을때)]
            else {
                Log.i("---","---");
                Log.w("//===========//","================================================");
                Log.i("","\n"+"[A_Finger >> FingerStart :: 모바일 버전 확인 및 지문 인증 수행 실시]");
                Log.i("","\n"+"[결 과 :: "+String.valueOf(FINGER_START_MSG)+"]");
                Log.w("//===========//","================================================");
                Log.i("---","---");

                //TODO [지문 인증 실행]
                generateKey();

                if(cipherInit()){
                    cryptoObject = new FingerprintManager.CryptoObject(cipher);
                    // [핸들러 실행]
                    C_FingerprintHandler fingerprintHandler = new C_FingerprintHandler(mContext);
                    fingerprintHandler.startAutho(fingerprintManager, cryptoObject);
                }

                // [디자인 표시 Alert 팝업창 알림 실시]
                showAuthAlert(
                        mContext,
                        FINGER_START_MSG
                );
            }
        }
        else{ //TODO [디바이스가 마시멜로 이하인 경우]
            Log.i("---","---");
            Log.e("//===========//","================================================");
            Log.i("","\n"+"[A_Finger >> FingerStart :: 모바일 버전 확인 및 지문 인증 수행 실시]");
            Log.i("","\n"+"[결 과 :: "+String.valueOf(VER_NO_DEVICE)+"]");
            Log.e("//===========//","================================================");
            Log.i("---","---");

            // [Alert 팝업창 알림 실시]
            showMessageAlert(
                    mContext,
                    AL_TITLE,
                    VER_NO_DEVICE,
                    AL_OK,
                    ""
            );
        }
    }
    //TODO [암호화 된 지문 관리자를 만드는 데 사용할 암호를 초기화 메소드]
    @RequiresApi(api = Build.VERSION_CODES.M)
    public boolean cipherInit(){
        Log.i("---","---");
        Log.d("//===========//","================================================");
        Log.i("","\n"+"[A_Finger >> cipherInit :: 지문 인증 암호 초기화 수행 실시]");
        Log.d("//===========//","================================================");
        Log.i("---","---");
        try {
            cipher = Cipher.getInstance(
                    KeyProperties.KEY_ALGORITHM_AES + "/"
                            + KeyProperties.BLOCK_MODE_CBC + "/"
                            + KeyProperties.ENCRYPTION_PADDING_PKCS7);
        }
        catch (NoSuchAlgorithmException |
                NoSuchPaddingException e) {
            throw new RuntimeException("Failed to get Cipher", e);
        }
        try {
            keyStore.load(null);
            SecretKey key = (SecretKey) keyStore.getKey(KEY_NAME,
                    null);
            cipher.init(Cipher.ENCRYPT_MODE, key);
            return true;
        }
        catch (KeyPermanentlyInvalidatedException e) {
            return false;
        }
        catch (KeyStoreException | CertificateException
                | UnrecoverableKeyException | IOException
                | NoSuchAlgorithmException | InvalidKeyException e) {
            throw new RuntimeException("Failed to init Cipher", e);
        }
    }
    //TODO [비밀 키를 생성하는 메소드]
    @RequiresApi(api = Build.VERSION_CODES.M)
    protected void generateKey() {
        Log.i("---","---");
        Log.d("//===========//","================================================");
        Log.i("","\n"+"[A_Finger >> generateKey :: 지문 인증 비밀키 생성 수행]");
        Log.d("//===========//","================================================");
        Log.i("---","---");
        try {
            keyStore = KeyStore.getInstance("AndroidKeyStore");
        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
            keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
        } catch (NoSuchAlgorithmException | NoSuchProviderException e) {
            throw new RuntimeException("Failed to get KeyGenerator instance", e);
        }
        try {
            keyStore.load(null);
            keyGenerator.init(new KeyGenParameterSpec.Builder(KEY_NAME,
                    KeyProperties.PURPOSE_ENCRYPT |
                            KeyProperties.PURPOSE_DECRYPT)
                    .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
                    .setUserAuthenticationRequired(true)
                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
                    .build());
            keyGenerator.generateKey();
        } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | CertificateException | IOException e){
            throw new RuntimeException(e);
        }
    }
    //TODO [지문 인식 수행 부분]
    @RequiresApi(api = Build.VERSION_CODES.M)
    public class C_FingerprintHandler extends FingerprintManager.AuthenticationCallback{

        // [지문인식 객체 선언]
        CancellationSignal cancellationSignal;
        private Context mContext;

        // [클래스 생성자 초기화]
        public C_FingerprintHandler(Context context){
            this.mContext = context;
        }

        // [지문인식 인증 시작 메소드]
        @RequiresApi(api = Build.VERSION_CODES.M)
        public void startAutho(FingerprintManager fingerprintManager, FingerprintManager.CryptoObject cryptoObject){
            Log.i("---","---");
            Log.w("//===========//","================================================");
            Log.i("","\n"+"[A_Finger >> startAutho :: 지문 인증 시작 실시]");
            Log.w("//===========//","================================================");
            Log.i("---","---");
            try {
                cancellationSignal = new CancellationSignal();
                fingerprintManager.authenticate(cryptoObject, cancellationSignal, 0, this, null);
            }
            catch (Exception e){
                e.printStackTrace();
            }
        }

        // [지문인식 인증 에러 메소드]
        @Override
        public void onAuthenticationError(int errorCode, CharSequence errString) {
            this.update(""+errString, false);
            Log.i("---","---");
            Log.e("//===========//","================================================");
            Log.i("","\n"+"[A_Finger >> onAuthenticationError :: 지문 인증 에러 발생]");
            Log.i("","\n"+"[Error :: "+String.valueOf(errString.toString())+"]");
            Log.e("//===========//","================================================");
            Log.i("---","---");

            // [에러 메시지 확인]
            String comment = String.valueOf(errString.toString());

            /**
             * // -----------------------------------
             * [주요 에러 메시지 정리]
             * // -----------------------------------
             * 1) 시도 횟수가 너무 많습니다. 나중에 다시 시도하세요.
             * // -----------------------------------
             */

            // [Alert 팝업창 알림 실시]
            showMessageAlert(
                    mContext,
                    AL_TITLE,
                    comment,
                    AL_OK,
                    ""
            );
        }

        // [지문인식 인증 실패 메소드]
        @Override
        public void onAuthenticationFailed() {
            this.update("지문인증 실패", false);
            Log.i("---","---");
            Log.e("//===========//","================================================");
            Log.i("","\n"+"[A_Finger >> onAuthenticationFailed :: 지문 인증 실패]");
            Log.e("//===========//","================================================");
            Log.i("---","---");

            // [에러 메시지 확인]
            String comment = String.valueOf(FAIL_FINGER);

            // [Alert 팝업창 알림 실시]
            showMessageAlert(
                    mContext,
                    AL_TITLE,
                    comment,
                    AL_OK,
                    ""
            );
        }

        // [지문인식 인증 에러 메소드]
        @Override
        public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
            this.update(""+helpString, false);
            Log.i("---","---");
            Log.e("//===========//","================================================");
            Log.i("","\n"+"[A_Finger >> onAuthenticationHelp :: 지문 인증 실패]");
            Log.i("","\n"+"[message :: "+String.valueOf(helpString.toString())+"]");
            Log.e("//===========//","================================================");
            Log.i("---","---");

            // [에러 메시지 확인]
            String comment = String.valueOf(helpString.toString());

            /**
             * // -----------------------------------
             * [주요 에러 메시지]
             * // -----------------------------------
             * 1) 손가락을 너무 빨리 움직였습니다. 다시 시도해 주세요.
             * // -----------------------------------
             * 2) 지문 센서를 깨끗이 닦고 다시 시도하세요.
             * // -----------------------------------
             */

            // [Alert 팝업창 알림 실시]
            showMessageAlert(
                    mContext,
                    AL_TITLE,
                    comment,
                    AL_OK,
                    ""
            );
        }

        // [지문인식 인증 성공 메소드]
        @Override
        public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
            this.update(SUCCESS_FINGER, true);
            Log.i("---","---");
            Log.w("//===========//","================================================");
            Log.i("","\n"+"[A_Finger >> onAuthenticationSucceeded :: 지문 인증 성공]");
            Log.w("//===========//","================================================");
            Log.i("---","---");
        }

        // [지문인식 인증 중도 취소 메소드]
        public void stopFingerAuth(){
            Log.i("---","---");
            Log.e("//===========//","================================================");
            Log.i("","\n"+"[A_Finger >> stopFingerAuth :: 지문 인증 중도 취소]");
            Log.e("//===========//","================================================");
            Log.i("---","---");
            try {
                if(cancellationSignal != null && !cancellationSignal.isCanceled()){
                    cancellationSignal.cancel();
                }
            }
            catch (Exception e){
                e.printStackTrace();
            }
        }

        // [지문인식 인증 진행 후 동적 콘텐츠 변경 메소드]
        private void update(String message, boolean success) {
            Log.i("---","---");
            Log.e("//===========//","================================================");
            Log.i("","\n"+"[A_Finger > stopFingerAuth : 지문 인증 진행 확인]");
            Log.i("","\n"+"[success :: "+String.valueOf(success)+"]");
            Log.i("","\n"+"[message :: "+String.valueOf(message)+"]");
            Log.e("//===========//","================================================");
            Log.i("---","---");

            // TODO [지문인증 성공한 경우]
            if(success == true) {

                // [Alert 팝업창 알림 실시]
                /*
                showMessageAlert(
                        mContext,
                        AL_TITLE,
                        message,
                        AL_OK,
                        ""
                );
                // */


                // TODO [지문 인증 성공 알림 전달]
                try {
                    Intent intent = new Intent(FINGER_AUTH_CHANNER); // [채널 명칭 지정]
                    intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); // [플래그 설정]
                    intent.putExtra("message", String.valueOf(message)); // [데이터 전달]
                    LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent); // [브로드 캐스트 알림 전달]
                }
                catch (Exception e){
                    e.printStackTrace();
                }


                // TODO [활성화된 팝업창이 있으면 종료 수행 실시]
                try {
                    if (alertDialog != null){
                        alertDialog.dismiss(); // [다이얼로그가 활성화 되어있으면 취소]
                    }
                }
                catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
        
    } // TODO [내부 클래스 종료]





    // TODO [메시지 팝업창 호출 실시]
    public void showMessageAlert(Context mContext, final String title, final String content, final String ok, final String no){
        Log.i("---","---");
        Log.d("//===========//","================================================");
        Log.i("","\n"+"[A_Finger >> showMessageAlert :: 메시지 팝업창 호출 수행 실시]");
        Log.i("","\n"+"[title :: "+String.valueOf(title)+"]");
        Log.i("","\n"+"[content :: "+String.valueOf(content)+"]");
        Log.d("//===========//","================================================");
        Log.i("---","---");
        // TODO [이미 활성화된 창이 있는지 확인 실시]
        try {
            if(alertDialog != null){
                alertDialog.dismiss(); // [다이얼로그가 활성화 되어있으면 취소]
            }
        }
        catch (Exception e){
            e.printStackTrace();
        }
        try {
            // TODO [AlertDialog 팝업창 생성]
            builder = new AlertDialog.Builder(mContext);
            builder.setTitle(title); // 팝업창 타이틀 지정
            //builder.setIcon(R.drawable.ic_launcher_foreground); // 팝업창 아이콘 지정
            builder.setMessage(content); // 팝업창 내용 지정
            builder.setCancelable(false); // 외부 레이아웃 클릭시도 팝업창이 사라지지않게 설정
            builder.setPositiveButton(ok, new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    // TODO [확인 버튼 클릭 로직 처리]
                }
            });
            builder.setNegativeButton(no, new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    // TODO [취소 버튼 클릭 로직 처리]
                }
            });
            alertDialog = builder.create();
            alertDialog.show();
        }
        catch (Exception e){
            //e.printStackTrace();
            Toast.makeText(mContext, content, Toast.LENGTH_SHORT).show();
        }
    }





    // TODO [커스텀 디자인 팝업창 호출 실시]
    public void showAuthAlert(Context mContext, final String title){
        Log.i("---","---");
        Log.d("//===========//","================================================");
        Log.i("","\n"+"[A_Finger >> showAuthAlert :: 지문 인증 팝업창 호출 수행 실시]");
        Log.i("","\n"+"[title :: "+String.valueOf(title)+"]");
        Log.d("//===========//","================================================");
        Log.i("---","---");
        // TODO [이미 활성화된 창이 있는지 확인 실시]
        try {
            if(alertDialog != null){
                alertDialog.dismiss(); // [다이얼로그가 활성화 되어있으면 취소]
            }
        }
        catch (Exception e){
            e.printStackTrace();
        }
        try {
            // TODO [커스텀 디자인 팝업창 레이아웃 지정]
            View dialogView=(View)View.inflate(mContext, R.layout.finger_alert,null);

            // TODO [AlertDialog 팝업창 생성]
            builder = new AlertDialog.Builder(mContext);
            builder.setTitle(title); // 팝업창 타이틀 지정
            builder.setView(dialogView); // [커스텀 뷰 지정]
            alertDialog = builder.create();
            alertDialog.show();
        }
        catch (Exception e){
            //e.printStackTrace();
            Toast.makeText(mContext, title, Toast.LENGTH_SHORT).show();
        }
    }


} // TODO [클래스 종료]
 

 

[finger_alert.xml : 소스 코드]

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="100dp"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:layout_marginTop="10dp">

    <ImageView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_marginTop="20dp"
        android:gravity="center"
        android:layout_gravity="center"
        android:src="@drawable/okfinger"/>

    <TextView
        android:layout_width="100dp"
        android:layout_height="30dp"
        android:gravity="center"
        android:layout_gravity="center"
        android:text=""/>

</LinearLayout>

 

[결과 출력]


반응형
Comments