Android App

camera, startActivityForResult() 대체, intent.resolveActivity(getPackageManager()) return null in Android Studio

YunSeong 2021. 8. 2. 13:11
728x90
반응형

 

Camera

 

https://youtu.be/MAB8LEfRIG8

1 코드

1_1 토대 만들기

 

일단 먼저 activity_main.xml에 imageView와 button을 만들고 각각의 id 또한 부여했다.

 

그리고 camera와 storage에 접근에 대한 허가를 app\manifests\AndroidManifest.xml의 <manifest> </manifest> 안에 아래 코드를 작성한다. 

1
2
3
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
cs

그리고 권한을 묻는 창을 쉽게 띄우기 위해서 GradleScripts\build.gradle (:app)의 dependencies {} 안에

implementation 'gun0912.ted:tedpermission:2.0.0' 을 추가해준다.

 


1_2 권한 묻기

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
 
    TedPermission.with(getApplicationContext()) //권한을 체크해주는 팝업창
        .setPermissionListener(permissionListener) //밑에서 permissionListener을 정의함
        .setRationaleMessage("카메라 권한이 필요합니다."//처음 뜨는 메시지
        .setDeniedMessage("거부하셨습니다."//거부 되었을 때 뜨는 메시지
        .setPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA) //권한의 종류(?)
        .check();
 
    ...
}
 
 
PermissionListener permissionListener = new PermissionListener() {
    @Override
    public void onPermissionGranted() { //권한이 허용됐을 때 이벤트
        Toast.makeText(getApplicationContext(), "권한이 허용됨", Toast.LENGTH_SHORT).show();
    }
 
    @Override
    public void onPermissionDenied(ArrayList<String> deniedPermissions) { //권한이 거부됐을 때 이벤트
        Toast.makeText(getApplicationContext(), "권한이 거부됨", Toast.LENGTH_SHORT).show();
    }
};
cs

6~11열에서 권한을 붇는다. 

7열에서 serPermissionListener()로 17~27열에서 구현되어있는 permissionListener을 가져온다. 

        그 permssionListener은 

        onPermissionGranted()

        , onPermissionDenied()의 두 권한이 허용됐을 때와 권한이 거부됐을 때의 이벤트를 정의해줄 수 있다.

        여기에서는 Toast를 띄우는 것으로 구현했다.

8열에서는 setRationaleMessage()로 팝업창에 뜰 메시지를 설정한다.

9열에서는 setDeniedMessage()로 권한 허용이 거부되었을 때의 메시지를 설정한다.

10열에서 setPermissions()으로 필요한 권한을 설정해줬다. 

        권한의 종류에는 

        Manifest.permission.WRITE_EXTERNAL_STORAGE

        Manifest.permission.ACCESS_FINE_LOCATION

        Manifest.permission.CAMERA 등이 있다.

11열에서 check()으로 권한의 승인을 요청한다.

 


1_3 촬영 버튼에 대한 이벤트

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
((Button)findViewById(R.id.capture)).setOnClickListener(new View.OnClickListener() { //촬영버튼을 눌렀을 때
    @Override
    public void onClick(View view) {
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); //카메라 앱을 띄움
        if (intent.resolveActivity(getPackageManager()) != null){ //이 intent가 실행가능한지 확인
            File photoFile = null//파일을 쓸 때는 항상 try catch문을 사용해야한다.
            try {
                photoFile = createImageFile();
            }catch (IOException e) {
 
            }
 
            if (photoFile != null) {
 
                photoUri = FileProvider.getUriForFile(getApplicationContext(), getApplicationContext().getPackageName(), photoFile); //photoFile의 Uri을 변수에 저장하는 듯
                intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri); //Uri을 사진 저장하는 경로로 지정
                startActivityForResult(intent, REQUEST_IMAGE_CAPTURE);
            }
        }
    };
});
cs

4 - Intent(MediaStore.ACTION_IMAGE_CAPTURE)를 이용해서 카메라 앱을 띄우기 위한 intent를 만든다.

8 - 밑의 코드로 정의한 createImageFile() 호출해서 jpg 파일을 받아서 photoFile을 만든다.

7~11 - try {} 안의 코드나 createImageFile()에서 일어나는 IOException가 만약 일어나도 강제 종료시키지 않는다.

        createImageFile()에서 일어나는 예외까지 처리하는 이유는 밑의 코드 1열의 throws IOException 때문이다.

        try catch 문에 대한 것은 밑에 있다.

15 - uri을 반환하는 것 같다. 이 코드를 사용하려면 manifest에 밑의 밑의 코드를 작성해야 한다. 

16 - intent에 사진을 저장할 Uri을 지정해주는 것 같다. 

17 - 실제로 intent를 실행시킨다. (2021-07-27 기준으로 더 이상 사용되지 않는 코드이다.)

 

1
2
3
4
5
6
7
8
9
10
11
12
private File createImageFile() throws IOException { //throws를 이용해서 IOException이 일어났을 때 호출 지점에 예외가 일어나도록 한다. 즉 그래서 위 try catch 문 으로 해결 할 수 있다.
    String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); //날짜와 시간으로 문자열 만듦
    String imageFileName = "TEST_" + timeStamp + "_"//위에서 만든 문자열로 이름을 만듦
    File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); //이 앱에서만 볼 수 있는 Directory를 만듦
    File image = File.createTempFile( //임시 파일을 만든다.
            imageFileName,
            ".jpg",
            storageDir
    );
    imageFilePath = image.getAbsolutePath();//onActivityResult()에서 사용하기 위해서 저장해놓는다.
    return image; // 만든 임시 jpg 파일을 반환한다. 
}
cs

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
        ...
    <application
        ...
        <provider
            android:authorities="com.example.cameraexample"
 
            android:name="androidx.core.content.FileProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths"/>
        </provider>
    </application>
</manifest>
cs

14열에서 지정해준 파일의 경로에 대한 xml파일도 만들어줘야 한다. 

 

 

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path
        name="my_images"
        path="Android/data/com.example.cameraexample/files/Pictures"/>
</paths>
cs

위 코드는 위에서 말한 xml 파일이다. 경로를 표시해준다. 

 

 

* try catch 문

여기서 사용되는 try catch문의 구조는 try { } catch( ) { } 이러한데 try { }에 시도할 코드를 작성하고 catch ( ) { }의 그냥 괄호에는 예외가 났을 때 넘길 예외의 종류를 적는 듯하다 

예를 들어서 

NullPointerException ex //null 값에 접근하려고 할 때 

ArithmaticException ex //수학적 계산 과정에서 에러가 발생할 때

IndexOutOfBoundsException ex //배열의 범위를 넘어갔을 때 (?)

등이 있고 

중괄호에는 예외가 일어났을 때 실행할 부분이다. 

 

 


 

1_4 startActivityForResult의 결과 

1
2
3
4
5
6
7
8
9
@Override
public void onActivityResult(ActivityResult result) {
    if (result.getResultCode() == Activity.RESULT_OK) {
 
        Bitmap bitmap = BitmapFactory.decodeFile(imageFilePath);
        ImageView iv_result = (ImageView)findViewById(R.id.iv_result);
       iv_result.setImageBitmap(bitmap);
    }
}
cs

2 - onActivityResult는 startActivityForResult로 intent를 실행시켰다가 다시 그 전의 activity로 돌아오면 호출되는 함수이다. 

5 - intent를 이용해서 찍은 사진의 위치와 BitmapFactory.decodeFile()을 이용해서 bitmap을 받아온다. 

 (bitmap은 디지털 이미지를 저장하는데 쓰이는 이미지 파일 포맷이다.)

6~7 - 아까 만들었던 imageView에 방금 받아온 bitmap과 setImageBitmap을 이용해서 사진을 띄운다.

 

 

이렇게까지만 하면 오류가 나면서 실행이 안 된다. 그 오류를 고치는 내용은 아래에 있다. 

 

 


 

2 오류 해결하기

 

2_1 에러 메시지 분석

먼저 2021-07-26 기준으로 위 youtube 강의를 그대로 따라 한다면 아래와 같은 에러가 뜬다.

(아래 링크의 페이지들을 보면 더 정확한 정보들이 있다.)

1
2
3
4
5
6
7
8
9
10
11
12
Duplicate class android.support.v4.app.INotificationSideChannel found in modules core-1.5.0-runtime (androidx.core:core:1.5.0) and support-v4-23.1.1-runtime (com.android.support:support-v4:23.1.1)
Duplicate class android.support.v4.app.INotificationSideChannel$Stub found in modules core-1.5.0-runtime (androidx.core:core:1.5.0) and support-v4-23.1.1-runtime (com.android.support:support-v4:23.1.1)
Duplicate class android.support.v4.app.INotificationSideChannel$Stub$Proxy found in modules core-1.5.0-runtime (androidx.core:core:1.5.0) and support-v4-23.1.1-runtime (com.android.support:support-v4:23.1.1)
Duplicate class android.support.v4.os.IResultReceiver found in modules core-1.5.0-runtime (androidx.core:core:1.5.0) and support-v4-23.1.1-runtime (com.android.support:support-v4:23.1.1)
Duplicate class android.support.v4.os.IResultReceiver$Stub found in modules core-1.5.0-runtime (androidx.core:core:1.5.0) and support-v4-23.1.1-runtime (com.android.support:support-v4:23.1.1)
Duplicate class android.support.v4.os.IResultReceiver$Stub$Proxy found in modules core-1.5.0-runtime (androidx.core:core:1.5.0) and support-v4-23.1.1-runtime (com.android.support:support-v4:23.1.1)
Duplicate class android.support.v4.os.ResultReceiver found in modules core-1.5.0-runtime (androidx.core:core:1.5.0) and support-v4-23.1.1-runtime (com.android.support:support-v4:23.1.1)
Duplicate class android.support.v4.os.ResultReceiver$1 found in modules core-1.5.0-runtime (androidx.core:core:1.5.0) and support-v4-23.1.1-runtime (com.android.support:support-v4:23.1.1)
Duplicate class android.support.v4.os.ResultReceiver$MyResultReceiver found in modules core-1.5.0-runtime (androidx.core:core:1.5.0) and support-v4-23.1.1-runtime (com.android.support:support-v4:23.1.1)
Duplicate class android.support.v4.os.ResultReceiver$MyRunnable found in modules core-1.5.0-runtime (androidx.core:core:1.5.0) and support-v4-23.1.1-runtime (com.android.support:support-v4:23.1.1)
 
 
cs

사실 이 에러를 거의 아예 이해하지 못했지만 android.support.~~ 이기 때문에 androidx 형태로 라이브러리가 구현되어있는 것이 아닌 것이 문제라고 생각했다. 

 


 

 

2_2 라이브러리, 버전 업그레이드 관련 오류 해결

그래서 gradle.properties 에 android.enableJetifier=true를 추가해줘서 androidx의 라이브러리가 아닌 라이브러리를 androidx의 종속 항목으로 만들도록 했다. 

 

https://pythonq.com/so/java/402281

 

java - 안드로이드 버전을 업그레이드 한 후“Duplicate class android.support.v4.app.INotificationSideChannel” -

java - 안드로이드 버전을 업그레이드 한 후“Duplicate class android.support.v4.app.INotificationSideChannel” 기사 출처 java android android-gradle-plugin build.gradle

pythonq.com

 

그러고 나서는 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<application
             ...
    <provider
        android:authorities="com.example.cameraexample"
 
        android:name="androidx.core.content.FileProvider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths"/>
    </provider>
</application>
 
cs

app\manifests\AndroidManifest.xml 에서 <provider>의 android:name에서 오류가 생기기에

android:name="androidx.core.content.FileProvider"와 같이 바꿨다.

 

https://blog.naver.com/websearch/221738662288

 

안드로이드에서 android.support.v4.content.FileProvider 클래스를 찾을 수 없음 오류 해결하는 방법

금일 새로운 안드로이드 앱 프로젝트를 생성한 후, AndroidManifest.xml 파일에 android.support.v4.cont...

blog.naver.com

 


 

2_3 startActivityForResult() 대체하기

이렇게 강제 종료되는 것은 막았지만 사실 제대로 실행되지는 않는다. 

 

그리고 이제 startActivityForResult()을 사용할 수 없기 때문에 startActivityResult.launch(intent);으로 바꾸고  

 

onActivityResult() 함수를 밑의 코드로 바꾸어줬다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ActivityResultLauncher<Intent> startActivityResult = registerForActivityResult(
                new ActivityResultContracts.StartActivityForResult(),
                new ActivityResultCallback<ActivityResult>() {
                    @Override
                    public void onActivityResult(ActivityResult result) {
                        if (result.getResultCode() == Activity.RESULT_OK) {
                            // There are no request code
                           
                            Bitmap bitmap = BitmapFactory.decodeFile(imageFilePath);
                            ImageView iv_result = (ImageView)findViewById(R.id.iv_result);
                            iv_result.setImageBitmap(bitmap);
                        }
                    }
                });
cs

 


 

 

2_4 intent.resolveActivity(getPackageManager()) 가 null을 반환하는 문제 해결

하지만 계속 제대로 실행은 되지 않았다. 

그래서 Log.d(tagName, message)를 이용해서 확인해보니깐 intent.resolveActivity(getPackageManager()) != null 가 false가 되어서 실행이 안 된 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.cameraexample">
         ...
    <queries>
        <intent>
            <action android:name="android.media.action.IMAGE_CAPTURE" />
        </intent>
    </queries>
 
    <application
         ...
    </application>
 
</manifest>
cs

그래서 app\manifests\AndroidManifest.xml에 5~8열의 코드를 추가해줬다.

 

https://stackoverflow.com/questions/62535856/intent-resolveactivity-returns-null-in-api-30

 

intent.resolveActivity returns null in API 30

Looking at intent.resolveActivity != null but launching the intent throws an ActivityNotFound exception I wrote opening a browser or an application with Deep linking: private fun openUrl(url: Strin...

stackoverflow.com

 


 

 

2_5 오류 해결

그렇게 하니깐 정상적이게 실행이 됐다.

 

옆의 사진과 같이 촬영 버튼을 눌러서 기본 카메라 앱으로 사진을 찍으면

그 사진이 외부에서 접근할 수 없는 Directory에 저장되고

그 사진을 가져와서 imageView에 띄워준다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

728x90
반응형