반응형

안녕하세요. Simple& Happy Dev입니다.

구글 플레이에는 수많은 손전등 / 플래시라이트(이하 손전등) 앱이 존재합니다.

일반적으로 스위치 버튼 하나 정도만 있으면 되는 비교적 간단한 UI에 카메라를 다뤄본 경험이 있다면 쉽게 만들 수 있기 때문일 겁니다. 여기서 카메라를 언급한 이유는 손전등의 불빛은 카메라 플래시(Camera Flash LED)를 사용하기에 그렇습니다.

손전등 앱을 만드는 방법은 몇 가지가 있습니다.
앞서 설명했듯이 카메라 플래시를 사용한다는 공통점이 있으나, 카메라 플래시 제어를 위해서 어떻게 접근하는지에 따라서 구분됩니다.

[손전등 앱의 카메라 플래시 제어하는 3가지 방법]

아래 그림에 보듯이 크게 카메라 권한을 가지고 (= 카메라 권한 필요) 플래시를 제어하는 방식과 카메라 권한 없이(카메라 권한 불필요) 플래시를 제어하는 방식으로 만들 수 있습니다.

카메라 플래시 제어 접근 방식

방법1 (Camera API 이용하는 방법)
 - 장점: 안드로이드 구버전도 지원 가능, 구현 용이
 - 단점: 카메라 권한 필요, deprecated API, 느린 속도(but, 약간 개선 가능 - 이글의 마지막에 다룰 예정)

방법2 (Camera2 API 이용하는 방법)
- 장점: 최신 Camera API
- 단점: 카메라 권한 필요, 구현 복잡, 안드로이드 5.0 (API Level 21) 이상부터 가능

방법3 (Flashlight API 이용하는 방법)
- 장점: 카메라 권한 불필요, 구현 매우 용이, 빠른 속도
- 단점: 안드로이드 6.0 (API Level 23) 이상부터 가능

Camera2 API 이용하는 방법은 거의 단점만 존재해서 일반적으로 사용하지 않는 방법입니다.
하지만, 궁금하시거나 해당 방법이 필요하신 분들이 있을 수도 있어서 다음 글에 다루도록 하겠습니다.

일단 이 글에서는 Camera API 이용해서 손전등 앱을 만드는 방법에 관해서 설명하도록 하겠습니다. 

[Camera API 이용한 손전등 앱]

애초에 플래시는 카메라를 보조해주기 위해서 들어간 디바이스입니다.
그래서, 하드웨어적인 연결이나 소프트웨어적인 접근이 카메라에 가깝게 되어 있습니다.

그 결과 플래시를 제어하기 위해서는 카메라에 접근한 이후 카메라 파라미터로 플래시를 넘겨줘야 했습니다.

플래시가 동작하는 순서는 다음과 같습니다.

플래시 켜기: 카메라 객체의 인스턴스 생성 및 연결(open)
                    - 카메라 파라미터에 플래시 모드를 Torch로 설정(setFlashMode)
                    - 카메라 프리뷰 시작(startPreview)

플래시 끄기 방법1: 카메라 프리뷰 종료 (stopPreview) - 카메라 연결 해제 및 릴리즈(release)

플래시 끄기 방법2: 카메라 파라미터에 플래시 모드를 Off로 설정(setFlashMode)
                             (중요 - 추후 카메라 프리뷰 종료 및 카메라 인스턴스를 릴리즈하는 코드가 존재해야 합니다.)

동작하는 순서에 대한 설명은 여기까지 하고 이제 소스로 들어가 보겠습니다.
아래 소스는 플래시를 끌 때 위에 있는 "플래시 끄기 방법1"로 하게 되어 있습니다.
"플래시 끄기 방법2"는 글의 마지막 부분에 설명되어 있습니다.

[소스 설명]

레이아웃 (activity_main.xml)
 

화면 한가운데 별 모양 버튼을 하나 추가해서 이걸 누르면 플래시가 켜지도록 할 예정입니다. 그리고, 한 번 더 누르면 버튼이 토글되어서 꺼지도록 하겠습니다. 별 모양 버튼은 SDK에 내장된 리소스를 사용하시면 됩니다.

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<ImageButton
android:id="@+id/ibFlashOnOff"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/btn_star_big_off"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

자바 (MainActivity.java)

onCreate에서는 기기에 플래시가 지원하지 않으면 메시지 출력하고 3초 후 종료하도록 처리(delayedFinish())하도록 만들어 줍니다.
또한 별 모양 버튼에 대한 클릭 리스너를 만들어서 클릭 시 플래시를 켜주거나 꺼주고(flashlight()), 플래시 켜짐 상태에 따른 별 모양 이미지(켜짐: 노란색 별, 꺼짐: 흰색 별)를 변경하도록 처리해줍니다.

package com.example.help.permissionflashlight;

import android.content.pm.PackageManager;
import android.hardware.Camera;
import android.os.Handler;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageButton;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {
private static Camera mCamera = null;
private ImageButton mImageButtonFlashOnOff;
private boolean mFlashOn;

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

if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)) {
Toast.makeText(getApplicationContext(), "There is no camera flash.\n The app will finish!", Toast.LENGTH_LONG).show();
delayedFinish();
return;
}

mImageButtonFlashOnOff = findViewById(R.id.ibFlashOnOff);
mImageButtonFlashOnOff.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
flashlight();
mImageButtonFlashOnOff.setImageResource(mFlashOn ? android.R.drawable.btn_star_big_on : android.R.drawable.btn_star_big_off);
}
});
}

private void delayedFinish() {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
finish();
}
}, 3000);
}

초기에는 플래시가 꺼진 상태이고, 카메라 인스턴스인 mCamera가 null이므로 onCreate에 있던 flashlight()는 openCamera()에서 카메라 객체의 인스턴스를 생성 및 연결하고 문제가 없으면 true로 리턴하고, 이후 mFlashOn를 토글시켜주어서 켜진 상태(mFlashOn => true)로 세팅됩니다.

그리고, 플래시를 Torch 모드로 파라미터 세팅을 해주고, 프리뷰를 시작해주면 플래시가 켜지게 됩니다.

반대로 플래시가 켜진 상태에서 openCamera()에 진입하면 이전에 mCamera가 생성된 상태라서 별다른 처리 없이 true로 리턴되고 mFlashOn이 토글되면서 꺼진 상태(mFlashOn => false)로 세팅됩니다.

꺼진 상태일 경우 프리뷰를 종료하도록 하고, 카메라 인스턴스를 릴리즈해줍니다. 이후 mCamera는 꼭 null로 만들어주어야 이후 오동작을 하지 않게 됩니다.

private boolean openCamera() {
if (mCamera == null) {
try {
mCamera = Camera.open();
} catch (RuntimeException e) {
Toast.makeText(getApplicationContext(), "Camera open failed", Toast.LENGTH_LONG).show();
e.printStackTrace();
return false;
}
}
return true;
}

private void flashlight() {
if (openCamera()) {
mFlashOn = !mFlashOn;

if (mFlashOn) {
Camera.Parameters params = mCamera.getParameters();
params.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);

mCamera.setParameters(params);
mCamera.startPreview();

} else {
mCamera.stopPreview();
mCamera.release();
mCamera = null;
}
} else {
delayedFinish();
}
}
}

여기까지 작업 후 실행을 하게 되면 아래와 같은 에러를 만나게 되고 openCamera()의 try ~ catch 에 의해 강제 종료(Force Close)가 되지 않고, 메시지 출력 후 openCamera() 리턴을 false로 해서 3초 후 종료할 수 있게 되어 있습니다. 

W 3858     3858     ServiceManager:    Permission failure: android.permission.CAMERA from uid=10210 pid=3165
E  3858     3858     CameraService:      Permission Denial: cant use the camera pid=3165, uid=10210
W 3165     3165     CameraBase:         An error occurred while connecting to camera 0: Service not available
W 3165     3165     System.err:             java.lang.RuntimeException: Fail to connect to camera service
W 3165     3165     System.err:             at android.hardware.Camera.<init>(Camera.java:519)
W 3165     3165     System.err:             at android.hardware.Camera.open(Camera.java:379)
W 3165     3165     System.err:             at com.example.help.permissionflashlight.MainActivity.openCamera(MainActivity.java:50)
W 3165     3165     System.err:             at com.example.help.permissionflashlight.MainActivity.flashlight(MainActivity.java:61)
W 3165     3165     System.err:             at com.example.help.permissionflashlight.MainActivity$1.onClick(MainActivity.java:32)

위와 같은 현상이 일어난 것은 에러 메시지의 내용과 같이 카메라 권한이 없는 상태에서 접근했기 때문에 발생한 것입니다.


메니페스트(AndroidManifest.xml)

카메라 권한을 추가하기 위해서는 아래와 같이 <uses-permission android:name="android.permission.CAMERA" />를 메니페스트 파일에 넣어주면 됩니다.

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

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

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

프로젝트 압축화일
PermissionFlashlight.zip


실행 결과


cf) 플래시 끄기 방법2

플래시 끄기 1번의 방법은 stopPreview 및 release도 해주기 때문에 플래시가 꺼질 때 시간이 좀 걸립니다.
개선된 방법인 플래시 끄기 방법2에서는 카메라 플래시 모드를 FLASH_MODE_OFF로 파라미터 설정해주기만 하면 되기에 속도가 빠릅니다.

그 외에 몇 군데 다르게 처리해야 하는 부분들도 있습니다.

앞서 간단히 언급한 부분이기도 한데, 플래시 끄는 곳에서는 카메라 파라미터에 플래시 모드를 off 해주는 것 외에는 다른 처리를 하지 않고 있어서 카메라 프리뷰 종료 및 카메라 인스턴스를 릴리즈하는 코드는 onPause 쪽에 넣어주어야 합니다.

그리고, 그 코드가 onPause로 이동했기에 객체의 인스턴스 생성하는 코드는 onResume에 넣어주는 게 적당합니다.

자세한 코드는 소스를 첨부하니 참고하시길 바랍니다.
PermissionFlashlight2.zip


이상으로 "손전등 앱 만들기 첫 번째 - 개요 및 Camera API 이용방법"에 대해서 설명해 드렸습니다.
다음번에는 Camera2 API 이용해서 손전등 앱을 만들어 보도록 하겠습니다.

조금이나마 도움이 되셨으면 아래 공감 버튼을 눌러주세요.
(If this article helps you, please press the button below.)

반응형

 

 

안드로이드 앱에 다국어 지원이라는 주제로 글을 적어봅니다.

 

문자열은 별도의 xml 파일에 저장 후 Layout이나 Java에서 이것을 이용해야 하지만, 귀찮다는 이유로 Java에 하드코딩 하는 경우도 있을 것입니다.

 

추천하는 방법은 아니지만, 앱에서 지원해야 언어가 많지 않고 굳이 Java에서 다국어 지원을 처리해야겠다 할 경우에 어떻게 하면 되는지 기술해보겠습니다.

 

간단한 테스트를 위해서 버튼을 누르면 텍스트가 출력되는 앱을 만들도록 하겠습니다.

 

언어 변경을 위해서는 "설정(Settings) 앱 - 언어(Language)" 에 가서 원하는 언어로 변경하면 되지만, 테스트를 위해서 매번 그렇게 하기에는 번거롭기 때문에 앱에서 볼륨키를 눌러서 언어를 변경하도록 예제를 만들어보겠습니다.

 

[다국어지원 - JAVA에서 하드코딩 방식]

 

레이아웃 (activity_main.xml)

 

레이아웃은 가운데 Button이 하나가 있고, 그 위에 문자열 출력을 위한 TextView가 있도록 만들었습니다.

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="35dp"
android:textColor="@android:color/black"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
</android.support.constraint.ConstraintLayout>

자바 (MainActivity.java)

실행하면 onCreate에서 TextView와 Button에 "Thanks"와 "Default(English)"가 화면에 출력됩니다.

 

dispatchKeyEvent에서는 볼륨 상하키를 누르는 방향에 따라서 언어를 지정하는 변수가 변경[각주:1]되고, 버튼을 누르면 기본 언어인 영어 "Thanks"에 해당하는 변경된 언어가 TextView에 표시됩니다.

 

package com.example.help.multilanguagetest;

import android.app.Activity;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends Activity {
private TextView mTextView;
private Button mButton;
private int mLang;

private final static String [] THANKS = {"Thanks.", "Gracias.", "Danke.", "Merci.", "감사합니다."};
private final static String [] LANGUAGES = {"Default(English)", "Spanish", "German", "French", "한국어"};

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

mTextView = findViewById(R.id.textView);
mTextView.setText(THANKS[0]);

mButton = findViewById(R.id.button);
mButton.setText(LANGUAGES[0]);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mTextView.setText(THANKS[mLang]);
}
});
}

@Override
public boolean dispatchKeyEvent(KeyEvent event) {

switch(event.getKeyCode()) {
case KeyEvent.KEYCODE_VOLUME_UP:
if (event.getAction() == KeyEvent.ACTION_UP) {
if (mLang < LANGUAGES.length - 1) {
mLang++;
}
mButton.setText(LANGUAGES[mLang]);
}
return true;
case KeyEvent.KEYCODE_VOLUME_DOWN:
if (event.getAction() == KeyEvent.ACTION_UP) {
if (mLang > 0) {
mLang--;
}
mButton.setText(LANGUAGES[mLang]);
}
return true;
default:
break;
}

return super.dispatchKeyEvent(event);
}
}

프로젝트 압축화일

MultiLanguageTest_VolUpDn.zip

 

실행결과

 

 

 

※볼륨키로 언어 변경 에뮬레이팅 없이 실제 세팅에서 언어를 변경할 경우

onCreate에서 로케일의 언어 값을 읽어와서 해당하는 언어에 맞는 문자열을 세팅해주면 됩니다.

세팅에서 변경한 언어가 지원하지 않으면 기본 언어인 영어로 해줍니다.

package com.example.help.multilanguagetest;

import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import java.util.Locale;

public class MainActivity extends Activity {
TextView mTextView;
Button mButton;

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

Locale locale;
String language;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
locale = getResources().getConfiguration().getLocales().get(0);
} else {
locale = getResources().getConfiguration().locale;
}
language = locale.getLanguage();

mTextView = findViewById(R.id.textView);
mButton = findViewById(R.id.button);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

}
});

switch(language) {
case "en":
mButton.setText("Default(English)");
mTextView.setText("Thanks.");
break;
case "es":
mButton.setText("Español");
mTextView.setText("Gracias.");
break;
case "de":
mButton.setText("Deutsch");
mTextView.setText("Danke.");
break;
case "fr":
mButton.setText("Français");
mTextView.setText("Merci.");
break;
case "ko":
mButton.setText("한국어");
mTextView.setText("감사합니다.");
break;
default:
mButton.setText(language);
mTextView.setText("Thanks.");
break;
}
}
}

프로젝트 압축화일

MultiLanguageTest.zip

 

 

[다국어지원 - xml 처리 방식]

 

Java에서 하드코딩 하는 방식은 가능은 하지만 지원하는 언어가 많거나 문자열이 여러 개인 경우 비효율적입니다.

 

xml에서 다국어 지원하는 방식은 기본 언어에 대해서만 작업을 해주고, 나머지 언어에 대해서는 번역업체에 아웃소싱으로 처리할 수 있기 때문에 프로젝트 진행하는 데 있어서 효율적입니다.

 

xml 방식은 기본언어의 경우 res/values 디렉터리에 strings.xml(프로젝트 생성시 기본으로 만들어짐)에 저장을 하고 추가 언어는 res/values-"language"-"country"/ 디렉터리에 언어별로 strings.xml 파일을 만들어줍니다.

 

예로 들면, 기본 언어(영어)와 한국어, 프랑스어(캐나다)를 지원하는 앱을 개발한다고 했을 경우 아래와 같이 디렉터리를 구성 후 strings.xml 파일을 만들어주면 됩니다.

res/values/strings.xml 
values-ko/strings.xml
values-fr-rCA/strings.xml

초기에는 디렉터리만 만들어주고, 파일은 기본언어의 strings.xml을 복사해서 넣어주면 됩니다.

그런 다음에 아웃소싱한 번역업체에 넘겨주어서 번역 작업을 의뢰합니다.

 

다행히도 안드로이드 스튜디오에서는 다국어 작업은 조금 편하게 할 수 있도록 Translations Editor라는 것을 가지고 있습니다. 이것을 이용하면 외부에서 언어별 디렉터리를 만들어주거나 디렉터리마다 파일을 복사해야 하는 번거로움이 해결됩니다.

 

앞서 만든 예제와 같게 xml 처리 방식으로 만들어 보겠습니다.

 

Translations Editor

 

먼저 strings.xml 편집 창에서 오른쪽 위에 있는 Open editor를 클릭하면 Translations Editor가 실행됩니다.

 

Translations Editor가 실행되면 아래와 같은 화면이 나옵니다.

 

아래 노란색 상자에 있는 버튼들에 대해 설명을 하겠습니다.

1번: 키를 추가합니다. 여기서 키는 그 문자열의 이름이 되겠습니다.

2번: 키를 삭제합니다.

3번: 로케일를 추가합니다. 지원하기 위한 언어를 추가하는 걸 말합니다.

 

한국어를 추가해보겠습니다.

3번 로케일 추가 버튼을 눌러서 한국어를 선택합니다.

 

지원하려는 나머지 언어도 추가해주면 아래와 같은 화면이 나옵니다.

언어별로 첫 번째 키(app_name)의 키값이 기본값으로 모두 채워져 있습니다.

Translations Editor가 동작하기 위해서 첫 번째 키값이 모두 채워진 상태로 시작되어야 합니다.

혹시 나중에 번역하면 넣으려고 첫 번째 키값을 삭제해버린 상태에서 Translations Editor를 종료하면, 그다음에 Translations Editor 진입하면 추가한 언어들이 나오지 않게 됩니다. (중요함)

 

번역이 필요한 키 항목은 빨간색 글자로 나타납니다.

 

 

첫 번째(app_name) 키값들은 나중에 처리하고, 두 번째(language) 키값들은 언어별 키값의 Translation 편집 창에 채워 넣습니다.

첫 번째 키값은 앱의 이름으로 번역을 하지 않고, 타 언어들에도 기본값으로 나오도록 할 예정입니다.

이때는 Untranslatable을 선택해줍니다.

 

이때 키 항목의 글자가 빨간색으로 표시되는 것은 번역이 필요 없는데 타언어들에 키값이 채워져있어서 발생하는 현상입니다. 그래서, 이 키 항목의 기본값을 제외하고 타언어들의 키값들은 삭제해줍니다. 

 

아래가 지원하는 언어의 번역을 추가해서 완료된 화면입니다.

왼쪽에 보면 지원하는 언어별 values 디렉터리가 만들어져 있는 것을 확인하실 수 있습니다.

 

레이아웃 (activity_main.xml)

앞선 JAVA에서 하드코딩 방식과 달리 레이아웃에 문자열을 바로 삽입해줍니다. "@string/키" 형태로 넣어주면 됩니다.

<android.support.constraint.ConstraintLayout 
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="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/thanks"
android:textColor="@android:color/black"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="35dp"
android:text="@string/language"
android:textColor="@android:color/black"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
</android.support.constraint.ConstraintLayout>

자바 (MainActivity.java)

다국어 작업을 Translations Editor로 처리했기 때문에 자바 쪽에서는 처리해줘야 할 것이 없습니다.

package com.example.help.multilanguagetest;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends Activity {
TextView mTextView;
Button mButton;

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

mTextView = findViewById(R.id.textView);
mButton = findViewById(R.id.button);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//TODO
}
});
}
}

프로젝트 압축화일

MultiLanguageTest2.zip

 

실행결과

 

 

이상으로 "안드로이드 다국어 지원"에 대해서 알아보았습니다.

 

조금이나마 도움이 되셨으면 아래 공감 버튼을 눌러주세요.

 

 

 

  1. 설정앱(Settings) - 언어(Language) 변경을 에뮬레이팅하는 것. [본문으로]
반응형

 

 

Build type(user/userdebug/eng)과 루트 권한에 대해서 알아봅니다.

아래는 Build type의 용도에 관한 간단한 설명입니다.

  Buildtype

  Use

  user

  limited access; suited for production 

  userdebug

  like "user" but with root access and debuggability; preferred for debugging

  eng

  development configuration with additional debugging tools

 

각 Build type 별 보안 및 디버그 관련 설정되는 System Property는 다음과 같습니다.

Buildtype

System Property

   user

  ro.secure = 1 , ro.debuggable = 0

   userdebug

  ro.secure = 1 , ro.debuggable = 1

   eng

  ro,secure = 0 , ro.debuggable = 1

 

System Property는 개별 또는 두 개 조합으로 Framework와 Application에서 Buildtype에 따른 로직 처리를 위해서 사용됩니다. 

대표적으로 Framework에서 Debug 모드 시 분기 조건으로 많이 사용되는 IS_DEBUGGABLE은 아래와 같이 정의되어 있습니다.

/frameworks/base/core/java/android/os/Build.java

 

/** * Returns true if we are running a debug build such as "user-debug" or "eng". * @hide */ public static final boolean IS_DEBUGGABLE = SystemProperties.getInt("ro.debuggable", 0) == 1;

 

 

안드로이드 단말기는 양산할 때 user 바이너리(Build type: user)가 들어갑니다.

물론 시중에 나와 있는 단말기 중에는 userdebug 바이너리가 들어간 것도 있습니다.

 

아래에서 다루겠지만, userdebug인 경우 쉽게 루트 권한을 가질 수 있고[각주:1] 커널 로그도 확인할 수 있어서 개발자 입장에서 선호하는 Build type입니다.

양산단계에 안드로이드 단말기에 user 바이너리가 들어가는 이유

 - 기본탑재 앱(Preload app)의 보호 (기본탑재 앱의 삭제 또는 변형에 따른 오동작 방지)

 - A/S 이슈 (루트 권한을 이용해서 시스템에 리스크를 줄 수 있는 변경에 따른 고장 유발)

 - 보안 이슈

단말기 제조업체에서는 개발 중에는 eng 또는 userdebug 바이너리로 단말기를 개발 또는 테스트합니다.

개발단계에 단말기는 eng / userdebug 바이너리를 사용하는 경우

 - 단말기 브링업/셋업시는 eng 바이너리가 작업에 유리한 점이 많습니다.

 - 단말기 안정화된 후 검증에는 user 바이너리와 가까운 userdebug 바이너리가 적합합니다.

    (예로들면 eng 바이너리는 상대적으로 로드가 있기 때문에 성능관련한 검증 및 사용성 검증 등에는 적합하지 않습니다.)

안드로이드 초기버전에는 user 바이너리 단말에서도 안드로이드 취약점을 이용한 루팅이 가능했지만, 현재는 안드로이드 보안패치로 인하여 막혔습니다. 단, 리커버리를 통한 커스텀 바이너리 업데이트로 루팅은 여전히 가능합니다.

 

userdebug 바이너리 단말기에서는 ADB(Android Debug Bridge) 데몬을 루트 권한으로 동작하게끔 명령(adb root)을 날려줌으로써 루트 권한을 가진 ADB로 restart 하는 방법과 ADB restart 없이 쉘에서 su 명령에 의한 루트 권한으로 해당 명령을 동작하게 하는 방법이 있습니다.

 

[userdebug 바이너리 단말기에서 su 명령에 의한 루트 권한으로 동작하게 하는 방법]

아래와 같이 커널로그(kmsg 로그)는 루트 권한이 없으면 명령이 먹히지 않습니다. 

 

그러나, su 명령으로 루트 권한을 주면 명령이 먹힙니다.

 

 

[userdebug 바이너리 단말기에서 루트 권한을 가진 ADB로 restart 하는 방법]

- adb root 명령 실행 후 uid/gid 모두 root로 변경된 것을 확인할 수 있습니다.

- adb root 명령 실행 후 adb는 루트 권한을 가지고, 이후 su 명령없이도 동작하는 것을 확인할 수 있습니다.

 

 

이상으로 Build type(user/userdebug/eng)과 루트 권한에 대해서 알아보았습니다.

 

조금이나마 도움이 되셨으면 아래 공감 버튼을 눌러주세요.

  1. 루팅(rooting) [본문으로]
반응형

 

 

 

1. Battery Service state (adb shell dumpsys battery) 정보

 

현재 폰의 배터리 상태를 알기 위해서 adb shell dumpsys battery 명령을 실행하면 아래와 같이 출력됩니다.

 

adb shell dumpsys battery
Current Battery Service state:
  mBootCompleted: true
  AC powered: false
  USB powered: true
  Wireless powered: false
  Max charging current: 0
  Max charging voltage: 0
  Charge counter: 0
  status: 2
  health: 2
  present: true
  level: 84
  scale: 100
  voltage: 4063
  temperature: 326
  technology: Li-ion
  batterySWSelfDischarging: false
  batteryMiscEvent: 0
  batteryCurrentEvent: 0
  mSecPlugTypeSummary: 2
  LED Charging: true
  LED Low Battery: true
  current now: 321
  charge counter: 0
  Adaptive Fast Charging Settings: true
USE_FAKE_BATTERY: false
SEC_FEATURE_BATTERY_SIMULATION: false
FEATURE_WIRELESS_FAST_CHARGER_CONTROL: true
  mWasUsedWirelessFastChargerPreviously: true
  mWirelessFastChargingSettingsEnable: true
LLB CAL:
LLB MAN: 20160426
LLB CURRENT: YEAR2018M9D15
LLB DIFF: 123
BatteryInfoBackUp
  mSavedBatteryAsoc: 77
  mSavedBatteryMaxTemp: 490
  mSavedBatteryMaxCurrent: 2872
  mSavedBatteryUsage: 176488
  FEATURE_SAVE_BATTERY_CYCLE: true

 

 

위의 각 항목에 관해서 설명하면 (단말기 제조사에서 추가해준 항목 제외) 아래와 같습니다.

AC Powered

 AC 어댑터 전원으로 충전여부

 

USB Powered

 USB 전원으로 충전여부

 

Wireless powered

무선충전기 전원으로 충전여부

 

Max charging current

최대 충전 전류

 

status

 1: 알수없음, 2: 충전중, 3: 방전중, 4: 충전완료후 충전중지, 5: 충전완료

 

health

 1: 알수없음, 2: 좋음, 3: 과열, 4: 불량  5: 과전압, 6: 불특정 실패, 7: 저온

 

present

 배터리 존재여부

 

level

 현재 배터리 레벨 (아래 scale 이 100일 경우 배터리양은 [level %] 로 읽으면 됨, 위의 경우 배터리양이 14%)

 

scale

 배터리 레벨의 최대값 (보통 100)

 

voltage

 현재 배터리 전압 레벨 (단위: mV)

 

temperature

 배터리 온도 (값을 10으로 나눠서 읽으면 됨, 위의 경우 28.9℃)

 

technology

 배터리 종류

 

 

2. 커널 메시지(kmsg) 배터리 정보

 

커널 메시지중에 배터리 정보를 얻기위해서 cat /proc/kmsg | grep battery 명령을 실행하면 아래와 같이 출력됩니다.

 

 

 

위 로그는 배터리를 모니터링 하는 데몬인 healthd에서 출력합니다.

 

l=59

현재 배터리 레벨

 

v=3878

 배터리 전압 레벨 (단위 mv)

 

t=25.0

 배터리 온도

 

h=2

 Health (1: 알수없음, 2: 좋음, 3: 과열, 4: 불량, 5:과전압, 6: 불특정 실패, 7: 저온)

 

st=3

 Status (1: 알수없음, 2: 충전중, 3: 방전중, 4: 충전완료후 충전중지, 5 충전완료)

 

c=-400

 현재 전류 (단위: mA, 마이너스(-)는 방전상태를 의미)

 

chg=

 충전기 종류 (' ': 충전상태아님, a: AC 어댑터 충전, u: USB 충전, w: 무선 충전) 

 

 

조금이나마 도움이 되셨으면 아래 공감 버튼을 눌러주세요.

반응형

 

메모리 누수 (Memory Leak) 이야기

 

간단하게 정의하면 앱에서 사용한 메모리를 반환하지 않는 현상입니다.

결과적으로 이후 가용 메모리가 줄어듭니다.

 

좀 더 자세히 설명하면, 안드로이드에서는 메모리가 부족하면 GC(Garbage Collector)가 동작해서 더는 유용하지 않는 객체들을 정리해서 메모리를 확보하게 됩니다.

 

그래서, 자바에서는 GC가 있기 때문에 메모리 누수가 없다고 하는데, 꼭 그런 것만은 아닙니다.

 

유용한 객체에서 유용하지 않는 객체를 참조(reference)하는 경우에 GC는 유용하지 않는 객체지만 제거할 수 없게 되어서 메모리 누수가 발생합니다. 

 

메모리 누수는 Exception이나 ANR 처럼 FC(Force Close) 되어서 문제가 있다는 것을 즉각 인지할 수 있는 것도 아니고, 직접적인 단서가 될 로그 정보가 있는 것도 아닙니다. 물론 누수가 심각해서 이후 메모리 할당을 못 할 경우 OOM(Out Of Memory) Exception까지 되는 경우도 있기는 합니다.

 

재현되는 경로가 확인되었고, 누수되는 메모리량이 커서 OOM 까지 되는 경우에는 그나마 인지할 수 있어서 다행이지만, 재현되는 경로 확인이 어렵고 누수되는 메모리량이 많지 않을 때에는 인지하기 어렵기 때문에 그대로 릴리즈될 수 있습니다.

 

이런 경우 문제는 메모리 누수가 있는 앱이 사용 빈도가 높을 경우 메모리 누수량이 계속 누적된다는 것입니다.

 

단말기 제조업체의 경우 QA에서 최종 릴리즈 전에 메모리 누수 테스트를 하고 있지만, 구글 플레이에 올라와 있는 앱들 중에서는 메모리 누수되는 앱들이 존재합니다.

 

이런 앱들은 사용자에게 어떤 영향을 미칠까요?

 

다운로드 한 앱들이 메모리 누수가 지속적으로 발생하게 되면 가용 메모리가 부족하게 되어 이후 메모리 확보를 위해서 GC가 동작하게 됩니다.

 

이때 화면이 렌더링 처리되어야 하는 16ms를 초과하는 GC시간이 빈번할 경우(특히 100ms 이상의 GC)에 일반적으로 버벅거린다고 말하는 Sluggish 현상을 사용자들이 경험하게 됩니다.

 

가용 메모리가 일정 수준 이하이면 LMK(Low Memory Killer) 를 트리거시킵니다. LMK 는 중요한 프로세스를 보호하기 위해서 중요하지 않은 프로세스를 Kill 후 메모리를 확보합니다. LMK 관련해서는 나중에 별도로 다룰 예정입니다.

 

최악의 경우 앞서 언급한 OOM 상황이 되면 다른 앱의 실행이 안 될 수도 있습니다.
많은 메모리 할당이 요구되는 앱들이 OOM으로 인한 FC로 괜한 오해를 받게 됩니다.

 

메모리 누수는 사용자 입장에서도 개발자 입장에서도 불편한 이슈입니다.

 

메모리 누수를 검출하는 많이 알려진 방법은 meminfo 나 procstats 명령을 통한 메모리양의 단순 비교로 메모리 누수 여부에 대해서 검출하는 것입니다. 하지만, 좀 더 편리하고 정확한 위치를 찾기 위한 툴들을 이용합니다.

 

Eclipse에서 안드로이드 개발할 때 DDMS(Dalvik Debug Monitor Server) 의 Heap dump와 Allocation Tracker를 이용했고, 플러그인 형태의 MAT(Memory Analysis Tool)를 사용해서 메모리 누수를 찾았습니다.

 

Android studio로 넘어오면서 Android Profiler라는 이름으로 CPU, MEMORY, NETWORK 항목들의 각 Profiler 가 통합되었고, MEMORY Profiler에서 이전 DDMS처럼 메모리 누수를 찾을 수 있었습니다.

 

이후 소개할 LeakCanary는 PC에서 동작하던 툴들과 달리 폰에서 메모리 누수가 있는 앱을 실행후 액티비티를 종료하게 되면 백그라운드 프로세스로 동작하면서 모니터링 후 메모리 누수가 검출시 Notificaiton으로 알려주기에 이전 툴들에 비해 사용이 쉽고 직관적입니다.

 

LeakCanary 를 이용한 메모리 누수 검출

 

먼저 LeakCanary 에 대한 간단한 설명을 하자면, 미국에 있는 Square라는 회사에서 메모리 누수를 검출하기 위해서 만든 라이브러리입니다.


참고로 이 회사는 모바일 지불 및 금융/가맹점 서비스 관련한 기업인데, 트위터 창업자 잭도시가 이 회사도 만들었습니다.

 

안드로이드 개발하면서 이 회사에서 공개한 라이브러리(Picasso, Moshi, OkHttp, Retrofit 등)를 알게 모르게 사용하거나 들어본 적이 있을 것입니다.


자사 앱의 메모리 누수를 해결하는 과정에 만들어진 라이브러리라고 하고, 이후 외부에 공개되었습니다.

 

LeakCanary는 액티비티가 종료(Destroy)되었지만, GC에 의해 회수되지 않는 메모리가 검출되면 별도의 프로세스에서 Heap Dump 후 hprof 파일로 저장하고 분석을 해서 결과를 리포트합니다. 

 

[LeakCanary 적용방법]

 

1. 아래 라이브러리를 build.gradle 화일에 추가합니다.

     LeakCanary는 Debug 모드에서만 동작합니다.

     Release 모드 라이브러리는 자바 쪽 LeakCanary 코드가 아무런 동작하지 않도록 처리되어 있습니다.

debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.1'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.1'

 

2. Custom Application class를 만들어주고, LeakCanary 설치 코드를 추가합니다.

package com.example.help.myapplication;

import android.app.Application;
import com.squareup.leakcanary.LeakCanary;

public class myapplication extends Application {

@Override
public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
}
}

android:name도 추가해줍니다.

<application
android:name=".myapplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

[LeakCanary 이용 메모리 누수 검출 테스트]

뷰나 액티비티를 static field로 참조하는 경우 액티비티가 종료되어도 GC로 메모리가 회수되지 않습니다.
아래의 경우 mText가 여기에 해당합니다.

public class MainActivity extends AppCompatActivity {
static TextView mText;

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

mText = new TextView(this);
mText.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
mText.setText(getString(R.string.app_name));
((ConstraintLayout) findViewById(R.id.mainActivityLayout)).addView(mText);
}
}

앞서 기술한 LeakCanary를 적용 후 앱 실행해서 액티비티를 종료하면 잠시후에 아래와 같이 Notification과 더불어 메모리를 Heap Dump 하고 있다고 알려주는 창이 나옵니다.

  

이때 Notification 창을 내려서 확인해보면 Heap Dump 완료 직후 분석을 진행하고 있습니다.
분석이 완료되면, 메모리 누수가 발생한 액티비티가 표시됩니다.

  

 

더 자세한 내용을 확인하기 위해서 Notification 창을 클릭하게 되면 아래와 같이 확인할 수 있습니다.

또한 Logcat을 통해서도 확인이 가능합니다.

프로젝트 압축화일  MyApplication.zip

이상으로 메모리 누수에 대한 이야기와 LeakCanary를 이용한 메모리 누수 검출방법에 대해서 알아보았습니다.

조금이나마 도움이 되셨으면 아래 공감 버튼을 눌러주세요.

반응형

특정 디렉토리의 미디어 화일(이미지, 동영상, 소리)을 삭제하지 않고 앱(갤러리, 비디오, 음악)에서 보이지 않도록 하는 방법

 

1. 마켓에서 앱을 설치했는데, 그 앱의 데이터중에 미디어 화일(이미지, 동영상, 소리)이 앱에 보이는 경우

2. 개발한 앱의 미디어 화일들이 다른 앱에서 보이는 경우

 

사실 1, 2 는 같은 내용입니다.

1의 경우 주로 소비자(고객)의 고민이고, 2의 경우 초보 개발자의 고민일 것입니다.

 

결론부터 말하면, 미디어 화일이 있는 디렉토리에 .nomedia 화일 하나만 만들어주면 됩니다.

 

.nomedia 화일을 추가한 이후 부터는 굳이 삭제하지 않아도 앱에서 보이지 않게 됩니다.

 

앱에서 사용하는 미디어 화일이 노출이 된다는 것은 해당 앱 개발시 실수라고 볼 수 있습니다.

 

혹 소비자가 해당 미디어 화일을 삭제한 경우에 앱에서 오동작이 있을 수 있기 때문입니다.

 

다른 관점에서 보면, 특정 디렉토리에 있는 미디어 화일들을 삭제하지 않고 단지 보이지 않도록 하기위한 방법으로 이용할 수도 있습니다.

 

.nomedia 화일 만드는 것에 어려움을 느낀다면 마켓에 앱을 다운받아서 대신 할 수도 있습니다.

 

StudioKUMA .nomedia Manager 라는 무료앱이면서 광고도 없는 앱을 추천드립니다.

 

 

 

 

사용방법은 아래에 적혀있는 것처럼 디렉토리를 길게 누르면 "Disable Media Scanning" 나오는데, 이것을 선택하면 해당 디렉토리에 있는 미디어 화일들은 앱에서 보이지 않게 되비다.

똑같이 한번 더 수행하면, "Enable Media Scanning" 나오면서 앱에서 미디어 화일들이 보이도록 할수  있습니다.

Android 4.3+ 이후부터는 Refresh (위치: 더보기 - Refresh) 를 해줘야 합니다.

 

 

감사합니다.

+ Recent posts