로컬서버와 테스트 시 일반적으로 localhost 또는 127.0.0.1로 테스트를 진행하지만, 안드로이드 에뮬레이터와 테스트 시에는 10.0.2.2 로 설정을 해야한다.

 

안드로이드 에뮬레이터 네트워킹 설정참고

https://developer.android.com/studio/run/emulator-networking?hl=ko

비교적 간단한 데이터를 저장하기위해 안드로이드 개발 시 SharedPreferences를 사용한다.
설정에 따라서 SharedPreferences 파일은 프레임워크에서 관리하며 비공개이거나 공개로 설정할 수 있다.


비공개로 설정할 경우 일반적인 방법으로는 저장된 데이터를 확인할수는 없지만, 루팅을 통해 관리자 권한을 획득한 경우 저장된 데이터를 확인할 수있다. 개인정보(카드번호, 인증 비밀번호) 등 중요한 데이터가 저장하여야 하는경우에는 SharedPreferences는 적합하지 않다.

Android SDK 23부터 androidx.security를 통해 EncryptedSharedPreferences를 제공한다.

https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences

 

EncryptedSharedPreferences  |  Android Developers

androidx.constraintlayout.core.motion.parse

developer.android.com

 

기본적인 사용방법은

MasterKey masterKey = new MasterKey.Builder(context)
        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
        .build();

_sharedPreferences = EncryptedSharedPreferences.create(
        context,
        "secret_shared_pref",
        masterKey,
        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
);

 

AES256방식에   암호화 키를 생성하여 생성한 암호화 키로 저장되는 데이터의 AES256_SIV(key) ,AES256_GCM (data)방식으로 키와 데이터를 각각 암호화한다. 

 

(암호화 키 생성 시 keystore를 사용하기 때문에 별도의 키입력을 하지 않으며, 생성된 암호화 키는 컨테이너에 저장되어 저장된 암호화키는 외부에서 추출하기 어렵다.)

https://developer.android.com/training/articles/keystore?hl=ko 

 

SharedPreferences와 동일하게 앱 삭제 시 저장된 데이터도 함꼐 삭제되며, 저장된 데이터는 아래와 같이 암호화되어 저장된다.

 

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="__androidx_security_crypto_encrypted_prefs_key_keyset__">12a90125a94b1b6aa4f97a630764baecf0e5f4c046a82c8b7947dabc06e71c5170a654d27c1903ffe2da75f84fd1dd011f37c917e9b8ce21d6b4673bbec9127ea066e6adb177f34bc98ff7259331f0d854b78d318226ed62dca1b38ec2dd32598a9357432e0e524a91e9a27393370337acbc5611f8808a6d8bcb724506dcb0e5513e6d6c0cb93d9af1f22cde840c93f3c213adce4bf1cd38aea6464724a1f0bf0bc7153ea84df9b5f4f4344b1a4408b191c8fc03123c0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696e6b2e4165735369764b6579100118b191c8fc032001</string>
    <string name="AT+SCLGWnuSnzmogArPsMQJbMe92S7STOQ==">AR4mc1e8vYohj66Q91oCIoaMmklTD+DDPStaqibMtsDXatXDJJeskjW7eW9WNTGR</string>
    <string name="__androidx_security_crypto_encrypted_prefs_value_keyset__">128801163831f1d036fdc9a33da123fee29b5cda21a49d89684f5fac67e3d54a634de5d8ca990106d0535f0ce74e824f8352e027a37a7512a189a67c183781bf6ce08894b3a622bda2632205c5e5f64389cbff75556bd05d0f8dcd8dc495b9cff09681a7e1e18bae625b7d1513e7301c4ed2aa1e592b97df72474ee9437f75e20e1a019803d7cb3cd9f78f1a4408d7e699f101123c0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696e6b2e41657347636d4b6579100118d7e699f1012001</string>
    <string name="AT+SCLEaHEykuXI9WqXm7wB0ZOTyX/dPWI4=">AR4mc1euqAo6fPFARiDB0ACt92fHt3w1P+64D76rk9Dlyv4ya/hKWgQ5dHy6COIN</string>
</map>

 

 

EncryptedSharedPreferences 을 사용하기 위해서는 Security 라이브러리를 추가 해야함.

(build.gradle의 dependencies추가)

 

implementation "androidx.security:security-crypto-ktx:1.1.0-alpha03"

 

EncryptedSharedPreferences 공통 Class로 사용하기 위한 샘플 소스

 

import android.content.Context;
import android.content.SharedPreferences;

import androidx.security.crypto.EncryptedSharedPreferences;
import androidx.security.crypto.MasterKey;

import java.io.IOException;
import java.security.GeneralSecurityException;

public class SharedPreferenceUtil {

    private static SharedPreferenceUtil sharedInstance;
    private Context _context = null;
    private MasterKey masterKey = null;

    private SharedPreferences _sharedPreferences = null;
    private SharedPreferences.Editor _shEditor = null;

    private final static String SECRET_PREFERENCE = "secret_shared_prefs";


    public static synchronized SharedPreferenceUtil getInstance(Context context) throws GeneralSecurityException, IOException {
        if (null == sharedInstance) {
            sharedInstance = new SharedPreferenceUtil(context);
        }
        return sharedInstance;
    }

    public SharedPreferenceUtil(Context context) throws GeneralSecurityException, IOException {
        MasterKey masterKey = new MasterKey.Builder(context)
                .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
                .build();

        _sharedPreferences = EncryptedSharedPreferences.create(
                context,
                SECRET_PREFERENCE,
                masterKey,
                EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
                EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
        );
    }

    public void putString(String key, String data) {
        _shEditor.putString(key, data);
        _shEditor.commit();
    }

    public void putInt(String key, int data) {
        _shEditor.putInt(key, data);
        _shEditor.commit();
    }

    public void putFloat(String key, float data) {
        _shEditor.putFloat(key, data);
        _shEditor.commit();
    }

    public void putLong(String key, long data) {
        _shEditor.putLong(key, data);
        _shEditor.commit();
    }

    public void putBoolean(String key, boolean data) {
        _shEditor.putBoolean(key, data);
        _shEditor.commit();
    }

    public void clear() {
        if (null != _shEditor) {
            _shEditor.clear();
            _shEditor.commit();
        }
    }

    public String getString(String key, String defValue) {
        return _sharedPreferences.getString(key, defValue);
    }

    public int getInt(String key, int defValue) {
        return _sharedPreferences.getInt(key, defValue);
    }

    public float getFloat(String key, float defValue) {
        return _sharedPreferences.getFloat(key, defValue);
    }

    public long getLong(String key, long defValue) {
        return _sharedPreferences.getLong(key, defValue);
    }

    public boolean getBoolean(String key, boolean defValue) {
        return _sharedPreferences.getBoolean(key, defValue);
    }
}

 

프로젝트 진행 중 실 시간으로 시간을 날짜 시간을 표기 해달라는 고객 요청사항이 있어 Thread와 Handler를 사용하여 TextView에 표시해야곘다는 생각을 했다. 

 

구글링을 하다가 TextClock으로 손쉽게 현재 날짜 시간을 표기할수 있다는것을 알게되었다.

(https://developer.android.com/reference/android/widget/TextClock)

 

API level 17부터 지원되기 시작한 기능으로 TextView를 상속받아 구현되었다.

<TextClock
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:format12Hour="yyyy년 MM월 dd일 EE요일"
    android:textSize="18dp" />

<TextClock
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:format12Hour="hh:mm a"
    android:textSize="45dp" />

format 안에 내용은

yyyy : 년

MM : 월

dd : 일

EE : 요일

hh : 시간

mm : 분

a : 오전/오후를 나타낸다

android:format12Hour="hh:mm a"
android:format24Hour="hh:mm"

format12Hour은 12시간 시간표기방식, format24Hour은 24시간 시간표기 방식을 설정할수 있다.

요일과 오전/오후의 경우는 시스템의 local언어에 따라 다르게 표기된다.

(개발 단말의 언어가 한국어인 경우는 기본적으로 위와 같이 한글로 요일과 오전/오후가 표기 되지만 영어인 경우는 영어로 요일과 AM/PM으로 표기 된다.)

 

예전에 시간 갱신하는 로직을 일일히 다 구현해 줘야하는 번거로움이 있었는데, TextClock사용으로 이러한 번거로움을 줄일수 있었다.

안드로이드 프로젝트 빌드 중 Execution failed for task ':app:validateSigningDebug'에러가 발생하였다.

 

이유는  프로젝트 내에 debug.keystore가 존재하지 않아서 발생하였다.

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:validateSigningDebug'.
> Keystore file '/Users/Documents/gitlab/app-frontend/android/app/debug.keystore' not found for signing config 'debug'.

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 13s

error Failed to install the app. Make sure you have the Android development environment set up: https://reactnative.dev/docs/environment-setup. Run CLI with --verbose flag for more details.
Error: Command failed: ./gradlew app:installDebug -PreactNativeDevServerPort=8081
Warning: Mapping new ns http://schemas.android.com/repository/android/common/02 to old ns http://schemas.android.com/repository/android/common/01

debug.keystore는 /Users/.android/debug.keystore 에 존재하며, 로컬에 존재하는 debug.keystore를 해당경로로 옮겨주니 정상적으로 빌드가 되었다.

프로젝트 진행 중 gitlab 에 React-Native로 되어 있는 소스를 빌드 후 실행 중 

Error: error:0308010C:digital envelope routines::unsupported

오류가 발생하였다.

 

Failed to construct transformer:  Error: error:0308010C:digital envelope routines::unsupported
    at new Hash (node:internal/crypto/hash:71:19)
    at Object.createHash (node:crypto:133:10)
    at stableHash (/Users/Documents/gitlab/app-frontend/node_modules/metro-cache/src/stableHash.js:19:8)
    at JsTransformer.getCacheKey (/Users/Documents/gitlab/app-frontend/node_modules/metro/src/JSTransformer/worker.js:478:7)
    at getTransformCacheKey (/Users/Documents/gitlab/app-frontend/node_modules/metro/src/DeltaBundler/Transformer/getTransformCacheKey.js:39:29)
    at new Transformer (/Users/Documents/gitlab/app-frontend/node_modules/metro/src/DeltaBundler/Transformer.js:147:28)
    at /Users/Documents/gitlab/app-frontend/node_modules/metro/src/Bundler.js:54:29
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
  opensslErrorStack: [ 'error:03000086:digital envelope routines::initialization error' ],
  library: 'digital envelope routines',
  reason: 'unsupported',
  code: 'ERR_OSSL_EVP_UNSUPPORTED'
}
[Tue Nov 29 2022 13:52:10.710]  BUNDLE  ./index.js 

error: TypeError: Cannot read properties of undefined (reading 'transformFile')
    at /Users/Documents/gitlab/app-frontend/node_modules/metro/src/Bundler.js:91:34
    at Generator.next (<anonymous>)
    at asyncGeneratorStep (/Users/Documents/gitlab/app-frontend/node_modules/metro/src/Bundler.js:14:24)
    at _next (/Users/Documents/gitlab/app-frontend/node_modules/metro/src/Bundler.js:34:9)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)

 

오류 내용으로 검색해보니 node v17이상에서는 digital envelope routines이 지원되지 않아서 발생하는 문제라고 되어있었다.

 

nvm으로 노드 버전을 v12.22.12 으로 변경 후(프로젝트의 ReadMe의 환경 설정에 노드 버전이 12.x.x버전) node_modules을 제거후 다시 npm install을 해보았다.

 

동일한 Error: error:0308010C:digital envelope routines::unsupported 오류가 계속 발생하여 반나절 정도 삽질을 하였다.

삽질 중 nvm 의 default버전이 v18.12.1로 되어 있어 있는데, 에러가 이부분에 연관된 것이 아닌가라는 의심이 들어 혹시나 하는 마음에 nvm default 버전을 변경해 보았다.

 

nvm alias default 12.22.12

변경 후 nvm list명령어로 nvm defalut가 정상적으로 변경되었음을 확인하였다.

 

node_module을 제거 후 새로운 터미널에서 다시 npm install 후 실행을 하니 정상적으로 앱이 실행되었다.

 

 

안드로이드에서 제공하는 생체 인증을 하는 방식에 대해 정리 한다.

 

안드로이드 버전

한국어명

영어명

버전

릴리즈

API 레벨

안드로이드 6 마시멜로우

Marshmallow

6.0-6.0.1

2015

23

안드로이드 7 누가

Nougat

7.0-7.1

2016-08-22

24-25

안드로이드 8 오레오

Oreo

8.0-8.1

2017-08-21

26-27

안드로이드 9 파이

Pie

9.0

2018-08-06

28

안드로이드 10

Android 10

10.0

2019-09-03

29

안드로이드 11

Android 11

11.0

TBD

-

생체 인식 기능은 Marshmallow부터 지원 가능.

 

Marshmallow ~ Oreo : Fingerprint

Pie~ Android 11 : BiometricPrompt

 

1) Fingerprint를 이용한 생체 인증

 

API Level 23~ API Level 27 지문인식을 제공해 주는 API

 

Android 6.0 API(API Level 23) 부터 지문 인증과 관련하여 지문 스캔을 사용하여 사용자를 인증하는 새로운 API를 제공

 

참고

https://developer.android.com/about/versions/marshmallow/android-6.0

 

Android 6.0 API  |  Android 개발자  |  Android Developers

Get to know the new developer features in Android 6.0 Marshmallow.

developer.android.com

 

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.dreamsecurity.bioauth.sample">
    
    <uses-permission android:name="android.permission.USE_FINGERPRINT" />

 

 

현재 Android studio에서는 권한을 추가하면 USE_FINGERPRINT is deprecated 경고가 발생

API Level 29 부터는 BiometricPrompt가 권장되기 때문이다.

 

 

생체 인증 가능 여부확인

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {   // Marshmallow부터 지원 가능 체크
     fingerprintManager = (FingerprintManager) getSystemService(FINGERPRINT_SERVICE);

     if (fingerprintManager.isHardwareDetected() == false) { //Manifest에 Fingerprint 퍼미션을 추가해야 사용이 가능함.
            Toast.makeText(this, "지문인식을 사용할수 없는 기기입니다.", Toast.LENGTH_LONG).show();
     } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.USE_FINGERPRINT) != PackageManager.PERMISSION_GRANTED) {
            Toast.makeText(this, "지문 사용을 여부를 허용해 주세요.", Toast.LENGTH_LONG).show();
     } else if (fingerprintManager.hasEnrolledFingerprints() == false) {
            Toast.makeText(this, "등록된 지문정보가 없습니다.", Toast.LENGTH_LONG).show();
     } else {    //  생체 인증 사용가능
            Toast.makeText(this, "지문인식을 해주세요.", Toast.LENGTH_LONG).show();
     }
}

 

 

생체 인증 등록

private CancellationSignal cancellationSignal = null;
private Context context;

public FingerprintHandler(Context context) {
      this.context = context;
}

public void startAuth(FingerprintManager fingerprintManager, FingerprintManager.CryptoObject cryptoObject) {
      cancellationSignal = new CancellationSignal();
      fingerprintManager.authenticate(cryptoObject, cancellationSignal, 0, this, null);
}

 

 

생체 인식 Callback

   @Override
   public void onAuthenticationError(int errorCode, CharSequence errString) {
        this.update("인증 에러 발생" + errString, false);
   }

   @Override
    public void onAuthenticationFailed() {
        this.update("인증 실패", false);
   }

   @Override
   public void onAuthenticationHelp(int errorCode, CharSequence error) {
        this.update("Error: " + error, false);
   }

    @Override
    public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
        this.update("앱 접근이 허용", true);
    }

    public void stopFingerAuth() {
        if (cancellationSignal != null && !cancellationSignal.isCanceled()) {
            cancellationSignal.cancel();
        }
    }

    private void update(String result, boolean isResult) {
        if (isResult == false) {
            Toast.makeText(MainActivity.this, "지문 인식 실패", Toast.LENGTH_LONG).show();
         } else {//지문인증 성공
            Toast.makeText(MainActivity.this, "지문 인식 성공. \nresult : "+result, Toast.LENGTH_LONG).show();
         }
    }

 

 

2) BiometricPrompt를 이용한 생체 인증

 

참고 URL : https://developer.android.com/training/sign-in/biometric-auth

API Level 29 부터는 BiometricPrompt 제공

 

build.gradle dependencies 추가

dependencies {
    ...
    implementation 'androidx.biometric:biometric:1.1.0'
}

 

permission 추가

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.dreamsecurity.bioauth.sample">

   <uses-permission android:name="android.permission.USE_BIOMETRIC"/>

 

생체 인증 가능 여부확인

	switch (manager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)) {
		case BiometricManager.BIOMETRIC_SUCCESS: {   //  생체 인증 가능
            Log.d("MainActivity", "Application can authenticate with biometrics.");
          	break;
		}
		case BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE: { //  기기에서 생체 인증을 지원하지 않는 경우
			Log.d("MainActivity", "Biometric facility is not available in this device.");
			break;
		}
		case BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE: {  
			Log.d("MainActivity", "Biometric facility is currently not available");
			break;
		}
		case BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED: {   //  생체 인식 정보가 등록되지 않은 경우
			Log.d("MainActivity", "Any biometric credential is not added in this device.");
			break;
		}
		default: {   //   기타 실패
			Log.d("MainActivity", "Fail Biometric facility");
			break;
		}
	}

 

생체 인증 등록

얼굴 인식의 경우는 Android 11 (API level 30)부터 지원

BiometricPrompt.PromptInfo.Builder promptBuilder = new BiometricPrompt.PromptInfo.Builder();

promptBuilder.setTitle("Biometric login for my app");
promptBuilder.setSubtitle("Log in using your biometric credential");
promptBuilder.setNegativeButtonText("Use account password");

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){ //  안면인식 ap사용 android 11부터 지원
    promptBuilder.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL);
}

promptInfo =  promptBuilder.build();

 

생체 인증 실행

biometricLoginButton.setOnClickListener(view -> {
    biometricPrompt.authenticate(promptInfo);
});

 

생체 인식 Callback

	biometricPrompt = new BiometricPrompt(MainActivity.this,
                executor, new BiometricPrompt.AuthenticationCallback() {
            @Override
            public void onAuthenticationError(int errorCode,
                                              CharSequence errString) {
                super.onAuthenticationError(errorCode, errString);
                Toast.makeText(getApplicationContext(),

                        "ErrorCode: "+errorCode+
                                "\nAuthentication error: " + errString, Toast.LENGTH_SHORT)
                        .show();
            }

            @Override
            public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
                super.onAuthenticationSucceeded(result);
                Toast.makeText(getApplicationContext(), "result : Authentication succeeded", Toast.LENGTH_SHORT).show();
            }
            
            @Override
            public void onAuthenticationFailed() {
                super.onAuthenticationFailed();
                Toast.makeText(getApplicationContext(), "Authentication failed",
                        Toast.LENGTH_SHORT)
                        .show();
            }
        });

 

추후 생체 인증 방법에 대해서는 조금 더 공부를 해보아야 겠다.

안드로이드 오픈 소스를 import 하여 사용중 switch case 문에서 Non-Constant Expressions : Migration Necessary 라는 오류가 발생하였다.

 

상세 내용


 

Non-Constant Expressions : Migration Necessary



As of ADT 14, the resource fields (such as R.id.img_btn_submit_auth) are no longer

constants when defined in library projects This is necessary to make library projects

reusable without recompiling them.



One consequence of this that you can no longer use the fields directly in switch

statements. You must use an if-else chain instead.


Ecplipse can automatically convert from a switch statement to an if-else statement.

Just place the caret on the switch keyword and invoke Quick Fix Ctrl-1 on Windows and 

Linux, Cmd-1 on Mac) then select "Convert 'switch' to if-else".


for more information, see :http://tools.android.com/tips/non-constant-fields


 


isLibrary로 되어 있는 프로젝트의 경우 Res영역이 상수가 아니기 때문에 case expressions must be constant expressions 문제가 발생한다고 한다.

 

해결방법으로 swich case문 대신 if else 문을 사용하였다.



 

Android 앱을 개발하면서 서버와 통신을 하게 되는데 그때 Rest API를 많이 사용한다. 앱 개발을 하면서 Rest를 사용하였고 주변에서도 Rest라는 용어를 많이 사용하지만 누군가 Rest가 무엇이냐고 물었을 때 "클라이언트와 서버 사이에 값을 주고 받는 것" 라는 두리뭉실한 이야기 밖에 해 줄수 없다는 것에 부끄러움을 느끼며, 블로그에 정리 하게되었다.

 

* Rest란?

Rest(Representational state transfer)를 있는 그대로 해석하면 표현을 통한 상태의 전이 라고 할수 있다. 여기서 표현이란 Http 요청 메시지와 거기에 해당하는 리소스의 내용을 전달하는 Http 응답 메시지로 간단히 설명하면 클라이언트와 서버간에 메시지로 서로의 상태변화를 공유하자는 것이다. 이런 Rest이론을 따르는 시스템구조를 Restful 시스템이라고 부른다.

 

* Rest의 설계 원칙

1) 모든 자원은 URI(URL)로 관리
2) 클라이언트의 상태와 기능은 서버로 요청한 자원으로 파악
3) URI(URL)는 클라이언트와 서버 간의 자원을 가리키는 유일한 인터페이스
4) 클라이언트의 요청 정보는 서버에 저장되지 않음

 

서버내의 자원은 URL(Uniform Resource Locator) 혹은 URI(Uniform Resource Identifier)을 이용하여 관리한다.

 

URL 형식 : 서버 주소 + 서비스 이름+ 자원

 

* 장점

- URI(URL)형태로 자원을 관리 하기 떄문에 느슨한 결합(loose coupling)이 가능하며, 속도 또한 빠르다.(URI(URL)형태로 자원을 관리 하기 떄문에 자주 사용하는 자원에 대해 캐싱 처리가 가능하다.)

- Http 프로토콜을 사용하여 패킷의 변화 없이 그래도 하용할수 있다.
- 결합도가 낮아 확장성이나 배포가 편리하다.
- 거의 모든 운영체제의 지원이 가능하며 별도의 라이브러리 배포없이 사용이 가능하다.
- 서버와 클라이언트의 역할이 독립되어 있어 변경이나 확장이 용이하다.

* 단점


- 공급자가 같더라도 서버별로 Rest API사이에 일관성이 없다.    
- Rest 모델에 적합하지 않은 형태의 URI(URL) 체계가 존재한다

- 표준화된 구성이나 정의가 없다.

 

이론적으로 Rest에 대해 간단히 정리하였고, 나중에 시간이 된다면 더 정리해 놓아야 할것 같다.

예전 중국 앱 프로젝트를 진행하다가 샤오미 폰에서 프로젝트내 RES영역에 있는 이미지가 나오지 않는 경우가 있었다.

문제의 원인이 뭔지 몰라서 한참 검색을 하다가 안드로이드에서 PNG 이미지 사용을 권장 하는 것을 알게 되었고 이미지 교체후 문제를 해결하였다.
문제의 원인은 JPG 이미지를 사용 이미지 손실이 발생하여 이미지가 보이지 않아 PNG이미지로 변경 후 사용했었다.

 

이미지 형식

설 명

확 장 자

PNG

무손실(권장)

.png

나인패치

무손실(권장)

.9.png

JPG

권장하지 않음(손실)

.jpg.jpeg

GIF

사용하지 않는 것이 좋음

.gif

 

안드로이드는 기본적으로 PNG이미지나 나인패치 이미지 사용을 권장한다고 한다.
JPG의 경우는 손실이 발생하여 권장하지 않고, GIF는 기본적으로 사용하지 않는것이 좋다고 하니... 꼭 필요한 경우에만 사용해야 한다.

안드로이드 앱 개발을 시작한지 2년즈음 됐을 때, 디버깅 로그를 추가 후 지우지 않고 그대로 배포하여 욕을 바가지로 먹고 피눈물을 쏟은 기억이 있다.
안드로이드 프로젝트 내 설정에서 막는 방법도 있지만, 간혹 로그가 그대로 노출 되는 경우도 있는 것 같다.
테스트 로그가 그대로 상용에 배포되는 실수를 반복하지 않고자 따로 Log 클래스를 만들어 상용배포시 boolean값만 변경하면 디버깅 내용이 나오지 않게 class를 만들었다.

/**  * Application Log  *   * @author dada  *   */ public class Looger {  	private final static String TAG = "Logger";  	private static Looger logger = null; 	private boolean debugAble = true;	//	true: 디버깅 가능, false : 디버깅 되지 않음  	private Looger(){} 	  	public static Looger getInstance(){ 		if (logger == null) { 			logger = new Looger(); 		} 		logger.debugAble = true; 		 		return logger; 		 	} 	public static Looger getInstance(boolean debuggerAble){ 		if (logger == null) { 			logger = new Looger(); 		}  		logger.debugAble = debuggerAble; 		 		return logger; 	}   	public void e(String message){ 		if (logger != null && debugAble == true){ 			Log.e(TAG, message); 		} 	}  	public void d(String message){ 		if (logger != null && debugAble == true){ 			Log.d(TAG, message); 		} 	}  	public void v(String message){ 		if (logger != null && debugAble == true){ 			Log.v(TAG, message); 		} 	}  	public void i(String message){ 		if (logger != null && debugAble == true){ 			Log.i(TAG, message); 		} 	}  	public void w(String message){ 		if (logger != null && debugAble == true){ 			Log.w(TAG, message); 		} 	} 	public void e(String TAG, String message){ 		if (logger != null && debugAble == true){ 			Log.e(TAG, message); 		} 	} 	 	public void d(String TAG, String message){ 		if (logger != null && debugAble == true){ 			Log.d(TAG, message); 		} 	} 	 	public void v(String TAG, String message){ 		if (logger != null && debugAble == true){ 			Log.v(TAG, message); 		} 	} 	 	public void i(String TAG, String message){ 		if (logger != null && debugAble == true){ 			Log.i(TAG, message); 		} 	} 	 	public void w(String TAG, String message){ 		if (logger != null && debugAble == true){ 			Log.w(TAG, message); 		} 	} } 

사용방법

 Looger.getInstance().e("===================결과값 확인===================");

따로 어려운 코드가 들어 간것은 아니지만 매번 새로 만들려면 귀찮으니 기록으로 남긴다.

+ Recent posts