반응형

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

앞선 글에서 손전등 앱을 개발하기 위해 Camera2 API를 이용해서 카메라 플래시를 제어하는 방법을 적어보았는데, 쉽지 않으셨을 겁니다.
손전등 앱으로는 잘 사용하지 않는 방법이니 Camera2에 대해서 공부했다고 위안으로 삼으셔도 됩니다.

이번에 알려드리는 방법은 이전 방법들 대비해서 매우 간단한  Flashlight API를 이용하는 방법입니다.

Android 6.0 이전까지 플래시는 카메라 디바이스의 보조 장치이고, 손전등에 대해서 고려되지 않았습니다.
그로 인하여 Camera/Camera2 API로 구현 시 비효율적인 부분이 많이 있었습니다.

Android 6.0(Marshmello, API level 23)부터는 손전등을 고려해서 Flashlight API가 추가되었고, 또한 안드로이드 시스템 UI에서 손전등이 자체 내장이 되기 시작했습니다.

아래와 같이 퀵세팅(QuickSetting)에 가면 손전등 타일이 추가되어 있습니다.

Flashlight API는 매우 간단하면서 카메라 권한이 필요하지 않다는 장점이 있습니다. 반면에 Android 6.0 이상의 단말기에만 동작한다는 제약성도 있습니다.

[Flashlight API 이용한 손전등 앱]

Camera API와 Camera2 API를 이용한 손전등 앱에서는 AndroidManifest.xml에 카메라 권한(android.permission.CAMERA)이 명시되어 있지 않으면 Exception이 발생합니다.

Camera / Camera2 API의 openCamera()를 호출하면, 내부에서 permission check하는 루틴에서 에러를 리턴서 Exception을 발생하게 합니다.

특히, Camera2에서는 Annotation(@RequiresPermission)으로 인하여 빌드 전에 미리 오류에 대한 확인이 가능합니다. 

@RequiresPermission(android.Manifest.permission.CAMERA)
public void openCamera(@NonNull String cameraId,
@NonNull final CameraDevice.StateCallback callback, @Nullable Handler handler)
throws CameraAccessException {

openCameraForUid(cameraId, callback, handler, USE_CALLING_UID);
}

그런데, Flashlight API의 경우 Camera2에 추가된 것이지만, 사전에 openCamera() 하지 않고 사용할 수 있습니다.
그 결과 카메라 권한이 없어도 된 것입니다.

Flashlight API(setTorchMode) 이용한 손전등 앱에서 플래시가 동작하는 과정은 아래와 같습니다.

준비작업: 플래시 존재 여부 체크 - CameraManager 객체의 인스턴스 생성

플래시 켜기: setTorchMode(cameraId, true)

플래시 끄기: setTorchMode(cameraId, false)

매우 간단하기에 바로 소스로 들어가 보겠습니다.

[소스 설명]

레이아웃 (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()), 플래시 켜짐 상태에 따른 별 모양 이미지(켜짐: 노란색 별, 꺼짐: 흰색 별)를 변경하도록 처리해줍니다. (여기까지는 앞에 있는 부분과 공통)

Flashlight API(setTorchMode)를 사용하기 위해서 CameraManager 객체의 인스턴스를 획득합니다.

package com.example.help.nopermissionflashlight;

import android.content.pm.PackageManager;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
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 ImageButton mImageButtonFlashOnOff;
private boolean mFlashOn;

private CameraManager mCameraManager;
private String mCameraId;

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

mCameraManager = (CameraManager) getSystemService(CAMERA_SERVICE);

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();
}
}, 3500);
}

버튼을 눌러 flashlight()가 호출되면, CameraManager를 통해 카메라 id들을 가져온 후 필요한 카메라 특성을 구하고(getCameraCharacteristics), 원하는 조건(플래시 사용이 가능 & 화면 기준으로 뒤쪽에 있는 카메라)에 해당하는 카메라 id(주로 "0")를 구합니다.

이후 플래시 상태를 반전하기 위해서 mFlashOn을 토글시켜주고, setTorch()에 위에서 구한 카메라 id와 mFlashOn을 argument로 넘겨줍니다. setTroch()가 호출이 되면 mFlashOn 상태에 따라서 플래시가 켜지거나 꺼지게 됩니다.

void flashlight() {
if (mCameraId == null) {
try {
for (String id : mCameraManager.getCameraIdList()) {
CameraCharacteristics c = mCameraManager.getCameraCharacteristics(id);
Boolean flashAvailable = c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
Integer lensFacing = c.get(CameraCharacteristics.LENS_FACING);
if (flashAvailable != null && flashAvailable
&& lensFacing != null && lensFacing == CameraCharacteristics.LENS_FACING_BACK) {
mCameraId = id;
break;
}
}
} catch (CameraAccessException e) {
mCameraId = null;
e.printStackTrace();
return;
}
}

mFlashOn = !mFlashOn;

try {
mCameraManager.setTorchMode(mCameraId, mFlashOn);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}

프로젝트 압축화일
NoPermissionFlashlight.zip


앱 동작 영상

 


기타정보

Flashlight API를 이용하게 되면 Flashlight를 독점적으로 소유할 수 없습니다.
이것의 의미는 Flashlight API를 사용하는 다른 앱에서 똑같이 Flashlight API를 호출하게 되면 그쪽으로 소유가 넘어간다는 의미입니다.

아래 테스트 영상과 함께 설명드리겠습니다. 영상에는 세 가지 손전등앱이 있습니다.
1. 시스템 UI인 QuickSetting의 손전등
2. Camera API로 만든 PermissionFlashlight
3. Flashlight API로 만든 NoPermissionFlashlight

[case 1]
PermissionFlashlight 앱에서 Flash를 켜고, QuickSetting의 손전등을 확인하면 아이콘이 On이지만 Dimming 된 상태입니다.
이것의 의미는 QuickSetting의 손전등에서 Flashlight를 소유해서 제어할 수 없다는 의미입니다.
그리고, QuickSetting에서 Dimming 된 손전등의 On 아이콘을 누르면, "카메라 앱에서 카메라 플래시를 사용 중이어서 손전등을 켤 수 없습니다."라는 토스트가 나옵니다. 이는 QuickSetting의 손전등에서 PermissionFlashlight 앱이 Camera API로 만들어져서 카메라 앱으로 간주하고 있습니다.

[case 2]
NoPermissionFlashlight 앱에서 Flash를 켜고, QuickSetting의 손전등을 확인하면 아이콘이 On 되어 있습니다.
이는 QuickSetting의 손전등에서 다른 앱에서 Flashlight API를 사용해서 On 시킨 것을 인지한 상태입니다.
그리고, QuickSetting에서 손전등의 On 아이콘을 누르면 Off 상태로 아이콘이 변경되면서 플래시가 꺼집니다.
이는 QuickSetting의 손전등으로 Flashlight 소유가 넘어가서 제어되고 있다는 것을 의미합니다.

그런데, NoPermissionFlashlight 앱으로 돌아오면 가운데 별 모양은 계속 On 상태로 남아있죠?
이건 왜 그럴까요? 정답은 NoPermisionFlashlight 앱에서는 다른 앱에서 Flashlight API를 사용해서 On 시킨 것을 인지하는 리스너가 구현되어 있지 않기 때문입니다. QuickSetting의 손전등처럼 리스너를 구현해주면 플래시가 꺼지면 콜백되어서 Off 상태 인지가 가능해서 아이콘 모양 변경이 가능합니다. 


이상으로 "손전등 앱 만들기 세 번째 - Flashlight API 이용방법"에 대해서 설명해 드렸습니다.
상황에 따라서 세 가지 방법을 적절히 이용하시면 손전등 앱을 구현하실 수 있을 겁니다.

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

반응형

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

이번에는 Camera2 API 이용해서 손전등 앱을 만들어 보도록 하겠습니다.
앞선 글에서 Camera2 API 이용하는 방법은 다른 방법들 대비해서 장점이 없기 때문에 일반적으로 사용하지 않는다고 말씀드렸습니다.

그래도, 궁금해하실 분이나 필요하실 분도 계실 것 같아서 글을 작성해봅니다.

[Camera2 API 이용한 손전등 앱]

안드로이드 초기 버전부터 사용되던 Camera API는 Android 5.0(API Level: 21)이 되어서야 Camera2 API로 Camera Framework이 변경되면서 구조적으로 큰 변화가 생기게 됩니다. 그 이전까지는 Camera API에서 기본 Feature(전면 카메라 지원, 얼굴인식 지원, 측광/초점 영역 지원, CAF 지원, AE/AWB Lock 지원 등) 추가하는 데 집중되었습니다.
(혹, 오해하실까 봐 말씀드리면, 위의 Feature들은 그 당시 단말기 업체의 카메라 앱에서는 추가되기 전부터 이미 있던 기능들입니다. 다만, 안드로이드 SDK에 API가 없어서 일반 앱 개발자들이 해당 기능들이 들어간 앱을 만들 수가 없었던 것이죠. 안드로이드 SDK에서 API로 지원하지 않는데, 단말기 업체에서는 어떻게 기능을 넣을 수 있게 된 건지에 대해서는 나중에 별도로 다룰 예정입니다)

Camera2 API로 넘어오면서 Camera Framework의 큰 구조적 변화는 성능 개선과 새로운 Feature 구현이 목적이었습니다.
플래시의 경우 이런 변화에 크게 영향은 없지만, 플래시를 제어하기 위한 접근이 조금 더 복잡해졌습니다.

아래는 여기에서 다루고 있는 예제로 만든 앱의 Sequence Diagram입니다. 이거 만드는 데 시간이 오래 걸렸습니다. :)

이후에 소스를 볼 때 아래 그림을 참조하면서 따라오시면 됩니다.

원본이미지 다운받기
FlashlightAppUsingCamera2API_sequence_diagram.zip

 

[소스 설명]

레이아웃 (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()), 플래시 켜짐 상태에 따른 별 모양 이미지를 변경하도록 처리해줍니다. (여기까지는 이전 글의 소스와 같습니다)

카메라 특성 정보를 얻고, 카메라 연결을 위해서는 먼저 CameraManager 객체의 인스턴스(mCameraManager)를 얻어야 합니다.

그 아래에 이미지 버튼의 클릭 리스너와 클릭시의 플래시 동작 및 이미지 버튼의 모양 변경에 대한 처리가 있습니다.
(이 부분은 뒷부분에 따로 빼내어서 설명합니다.) 


 

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

mCameraManager = (CameraManager) getSystemService(CAMERA_SERVICE);

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

 

onResume의 openCamera()에서 CameraManager를 통해 카메라 id들을 가져온 후 필요한 카메라 특성을 구하고(getCameraCharacteristics), 원하는 정보(여기서는 플래시 사용이 가능하고, 화면 기준으로 뒤쪽에 있는 카메라) 조건에 해당하는 카메라 id(주로 "0")를 구합니다.
 
 

  

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


private void openCamera() {
try {
for (String id : mCameraManager.getCameraIdList()) {
CameraCharacteristics c = mCameraManager.getCameraCharacteristics(id);
Boolean flashAvailable = c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
Integer lensFacing = c.get(CameraCharacteristics.LENS_FACING);
if (flashAvailable != null && flashAvailable
&& lensFacing != null && lensFacing == CameraCharacteristics.LENS_FACING_BACK) {
mCameraId = id;
break;
}
}

...

}

 

다음으로 CameraManager 객체의 openCamera()에 대한 설명입니다.
이것은 Camera API에서 Camera 객체의 open()에 해당합니다. 즉, 카메라 연결하는 것입니다.

openCamera()의 argument를 보시면, 첫 번째는 카메라 id로 앞서 구한 id를 넣어줍니다.
두 번째는 카메라 장치(CameraDevice)의 상태가 변경되면 콜백되는 CameraDevice.StateCallback 객체의 인스턴스를 넣어줍니다.
마지막은 핸들러인데, 여기서는 사용하지 않을 것입니다.

CameraDevice.StateCallback 콜백은 4가지 상태에 대해서 알려줍니다.
그중 아래 3가지는 반드시 구현해서 넣어줘야 합니다.
 - onOpened: 카메라 연결이 완료될 경우
 - onDisconnected: 카메라 장치를 더 이상 사용할 수 없을 경우
 - onError: 카메라 오류가 발생할 경우

onOpened이 콜백되면 parameter인 cameraDevice는 카메라 장치(CameraDevice) 객체의 인스턴스로 카메라 장치 연결종료에 사용이 필요하므로 mCameraDevice에 저장해둡니다. onOpened의 추가설명은 다음단계에서 계속합니다.
onDisconnected와 onError이 콜백될 경우 카메라 장치 연결을 종료합니다.

 

private void openCamera() {

...

mCameraManager.openCamera(mCameraId, new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
mCameraDevice = cameraDevice;

... 

}

@Override
public void onDisconnected(@NonNull CameraDevice cameraDevice) {
cameraDevice.close();
mCameraDevice = null;
}

@Override
public void onError(@NonNull CameraDevice cameraDevice, int error) {
cameraDevice.close();
mCameraDevice = null;
Toast.makeText(getApplicationContext(), "A camera error has occurred. The app will finish!", Toast.LENGTH_LONG).show();
delayedFinish();
}
}, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}

 

다음으로 CaptureRequest를 만들어줘야 하는데, 이름만 보면 Capture 처리요청하는 객체로 생각될 수도 있지만,
이것은 Capture 뿐만 아니라 Preview, Recording, Video Snapshot, Manual control에 대한 처리요청을 위한 객체입니다.

CameraDevice의 CreateCaptureRequest()를 호출하면 argument로 넣은 템플릿 설정(여기서는  TEMPLATE_PREVIEW)으로 초기화되고, CaptureRequest.Builder 객체의 인스턴스를 구할 수 있습니다.

참고로 CaptureRequest.Builder는 CaptureRequest 객체를 포함한 빌더 클래스입니다.

사전에 CaptureRequest의 출력 Target을 설정(addTarget)해주는데, Target은 Surface가 됩니다.
플래시의 경우 Surface로 Preview가 되지 않아도 되기 때문에 Surface는 dummy surface로 만들어줍니다.
혹시 Surface 필요 없다 생각해서 출력 Target을 설정하지 않으면, Exception이 발생하게 됩니다.
최소한 한 개의 Target Surface가 있어야 합니다.

예제에서는 CreateCaptureRequest()를 호출하면 CaptureRequest.Builder 객체의 인스턴스를 mPreviewRequestBuilder에 저장하게 되어 있습니다. 


private void openCamera() {

...

mCameraManager.openCamera(mCameraId, new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
mCameraDevice = cameraDevice;

try {
List<Surface> list = new ArrayList<>();
if (mTexture != null) {
mTexture.release();
}
mTexture = new SurfaceTexture(1);
Surface surface = new Surface(mTexture);
list.add(surface);
mPreviewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
mPreviewRequestBuilder.addTarget(surface);

...

 

 

다음으로 CaptureSession을 만들어줘야합니다. CaptureSession은 전달받은 CaptureRequest들이 처리되는 곳입니다.
CameraDevice의 createCameraSession()를 호출후 콜백으로 CaptureSession의 인스턴스를 넘겨받을 수 있습니다.

creatureCamreaSeesion()의 argument를 보시면, 첫 번째는 Target Surface 리스트를 넣어줍니다.
이것은 앞 단계의 소스에 보면 dummy surface를 만들어서 리스트에 추가해주는 부분이 있습니다.
두 번째는 CaptureSession의 상태에 대한 업데이트가 되면 콜백되는 CameraCaptureSession.StateCallback 객체의 인스턴스를 넣어줍니다.
마지막은 핸들러인데, 여기서는 사용하지 않을 것입니다.

카메라 장치가 자체 구성을 완료하면 onConfigured가 호출됩니다. 이때 parameter로 cameraCaptureSession 객체의 인스턴스가 넘어옵니다. 이것은 다른곳에서도 사용이 필요하니, mCaptureSession에 저장해 둡니다.

마지막으로 CaptureRequest에 대한 처리요청을 CaptureSession에서 보내는 곳입니다.
이것을 수행 후 실제 동작이 이뤄지게 됩니다.
CameraCaptureSession의 setRepeatingRequest() 에 CaptureRequest를 argument로 넣어서 보내면 됩니다.
mPreviewRequestBuilder는 CaptureRequest.Builder의 인스턴스인데, build() 메서드를 이용하면 CaptureRequest를 가져올 수 있습니다.

setRepeatingRequest 이름처럼 CaptureRequest에 대한 처리를 반복하도록 합니다.
캡처의 경우 일회성이지만, 프리뷰의 경우 프레임이 계속 들어와야해서 setRepeatingRequest을 사용해야 하고 플래시도 사용자가 요청하기 전까지는 계속 켜져 있어야 하기 때문에 역시 setRepeatingRequest을 사용합니다.

Target Surface가 dummy surface라서 레이아웃으로 만든 화면에 어떠한 변화도 없지만, 만약 레이아웃에 Texture를 추가해주고 이것을 Surface로 만들어서 Target Surface로 추가해주면 카메라 프리뷰화면이 나올 것입니다.

여기까지 하면 플래시 제어를 위한 기본 작업이 완료됩니다.

 

private void openCamera() {

...

mCameraManager.openCamera(mCameraId, new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
                    ...
cameraDevice.createCaptureSession(list, new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
if (null == mCameraDevice) {
return;
}
try {
mCaptureSession = cameraCaptureSession;
cameraCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}

@Override
public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
}
}, null);

}

...

}

 

이제부터 flashlight() 호출만으로 플래시가 작동합니다.

CaptureRequest.Builder의 set()은 Camera API의 Camera.Parameters의 set()이라고 생각하시면 쉽게 이해되실 겁니다.

그리고, mPreviewRequestBuilder.build()를 하면 플래시 세팅을 해준 CaptureRequest가 만들어져서 나오고, 이를 CameraCaptureSession에 실행하도록 전달하면 플래시가 동작하게 됩니다.
앞에서 설명했지만, setRepeatingRequest는 만든 CaptureRequest를 반복해서 CameraCaptureSession에 요청하기 때문에 사용자가 꺼주는 플래시 세팅을 다시 해주기 전까지는 계속 플래시가 켜져 있게 됩니다.

 

 

 

private void flashlight() {
mFlashOn = !mFlashOn;

mPreviewRequestBuilder.set(CaptureRequest.FLASH_MODE, mFlashOn ? CaptureRequest.FLASH_MODE_TORCH : CaptureRequest.FLASH_MODE_OFF);

try {
mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}

메니페스트(AndroidManifest.xml)

CameraManager의 openCamera() 실행시 카메라 권한을 요구하기 때문에 아래와 같이 <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>

프로젝트 압축화일
PermissionFlashlight3.zip

 

앱 동작 영상


이상으로 "손전등 앱 만들기 두 번째 - Camera2 API 이용방법"에 대해서 설명해 드렸습니다.
Camera2 API를 처음 접하신 분들은 어려울 수도 있는 내용이었습니다.
다음번에는 간단하고 쉬운 방법인 Flashlight API를 이용한 손전등 앱을 만들어 보도록 하겠습니다.

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

반응형

안녕하세요. 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.)

+ Recent posts