Intro

SSL Handler가 구현되어있는 최초 버전 (버전코드 1번)이 테스트 트랙에 올라가 있다는 이유로 구글플레이에서 삭제되었다.

당장 해당 트랙을 비활성화 한 뒤 업데이트를 시도해 보았지만 Cross App Scripting Vulnerability로 리젝되어 클라이언트의 요청이 지속되는 상황이었다.

Vulnerability APK Version(s) Past Due Date
Cross-app Scripting
Your app(s) are using a WebView that is vulnerable to cross-app scripting.
To address this issue, follow the steps in this Google Help Center article.
33 August 23, 2021

저 버전코드가 보이는가? 참고로 라이브 서비스중이었던 버전코드는 13이었다.

개빡친다

많고 많은 티스토리 블로그의 해결방법은 딱 두가지였는데,
내가 시도했던 방법은 이러하다.

AndroidMenifest 수정

많고 많은 티스토리 블로그들의 해결방법을 보면 아래 한 줄을 AndroidMenifest에 추가해줌으로써 해결할 수 있다고 쓰여있다.

<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing" android:value="true" />

그러나 프로젝트에는 이미 해당 메타데이터가 추가되어있는 상태였다.

webview target url string to final

private final static String target_url = "https://블라블라";

...

    webView.loadUrl(target_url);

evaluateJavascript 보안

Scheme이 "javascript:" 인 것을 죄다 evaluateJavascript로 바꿔주고,
그마저도 사전에 약속된 스크립트가 아니면 실행하지 않도록 입구컷 시켰다.

구글한테 입구컷당했다.

FCM으로 넘어오는 url 검사

bundle.getString("url").contains(target_url)

위 코드가 true일 때만 해당 URL을 로드하도록 했다.

안된다.

android:exported="false" 설정

해당 속성을 웹뷰가 있는 Activity에 넣으면 된다고 한다.
근데.. 카카오톡 등 다른 앱 intent 사용은 안하시게요? 역시 패스....하려다가 혹시나 싶어 넣어봤다.

 

앱 실행이 안된다 ㅋㅋ

Unity Project 제거

해당 프로젝트는 Unity As A Library를 이용해 Unity Player Activity와 웹뷰가 상호 통신을 한다.
일단은.. 인텐트로 처리하니 빼보자..

***구글: 응~ 안돼~ 돌아가~

Camera / File Choose Handler 제거

카메라로 사진을 찍거나 파일선택기로 파일을 가져오는 것도 역시나 인텐트로 처리했으니 일단은 지워보았다.
다른 웹뷰 서비스들은 잘 올라가있는데 이게 맞을 리 없지.

죄다 지우고 함수 하나씩 살리기

개노가다 ON
버전코드가 33(지금은 40)까지 올라간 이유가 있다.

살리다보니 handleIntent 함수에서 딱 걸렸다.

혹시?

혹시나 싶어 FCM으로 넘어오는 url 검사 부분을 주석처리 해봤다.

와 Tlqkf

결론

구글새끼들은 loadUrl 메서드로 어떤 Url을 호출하는지에 관심이 없다.
그 Url이 final로 선언한 Url을 포함하고 있더라도..

단지 해당 Url이 final 인지 아닌지에만 관심이 있는 것 같다.

어휴..

해당부분은 이렇게 수정해 주고 업데이트를 올렸다.

 private final static String target_url = "https://블라블라";

 ...


 if(bundle.getString("url") != null) {
     final String target = target_url + bundle.getString("url");

     webView.loadUrl(target);
 }

fcm 데이터에 url 전체를 줬었는데 수정을 요청해서 url path만 주도록 바꿨다.

 

 

하........현타온다............

카톡로그인 인텐트를 아래와 같이 처리해줬다.

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
  if (url.startsWith("intent:")) {
    try {
        Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
        Intent existPackage = getPackageManager().getLaunchIntentForPackage(intent.getPackage());
        if (existPackage!=null) {
        	startActivity(intent);
        } else {
        	Intent marketIntent = new Intent(Intent.ACTION_VIEW);
        	marketIntent.setData(Uri.parse("market://details?id=" + intent.getPackage()));
        }
      return true;
    } catch (Exception e) {
    	e.printStackTrace();
    }
  } 
  else {
  	view.loadUrl(url);
  }
  return true;
}

실사용에서는 참 편하지만 테스트할 때 만큼은 정말 불편하다.

특히 여러대의 테스트기기로 로그인을 해야하는데..
현재 개발중인 서비스는 카톡로그인 외의 로그인방법이 존재하지 않는다.

 

방법이 없을까 고민하다가 Extra를 자세히 보니 브라우저에서 로드되는 URL이 있어서 아래와 같이 처리해줬다.

  @Override
  public boolean shouldOverrideUrlLoading(WebView view, String url) {
    Log.d("MainIntent URL", url);
    if (url.startsWith("intent:")) {
    	try {
    		Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
    		Intent existPackage = getPackageManager().getLaunchIntentForPackage(intent.getPackage());
    		if (existPackage!=null) {
    			startActivity(intent);
    		} else {
    			Intent marketIntent = new Intent(Intent.ACTION_VIEW);
    			marketIntent.setData(Uri.parse("market://details?id=" + intent.getPackage()));
    		}
    		return true;
    	} catch (Exception e) {
        	try{
            	Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);

                if(intent.getAction().contains("kakao")){
                	view.loadUrl(intent.getStringExtra("browser_fallback_url"));
                }
                else {
                    Intent marketIntent = new Intent(Intent.ACTION_VIEW);
                    marketIntent.setData(Uri.parse("market://details?id=" + intent.getPackage()));
                }
            }
            catch (Exception ex){
                ex.printStackTrace();
            }
      	}
    } 
    else {
      view.loadUrl(url);
    }
    return true;
  }

 

이제 카톡이 깔려있지 않아도 카카오 계정으로 사용이 가능하다.

 

MainActivity.java

    private long backKeyPressedTime = 0;
    private Toast toast;
    
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if ((keyCode == KeyEvent.KEYCODE_BACK)) {
        	// 웹뷰 History상 이전 페이지가 있을 경우
            if(mWebView.canGoBack()){
                mWebView.goBack(); // 뒤로가기
                return true;
            }
            // 없을 경우 앱 종료 전 Toast로 물어보기
            else{
            	// 토스트메세지 출력
                if (System.currentTimeMillis() > backKeyPressedTime + 2000) {
                    backKeyPressedTime = System.currentTimeMillis();
                    toast = Toast.makeText(this, "뒤로가기 버튼을 한번 더 누르시면 종료됩니다.", Toast.LENGTH_SHORT);
                    toast.show();
                    return true;
                }
                // 토스트메세지가 있는 상태에서 뒤로가기를 한번 더 누르면 앱 종료
                else if (System.currentTimeMillis() <= backKeyPressedTime + 2000) {
                    finish();
                    toast.cancel();
                }
            }
        }
        return super.onKeyDown(keyCode, event);
    }

 

최종 결과물 

Activity에서 처리하는 경우

activity_main.xml

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <WebView
            android:layout_width="match_parent"
            android:layout_height="match_parent"

            android:id="@+id/mWebview"/>
    </LinearLayout>

MainActivity.java

    WebView mWebView;
    WebSettings mWebSettings;

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

        mWebView = (WebView) findViewById(R.id.mWebview); // 웹뷰 선언
        mWebSettings = mWebView.getSettings();

        mWebView.setWebChromeClient(new WebChromeClient()); // 크롬클라이언트 사용
        mWebView.setWebViewClient(new WebViewClientClass()); // 웹클라이언트 설정 (아래)
        mWebSettings.setJavaScriptEnabled(true); // 자바스크립트 허용
        mWebSettings.setSupportMultipleWindows(false); // 여러 창 또는 탭 열리는 것 비허용
        mWebSettings.setLoadWithOverviewMode(true); // 페이지 내에서만 이동하게끔
        mWebSettings.setUseWideViewPort(true); // 페이지를 웹뷰 width에 맞춤
        mWebSettings.setSupportZoom(false); // 확대 비활성화
        mWebSettings.setBuiltInZoomControls(false); // 확대 비활성화
        mWebSettings.setCacheMode(mWebSettings.LOAD_NO_CACHE); // 캐시 사용안함 (매번 새로 로딩)
        mWebSettings.setDomStorageEnabled(true); // 로컬스토리지 사용 허용

        mWebView.loadUrl("접속할 URL");
    }

    // 웹클라이언트 세부 설정
    private class WebViewClientClass extends WebViewClient {
        // SSL 인증서 무시
        @Override
        public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
            handler.proceed();
        }

        // 페이지 내에서만 url 이동하게끔 만듬
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            view.loadUrl(url);
            return true;
        }
    }

AndroidManifest에서 처리하는 경우

AndroidManifest.xml

    <application
        ...
        android:usesCleartextTraffic="true">
        ...

이전 포스팅 : 안드로이드 프래그먼트 활용하기

 

안드로이드 프래그먼트 활용하기

MainActivity.java public class MainActivity extends AppCompatActivity { private FragmentManager fragmentManager; private Fragment_Main fragment_main; private Fragment_MyPage fragment_my_page; privat..

jyspw.tistory.com

 

사전 작업
1. res 폴더 우클릭 -> New -> Android Resources Directory -> menu 폴더 생성
2. res/drawable -> New -> Vector Asset -> 사용할 Clipart 이미지 생성

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private FragmentManager fragmentManager;
    private Fragment_Main fragment_main;
    private Fragment_MyPage fragment_my_page;
    private FragmentTransaction fragmentTransaction;

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

        fragmentManager = getSupportFragmentManager();

        fragment_main = new Fragment_Main();
        fragment_my_page = new Fragment_MyPage();

        fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.replace(R.id.frameLayout, fragment_my_page).commitNowAllowingStateLoss();
 
 	// 이벤트 리스너 등록
        BottomNavigationView bottomNavigationView = findViewById(R.id.navBar);
        bottomNavigationView.setOnNavigationItemSelectedListener(new MenuClickEventListener()); 
    }
    class MenuClickEventListener implements BottomNavigationView.OnNavigationItemSelectedListener{
        @Override
        public boolean onNavigationItemSelected(@NonNull MenuItem item) {
            fragmentTransaction = fragmentManager.beginTransaction();

            switch(item.getItemId())
            {
                case R.id.btn_main:
                    fragmentTransaction.replace(R.id.frameLayout, fragment_main).commitNowAllowingStateLoss();
                    break;
                case R.id.btn_my_page:
                    fragmentTransaction.replace(R.id.frameLayout, fragment_my_page).commitNowAllowingStateLoss();
                    break;
            }
            return true;
        }
    }

    /* 
    // 이전 포스팅 코드
    public void MenuClickHandler(View v){
        fragmentManager = getSupportFragmentManager();
        fragmentTransaction = fragmentManager.beginTransaction();

        switch (v.getId()){
            case R.id.btn_main:
                fragmentTransaction.replace(R.id.frameLayout, fragment_main).commitNowAllowingStateLoss();
                break;
            case R.id.btn_my_page:
                fragmentTransaction.replace(R.id.frameLayout, fragment_my_page).commitNowAllowingStateLoss();
                break;
        }
    }*/
}

 

res/menu/nav.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/btn_main"
        android:icon="@drawable/ic_baseline_home_24"
        android:title="Main" />
    <item
        android:id="@+id/btn_my_page"
        android:icon="@android:drawable/ic_menu_manage"
        android:title="My Page" />
</menu>

 

activity_main.xml

<?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">

    <FrameLayout
        android:id="@+id/frameLayout"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@id/navBar"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent">

    </FrameLayout>

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/navBar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="?android:attr/windowBackground"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="parent"
        app:menu="@menu/nav" />

</androidx.constraintlayout.widget.ConstraintLayout>

최종 결과물

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private FragmentManager fragmentManager;
    private Fragment_Main fragment_main;
    private Fragment_MyPage fragment_my_page;
    private FragmentTransaction fragmentTransaction;

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

        fragmentManager = getSupportFragmentManager();

        fragment_main = new Fragment_Main();
        fragment_my_page = new Fragment_MyPage();

        fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.replace(R.id.frameLayout, fragment_my_page).commitNowAllowingStateLoss();
    }

    public void MenuClickHandler(View v){
        fragmentManager = getSupportFragmentManager();
        fragmentTransaction = fragmentManager.beginTransaction();

        switch (v.getId()){
            case R.id.btn_main:
                fragmentTransaction.replace(R.id.frameLayout, fragment_main).commitNowAllowingStateLoss();
                break;
            case R.id.btn_my_page:
                fragmentTransaction.replace(R.id.frameLayout, fragment_my_page).commitNowAllowingStateLoss();
                break;
        }
    }
}

 

activity_main.xml

    <FrameLayout
        android:id="@+id/frameLayout"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@id/linearLayout"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent">

    </FrameLayout>

    <LinearLayout
        android:id="@+id/linearLayout"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:orientation="horizontal"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent">

        <Button
            android:id="@+id/btn_main"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="MenuClickHandler"
            android:layout_weight="1"
            android:text="Main" />

        <Button
            android:id="@+id/btn_my_page"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="MenuClickHandler"
            android:layout_weight="1"
            android:text="MyPage" />
    </LinearLayout>

 

각 Fragment Java

public class Fragment_이름 extends Fragment {
    @Nullable
    @Override
    public View onCreateView(
        @NonNull LayoutInflater inflater,
        @Nullable ViewGroup container,
        @Nullable Bundle savedInstanceState
    ){
        return inflater.inflate(R.layout.fragment_이름, container, false);
    }
}

 

테스트용 Fragment Layout xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="This" />

        <Button
            android:id="@+id/button2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="is" />

        <Button
            android:id="@+id/button3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="a" />

        <Button
            android:id="@+id/button4"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Main Page" />
    </LinearLayout>
</LinearLayout>

 

 

결과물

 

+ Recent posts