※ 하이브리드 App 구현에 필요한 필 수 기능
• Web Site의 Main 페이지인 경우 Android 뒤로가기 버튼 더블 클릭시 App 종료
• 결제 진행을 위한 사용자 디바이스의 App 실행 및 Google Play 에서 설치 유도
• <input type="file"> 태그 클릭시 카메라 및 갤러리 App 실행 하여 이미지 업로드 가능
• JavaScript에서 window.android 객체를 이용하여 Android Native 코드 실행
1. Project 생성하기
Android Studio에서 New Project를 선택하여 프로젝트를 새로 생성하여 준다.
좌측 메뉴에서 Phone and Tablet을 선택하고 Empty Views Activity를 선택하여 준다.
기본적인 Empty Views Activity를 사용하기위해 아래와 같이 값을 입력하여준다.
입력 및 선택이 마무리 되었다면 [ Finish ] 버튼을 클릭하여 프로젝트를 새로 생성하여 준다.
2. Gradle 설정
프로젝트가 생성되면 build.gradle 파일을 오픈하여 viewBinding 설정을 활성화( true ) 시켜 준다.
build.gradle( Module : app )
plugins {
alias(libs.plugins.android.application)
}
android {
namespace "com.example.hybrid"
compileSdk 34
defaultConfig {
applicationId "com.example.hybrid"
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
buildFeatures {
viewBinding = true
}
}
dependencies {
implementation libs.appcompat
implementation libs.material
implementation libs.activity
implementation libs.constraintlayout
testImplementation libs.junit
androidTestImplementation libs.ext.junit
androidTestImplementation libs.espresso.core
}
변경을 완료하였다면 [ Sync Now ] 를 클릭하여 Build 하여 준다.
3. Manifest 설정
AndroidManifest.xml 파일에서 개발할 하이브리드 App에서 인터넷 이용 권한을 허용하고
<queries> 속성을 이용하여 App이 사용자 디바이스에 설치된 다른 App이나
시스템의 특정 구성 요소에 접근하기 위해 수행되는 쿼리를 선언하여 준다.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- 인터넷 연결 권한 허용 -->
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.WebView"
tools:targetApi="31">
<activity
android:name=".activity.WebViewActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
<!-- 다른 앱 쿼리를 허용하는 설정 -->
<queries>
<!-- 간편결제 -->
<package android:name="finance.chai.app"/> <!-- 차이페이 -->
<package android:name="com.nhnent.payapp"/> <!-- 페이코 -->
<package android:name="com.lottemembers.android"/> <!-- LPAY -->
<package android:name="com.ssg.serviceapp.android.egiftcertificate"/> <!-- SSGPAY -->
<package android:name="com.inicis.kpay"/> <!-- KPAY -->
<package android:name="com.tmoney.tmpay"/> <!-- 티머니페이 -->
<package android:name="viva.republica.toss"/> <!-- 토스페이 -->
<package android:name="com.samsung.android.spay"/> <!-- 삼성페이 -->
<package android:name="com.kakao.talk"/> <!-- 카카오페이 -->
<package android:name="com.nhn.android.search"/> <!-- 네이버 -->
<package android:name="com.mysmilepay.app"/> <!-- 스마일페이 -->
<!-- 카드 -->
<package android:name="kvp.jjy.MispAndroid320"/> <!-- ISP페이북 -->
<package android:name="com.kbcard.cxh.appcard"/> <!-- KBPay -->
<package android:name="com.kbstar.liivbank"/> <!-- 리브 -->
<package android:name="com.kbstar.reboot"/> <!-- 리브 -->
<package android:name="com.samsung.android.spaylite"/> <!-- 삼성페이 -->
<package android:name="com.lge.lgpay"/> <!-- 엘지페이 -->
<package android:name="com.hanaskcard.paycla"/> <!-- 하나 -->
<package android:name="kr.co.hanamembers.hmscustomer"/> <!-- 하나멤버스 -->
<package android:name="com.hanaskcard.rocomo.potal"/> <!-- 하나공인인증 -->
<package android:name="com.citibank.cardapp"/> <!-- 씨티 -->
<package android:name="kr.co.citibank.citimobile"/> <!-- 씨티모바일 -->
<package android:name="com.lcacApp"/> <!-- 롯데 -->
<package android:name="kr.co.samsungcard.mpocket"/> <!-- 삼성 -->
<package android:name="com.shcard.smartpay"/> <!-- 신한 -->
<package android:name="com.shinhancard.smartshinhan"/> <!-- 신한(ARS/일반/smart) -->
<package android:name="com.hyundaicard.appcard"/> <!-- 현대 -->
<package android:name="nh.smart.nhallonepay"/> <!-- 농협 -->
<package android:name="com.wooricard.smartapp"/> <!-- 우리WON카드 -->
<package android:name="com.wooribank.smart.npib"/> <!-- 우리WON뱅킹 -->
<!-- 백신 -->
<package android:name="com.TouchEn.mVaccine.webs"/> <!-- TouchEn -->
<package android:name="com.ahnlab.v3mobileplus"/> <!-- V3 -->
<package android:name="kr.co.shiftworks.vguardweb"/> <!-- vguard -->
<!-- 신용카드 공인인증 -->
<package android:name="com.hanaskcard.rocomo.potal"/> <!-- 하나 -->
<package android:name="com.lumensoft.touchenappfree"/> <!-- 현대 -->
<!-- 계좌이체 -->
<package android:name="com.kftc.bankpay.android"/> <!-- 뱅크페이 -->
<package android:name="kr.co.kfcc.mobilebank"/> <!-- MG 새마을금고 -->
<package android:name="com.nh.cashcardapp"/> <!-- 뱅크페이 -->
<package android:name="com.knb.psb"/> <!-- BNK경남은행 -->
<package android:name="com.lguplus.paynow"/> <!-- 페이나우 -->
<package android:name="com.kbankwith.smartbank"/> <!-- 케이뱅크 -->
<!-- 해외결제 -->
<package android:name="com.eg.android.AlipayGphone"/> <!-- 페이나우 -->
<!-- 기타 -->
<package android:name="com.sktelecom.tauth"/> <!-- PASS -->
<package android:name="com.lguplus.smartotp"/> <!-- PASS -->
<package android:name="com.kt.ktauth"/> <!-- PASS -->
<package android:name="kr.danal.app.damoum"/> <!-- 다날 다모음 -->
<!-- 보안 -->
<package android:name="com.ahnlab.v3mobileplus"/>
<package android:name="com.TouchEn.mVaccine.webs"/>
<!-- 우리카드 -->
<package android:name="com.wooricard.wpay"/>
<package android:name="com.wooricard.smartapp"/>
<package android:name="com.wooribank.smart.npib"/>
<package android:name="com.mysmilepay.app"/>
<!-- 씨티카드 -->
<package android:name="kr.co.citibank.citimobile"/>
<package android:name="com.citibank.cardapp"/>
<!-- 신한카드 -->
<package android:name="com.shcard.smartpay"/>
<package android:name="com.shinhancard.smartshinhan"/>
<!-- ISP -->
<package android:name="kvp.jjy.MispAndroid320"/>
<!-- 현대카드 -->
<package android:name="com.hyundaicard.appcard"/>
<package android:name="com.samsung.android.spaylite"/>
<package android:name="com.ssg.serviceapp.android.egiftcertificate"/>
<!-- 삼성카드 -->
<package android:name="kr.co.samsungcard.mpocket"/>
<package android:name="com.nhnent.payapp"/>
<!-- 하나카드 -->
<package android:name="com.hanaskcard.paycla"/>
<package android:name="kr.co.hanamembers.hmscustomer"/>
<package android:name="com.kakao.talk"/>
<package android:name="com.hanaskcard.rocomo.potal"/>
<package android:name="com.ahnlab.v3mobileplus"/>
<package android:name="com.lge.lgpay"/>
<package android:name="viva.republica.toss"/>
<package android:name="com.samsung.android.spay"/>
<package android:name="com.nhnent.payapp"/>
<!-- 롯데카드 -->
<package android:name="com.lcacApp"/>
<package android:name="com.lotte.lpay"/>
<package android:name="com.lottemembers.android"/>
<!-- 농협카드 -->
<package android:name="nh.smart.nhallonepay"/>
<!-- 국민카드 -->
<package android:name="com.kbstar.reboot"/>
<package android:name="com.kbstar.kbbank"/>
<package android:name="com.kbcard.cxh.appcard"/>
<package android:name="com.nhnent.payapp"/>
<!-- 기타 보안 및 페이 관련 -->
<package android:name="net.nshc.droidx3web"/>
<package android:name="kr.shiftworks.vguardweb"/>
<package android:name="com.payprotocol.walletkr"/>
<package android:name="kr.danal.app.damoum"/>
<package android:name="uplus.membership"/>
<package android:name="com.bankpay.android"/>
<!--pass-->
<package android:name="com.sktelecom.tauth"/>
<package android:name="com.lguplus.smartotp"/>
<package android:name="com.kt.ktauth"/>
<!--kakao-->
<package android:name="net.daum.android.map"/>
<package android:name="com.locnall.KimGiSa"/>
</queries>
</manifest>
해당 포스팅의 AndroidManifest.xml 파일은 <queries> 속성 안에
간편 결제 및 PG사 결제 시스템 개발에 사용을위한 <package>들을 추가하여 주었다.
4. 하이브리드 App으로 Mobile Web 페이지의 URL 관리
안드로이드의 strings.xml 파일은 앱에서 사용되는 문자열 리소스를 관리하는 XML 파일이다.
이 파일은 주로 앱의 텍스트 콘텐츠를 정의하고, 코드에서 문자열을 직접 하드코딩하는 대신
Text 형태의 리소스 파일을 관리할 수 있도록 도와주는 역할을 한다.
string.xml 파일을 열고 아래와 같이 내용을 추가하여 준다.
strings.xml
<resources>
<string name="app_name">HybridApp</string>
<!-- 모바일 Web Page URL 지정 -->
<string name="prod_web_url" translatable="false">https://www.saakmiso.com/</string>
<string name="main_page" translatable="false">/main.do</string>
</resources>
호출할 Mobile Web의 URL 주소( https://www.saakmiso.com/ )와 패스 경로( /main.do ) 를 지정하였다.
5. 화면 Layout 구성 및 Activity 파일 생성
화면 레이아웃을 구성을 하기전에 프로젝트 생성과 동시에 구성된 MainActivity.java 파일과
레이아웃을 구성는 acitivy_main.xml 파일을 삭제하여 준다.
두개의 파일을 선택하고 마우스 우클릭 하여 [ Delete ] 하여 준다.
이후 res/layout 경로에 activity_web_view.xml 파일을 새로 생성하여 준다.
파일이 생성되었다면 아래 코드를 작성하여 준다.
activity_web_view.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.WebViewActivity">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/webview_frame">
<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent">
</WebView>
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
다음으로 WebView를 컨트롤할 activity 클래스를 생성하기에 앞서
패키지 경로에 activity 라는 패키지 경로를 하나 추가하여 준다.
New Package | com.example.hybrid.activity |
activity 패키지가 생성되면 해당 패키지 경로에 WebViewActivity.java 클래스를 추가하여 준다.
파일이 추가되었다면 아래와 같이 코드를 작성하여 준다.
WebViewActivity.java
package com.example.hybrid.activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.webkit.WebSettings;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.example.hybrid.R;
import com.example.hybrid.custom.CustomWebAppInterface;
import com.example.hybrid.custom.CustomWebChromeClient;
import com.example.hybrid.custom.CustomWebViewClient;
import com.example.hybrid.databinding.ActivityWebViewBinding;
import com.example.hybrid.utility.PermissionUtils;
public class WebViewActivity extends AppCompatActivity {
private ActivityWebViewBinding binding;
private static final String TAG = WebViewActivity.class.getSimpleName();
private boolean doubleBackToExitPressedOnce = false;
private Handler handler = new Handler();
private PermissionUtils permission;
private CustomWebViewClient customWebViewClient; // WebView가 로드하는 Web 콘텐츠와의 상호작용을 관리
private CustomWebChromeClient customWebChromeClient; // WebBrowser의 기능을 처리하는데 사용
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 뷰 바인딩 객체 획득
binding = ActivityWebViewBinding.inflate(getLayoutInflater());
// 액티비티 화면 출력
setContentView(binding.getRoot());
// CustomWebChromeClient 및 CustomWebViewClient 인스턴스 생성
customWebViewClient = new CustomWebViewClient(this);
customWebChromeClient = new CustomWebChromeClient(this, binding.webviewFrame);
// WebViewClient 설정
binding.webView.setWebViewClient(customWebViewClient);
// WebChromeClient 설정
binding.webView.setWebChromeClient(customWebChromeClient);
// WebSettings 설정
WebSettings webSettings = binding.webView.getSettings();
webSettings.setJavaScriptEnabled(true); // JavaScript 사용 설정( Default : FALSE )
webSettings.setJavaScriptCanOpenWindowsAutomatically(true); // JavaScript 새 창 열기 제어 설정( Default : FALSE )
webSettings.setBuiltInZoomControls(true); // 줌 아이콘 설정( Default : FALSE )
webSettings.setSupportZoom(true); // 확대 / 축소 기능 사용 설정( Default : FALSE )
webSettings.setAllowFileAccess(true); // WebView 내의 파일 접근 설정( Default : FALSE )
webSettings.setSupportMultipleWindows(true); // 여러 개의 윈도우를 사용할 수 있도록 설정( Default : FALSE )
webSettings.setDatabaseEnabled(true); // DataBase를 사용할 수 있도록 설정( Default : FALSE )
webSettings.setBlockNetworkImage(false); // NetWork의 이미지 리소스를 사용할 수 있도록 설정( Default : FALSE )
webSettings.setBlockNetworkLoads(false); // NetWork의 외부 리소스를 로드 할 수 있도록 설정( Default : FALSE )
webSettings.setLoadsImagesAutomatically(true); // WebView가 App에 등록되어 있는 이미지 리소를 자동으로 로드하도록 설정( Default : TRUE )
webSettings.setDomStorageEnabled(true); // DOM 저장소 사용 설정( Default : FALSE )
webSettings.setLoadWithOverviewMode(true); // Web Page의 내용이 WebView의 크기에 맞춰 축소되어 표시 되도록 설정( Default : FALSE )
webSettings.setUseWideViewPort(true); // Web Page가 설정한 viewport를 기준으로 레이아웃을 조정( Default : FALSE )
webSettings.setTextZoom(100); // 글자 크기를 고정(이 부분은 시스템 글자 크기에 영향받지 않게 설정)
// WebView가 Web Page를 로드할 때 네트워크 요청과 캐시를 어떻게 사용할지 결정
webSettings.setCacheMode(WebSettings.LOAD_DEFAULT); // `WebSettings.LOAD_DEFAULT` 네트워크가 사용 가능할 때는 네트워크를 사용
// webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE); // WebView가 캐시를 사용하지 않고매번 서버에 데이터 요청
// JavaScript 인터페이스 추가
binding.webView.addJavascriptInterface(new CustomWebAppInterface(this), "Android");
// 웹 페이지 로드
String url = getString(R.string.prod_web_url);
binding.webView.loadUrl(url);
// CustomWebChromeClient에서 사용할 WebViewClient와 WebChromeClient 설정
customWebChromeClient.setWebViewClient(customWebViewClient);
customWebChromeClient.setWebChromeClient(customWebChromeClient);
// 뒤로가기 버튼 콜백 처리
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
handleBackPress();
}
});
}
@Override
protected void onStart() {
/*
* onStart( )
* Activity가 사용자에게 보이기 직전에 호출되는 메서드 이 시점에 Activity는 화면에 표시되기 시작한다.
* Activity가 실행되면 카메라, 위치정보 수집, 전화등을 사용하기 위한 권한 허용 유무를 확인한다.
*/
super.onStart();
permissionCheck();
}
// 권한 체크
private void permissionCheck() {
// SDK 23버전 이하 버전에서는 권한이 필요하지 않음
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// 클래스 객체 생성
permission = new PermissionUtils(this, this);
// 권한 체크한 후에 리턴이 false일 경우 권한 요청을 해준다.
if(permission.checkPermission() == false) {
permission.requestPermission();
}
}
}
private void handleBackPress() {
/*
* handleBackPress( )
* 사용자 정의 메서드
* Web Site 메인 페이지에서 뒤로가기 버튼을 빠르게 두번 클릭한 경우 App을 종료시킨다.
*/
Uri now_url = Uri.parse(binding.webView.getUrl()); // 현재 URL을 반환
String scheme = now_url.getScheme(); // URL의 스킴을 반환
String host = now_url.getHost(); // URL의 호스트 부분 반환
String path = now_url.getPath(); // URL의 경로 부분 반환
String fullUrl = scheme + "://" + host;
boolean isMainPage = path.equals("/") || path.equals(getResources().getString(R.string.main_page));
boolean isAtMainPage = fullUrl.equals(getString(R.string.prod_web_url)) && isMainPage;
if(isAtMainPage == true || binding.webView.canGoBack() == false) {
if(doubleBackToExitPressedOnce) {
finish(); // 앱 종료
}
else {
doubleBackToExitPressedOnce = true;
Toast.makeText(WebViewActivity.this, "한 번 더 빠르게 누르시면 앱이 종료됩니다.", Toast.LENGTH_SHORT).show();
handler.postDelayed(() -> doubleBackToExitPressedOnce = false, 2000);
}
}
else {
binding.webView.goBack();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
/*
* onActivityResult( )
* Activity나 App에서 반환된 결과를 반환
*/
super.onActivityResult(requestCode, resultCode, data);
// CustomWebChromeClient에서 결과 처리 호출
customWebChromeClient.handleActivityResult(requestCode, resultCode, data);
}
@Override
protected void attachBaseContext(Context newBase) {
/*
* attachBaseContext()
* Activity 또는 ContextWrapper에서 「최초로 컨텍스트(Context)」를 설정하는 메서드
* Activity의 라이프사이클 중 초기화 단계에서 호출되며,
* Activity가 사용할 기본 컨텍스트를 설정하는 역할
*/
Configuration configuration = newBase.getResources().getConfiguration();
// 시스템의 글자 크기 변경을 무시하기 위한 컨텍스트 설정
configuration.fontScale = 1.0f; // 시스템 글자 크기 설정을 무시하고 1.0으로 고정
Context context = newBase.createConfigurationContext(configuration);
super.attachBaseContext(context);
}
// 글꼴 크기 설정이 변경되었을 때 호출
@Override
public void onConfigurationChanged(Configuration newConfig) {
/*
* onConfigurationChanged()
* App 실행 중에 기기 설정이 변경될 때 호출되는 메서드이다.
* Activity가 이미 실해 중인 상태에서 화면 회전, 언어 변경, 폰트 크기 변경 등의 기기 설정 변화가 발생 했을 때 호출된다.
*/
super.onConfigurationChanged(newConfig);
// 글자 크기 변경에 상관없이 WebView의 텍스트 크기를 100으로 고정
binding.webView.getSettings().setTextZoom(100);
}
}
6. 하이브리드 App 구현을 위한 WebView 지원 클래스 구현
하이브리드 App 구현을 위하여 WebAppInterface, WebChromeClient, WebViewClient 클래스 들을 필요로 한다.
이들은 모두 Android App 내에서 WebView를 사용하여 Web Content를 표시하고,
Web Page와 상호작용을 하는 기능을 제공하는 데 필수적인 역할을 한다.
각각의 클래스들은 WebView의 다양한 측면을 관리하기 위한 제어도구로 사용된다.
먼저 WebView 지원 클래스들을 담을 패키지 경로를 다시 추가하여 준다.
New Package | com.example.hybrid.custom |
패키지 구성이 완료되었다면 아래 순서대로 클래스 파일들을 추가하여 준다.
1) WebViewClient 클래스: Web Page 로딩과 네비게이션 제어
WebViewClient는 WebView 내에서 웹 페이지를 로드하고, 네비게이션 이벤트를 제어하는 클래스이다.
사용자가 클릭한 링크를 WebView 내에서 열리게 하거나,
특정 URL 로드 전후에 추가적인 작업을 수행하는 데 사용된다.
생성한 custom 패키지 안에 CustomWebViewClient.java 클래스 파일을 만들어 준다.
클래스 파일이 생성되었으면 아래 코드를 클래스 파일에 작성한다.
CustomWebViewClient.java
package com.example.hybrid.custom;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import android.webkit.WebResourceRequest;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.annotation.NonNull;
import java.net.URISyntaxException;
/*
* WebViewClient
* • Web Page의 로딩과 네비게이션 이벤트를 관리하고, WebView 내에서 링크 클릭, URL 로드등을 제어하는 클래스
* • 페이지 로드 이벤트 처리
* • 링크 클릭 제어
* • HTTP, HTTPS 인증 및 오류 처리
*/
public class CustomWebViewClient extends WebViewClient {
private static final String TAG = CustomWebViewClient.class.getSimpleName();
private final Context context;
public CustomWebViewClient(Context context) {
this.context = context;
}
@Override
public boolean shouldOverrideUrlLoading(@NonNull WebView view, @NonNull WebResourceRequest request) {
String url = request.getUrl().toString();
Log.d(TAG, url);
if("intent".equals(request.getUrl().getScheme())) {
try {
Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
if(intent.resolveActivity(context.getPackageManager()) != null) {
context.startActivity(intent);
Log.d(TAG, "ACTIVITY : " + intent.getPackage());
return true;
}
else {
String packageName = intent.getPackage();
if (packageName != null) {
Intent playStoreIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + packageName));
if(playStoreIntent.resolveActivity(context.getPackageManager()) != null) {
context.startActivity(playStoreIntent);
Log.d(TAG, "Play 스토어로 재전송 중 : " + packageName);
}
else {
playStoreIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + packageName));
context.startActivity(playStoreIntent);
Log.d(TAG, "Play 스토어 Web Site로 재전송 중 : " + packageName);
}
}
return true;
}
} catch (URISyntaxException e) {
Log.e(TAG, "잘못된 인텐트 요청", e);
}
}
return false; // 나머지 URL 로딩 처리
}
}
2) WebChromeClient 클래스 : Web Browser의 경험 확장
WebChromeClient는 Web Page의 Browser 관련 기능을 제어하는 클래스이다.
Web Page의 타이틀 변경, 로딩 프로그레스 업데이트, JavaScript 대화상자( alert, confirm, prompt ) 처리 등을 담당한다.
사용자가 Web Page를 탐색할 때 Browser와 유사한 경험을 제공해 주거나
혹은, Browser가 아닌 App이기에 지원되어야 할 기능들을 제공하는 클래스이다.
custom 패키지 내에 CustomWebChromeClient.java 클래스 파일을 생성한다.
클래스 파일이 추가 되면 아래 코드를 작성하여 준다.
CustomWebChromeClient.java
package com.example.hybrid.custom;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Environment;
import android.os.Message;
import android.provider.MediaStore;
import android.util.Log;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.FrameLayout;
import androidx.core.content.FileProvider;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
/*
* WebChromeClient
* • Web Page의 Browser 관련 이벤트를 처리하는 클래스
* • ( 타이틀 변경, 프로그레스 업데이트, JavaScript 대화 사장 등 )
*/
public class CustomWebChromeClient extends WebChromeClient {
private static final String TAG = CustomWebChromeClient.class.getSimpleName();
private static final int FILE_CHOOSER_REQUEST_CODE = 1; // 파일 요청 코드
private static final int CAMERA_REQUEST_CODE = 2; // 카메라 요청 코드
private WebViewClient customWebViewClient;
private WebChromeClient customWebChromeClient;
private ValueCallback<Uri[]> mFilePathCallback;
private Context mContext;
private FrameLayout webViewLayout;
private Uri mCameraImageUri;
public CustomWebChromeClient(Context context, FrameLayout webViewLayout) {
this.mContext = context;
this.webViewLayout = webViewLayout;
}
public void setWebViewClient(CustomWebViewClient customWebViewClient) {
this.customWebViewClient = customWebViewClient;
}
public void setWebChromeClient(CustomWebChromeClient customWebChromeClient) {
this.customWebChromeClient = customWebChromeClient;
}
@Override
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
/*
* Web Page에서 위치 정보 접근 권한을 요청할 때 호출되는 메서드
* origin: 권한을 요청한 웹 페이지의 URL
* callback: 사용자가 권한을 허용하거나 거부할 수 있도록 웹뷰에 결과를 전달하는 콜백
*/
// 위치 정보 접근 권한을 자동으로 허용함
callback.invoke(origin, true, false);
}
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
/*
* onShowFileChooser( )
* Web Browser 내에서 파일 선택 대화 상자를 표시하는 데 사용되는 메서드
* 파일 선택 기능을 커스터 마이징 하거나 제어하는 경우 사용
*/
// 기존 콜백을 초기화
if (mFilePathCallback != null) {
mFilePathCallback.onReceiveValue(null);
}
mFilePathCallback = filePathCallback;
// 파일 선택기 및 카메라 인텐트를 준비
Intent[] intentArray = prepareFileAndCameraIntents();
// 인텐트 선택기를 사용하여 파일 선택 및 카메라 촬영을 위한 선택 UI를 표시
Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER);
chooserIntent.putExtra(Intent.EXTRA_INTENT, intentArray[0]); // 파일 선택 인텐트
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[]{intentArray[1]}); // 카메라 인텐트
if (mContext instanceof Activity) {
// 요청 코드 FILE_CHOOSER_REQUEST_CODE를 사용하여 인텐트를 시작합니다.
((Activity) mContext).startActivityForResult(chooserIntent, FILE_CHOOSER_REQUEST_CODE);
}
return true;
}
private Intent[] prepareFileAndCameraIntents() {
/*
* prepareFileAndCameraIntents( )
* 파일 선택 및 카메라 촬영을 위한 Intent를 준비하는데 사용
* 파일 선택 대화상자에서 파일선택 / 사진 촬영 옵션을 제공 하는데 주로 사용됨
*/
Log.d(TAG, "파일과 카메라 인텐트를 준비하는 중");
// 파일 선택 인텐트
Intent fileIntent = createFileIntent();
Log.d(TAG, "파일 인텐트가 생성되었습니다: " + fileIntent.toUri(Intent.URI_INTENT_SCHEME));
// 카메라 인텐트
Intent cameraIntent = createCameraIntent();
Log.d(TAG, "카메라 인텐트가 생성되었습니다: " + cameraIntent.toUri(Intent.URI_INTENT_SCHEME));
return new Intent[]{fileIntent, cameraIntent};
}
public void handleActivityResult(int requestCode, int resultCode, Intent data) {
/*
* handleActivityResult( )
* 사용자가 파일 선택 대화 상자에서 파일을 선택하거나 사진을 찍은 후, 그 결과를 처리하는 역할을 수행
* 주로 'onActivityResult( )' 메서드 내에서 호출되며, 파일이나 이미지 URI를 WebView 또는 다른 구성 요소에 전달하는 데 사용된다.
*/
Log.d(TAG, "handleActivityResult 호출됨. 요청 코드: " + requestCode + ", 결과 코드: " + resultCode);
Uri[] results = null;
// 디바이스의 이미지 파일 선택 업로드인 경우
if (requestCode == FILE_CHOOSER_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
if (data == null || data.getData() == null) {
results = new Uri[]{mCameraImageUri};
} else {
results = new Uri[]{data.getData()};
}
}
}
// 카메라 사진 촬영 이미지 업로드인 경우
else if (requestCode == CAMERA_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
results = new Uri[]{mCameraImageUri};
} else {
mCameraImageUri = null;
}
}
if(mFilePathCallback != null) {
mFilePathCallback.onReceiveValue(results);
mFilePathCallback = null;
}
}
private Intent createFileIntent() {
Intent fileIntent = new Intent(Intent.ACTION_GET_CONTENT);
fileIntent.addCategory(Intent.CATEGORY_OPENABLE);
fileIntent.setType("image/*");
return fileIntent;
}
private Intent createCameraIntent() {
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// 인텐트가 처리 가능한지 확인
if (cameraIntent.resolveActivity(mContext.getPackageManager()) != null) {
File photoFile = null;
// 파일 생성
try {
photoFile = createImageFile();
if (photoFile != null) {
Uri photoURI = FileProvider.getUriForFile(
mContext
, "com.example.webview.fileprovider"
, photoFile
);
mCameraImageUri = photoURI;
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
cameraIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Log.d(TAG, "URI가 포함된 카메라 인텐트: " + photoURI.toString());
}
} catch (IOException ex) {
Log.e(TAG, "이미지 파일 생성 오류: ", ex);
}
}
return cameraIntent;
}
private File createImageFile() throws IOException {
// 이미지 파일 생성
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String imageFileName = "JPEG_" + timeStamp + "_";
File storageDir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File imageFile = File.createTempFile(
imageFileName // 파일 이름의 접두사( prefix )
, ".jpg" // 파일 이름의 접미사( suffix )
, storageDir // directory
);
Log.d(TAG, "이미지 파일이 생성되었습니다: " + imageFile.getAbsolutePath());
return imageFile;
}
@Override
public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
/*
* onCreateWindow( )
* Web Page에 새로운 창이 오픈될 때 호출
* KakaoTalk 간편 로그인 구현을 위해 필요
*/
WebView childWebView = new WebView(view.getContext());
// JavaScript 사용을 허용하여 웹 페이지에서 JavaScript를 실행할 수 있도록 설정
childWebView.getSettings().setJavaScriptEnabled(true);
// JavaScript가 자동으로 새 창을 열 수 있도록 허용 (window.open 호출 등을 지원)
childWebView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);
// 여러 개의 창(탭)을 지원하도록 설정하여 WebView 내에서 팝업 창을 열 수 있도록 지원
childWebView.getSettings().setSupportMultipleWindows(true);
// 위치 정보 사용을 허용하여 WebView에서 위치 기반 기능(GPS 등)을 사용할 수 있도록 설정
childWebView.getSettings().setGeolocationEnabled(true);
// 위치 정보 사용을 허용하여 WebView에서 위치 기반 기능(GPS 등)을 사용할 수 있도록 설정
childWebView.getSettings().setDomStorageEnabled(true);
// Web Page의 글씨 크기를 디바이스에 맞게 조정
childWebView.getSettings().setTextZoom(100);
childWebView.setLayoutParams(view.getLayoutParams());
childWebView.setWebViewClient(customWebViewClient);
childWebView.setWebChromeClient(customWebChromeClient);
// 생성된 childWebView를 기존 레이아웃(webViewLayout)에 추가하여 사용자에게 표시될 수 있도록 설정
webViewLayout.addView(childWebView);
WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
transport.setWebView(childWebView);
resultMsg.sendToTarget();
return true;
}
@Override
public void onCloseWindow(WebView window) {
/*
* onCreateWindow( )
* Web Page에 새로운 창이 오픈될 때 호출
* KakaoTalk 간편 로그인 구현을 위해 필요
*/
super.onCloseWindow(window);
webViewLayout.removeView(window);
}
}
3) WebAppInterface 클래스 : Native Code와 JavaScript의 상호작용 지원
WebAppInterface는 Android 네이티브 코드와 Web Page의 JavaScript 간의 상호작용을 가능하게 해주는 클래스이다.
이 클래스는 JavaScript에서 Android의 네이티브 기능을 호출할 수 있는 인터페이스를 제공한다.
사용자 지정 메서드를 정의하여 WebAppInterface 클래스의 메서드를 호출 할 수 있다.
custom 패키지 내부에 CustomWebAppInterface.java 클래스 파일을 생성하여준다.
클래스 파일이 생성되었다면 아래와 같이 코드를 작성하여 준다.
CustomWebAppInterface.java
package com.example.hybrid.custom;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.webkit.JavascriptInterface;
/*
* WebAppInterface
* • JavaScript와 Android 네이티브 코드 간의 상호작용을 가능하게 하는 클래스.
*/
public class CustomWebAppInterface {
Context mContext;
// 생성자
public CustomWebAppInterface(Context context) {
this.mContext = context;
}
// JavaScript에서 호출할 사용자 지정 메소드
@JavascriptInterface
public void openBrowser(String url) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
mContext.startActivity(intent);
}
}
위에서 지정한 openBrowser( ) 메서드를 호출하기 위하 JavaScript 샘플 코드는 아래와 같다.
window_android.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>:: 테스트 ::</title>
</head>
<body>
<button type="button" id="btnWindowAndroid">안드로이드 버튼</button>
</body>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function() {
document.querySelector("#btnWindowAndroid").addEventListener("click", function () {
openExternalUrl();
});
});
function openExternalUrl() {
let url = "";
if(window.Android) {
url = "https://www.saakmiso.com/";
// Android 네이티브 코드의 openBrowser( ) 메서드 호출
window.Android.openBrowser(url);
}
else {
url = "https://saakmiso.tistory.com/";
// Android가 아닌 경우, 단순히 href로 처리
window.location.href = url;
}
}
</script>
</html>
제작한 Android App의 Web View를 통해 위 HTML 파일의 Web Page를 호출하고
버튼을 클릭하면 Android App에서의 결과와 Web Browser에서의 결과가 다른 것을 확인 할 수 있다.
7. 권한 관리 Utility 클래스 구현
마지막으로 하이브리드 App에서 카메라, 위치정보 수집, 전화 기능 사용하기 위해
App이 실행되면 권한 사용유무를 확인하는 기능을 구현해 준다.
먼저 하이브리드 App 프로젝트에 utility 패키지를 생성하여 준다.
New Package | com.example.hybrid.utility |
utility 패키지가 생성되면 PermissionUtils.java 파일을 추가하여 준다.
PermissionUtils.java 클래스에 사용자 권한을 요청할 수 있도록 아래 코드를 작성해 준다.
PermissionUtils.java
package com.example.hybrid.utility;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.util.ArrayList;
import java.util.List;
public class PermissionUtils {
private final Context context;
private final Activity activity;
// Manifest에 권한을 작성 후 요청할 권한을 배열로 저장
private String[] permissions = {
Manifest.permission.CAMERA // 카메라 권한
, Manifest.permission.CALL_PHONE // 전화 권한
, Manifest.permission.ACCESS_FINE_LOCATION // 위치 권한
, Manifest.permission.ACCESS_COARSE_LOCATION // 위치 권한 (단기 위치)
};
private List<String> permissionList;
private final int MULTIPLE_PERMISSIONS = 1023;
public PermissionUtils(Activity activity, Context context) {
this.activity = activity;
this.context = context;
}
// 허용할 권한 요청이 남았는지 체크
public boolean checkPermission() {
int result;
permissionList = new ArrayList<>();
// 배열로 저장한 권한 중 허용되지 않은 권한이 있는지 체크
for(String permission : permissions) {
result = ContextCompat.checkSelfPermission(context, permission);
if(result != PackageManager.PERMISSION_GRANTED) {
permissionList.add(permission);
}
}
return permissionList.isEmpty();
}
// 권한 허용 요청
public void requestPermission() {
ActivityCompat.requestPermissions(activity, permissionList.toArray(new String[0]), MULTIPLE_PERMISSIONS);
}
// 권한 요청에 대한 결과 처리
public boolean permissionResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if(requestCode == MULTIPLE_PERMISSIONS && grantResults.length > 0) {
for(int num = 0; num < grantResults.length; num++) {
// grantResults == PackageManager.PERMISSION_GRANTED 사용자가 허용한 것
// grantResults == PackageManager.PERMISSION_DENIED 사용자가 거부한 것
if(grantResults[num] == PackageManager.PERMISSION_DENIED) {
return false;
}
}
}
return true;
}
}
8. App 실행 결과 - 이미지 생성중
해당 포스팅은 Hybrid App을 개발했던 내용을 정리하기 위해 작성한 문서로
[Android] App 실행시 Intro 화면 제작
[Android] 권한 관리 Utility 클래스 구현 가이드(카메라, 전화, 위치)
위 포스팅 내용을 조합하여 Hybrid App을 출시하였다.
'Android > Java Code' 카테고리의 다른 글
[Android] 사용자 권한 사용유무 체크(카메라, 전화, 위치) (0) | 2024.07.29 |
---|---|
[Android] App 실행시 Intro 화면 제작 (0) | 2024.07.29 |
[Android] Retrofit2를 사용한 API 통신 설정 및 Data 송수신 (0) | 2023.02.07 |
[Android] Keyboard위에 Edit Text 올리기 (0) | 2023.02.06 |
[Android] 출력 위치를 확인하는 Custom Log Message 제작 (0) | 2023.02.02 |