단축키를 잘 사용하면

드라마틱하게 생산성을 높일 수 있다.

 

자주 쓰는 단축키를 모아놓고 외워두자!

 

 

실행 내용

Windows 단축키

Mac OS 단축키

단위선택

Ctrl + W

Option + Up

라인삭제

Ctrl + Y

Cmd + Del

코드재정렬

Ctrl + Alt + L

Cmd + Option + L

Multi Selection

Ctrl * 2 + Up / Down

 

OR

 

Alt + Drag

Option * 2  + Up / Down

 

OR

 

Option + Drag

부분적으로 다중선택

Alt + shift

Option + Shift

밑에있는같은키워드선택

Alt + J

Ctrl + G

같은키워드전체선택

Shift + Ctrl + Alt + J

Ctrl + Cmd + G

히스토리에서 붙여넣기

Ctrl + Shift + V

Cmd + Shift + V

스네이크 케이스

 -> 카멜케이스변환
(플러그인설치
사용)

Shift + Alt + U

Option + Shift + U

라인단위코드위치옮기기

Alt + Shift + Up / Down

Alt + Shift + Up / Down

메서드단위
코드위치옮기기

Ctrl + Shift + Up / Down

Cmd + Shift + Up / Down

메서드단위로커서옮기기

Alt + Up / Down

 

메서드선언위치로이동

Ctrl + B

Cmd + B

인터페이스구현부이동

Ctrl+ Shift + B

Cmd + Shift+ B

 

 

 

 

 

 

 

 

실행 내용

Windows 단축키

Mac OS 단축키

프로젝트탭

Alt + 1

Cmd + 1

구조탭

( 변수, 메서드한눈에보기)

Alt + 7

Cmd + 7

계층보기

Ctrl + H

Ctrl + H

활성 또는 마지막 활성 
도구 창 숨기기

Shift + ESC

Shift + ESC

매개변수표시

Ctrl + P

Cmd + P

코드 감싸기(if ..)

Ctrl + Alt + T

Cmd + Option + T

메서드로 만들기

Ctrl + Alt + M

Cmd + Option + M

Join Line

Shift + Ctrl + J

Shift + Ctrl + J

반복되는구문변수로 생성

Ctrl + Alt + V

Cmd + Option + V

Postfix Completion

다양한상용구세팅에서확인

라이브템플릿지정

자주사용하는템플릿지정(ex. llv, llh)

 

 

 

** 카멜 케이스 플러그인 설치

plugin -> market place -> CamelCase 설치

 

 


 

 

 

 

 

도움이되는 글이었다면

로그인이 필요없는 공감 버튼 꾹 눌러주세요! 

 

 

 

 

 


많은 시간을 들인 게시글입니다. 

무단으로 복사해서 게시하지 말아 주세요.


1.에러발생 

앞선 글에서 자체 인증서를 통한 Retrofit 통신 시

발생하는 에러에 대한 해결을 올렸다. 

 

 

[Android] Retrofit - SSL 에러 해결 : Trust anchor for certification path not found.

 

[Android] Retrofit - SSL 에러 해결 : Trust anchor for certification path not found.

많은 시간을 들인 게시글입니다. 무단으로 복사해서 게시하지 말아 주세요. 앱과 서버를 만들어서 HTTP로 통신하게 만들었다. 만들고 나니 HTTPS로 통신하게 만들고 싶어 졌다. 그리고 문제가 생겼

kyome.tistory.com

 

이 문제의 연장선으로 자체인증서를 가진 서버에

Glide를 이용해 HTTPS URL로 이미지를 가져오려고 하면 

다음과 같은 에러가 발생한다.

 

 

 

 

 

"I/Glide: Root cause (1 of 1) javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found."

 

 

 

 

그리고 화면은 아래와 같이 아무 이미지도 가져오지 못한다.

 

 

 

 

 

 

2. 해결 방안

Glide에 지정한 OkHttpClient를 가지고

통신하게 코드를 변경해주면 된다.

 

OkHttpClient에 설정을 변경하여

자체 인증서를 사용하는 서버와 HTTPS 통신이

가능하게 해주고 (이전 게시글에서 적용했던 방식)

이렇게 만든 OkHttpClient를 Glide 통신할 때 사용하도록

지정해주면 해결할 수 있다.

 

 

 

 

3. 상세 방법 및 소스

1) OkHttpClient 세팅 클래스 선언

 

이전 게시글에서 

 

[Android] Retrofit - SSL 에러 해결 : Trust anchor for certification path not found.

많은 시간을 들인 게시글입니다. 무단으로 복사해서 게시하지 말아 주세요. 앱과 서버를 만들어서 HTTP로 통신하게 만들었다. 만들고 나니 HTTPS로 통신하게 만들고 싶어 졌다. 그리고 문제가 생겼

kyome.tistory.com

 

4. 상세 방법 및 소스

  1) raw 디렉터리에 crt 인증서 넣기

  2) 자체 인증 빌더 클래스 생성

  4) Hostname (ip) not verified 에러

 

까지 작업해두기 

 

 

 

 

 

 

 

 

 

 

 

2) build.grade 에 dependency 추가

 

// build.gradle (:app)

dependencies {
	...
	
    implementation 'com.github.bumptech.glide:okhttp3-integration:4.6.1'
	
    ...
}

 

 

 

 

3) GlideModule 클래스 생성

 

@GlideModule
public class SSLGlideModule extends AppGlideModule {
    @Override
    public void registerComponents(
    	@NonNull Context context,
        @NonNull Glide glide, 
        @NonNull Registry registry) {
        
          super.registerComponents(context, glide, registry);

          // OkHttpClient Builder에 
          // 자체인증서 HTTPS 통신하도록 설정
          SelfSigningHelper helper = SelfSigningHelper.getInstance();
          OkHttpClient.Builder builder = new OkHttpClient.Builder();
          helper.setSSLOkHttp( builder,"10.0.2.2");

          registry.replace(GlideUrl.class, InputStream.class,
          	new OkHttpUrlLoader.Factory(builder.build()));
    }
}

 

참고자료 : Gilide공식문서

 

Glide v4 : Configuration

Setup Starting in Glide 4.9.0, setup is required in a couple of cases. For applications, setup is only required if the application wants to: Use one or more integration libraries Change Glide’s configuration (disk cache size/location, memory cache size e

bumptech.github.io

 

 

 

 

5) 사용하기

 

이렇게 준비했다면 사용하는 건 완전히 동일하다.

 

Glide.with(getApplicationContext())
  .load(Config.BASE_URL).into(imageView);

 

 

 


 

 

 

 

도움이 되는 글이었다면

로그인이 필요없는 공감 버튼 꾹 눌러주세요! 

 

 

 

 


많은 시간을 들인 게시글입니다. 

무단으로 복사해서 게시하지 말아 주세요.


 

 

앱과 서버를 만들어서 HTTP로 통신하게 만들었다.

만들고 나니 HTTPS로 통신하게 만들고 싶어 졌다.

그리고 문제가 생겼다.

 

1. 에러 발생

 

" javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found. "

 

직접 제작한 인증서를 서버에 적용하고

안드로이드에서 통신하려 하는데 에러가 발생했다.

 

 

 

 

 

답답한 마음에 파파고한테 물어봤다...

파파고가 알려주기를

 

인증 경로에 대한 신뢰 앵커를 찾을 수없다는데......

무슨 말인지 모르겠닼ㅋㅋㅋㅋ

 

 

 

 

2. 원인 추정

 

 

느낌상 원인은 OpenSSL로 직접 만든 인증서를 

서버에 적용했기 때문에 

프로토콜은 HTTPS이지만 신뢰할 수 없다는 거 아닐까 싶다.

 

추가적으로 찾아보니 공식문서에

"시스템에서 신뢰할 수 없는 CA를 보유했기 때문에 SSLHandshakeException이 발생합니다"

"공개 CA가 아니라 자체 사용을 위해 정부, 회사 또는 교육 기관 같은 조직에서 발급하는 비공개 CA이기 때문"

라는 문구를 찾아볼 수 있었다.

 

3. 해결 방안

이런 문제를 해결을 위해 참고할만한 공식문서가 있다.

 

https://developer.android.com/training/articles/security-ssl?hl=ko#UnknownCa

 

HTTPS 및 SSL을 사용한 보안  |  Android 개발자  |  Android Developers

현재 기술적으로 전송 계층 보안(TLS)이라고 알려진 보안 소켓 레이어(SSL)는 클라이언트와 서버 간의 암호화된 통신을 위한 공통 기본 토대입니다. 애플리케이션이 SSL을 잘못 사용하면 악성 개체

developer.android.com

공식문서의 내용은

특정 CA 집합을 신뢰하도록

HttpsURLConnection에 알리는 방식이다.

 

간략하게 이해하기로 SSL 인증서 중 .crt 형식의 인증서 파일이 있는데

이 파일로 SSL 소켓 팩토리를 만들고 urlConnection에 붙이는 방식이다.

 

이러한 내용을 조금 변경해서

urlConnection 대신 OkHttpClient를 세팅하였고

이렇게 만든 OkHttpClient를 Retrofit에 빌더에 추가해주면

해당 에러는 해결할 수 있었다.

 

 

4. 상세 방법 및 소스

1) raw 디렉터리에 crt 인증서 넣기

 

 - 우선 안드로이드 raw 밑에 raw디렉터리를 만든다.

 - SSL소켓 팩토리를 만들기 위해 crt 인증서를 raw밑에 넣는다.

 

1-1 새 디렉토리

 

1-2 raw 디렉토리 선택

 

1-3 crt 인증서 붙여넣기

 

 

 

 

 

 

 

 

2) 자체 인증 빌더 클래스 생성

 

Retrofit으로 통신을 할 것이기 때문에 OkHttp를 사용해야 한다.

OkHttp.Builder를 받아 SSL소켓 팩토리를 붙인 Builder를

반환하는 메서드를 만들었다.

 

public class SelfSigningHelper {
    private SSLContext sslContext;
    private TrustManagerFactory tmf;

    private SelfSigningHelper() {
        setUp();
    }
    
	// 싱글턴으로 생성
    private static class SelfSigningClientBuilderHolder{

        public static final SelfSigningHelper INSTANCE = new SelfSigningHelper();
    }
	
    public static SelfSigningHelper getInstance() {
        return SelfSigningClientBuilderHolder.INSTANCE;
    }

    public void setUp() {

        CertificateFactory cf;
        Certificate ca;

        InputStream caInput;
        try {
            cf = CertificateFactory.getInstance("X.509");
            // Application을 상속받는 클래스에 
            // Context 호출하는 메서드 ( getAppContext() )를 
            // 생성해 놓았음
            caInput = AppApplication.getAppContext().getResources()
            		.openRawResource(R.raw.my_cert);

            ca = cf.generateCertificate(caInput);
            System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN());

            // Create a KeyStore containing our trusted CAs
            String keyStoreType = KeyStore.getDefaultType();
            KeyStore keyStore = KeyStore.getInstance(keyStoreType);
            keyStore.load(null,null);
            keyStore.setCertificateEntry("ca", ca);

            // Create a TrustManager that trusts the CAs in our KeyStore
            String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
            tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
            tmf.init(keyStore);

            // Create an SSLContext that uses our TrustManager
            sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, tmf.getTrustManagers(), new java.security.SecureRandom());

            caInput.close();
        } catch (KeyStoreException 
        		| CertificateException 
                | NoSuchAlgorithmException 
                | IOException 
                | KeyManagementException e) {
            e.printStackTrace();
        }
    }

    public OkHttpClient.Builder setSSLOkHttp(OkHttpClient.Builder builder){

        builder.sslSocketFactory(getInstance().sslContext.getSocketFactory(), 
        	(X509TrustManager)getInstance().tmf.getTrustManagers()[0]);
            
        return builder;
    }
}

 

 - 싱글턴으로 생성, 생성자에서 setUp() 호출

 - setUp() 메서드를 사용해서 SSLContext와 TrustManagerFactory를 생성, 할당

 - setSSLOkHttp(OkHttpClient.Builder builder() 에서 SSL 소켓 팩토리 추가

 

 

 

 

 

 

 

 

 

 

3) Retrofit.Builder에 Client변경

 

Retrofit에서 build 설정으로

사설 SSL이 통신 가능한 client를 지정한다.

 

public class RetrofitFactory {

    public static Retrofit createRetrofit(Context context,String baseUrl) {
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(baseUrl)
                .client(createOkHttpClient())
                .build();
        return retrofit;
    }

    private static OkHttpClient createOkHttpClient() {
        SelfSigningHelper helper = SelfSigningHelper.getInstance();
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        helper.setSSLOkHttp( builder);

        return builder.build();
    }
}

- Retrofit.Builder에 client로 OkHttpClient를 지정

- 새로 만든 OkHttpClient에 위에서 만든 메서드를 활용해서 SSL 통신이 되도록 변경

 

 

** 참고 레트로핏 세팅방법 

 

 

 

 

4) Hostname (ip) not verified 에러

 

 

"javax.net.ssl.SSLPeerUnverifiedException:Hostname 10.0.2.2 not verified"

 

 

위의 내용을 다 처리하면

이제 새로운 에러가 나온다 ㅎㅎㅎㅎ

 

다행히 다음의 내용을 참고할 수 있었다.

 

HTTPS 및 SSL을 사용한 보안  |  Android 개발자  |  Android Developers

현재 기술적으로 전송 계층 보안(TLS)이라고 알려진 보안 소켓 레이어(SSL)는 클라이언트와 서버 간의 암호화된 통신을 위한 공통 기본 토대입니다. 애플리케이션이 SSL을 잘못 사용하면 악성 개체

developer.android.com

해당 사이트에서 원인을 친절하게 알려줬다.

 

현재 통신하고 있는 서버가 올바른 인증서를 제시하는지 확인하는 절차에서 에러가 난 것이다.

 

지금까지 조치한 내용은 인증서 자체가

신뢰할 수 없는 소스라고 해서 에러가 난 것이고

이번 에러는 서버가 올바른 인증서를

주는지에 대해서 검증하는 과정에 난 것이다.

 

결국 자체 SSL이기 때문에 난 것 같은데 

찾아보니 검증 도구를 변경하여 강제로

인증하는 방법이 있다.

 

공식문서에서 

"최소한 앱에서 예상한 위치에 있음을

확인하는 인증 도구로 교체" 

하는 방법이라고 소개하는데 

주의해서 사용하라고 하고 있으니 

주의하면 될 것 같다.

 

// 4. 2) 의 소스를 다음과 같이 변경

	public OkHttpClient.Builder setSSLOkHttp(OkHttpClient.Builder builder,String target){

        builder.sslSocketFactory(getInstance().sslContext.getSocketFactory()
        		, (X509TrustManager)getInstance().tmf.getTrustManagers()[0]);
        
        // 여기서부터 추가
        builder.hostnameVerifier((hostname, session) -> {
            if (hostname.contentEquals(target)) {
                Log.d("test", "Approving certificate for host " + hostname);
                return true;
            }else {
                Log.d("test", "fail " + hostname);
                return false;
            }
        });
        // 여기까지 추가
        return builder;
    }

 - '4. 2) 자체 인증 빌더 클래스 생성'에서 setSSLOkHttp의 메서드를 수정한다.

 - 파라미터로 String target를 받아 hostname.contentEquals(target)에서 검증한다.

 

 

public class RetrofitFactory {

    public static Retrofit createRetrofit(Context context,String baseUrl) {
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(baseUrl)
                .client(createOkHttpClient())
                .build();
        return retrofit;
    }

    private static OkHttpClient createOkHttpClient() {
        SelfSigningHelper helper = SelfSigningHelper.getInstance();
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        
        // 여기 변경
        helper.setSSLOkHttp( builder,"10.0.2.2");

        return builder.build();
    }
}

 - 사용 시 Host의 IP를 입력

 - 인증서가 있을 hostname과 HTTPS를 통해

요청하는 목적지가 동일한지 확인하여 검증

 

 

 

 

 

Retrofit 이제 정상적으로 요청이 간다!!

 

 


참조 사이트 :

gist.github.com/nowke/75037c42171d9ea5ce87a49a982c4c39 

 

 

 

 

 

도움이 되는 글이었다면

로그인이 필요 없는 공감 버튼 꾹 눌러주세요! 

 

 

 

 

 

 

 

 

 

 

웹뷰 설정은 매번 기억이 안나기 때문에

기록해두는게 마음편하다.

 

 

WebView webView = findViewById(R.id.webView);


webView.getSettings().setJavaScriptEnabled(true); // 자바스크립트 사용여부

webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);  // 자바스크립트가 창을 자동으로 열 수 있게할지 여부

webView.getSettings().setLoadsImagesAutomatically(true); // 이미지 자동 로드

webView.getSettings().setUseWideViewPort(true); // wide viewport 설정
//  설정값이 false인 경우, layout 너비는 디바이스 픽셀에 맞추어 설정된다.
//  값이 true이고 페이지에 뷰포트 메타 태그가 있으면 태그에 지정된 너비 값이 사용된다.
//  페이지에 태그가 없거나 너비가 없는 경우 넓은 뷰포트가 사용된다.


webView.getSettings().setLoadWithOverviewMode(true); //컨텐츠가 웹뷰보다 클때 스크린크기에 맞추기

webView.getSettings().setSupportZoom(false); // 줌설정

webView.getSettings().setBuiltInZoomControls(true); // 줌아이콘

webView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE); // 캐시설정
//  LOAD_CACHE_ELSE_NETWORK  : 캐시 기간만료 시 네트워크 접속
//  LOAD_CACHE_ONLY : 캐시만 불러옴 (네트워크 사용 X)
//  LOAD_DEFAULT : 기본 모드, 캐시 사용, 기간 만료 시 네트워크 사용
//  LOAD_NO_CACHE : 캐시모드 사용안함
//  LOAD_NORMAL : 기본모드 캐시 사용 @Deprecated

 

 

 

 

 

 

 

 

webView.getSettings().setAppCacheEnabled(false); //앱내부의 캐시 사용 여부

webView.getSettings().setDomStorageEnabled(true);  // 로컬 스토리지 사용여부

webView.getSettings().setAllowFileAccess(true); // 파일 액세스 허용 여부

webView.getSettings().setUserAgentString("app"); // 사용자 문자열 설정

webView.getSettings().setDefaultTextEncodingName("UTF-8"); // 인코딩 설정

webView.getSettings().setAllowUniversalAccessFromFileURLs(true);

webView.getSettings().setBlockNetworkImage(false); // 네트워크를 통해 이미지리소스 받을지 여부

webView.getSettings().setSupportMultipleWindows(true); //  멀티윈도우를 지원할지 여부
//  {@link WebChromeClient#onCreateWindow} must be implemented by the host application. -

webView.getSettings().setDatabaseEnabled(false); //database storage API 사용 여부

webView.getSettings().setAllowContentAccess(true); // 웹뷰를 통해 Content URL 에 접근할지 여부

// a class -> 내부에 @JavascriptInterface 메서드 구현
// webView.addJavascriptInterface(new a(this), "app");


webView.setWebChromeClient(new WebChromeClient());
webView.clearCache(true);
webView.loadUrl("https://kyome.tistory.com/"); //웹뷰 URL로드 

 

 

 

도움이 되는 자료였다면
로그인이 필요없는 공감 버튼 꾹 눌러주세요! 

 

 

 

 

 

1. Retrofit ?

 

 

모바일에서 HTTP API통신을 경우

많이 사용하는 라이브러리이다.

 

Retrofit은 Gson, Jackson 등 여러 컨버터를

지원하여 다양한 형태의 데이터를 

편하게 주고받을  있다.

 

<참고 - Retrofit 한글 문서 : http://devflow.github.io/retrofit-kr/>

 

 

2. 사용하기

1) 설치 및 권한 설정 

 

<!-- build.gradle  -->
...

dependencies {
    ...
    // Retrofit
    implementation 'com.squareup.retrofit2:retrofit:2.6.2' // 레트로핏 설치 (필수)
    implementation 'com.squareup.retrofit2:converter-gson:2.6.2' // 컨버터 (선택)
    implementation 'com.google.code.gson:gson:2.8.5'  //GSON (선택)
    ...
}

 

 

<!-- AndroidManifest.xml  -->
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.test">
    .....
    <uses-permission android:name="android.permission.INTERNET"/>
    ....
    

Http 통신을 하기 때문에 인터넷 사용에 대한 권한을 추가해 주어야 한다.

 

 

 

2) Retrofit 객체 생성하기

 

public class RetrofitFactory {

    public static Retrofit createRetrofit(String baseUrl) {
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(baseUrl)
                .addConverterFactory(GsonConverterFactory.create())
                .build();
        return retrofit;
    }
 }

 

Retrofit을 사용하기 위해서는 Retrofit객체를 생성해야 한다

보통 Retrofit 객체를 생성하는 메서드를 선언해두고 재사용한다.

 

Retrofit 객체를 생성하기 위해 빌더 객체를 생성해야 하고

필요한 설정 사항을 추가해서 빌드한다.

 

baseUrl은 URL 호출 시에 반복을 줄여주는 역할을 한다.

예를 들면 

kyome.tistory.com/150 호출해야 한다고 하면

보통 'kyome.tistory.com/' 까지는 고정이고

'150' 부분이 변하는 부분이기 때문에 

 ''kyome.tistory.com/'를 baseUrl로 세팅한다.

 

addConverterFactory는

HTTP 통신 시에 주고받는 데이터 형태를 

변환시켜 주는 컨버터를 지정하는 설정이다.

Gson, Jackson 등 다양하다.

 

 

 

 

3) 서비스 생성하기

 

public interface ExampleService {

    @GET("/example/info")
    Call<String> getExampleBySeq(@Query("seq") String param);
}

 

이제 Service (Retrofit 용어) 클래스를 만든다.

Retrofit에서 Service API 정의하는 인터페이스를 말한다.

 

Service는 interface로선언하기 때문에

구현할 필요는 없고 어떤 형태와

방식으로 통신을 할지를

어노테이션과 파라미터로 지정하면 된다.

(Retrofit 알아서 구현해준다)

 

Service 기본적으로 Call<T> 객체를  반환한다.

서버의 API String 반환한다고 가정하면

클라이언트는 Retrofit 통해 Call <String>를받게 것이다.

 

Call<T> 객체에 대해서는 아래에서 다룬다.

 

 

 

 

 

 

 

 

 

4) Retrofit - Service 연결하기 (Call<T> 객체얻기)

 

public class ExampleRepository {
    private ExampleRepository() {
    }
    
    // 싱클턴으로 사용
    public static ExampleRepository getInstance(){
        return ExampleRepositoryHolder.INSTANCE;
    } 
    
	public Call<String> getExampleBySeq(String seq) {
        return RetrofitFactory
        	.createRetrofit("http://10.0.2.2:8080/") // Retrofit객체 반환
        	.create(ExampleService.class)
            .getExampleBySeq(seq);
    }
}

 

Retrofit create 메서드에 Service 클래스를 넣어서

HTTP에접근할 Retrofit 객체를 만든다.

이렇게 만든 Retrofit객체를 만들었으면

클라이언트와 서버가 통신할 길을

열렸다고 있다.

Retrofit에서 Service 선언한 메서드를 호출하는

방식으로 통신의 길을 있다.

 

통신의 길이라고 표현한 이유 

더보기

 

모호하게 통신의 열었다고 표현한 이유는

Retrofit 객체를 사용해 Service에 

선언한 메서드를 호출하면 Call객체를 받는다.

 

이렇게 Call객체를 받은 것만으로 통신이되는 것이 아니라

Call객체의 enqueue 메서드를 호출해야 해당 시점에서

API 통신을 하기 때문이다.

 

실제 개발 재사용성을 높이고 

유지보수를 편하게 하기위해

 

Retrofit 생성하는

중간 클래스인 Repository 만들어 두고

필요 따른 Retrofit 생성 메서드를 저장한다.

 

 

5) Call<T> 객체 사용하기

 

 

...

ExampleRepository.getInstance()
	.getExampleBySeq(editTextSeq.getText().toString())
    	.enqueue(new Callback<String>(){
          @Override
          public void onResponse(Call<String> call, Response<String> response) {
          }

          @Override
          public void onFailure(Call<String> call, Throwable t) {
          Log.d("test", t.toString());
       	}
});

...

 

 

Call<T>은 인터페이스이다.

 

Call 인터페이스는

enqueue(Callback<T> callback); 메서드를

갖고 있어야한다.

그러니 통신을 후에 받은

Call<T> 객체는 enqueue가

구현된 상태라는 것이다.

 

이를 통해 받은 통신의 결과에 대한 처리를 있다.

Retrofit은 통신의 결과에 따라 

파라미터로 받는 Callback 의 메서드를 실행해준다.

성공시 onResponse 를 실행하고

실패  onFailure 실행한다.

 

참고로 Callback<T> 또한 인터페이스다.

void onResponse(Call<T>call,Response<T>response);

void onFailure(Call<T>call,Throwablet);

메서드가 구현을해야 사용할 수 있다.

 

 

 

 

도움이 되는 게시글이었다면 

로그인이 필요 없는 공감 버튼 꾹 눌러주세요! 

 

 

 

 

 

 

 

1. ViewHolder의 필요성

 

앞선 글에서 설명했듯 많은 데이터를 

항목화하여 표현하는 AdapterView 에서는

동작할 때마다 각 항목

(xml형식의 레이아웃)을

View객체로 가져와야 한다

 

그리고 이렇게 생성한 View에 

포함된 뷰를 찾아

( Java 에서는 findViewById)

접근하는 일도 아주 빈번하게 일어난다.

 

문제는 이 작업은 

많은 리소스를 필요로 한다는 점이다. 

 

*참고

 

 

이를 해결하기 위해 사용되는 것이 ViewHolder이다.

한번 inflate 된 View를 객체에 담아 두어 

재사용하는 방식이다.

 

즉, View를 생성하고 활용하는 과정에서

리소스를 효율적으로 사용하기 위한

패턴이라고 볼 수 있다.

 

 

 

2. ListView에 ViewHolder 적용하기

 


이러한 ViewHolder 패턴은

꼭 RecyclerView에서만 사용되는 건 아니다.

오히려 ListView에

ViewHolder를 적용해보면
어떤 이점이 있는지 이해하기 편할 것 같다.

1) 기본 구성 (ViewHolder 없이 구현)

(1) activity_main.xml

먼저 기본적인 Main Activity의 화면 구성은 
다음과 같이 단순하게 해 놓았다

 

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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">

	<ListView
      android:id="@+id/listView"
      android:layout_width="0dp"
      android:layout_height="0dp"
      android:layout_marginStart="1dp"
      android:layout_marginLeft="1dp"
      android:layout_marginTop="1dp"
      android:layout_marginEnd="1dp"
      android:layout_marginRight="1dp"
      android:layout_marginBottom="1dp"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

 

 

(2) item.xml

ListView에 요소로 들어갈 

item 레이아웃을 생성한다.

 

 

 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:gravity="center_vertical"
    android:orientation="horizontal">

    <TextView
        android:id="@+id/textName"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="MacBook Pro" />

    <TextView
        android:id="@+id/textColor"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="sliver" />

    <TextView
        android:id="@+id/textOs"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="OS X" />
</LinearLayout>

 

 

(3) NoteBook.kt

Data class를 선언한다.

하나의 항목에 들어갈 데이터를

저장하는 객체라고 보면 된다.

 

data class NoteBook(val name:String, val color:String, val os:String)

 

 

 

 

 

 

 

 

 

(4) CustomBaseAdapter.kt

일반적으로 구현하는 어댑터이다.

getView를 자세히 보면 

Item.xml을 inflate 하고 inflate 한 View에서 

textView를 찾아서 데이터를 바인딩해준다.

 

그리고 마지막으로 inflate한 View전체에 

click이벤트를 지정한다.

 

class CustomBaseAdapter(private val context:Context, private val dataList: List<NoteBook>) : BaseAdapter() {
    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        // Inflating
        val view = LayoutInflater.from(parent?.context).inflate(R.layout.item,parent,false)

        val getData = dataList[position]

        // findViewById
        view.textName.text = getData.name
        view.textOs.text = getData.os
        view.textColor.text = getData.color

        // 이벤트 생성
        view.setOnClickListener(View.OnClickListener {
            Toast.makeText(context,getData.toString(),Toast.LENGTH_SHORT).show()
        })
        return view
    }

    override fun getItem(position: Int): Any {
        return dataList[position]
    }

    override fun getItemId(position: Int): Long {
        return 0
    }

    override fun getCount(): Int {
        return dataList.size
    }
}

 

 

** 이전 글에서 Adapter에 대한 내용과 일치하는지 확인하기

더보기

Adapter가 알고 있어야 하는 것


(1) Data가 무엇인지 알고 있어야 한다.
(2) 어떤 형태로 item을 구성할지 layout을 알고 있어야 한다.
(3) Data와 layout을 매핑할 수 있어야 한다.
(4) item도 화면에 표현하는 내용이기 때문에 Context를 알고 있어야 한다.

 

 

 

[Android] RecyclerView 이해하기 (1) - Adapter는 무엇인가

RecyclerView를 이해하기에 앞서 AdapterView를 알고 있어야 한다. 왜냐하면 나는 RecyclerView를 AdapterView의 확장판쯤으로 이해했기 때문이다. 1. AdapterView AdapterView는 많은 데이터를 하나의 뷰로 보여..

kyome.tistory.com

 

 

 

(5) MainActivity.kt

Main Activity에서는 지금까지 작성한 내용을 조합한다고 보면 된다.

Data를 list로 선언해서 Adapter에 초기화 값으로 선언한 후 

Adapter 를 ListView에 연결하면 정상적으로 동작한다.

 

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Data
        val noteBookList = listOf<NoteBook>(
            NoteBook("MacBook Pro","Sliver","OS X"),
            NoteBook("MacBook Pro","Gray","OS X"),
            NoteBook("Galaxy Book","Sliver","Windows")
        )

        //CustomAdapter를 View에 추가
        var arrayAdapter = CustomBaseAdapter(this,noteBookList)
        listView.adapter =arrayAdapter
}

 

2) 문제점

getView메서드 내에

Log.d를 찍어보면 알겠지만

화면에 Item이 보여야 할 때마다 

계속 반복적으로 호출된다.

 

앞에서 반복적으로 이야기했듯

문제는 여기서 생긴다.

 

getView 안을 살펴보면 아래와 같은 구문들이

아무 조건 없이 반복적으로 실행되기 때문이다.

 

...

override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
    
    /* 리소스 사용!! */
    // Inflating
    val view = LayoutInflater.from(parent?.context).inflate(R.layout.item,parent,false)
    
    ...
    
    /* 리소스 사용!! */
    // findViewById
    view.textName.text = getData.name
    view.textOs.text = getData.os
    view.textColor.text = getData.color
    
    ...
    
    return view
}

 

 

3) 해결방법 : ViewHolder

해결방법은 간단하다.

View를 한번 만들었으면

어딘가에 담아 두었다가

필요할 때 재사용하는 것이다.

 

(1) VIewHodler class 생성

그 '어딘가'가 ViewHolder 클래스이다.

Item.xml에서 사용한 구성요소에 맞추어

변수를 선언하고 setter를 생성해두면 된다.

 

class ViewHolder() {
    // item을 구성하고 있는 View에 맞게 변수 생성
    
    var textName:TextView? = null
    var textOs:TextView? = null
    var textColor:TextView? = null

    // setter 생성
    fun setNoteBook(noteBook: NoteBook){
        textName?.text = noteBook.name
        textOs?.text = noteBook.os
        textColor?.text = noteBook.color
    }

}

 

 

(2) ViewHolder 사용하기

 

이렇게 생성된 ViewHolder는 

getView 메서드 내에서 사용된다.

 

이전에 View로 inflate 한 xml을

다시 inflate 하지 않기 위해
getView의 두 번째 매개변수를 활용한다.

 

두 번째 매개변수 convertView는 

이전에 inflate 한 View를 그대로 전달한다.

 

 

 

 

 

 

 

즉, convertView가 null이라면 최초 생성이라는

말이기 때문에 Inflate를 수행한다.

 

위에서 만들어 놓은 ViewHolder class를 

객체로 생성하고

이 객체에 Inflate 된 View를 매핑한다.

 

ViewHolder객체를 잘 생성했다면 

Inflate 된 View에 tag의 값으로

저장하고 반환한다.

 

override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
    // return 
    val view:View
    val getData = dataList[position]

    val vh:ViewHolder

    if (convertView == null){
        // Inflating
        view = LayoutInflater.from(parent?.context).inflate(R.layout.item,parent,false)

        // 매핑해놓음
        vh = ViewHolder()
        vh.textName = view.textName
        vh.textOs = view.textOs
        vh.textColor =view.textColor

        // view에 viewholder 저장
        view.tag = vh

        // 이벤트 생성
        view?.setOnClickListener{
            Toast.makeText(context,getData.toString(),Toast.LENGTH_SHORT).show()
        }

    }else {
        view = convertView
        vh = convertView.tag as ViewHolder
    }
    //값 매칭
    vh.setNoteBook(getData)

    return view
}

이렇게 반환된 View는 화면에서 보이고

이후에 다시 getView가 실행될 때 convertView로 재사용된다.

convertView로 재사용되었다면

convertView안에 tag를 꺼내어 

ViewHolder로 활용한다.

 

ViewHolder에 Inflate 된 View가 다 매핑이 되었다면

ViewHolder의 setter를 통해

데이터를 세팅해주면 손쉽게 

값이 바인딩된다.

 

 

 

4. RecyclerView와 다른 점

RecyclerView는 ListView와 달리

앞에 설명한 내용을 

반드시 구현하도록 되어있다.

 

덕분에 자연스럽게 각 요소가 재활용될 수 있으며

추가적으로 배치 방법과 방향등을

LayoutManager를 통해 조작할 수 있어 

더 편하게 구현이 가능하다.

 

 

 

RecyclerView를 이해하기에 앞서

AdapterView를 알고 있어야 한다.

왜냐하면 나는 RecyclerView를

AdapterView의 확장판쯤으로 이해했기 때문이다.

 

 

1. AdapterView

AdapterView는 많은 데이터를

하나의 뷰로 보여주기 위해 만들어진 뷰이다.

즉, 여러 항목을 보여주는 뷰이다.

 

많은 데이터를 View로 보여주기 위해

Adapter라는 것을 사용하기 때문에

지어진 이름이지 않을까 싶다.

 

 

1) 구성요소

 

 

AdapterView에 대한 정의를 내린것에서

구성요소를 추출할 수 있다.

 

"항목을 보여준다"

 

항목을 보여준다는 표현에서 알 수 있듯

 

데이터로 구성되어 있는 하나의 요소가 존재하고

이러한 요소를 각자의 방식대로 보여주는 것이다.

 

즉, (1) 데이터를 (2)하나의 요소로 구성하고

여러 개의 요소들을 (3) View에 나열한다는 것이다.

 

 

 

 

 

 

 

위의 정의에서 볼 수 있듯 크게 3가지 구성요소로 정리할 수 있다.

 

(1) Data

뷰의 내용이 되는 정보

데이터 저장소

 

(2) Adapter

Data를 요소(Item)로 구성하여

View에서 활용 가능하도록 만드는 객체

 

(3) View

나름의 표현방식으로 Item을 보여주는 View

 

 

2) 여전히 정의되지 않은 Adapter

 

 

위의 내용처럼 정리를 해보니

Data와 View의 역할을 뚜렷한데

Adpater는 그래서 뭘 하겠다는 건지

여전히 정리가 잘 안 돼서

다시 세부적으로 정리했다.

 

Adapter는 정보를 가공하여

하나의 View(item)를 만든다.

 

Adpater가 만든 View는

(3) View의 하나의 항목이 된다.

 

 

- Adapter가 알고 있어야 하는 것

(1) Data가 무엇인지 알고 있어야 한다.

(2) 어떤 형태로 item을 구성할지 layout을 알고 있어야 한다.

(3) Data와 layout을 매핑할 수 있어야 한다.

(4) item도 화면에 표현하는 내용이기 때문에 Context를 알고 있어야 한다.

 

( 이 내용은 제가 이해하기 위해 추측을 더하여 작성한 부분이 기입니다.

실제와 다를 수 있습니다. 더 공부가 필요한 부분입니다. )

 

 

3) 예시

대표적인 AdpaterView 인 Spinner를 통해

정리한 내용이 맞는지 확인해보았다.

 

val spinner: Spinner = findViewById(R.id.spinner)
// Create an ArrayAdapter using the string array and a default spinner layout
// Spinner 생성

// Adapterter 생성 
// (1) Context 넣음
// (2) 리소스에서 R.array.planets_array 배열을 데이터로 넣음
// (3) item layout 넣음

ArrayAdapter.createFromResource(
        this,
        R.array.planets_array,
        android.R.layout.simple_spinner_item
).also { adapter ->

    // Specify the layout to use when the list of choices appears
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
    
    // Spinner에 adapter 연결
    spinner.adapter = adapter
}

출처:  공식문서 [developer.android.com/guide/topics/ui/controls/spinner?hl=ko]

 

 

 

 

 

 

ArrayAdapter의 생성 파라미터를

통해 확인할 수 있듯

Adapter는 컨텍스트가 무엇인지,

데이터가 무엇인지,

item은 어떤 레이아웃을 사용할지를 알고 있다.

ArrayAdapter는 배열 형식의 데이터를

안드로이드 기본 layout에 매칭 할 수 있다.

 

 

 

 

4) 기존 AdapterView의 한계는?

Spinner나 ListView (Legacy)와

같은 AdapterView에서는

매번 Item을 만들 때마다 Inflating을 해야 한다.

xml을 객체로 만드는 과정인데 수많은 요소들을
반복해서 View를 만드는 것은 리소스 낭비이며

부하를 일으킨다고 한다.

 

 

 

 

5) RecyclerView는 한계를 해결했는가?

화면에 보이는 View는 어차피 한정되어있다

예를 들어 500개의 항목을

보여주는 View가 있다고 하더라도

한 화면에서 볼 수 있는 항목이 10개라고 하면

Item을 10개만 생성해놓고

Item안의 데이터만 변경하는 방식으로

재사용하면 되는 것이다.

 

이러한 컨셉으로 도입된 것이 ViewHolder이고

이를 강제화한 것이 RecyclerView이다.

 

 

 

 

이어지는 글 : [Android] RecyclerView 이해하기 (2) - ViewHolder는 무엇인가

 

[Android] RecyclerView 이해하기 (2) - ViewHolder는 무엇인가

1. ViewHolder의 필요성 앞선 글에서 설명했듯 많은 데이터를 항목화하여 표현하는 AdapterView 에서는 동작할 때마다 각 항목 (xml형식의 레이아웃)을 View객체로 가져와야 한다 그리고 이렇게 생성한

kyome.tistory.com

 

 

 

 

도움이 되었다면

로그인이 필요 없는 공감 버튼 꾹 눌러주세요! 

 

 

 

 

안드로이드에서 뷰 객체에

이벤트를 만드는 건
패턴화 되어 있어서 

어렵지 않게 개발할 수 있다.

 

TextView textView = (TextView)findViewById(R.id.textView); 

textView.setOnClickListener(new View.OnClickListener() { 
  @Override 
  public void onClick(View view){ 

  } 
}); 

 


그렇지만 왜 이런 패턴과 구조로

작성해야 하는지 궁금했다.

무작정 외우기보단

이해하고 싶어서

안드로이드 이벤트에

공부해보기로 했다

 

 

 

 


이벤트 쉽게 이해하기

 

 

1. 이해하기 어려운 이유

 

안드로이드 이벤트에 대해 찾아보니
이벤트 소스, 이벤트 핸들러, 

이벤트 리스너 같은 낯선 용어들이 

이해를 더 어렵게 하는 것 같다.
그래서 이런 용어들을 빼고

최소한의 용어들로 이해한 것들을

정리해보았다.

 

 

 

 

 

 

 

2. 뷰 (View)는 누구인가

 

 

안드로이드에서 보여지는 

버튼, 텍스트 등은 다 뷰라는 

이름의 객체이다.

 

 

 

특징 - 1. 뚜렷한 목적

 

뷰들은 다 제각기 

생성된 목적이 뚜렷하다.

 

(Ex, Button은 눌림 (Click) 을 당하기 위해, 

CheckBox는 선택 (Checked) 이 되기 위해..)

 

 

특징 - 2. 다양한 이벤트

또 하나의 특징으로는 

뷰는 다양한 이벤트를 가질 수 있다.

즉, TextView도 클릭이벤트를 

가질 수 있다는 말이다.

 

이처럼 다양한 뷰를

효율적으로 관리하기 위해

뷰는 이벤트를 실행할 때마다 

'on이벤트명()'이라는 메서드가 

실행되도록 불러준다.

 

 


3. 우리가 기대하는 뷰

 

 

사용자는 안드로이드 화면에서 

뷰를 눌렀을 때 기대하는 바가 있다

 

만약, 전송 버튼을 눌렀는데 

버튼이 눌린 시늉만 하고 

아무 일도 생기지 않으면

사용자는 앱이 고장 났다고 

볼 것이다.

 

 

 

 

 

 

 

4. 뷰의 입장 이해하기

 

 

클릭 이벤트의

패턴을 이해하기 위해서

뷰의 입장이 되어 보았다.

뷰의 입장이 돼 보면 클릭이벤트 패턴을

이해하기가 한결 수월해진다.

 

 

 

 

뷰 입장 - 1. 난처한 뷰

 

"세세한 것 하나하나 다 해달라고?

너무한 거 아니야?"

 

[그림] 뷰가 알아야 할 것들이 너무 많아진다

 

뷰 입장에서 생각해보면 

사용자가 버튼을 눌렀으면 

눌린 티만 내면 그만이지 


굳이 눌렸을 때 

어떤 일들이 어떻게 진행되는지 

다 알고 있을 이유가 없다. 

뷰가 수행해야 하는 이벤트가 한 개만

있는 것도 아니고 많은 이벤트에 대한 

세세한 내용까지 뷰가 다 관리하자니

뷰 입장에선 너무 부담스럽다.

 

더 정확히 말하자면,

뷰에 로직을 다 넣으면

무거워지고 의존성이 높아지고 등등..

아무튼 지저분해져서 보고 싶지 

않은 코드가 될 것이다. 

 

 

 

 

뷰 입장 - 2. 뷰의 책임감

 

"그래도 기능을 성실하게

수행해야 하긴 할 텐데.."

 

뷰는 사용자와 직접 소통하는 

역할을 담당하기 때문에 
아무 기능을 하지 않는 뷰는

사실 존재의 가치가 없다고

해도 과언이 아니다. 


이벤트를 효율적으로 처리할

방법이 필요한 상황이다. 

 

 

 

 

 

 

5. 뷰의 묘수

 

[그림] 로직을 대신 수행할 객체를 모집

 

뷰의 묘수 - 1. 대리자 모집

 

세세한 로직을

다 알고 싶지 않은 뷰는 로직을

다 알고 있는 대리자를 모집하기로 한다.

 

그렇다고 아무나 불러서 

일을 수행하라고 할 수는 없으니

최소한의 자격요건을 충족하는 

대리자를 모집한다.

 

 

뷰가 내건 최소한의 자격요건


 " 'on이벤트명' 메서드를 

잘 구현해낸 자"


 

이벤트가 일어날 때마다 

이벤트 이름 앞에 on이 붙은 
'on이벤트명' 메서드를 같이 호출할 테니
이벤트에 함께 호출되고 싶다면

'on이벤트명' 메서드를

작성해와서 등록하라고 한 것이다.

 

 

 

 

 

 

뷰의 묘수 - 2. 관리소장 두기

 

이제 지원한 대리자를 

관리하는 일이 남았다.  

 

뷰는 내부에 대리자 관리소장을

하나 두기로 한다.

 

관리소장은 대리자를 등록하는 

일과 이벤트가 일어났을 때 

대리자를 호출해서 로직을 

수행하도록 하는 일을 한다.

 

 

 

6. 결과 

 

[그림] 클릭 이벤트가 발생시 처리

 

이렇게 구조를 정리해 놓으니

뷰는 이제 이벤트가 발생하면

내부에 관리 소장을 불러

어떤 이벤트가 발생했다는 사실만

알려주면 된다.

 

 


 

제가 공부 후 정리한 것에 대해

혹시 다른 의견이나 수정해주실 부분 있으면

꼭 댓글 달아주세요 

저도 아직 공부하는 중이랍니다.

 

 

 

 

도움이 되는 게시글이었다면

로그인이 필요 없는 공감 버튼 꾹 눌러주세요! 

 

 

 

 

 

 

 

1. android.os.FileUriExposedException

 file:///storage/~~.jpg exposed beyond app through ClipData.Item.getUri()

간단한 예제를 따라하며 안드로이드 개발을 하던 중 예외가 발생해서 문제점을 찾아서 올립니다!


버튼을 누르면 카메라를 실행하는 부분을 구현하고 있었습니다...

세부적으로 말하자면 프래그먼트 상에 ImageButton을 생성하고 OnClick 메서드에 카메라를 실행하는 인텐트를 호출하는 그런 작업중이었어요

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_camera, container, false);

mPhotoButton = (ImageButton)v.findViewById(R.id.camera);

final Intent captureImage = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

Uri uri = Uri.fromFile(mPhotoFile); // --------
captureImage.putExtra(MediaStore.EXTRA_OUTPUT, uri); // --------


mPhotoButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivityForResult(captureImage,REQUEST_PHOTO); // --------③

}
});

mPhotoView = (ImageView) v.findViewById(R.id.crime_photo);


}

저는 맨 처음 에러메시지와 표시해주는 코드라인을 따라가보니 ③에서 문제가 생겼다고 생각이 들었습니다. 


일단 어떤 에러때문에 어플리케이션이 작동을 안했는지 봐볼까요?


라는 메시지와 함께 그 아래로 쭉쭉.. 메시지를 출력해주더라고요 ㅠㅠ


다음의 내용들이 중요해보이는 메시지에요 세부 디렉토리는 잘라냈습니다


그런데 자세히 볼수록 ①, ②에서 문제가 있다는 생각이 들었고 찾아보니 FileUri 부분이 문제였습니다.

 

2. 해결

Android 7.0 부터 API정책의 변경

안드로이드 7.0 (누가) 부터 앱사이의 공유가 더 엄격해져서 file:// URI 가 직접 노출되지 않도록 content:// URI를 보내고 

이에 대해서 임시 액세스 권한을 부여하는 방식으로 변경되었다고 합니다. 

이에 대한 자세한 설명은 밑에 공식참고문서(구글번역기로 번역된 글입니다)를 보시됩니다.


앱 사이의 파일 공유


Android 7.0을 대상으로 하는 앱의 경우, Android 프레임워크는 앱 외부에서 file:// URI의 노출을 금지하는 StrictMode API 정책을 적용합니다. 파일 URI를 포함하는 인텐트가 앱을 떠나면 FileUriExposedException 예외와 함께 앱에 오류가 발생합니다.

애플리케이션 간에 파일을 공유하려면 content:// URI를 보내고 이 URI에 대해 임시 액세스 권한을 부여해야 합니다. 이 권한을 가장 쉽게 부여하는 방법은 FileProvider 클래스를 사용하는 방법입니다. 권한과 파일 공유에 대한 자세한 내용은 파일 공유를 참조하세요.

https://developer.android.com/about/versions/nougat/android-7.0-changes.html#accessibility



위에 설명에서와 같이 FileProvider클래스를 사용해서 content:// URI에 권한을 부여하는 방법을 보면 다음과 같습니다.


1. AndroidManifest.xml 수정

<application>
...
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.bignerdranch.android.test.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
...
</application>

2. res/xml/filepaths.xml 생성

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="storage/emulated" path="."/>
</paths>


3. Uri.fromFile(File mFile) 코드 대체

Uri uri = Uri.fromFile(mPhotoFile);


-> Uri uri = FileProvider.getUriForFile(getContext(), "com.bignerdranch.android.test.fileprovider", mPhotoFile);


*com.bignerdranch.android.test 부분은 개인의 도메인을 입력하시고 mPhotoFile 대신 생성한 파일 객체를 넣으시면 됩니다!

참고로 filepaths.xml과 AndroidManifest.xml 내부의 meta-data, android:resource="@xml/filepaths"가 일치해야합니다.





+ Recent posts

"여기"를 클릭하면 광고 제거.