RelativeLayout을 상속받은 IWebview 클래스에 WebView 생성해서 사용하는 방식입니다.
웹에서 OpenWindow 호출로 새창 호출시 자동으로 새창이 생성되는 기능과 App <-> Web Javascript 연동 하는 Brige 클래스도
포함합니다.
[지원기능]
웹 링크 다운로드, blob 다운로드, data base64 다운로드 지원
App <-> Web Javascipt 연동 지원
새창 열기 지원
웹에 파일 첨부 지원
[필요권한]
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
WebViewBridge.java
public class WebViewBridge {
private final WeakReference<Context> contextWeakReference;
private final WeakReference<IWebview> iWebviewWeakReference;
public WebViewBridge(Context context, IWebview iWebView) {
this.contextWeakReference = new WeakReference<Context>(context);
this.iWebviewWeakReference = new WeakReference<IWebview>(iWebView);
}
/** Todo : Activity Contect 객체
* 사용방법 : Activity activity = (Activity)getContext();
*/
private Context getContext() {
return this.contextWeakReference.get();
}
/** Todo : IWebview 커스텀 클래스 객체
* 사용방법 : IWebview iWebview = getIWebview();
* IWebview 의 evaluateJavascript 연동시 사용
*/
private IWebview getIWebview() {
return this.iWebviewWeakReference.get();
}
/** 예제1
* Todo : 앱 종료
* */
@JavascriptInterface
public void Finish() {
Log.d("[ Finsh","앱종료 호출");
getContext().fileList();
android.os.Process.killProcess(android.os.Process.myPid());
}
/** 예제2
* Todo : 외부 브라우저 실행
* */
@JavascriptInterface
public void externalBrowser(final String message) {
Log.d("[KFCC externalBrowser",message);
try {
JSONObject jsonObject = new JSONObject(message);
String uriString = jsonObject.getString("target_url");
CommonUtil.externalOpen(getContext(), uriString);
} catch (JSONException e) {
Log.e("[ externalBrowser Exception", e.getMessage());
}
}
/**
* Todo : blob 다운로드 (downloadFileFromBlob)
* 공유앱(파일앱) 경로 선택 후 해당 경로에 다운로드
* webViewDownloadListener blob -> IWebview downloadFileFromBlob javascript(blob -> data) -> WebViewBridge downloadFileFromBlob WebViewBridge
*/
@JavascriptInterface
public void downloadFileFromBlob(String base64Data, String mimeType) throws IOException {
DLog.i(new Exception(), base64Data);
// getIWebview().convertBase64StringToFileAndStoreIt(base64Data, mimeType, null);
IWebViewAdapter callbackAdapter = new IWebViewAdapter() {
@Override
public void onBlobDownloadCallback(Uri uri) {
super.onBlobDownloadCallback(uri);
IWebview iWebView = getIWebview();
iWebView.convertBase64StringToFileAndStoreIt(uri, base64Data, "fileName");
}
};
((MainActivity) getContext()).setiWebViewAdapter(callbackAdapter);
// 파일앱(공유파일앱) 연계해서 다운로드 디렉토리 설정 후 다운로드
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("application/pdf");
intent.putExtra(Intent.EXTRA_TITLE, "fileName");
((MainActivity) getContext()).getmBlobDownloadResultLanucher().launch(intent);
}
/**
* Todo : PDF 다운로드 연계 (blob)
* 공유앱(파일앱) 경로 선택 후 해당 경로에 다운로드
* (web -> WebViewBridge)
*
* @param filename
* @param base64
* @throws IOException
*/
@JavascriptInterface
public void blob(final String filename, final String base64) throws IOException {
DLog.i(new Exception(), base64);
IWebViewAdapter callbackAdapter = new IWebViewAdapter() {
@Override
public void onBlobDownloadCallback(Uri uri) {
super.onBlobDownloadCallback(uri);
IWebview iWebView = getIWebview();
iWebView.convertBase64StringToFileAndStoreIt(uri, base64, filename);
}
};
((MainActivity) getContext()).setiWebViewAdapter(callbackAdapter);
// 파일앱(공유파일앱) 연계해서 다운로드 디렉토리 설정 후 다운로드
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("application/pdf");
intent.putExtra(Intent.EXTRA_TITLE, filename);
((MainActivity) getContext()).getmBlobDownloadResultLanucher().launch(intent);
}
}
IWebview.java
public class IWebview extends RelativeLayout {
private ArrayList<WebView> webViewList;
private int tag = 1000;
private CookieManager cookieManager;
public WebViewBridge webViewBridge;
private CustomDialog mCustomDialog;
public IWebview(Context context) {
super(context);
webViewList = new ArrayList<WebView>();
webviewCreate(new WebView(context));
}
public IWebview(Context context, AttributeSet attrs) {
super(context, attrs);
webViewList = new ArrayList<WebView>();
webviewCreate(new WebView(context));
}
public IWebview(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
webViewList = new ArrayList<WebView>();
webviewCreate(new WebView(context));
}
public IWebview(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
/**
* 웹뷰 페이지 로드
* @param url
*/
public void setLoadUrl(String url) {
WebView webView = getWebview(getWebviewListIndex());
webView.loadUrl(url);
}
/**
* 웹뷰 페이지 로드 (헤더추가)
* @param url
* @param header
* Map<String, String> header = new HashMap<>();
* header.put("uuid", devID);
* header.put("token", token);
* webView.loadUrl(url,header);
*/
public void setLoadUrl(String url, Map<String, String> header) {
WebView webView = getWebview(getWebviewListIndex());
webView.loadUrl(url,header);
}
/**
* Post 데이터 전송
* @param url
* @param params params = "username=" + URLEncoder.encode(myname, "UTF-8")
* (get 파라메터부를 URL (BASE64) 인코딩
*/
public void setPostLoadUrl(String url, @NonNull String params) {
WebView webView = getWebview(getWebviewListIndex());
webView.postUrl(url, params.getBytes());
}
public WebView getWebview(int index) {
return webViewList.get(index);
}
public int getWebviewListIndex() {
return webViewList.size() - 1;
}
@SuppressLint({"ResourceType", "JavascriptInterface"})
private void webviewCreate(WebView webview) {
webViewList.add(webview);
webview.setId(tag++);
webview.setScrollContainer(false);
// 팬딩 추가 없이 가장자리에 배치, 내용물 위에 투명하게 올라감.
webview.setScrollBarStyle(WebView.SCROLLBARS_OUTSIDE_OVERLAY);
webview.setVerticalScrollBarEnabled(true);
webview.setNestedScrollingEnabled(true);
webview.requestFocus();
webview.clearCache(false);
webview.setWebContentsDebuggingEnabled(true);
webViewBridge = new WebViewBridge(getContext(), this);
webview.addJavascriptInterface(webViewBridge, "android");
//WebSettings settings = webview.getSettings();
// 자바스크립트 사용여부
webview.getSettings().setJavaScriptEnabled(true);
// 새창 띄우기 허용여부
webview.getSettings().setSupportMultipleWindows(true); // true
// 자바스크립트가 window.open()을 사용할 수 있도록 설정
webview.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);
// 캐시 사용 방식
webview.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);
// ERR_CACHE_MISS 오류관련 설정 (보안적인 문제 발생할 수 있음 주의) PG 결제시 문제 발생 할 수도 있음.
// settings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
// html의 컨텐츠가 웹뷰보다 클 경우 스크린 크기에 맞게 조정
webview.getSettings().setLoadWithOverviewMode(true);
// 화면 사이즈 맞추기 허용여부
webview.getSettings().setUseWideViewPort(true);
// DOM(html 인식) 저장소 허용여부
webview.getSettings().setDomStorageEnabled(true);
//database storage API 사용여부
webview.getSettings().setDatabaseEnabled(true);
// https 및 http 콘텐츠가 섞여 있을 경우 사용
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
webview.getSettings().setTextZoom(100);
// 파일 허용
webview.getSettings().setAllowFileAccess(true);
webview.getSettings().setAllowContentAccess(true);
webview.getSettings().setLoadsImagesAutomatically(true);
/**
* Todo: User Agent
*/
String defaultAgent = webview.getSettings().getUserAgentString();
if (!defaultAgent.contains("AOS")) {
settings.setUserAgentString(defaultAgent + "/AOS");
}
Log.i("[ IWebView(Agent String) : " + webview.getId() , webview.getSettings().getUserAgentString());
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
webview.setLayoutParams(params);
addView(webview);
webViewClientSetting(webview);
webChomeClientSetting(webview);
webViewDownloadListener(webview);
/**
* Todo: 세션 유지를 위한 세션 메니져
*/
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
CookieSyncManager.createInstance(getContext());
}
setCookieAllow();
webview.clearCache(true);
webview.clearHistory();
}
/**
* Todo : 추가된 새창 삭제 로직
* @param index 삭제할 페이지 index
*/
public void removeWebview(int index) {
removeViewAt(index);
webViewList.remove(index);
tag--;
}
// 출처 : https://blog.naver.com/jogilsang/221641656097
private void setCookieAllow() {
try {
cookieManager = CookieManager.getInstance();
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
WebView webview = webViewList.get(getWebviewListIndex());
cookieManager.setAcceptCookie(true);
cookieManager.setAcceptThirdPartyCookies(webview, true);
} else {
// Lollipop 이하 버전
CookieSyncManager.createInstance(getContext());
}
} catch (Exception e) {
Log.e("[ setCookieAllow Exception", e.getMessage());
}
}
public void cookieSyncManagerStartSync() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
CookieSyncManager.getInstance().startSync();
} else {
CookieManager.getInstance().flush();
}
}
public void cookieSyncManagerStopSync() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
CookieSyncManager.getInstance().stopSync();
} else {
CookieManager.getInstance().flush();
}
}
/**
* Todo : 쿠키 확인
*/
private boolean hasCookies() {
return cookieManager.hasCookies();
}
/**
* Todo : 쿠키 불러오기
*/
private String getCookie(String key) {
return cookieManager.getCookie(key);
}
/**
* Todo : 쿠키 설정
*/
private void setCookie(String key, String value) {
cookieManager.setCookie(key, value);
}
/**
* Todo : 쿠키 삭제
*/
private void crearCookies() {
cookieManager.removeAllCookies(null);
cookieManager.flush();
}
/**
* Todo : 자바스크립트 호출 (APP -> Web)
* webview.evaluateJavascript("javascript:window.handleBackButton()", new ValueCallback<String>() {
* @Override
* public void onReceiveValue(String s) {
*
* }
* });
*
* @param script
* @param resultCallback
*/
public void evaluateJavascript(String script, ValueCallback<String> resultCallback) {
WebView webview = webViewList.get(getWebviewListIndex());
//javascript:NativeAppInterface."+script(함수명) 이부분은 적절하게 변경해서 사용해주세요
webview.evaluateJavascript("javascript:NativeAppInterface."+script, resultCallback);
}
/** Todo: WebViewChomeClient ======================================
*
*/
private void webChomeClientSetting(@NonNull WebView webview) {
webview.setWebChromeClient(new WebChromeClient(){
@Override
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
// Log.w("onConsoleMessage" + "(" + consoleMessage.messageLevel() + ")", consoleMessage.message());
return super.onConsoleMessage(consoleMessage);
}
/**
* 웹에서 windowOpen 사용시 새창 오픈시 사용
* 웹뷰에서 새창이 로딩될 때 호출되며 앱에서 제어할 수 있다. 반환 default는 false이며 로딩 제어시 true를 반환해주어야 한다.
* @param view
* @param isDialog
* @param isUserGesture
* @param resultMsg
* @return
*/
@Override
public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
Log.i("[ IWebView(onCreateWindow) : " + view.getId() , resultMsg.toString());
WebView newWebView = new WebView(getContext());
webviewCreate(newWebView);
WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
transport.setWebView(newWebView);
resultMsg.sendToTarget();
return true;
}
/**
* 웹뷰가 창을 닫는 시점에 호출된다.
* @param window
*/
@Override
public void onCloseWindow(WebView window) {
super.onCloseWindow(window);
Log.i("[ IWebView(onCloseWindow) : " + window.getId(), window.getUrl());
WebView webview = getWebview(getWebviewListIndex());
webview.loadUrl("javascript:self.close();"); //??????
removeWebview(getWebviewListIndex());
}
/**
* 웹페이지 파일 첨부. 반환값을 true설정하고 인텐트를 통해 데이터를 전송한다.
* 참고자료 : https://42kchoi.tistory.com/384
* @param webView
* @param filePathCallback
* @param fileChooserParams
* @return
*/
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
Log.d("[ IWebView(onShowFileChooser) : " + webView.getId(), fileChooserParams.getFilenameHint() + " / " + fileChooserParams.getTitle());
for (int i = 0; i < fileChooserParams.getAcceptTypes().length; i++) {
Log.d("[ IWebView(onShowFileChooser)", fileChooserParams.getAcceptTypes()[i]);
}
MainActivity activity = (MainActivity)getContext();
ValueCallback<Uri[]> callback = activity.getmFilePathCallback();
if (callback != null) {
callback.onReceiveValue(null);
callback = null;
}
activity.setmFilePathCallback(filePathCallback);
Intent i = new Intent(Intent.ACTION_GET_CONTENT);
i.addCategory(Intent.CATEGORY_OPENABLE);
// 여러장의 사진을 선택하는 경우
i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
i.setType("*/*");
// i.setType("image/*");
Intent intent = Intent.createChooser(i, "File Chooser");
activity.getmFileForResult().launch(intent);
return true;
}
/**
* 웹뷰에서 권한을 사용 시 호출되며 권한 사용을 수락할 수 있다.
* @param request
*/
@Override
public void onPermissionRequest(PermissionRequest request) {
super.onPermissionRequest(request);
}
/**
* 사용자가 웹뷰에서 요청한 권한을 취소했을 경우
* @param request
*/
@Override
public void onPermissionRequestCanceled(PermissionRequest request) {
super.onPermissionRequestCanceled(request);
}
/**
* 플레이어 화면을 로딩할 때 디폴트 포스터가 노출된다. 반환 값을 수정해서 포스터를 없앨 수 있다.
* @return
*/
@Nullable
@Override
public Bitmap getDefaultVideoPoster() {
return super.getDefaultVideoPoster();
}
});
}
/** Todo: WebViewClient ======================================
*
*/
private void webViewClientSetting(@NonNull WebView webview) {
webview.setWebViewClient(new WebViewClient() {
// Todo : (선택) API 수준 24 미만을 타겟팅 하려면 다음 코드를 추가해 주세요
@SuppressWarnings("deprecation")
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
// if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
// return handleUrl(url);
// }
return super.shouldOverrideUrlLoading(view, url);
}
// Todo: API 수준 24 이상 에서만 동작합니다.
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
String url = request.getUrl().toString();
Log.i("[ IWebView(shouldOverrideUrlLoading) : " + view.getId(), url);
Intent intent;
Context context = view.getContext();
String scheme = request.getUrl().getScheme();
switch (scheme) {
case "tel":
intent = new Intent(Intent.ACTION_DIAL, Uri.parse(url));
context.startActivity(intent);
return true;
case "mailto":
intent = new Intent(Intent.ACTION_SENDTO, Uri.parse(url));
context.startActivity(intent);
return true;
case "sms":
intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
context.startActivity(intent);
return true;
case "intent":
try {
intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
Intent existPackage = getContext().getPackageManager().getLaunchIntentForPackage(intent.getPackage());
if (existPackage != null) {
getContext().startActivity(intent);
} else {
Intent marketIntent = new Intent(Intent.ACTION_VIEW);
marketIntent.setData(Uri.parse("market://details?id=" + intent.getPackage()));
getContext().startActivity(marketIntent);
}
} catch (URISyntaxException e) {
Log.e("[ URISyntaxException", e.getMessage());
}
return true;
}
// Todo : 스토어 연결 URL일 경우 스토어로 이동
if (url.startsWith("https://play.google.com/store/apps/details?id=") || url.startsWith("market://details?id=")) {
Uri uri = Uri.parse(url);
String packageName = uri.getQueryParameter("id");
if (packageName != null && !packageName.equals("")) {
getContext().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + packageName)));
}
return true;
}
return false;
// return super.shouldOverrideUrlLoading(view, request);
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
Log.i("[ IWebView(onPageStarted) : " + view.getId(), url);
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
Log.i("[ IWebView(onPageFinished) : " + view.getId(), url);
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
CookieSyncManager.getInstance().sync();
} else {
CookieManager.getInstance().flush();
}
}
@Override
public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
super.onReceivedHttpError(view, request, errorResponse);
Log.e("[ IWebView(onReceivedHttpError) : " + view.getId(), errorResponse.toString());
}
/** 출처 : https://velog.io/@pachuho/Android-WebView-%EC%95%8C%EA%B3%A0-%EC%93%B0%EA%B8%B0
* http 접속이나 https 인증서 관련 오류로 접속 불가시 사용
* @param view
* @param handler
* @param error
*/
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
String message = "SSL Certificate error";
switch (error.getPrimaryError()) {
case SslError.SSL_UNTRUSTED:
message = "신뢰할 수 없는 사이트입니다.";
break;
case SslError.SSL_EXPIRED:
message = "만료된 사이트입니다.";
break;
case SslError.SSL_IDMISMATCH:
message = "도메인이 없습니다.";
break;
case SslError.SSL_NOTYETVALID:
message = "검증되지 않은 사이트입니다.";
break;
default:
message = "이 사이트의 보안 인증서는 신뢰하는 보안 인증서가 아닙니다.";
break;
}
Log.e("[ SSL Error : " + view.getId(), message);
if(handler != null) {
// handler.proceed();
View.OnClickListener confirmListener = new View.OnClickListener() {
public void onClick(View v) {
mCustomDialog.dismiss();
handler.proceed();
}
};
View.OnClickListener exitListener = new View.OnClickListener() {
public void onClick(View v) {
mCustomDialog.dismiss();
handler.cancel();
// android.os.Process.killProcess(android.os.Process.myPid());
// setLoadUrl(App.getInstance().getMain_url());
}
};
mCustomDialog = new CustomDialog(getContext(),
"안내", // 제목
message + "\r\n페이지로 이동 하시겠습니까?", // 내용
"취소",
"이동하기",
exitListener, // 왼쪽 버튼 이벤트
confirmListener); // 오른쪽 버튼 이벤트
mCustomDialog.show();
} else {
super.onReceivedSslError(view, handler, error);
}
// handler.cancel();
}
/**
* request 에 대해 에러가 발생했을 때 호출되는 콜백 메소드. error 변수에 에러에 대한 정보가 담겨있음
* @param view
* @param request
* @param error
*/
@Override
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
super.onReceivedError(view, request, error);
Log.e("[ IWebView(onReceivedError) : " + view.getId(), error.getDescription().toString());
// onReceivedError 호출은 페이지의 모든 리소스 로드시 오류발생하면 발생한다. isForMainFrame()사용해서 메인프레임 오류일 때만 에러 표시되게 분기해야됨.
// 메인 페이지 요청에 대해서만 오류 처리를 수행합니다.
if(request.isForMainFrame()) {
// 에러 UI 표시
}
}
/**
* 파라미터로 넘어온 url 에 의해 특정 리소스를 load 할 때 호출되는 콜백 메소드
* @param view
* @param url
*/
@Override
public void onLoadResource(WebView view, String url) {
super.onLoadResource(view, url);
// Log.i("IWebView(onLoadResource) : " + view.getId(), url);
}
});
}
/** Todo : Blob -> Data 타입 변환 Javascript
* 참고사이트 : https://stackoverflow.com/questions/72181133/how-to-download-blob-url-in-webview
* @param url
* @param userAgent
* @param contentDisposition
* @param mimeType
* @param contentLength
*/
private void downloadFileFromBlob(String url, String userAgent, String contentDisposition, String mimeType, long contentLength) {
WebView webview = webViewList.get(getWebviewListIndex());
webview.evaluateJavascript(
"javascript: var xhr = new XMLHttpRequest();" +
"xhr.open('GET', '"+ url +"', true);" +
"xhr.responseType = 'blob';" +
"xhr.onload = function(e) {" +
" if (this.status == 200) {" +
" var blob = this.response;" +
" var reader = new FileReader();" +
" reader.readAsDataURL(blob);" +
" reader.onloadend = function(file) {" +
" base64data = reader.result;" +
" android.downloadFileFromBlob(base64data, '"+ mimeType +"');" +
" }" +
" }" +
"};" +
"xhr.send();"
, null
);
}
/** Todo : JS Interface용 파일 저정
* downloadFileFromBlob / blob
*/
public void convertBase64StringToFileAndStoreIt(Uri uri, String base64, String fileName) {
/**
* 노티피케이션 퍼미션 여부 확인
* 퍼미션 없을 경우 다이알로그 노출 및 설정 앱으로 이동
*/
if (Utils.notificationPermissionCheck(getContext(), "알림설정", "푸쉬 알림이 차단되어있습니다.\r\n알림 설정해주세요.") == false) {
return;
}
final int notificationId = 1;
String tmp = base64.split("\\,")[1];
byte[] pdfAsBytes = Base64.decode(tmp, 0);
OutputStream os = null;
try {
os = getContext().getContentResolver().openOutputStream(uri);
os.write(pdfAsBytes);
os.flush();
} catch (FileNotFoundException e) {
DLog.e(new Exception(), e.getMessage());
return;
} catch (IOException e) {
DLog.e(new Exception(), e.getMessage());
return;
}
// 다운로드 알림 푸쉬 전송
Intent intent = new Intent();
intent.setAction(android.content.Intent.ACTION_VIEW);
intent.setDataAndType(uri, "application/pdf");
// intent.setDataAndType(uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension));
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
PendingIntent pendingIntent;
if (Build.VERSION.SDK_INT >= 31) {
if (Build.VERSION.SDK_INT >= 34) {
pendingIntent = PendingIntent.getActivity(getContext(), 0, intent, PendingIntent.FLAG_IMMUTABLE);
} else {
pendingIntent = PendingIntent.getActivity(getContext(), 0, intent, PendingIntent.FLAG_IMMUTABLE);
}
} else {
pendingIntent = PendingIntent.getActivity(getContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
String NOTIFICATION_CHANNEL_ID = "DEFAULT";
final NotificationManager notificationManager = (NotificationManager) getContext().getSystemService(NOTIFICATION_SERVICE);
NotificationCompat.Builder builder = new NotificationCompat.Builder(getContext(), NOTIFICATION_CHANNEL_ID)
.setContentTitle("MG새마을금고보험")
.setContentText(fileName + " 파일다운로드 완료.")
.setSmallIcon(R.drawable.launcher_icon)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(pendingIntent)
.setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE)
.setAutoCancel(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // >= 26
CharSequence channelName = getContext().getPackageName();//"노티피케이션 채널";
String description = "해당 채널에 대한 설명";
int importance = NotificationManager.IMPORTANCE_HIGH;
NotificationChannel notificationChannel = new NotificationChannel(
NOTIFICATION_CHANNEL_ID,
channelName,
importance
);
notificationChannel.setDescription(description);
if (notificationManager != null) {
notificationManager.createNotificationChannel(notificationChannel);
}
}
notificationManager.notify(notificationId, builder.build());
}
/** Todo : data 타입(base64) 파일 다운로드
* webViewDownloadListener -> data base64 다운로드용
* @param base64
* @param fileMimeType
* @throws IOException
*/
public void convertBase64StringToFileAndStoreIt(String base64, String fileMimeType, String fileName) throws IOException {
/**
* 노티피케이션 퍼미션 여부 확인
* 퍼미션 없을 경우 다이알로그 노출 및 설정 앱으로 이동
*/
if (CommonUtil.notificationPermissionCheck(getContext(), "알림설정", "푸쉬 알림이 차단되어있습니다.\r\n알림 설정해주세요.") == false) {
return;
}
final int notificationId = 1;
MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
// API 26 시뮬레이터에서 테스트시 다운로드 정보에 fileMimeType null인 경우가 있어 base64 에서 mimeType 추출
String extension = "";
if (fileMimeType.length() == 0) {
int beginIndex = base64.indexOf(":");
int endIndex = base64.indexOf(";");
fileMimeType = base64.substring(beginIndex + 1, endIndex);
extension = mimeTypeMap.getExtensionFromMimeType(fileMimeType);
} else {
extension = mimeTypeMap.getExtensionFromMimeType(fileMimeType);
}
// 파일생성 (파일명 처리)
final File dwldsPath;
if (fileName == null || fileName.length() <= 0) {
String currentDateTime = DateFormat.getDateTimeInstance().format(new Date());
String newTime = currentDateTime.replaceFirst(", ","_").replaceAll(" ","_").replaceAll(":","-");
fileName = newTime + "." + extension;
dwldsPath = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS) + "/" + fileName);
} else {
dwldsPath = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS) + "/" + fileName);
}
// 파일 데이터 추출
String regex = base64.split("\\,")[1];
byte[] fileAsBytes = Base64.decode(regex, 0);
// 파일 데이터 저장
try {
FileOutputStream os = new FileOutputStream(dwldsPath);
os.write(fileAsBytes);
os.flush();
os.close();
} catch (Exception e) {
Toast.makeText(getContext(), "FAILED TO DOWNLOAD THE FILE!", Toast.LENGTH_SHORT).show();
Log.e("[ FileOutputStreak Exception", e.getMessage());
}
// 다운로드 알림 푸쉬 전송 (없어도됨)
if (dwldsPath.exists()) {
Intent intent = new Intent();
intent.setAction(android.content.Intent.ACTION_VIEW);
//Provider xml 파일 필요 (Manifest.xml 추가)
Uri apkURI = FileProvider.getUriForFile(getContext(),getContext().getPackageName() + ".fileprovider", dwldsPath);
intent.setDataAndType(apkURI, MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension));
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
PendingIntent pendingIntent;
if (Build.VERSION.SDK_INT >= 31) {
if (Build.VERSION.SDK_INT >= 34) {
pendingIntent = PendingIntent.getActivity(getContext(), 0, intent, PendingIntent.FLAG_IMMUTABLE);
} else {
pendingIntent = PendingIntent.getActivity(getContext(), 0, intent, PendingIntent.FLAG_IMMUTABLE);
}
} else {
pendingIntent = PendingIntent.getActivity(getContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
String NOTIFICATION_CHANNEL_ID = "DEFAULT";
final NotificationManager notificationManager = (NotificationManager) getContext().getSystemService(NOTIFICATION_SERVICE);
NotificationCompat.Builder builder = new NotificationCompat.Builder(getContext(), NOTIFICATION_CHANNEL_ID)
.setContentTitle("타이틀")
.setContentText(fileName + " 파일다운로드 완료.")
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(pendingIntent)
// .setWhen(System.currentTimeMillis())
.setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE)
.setAutoCancel(true);
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence channelName = getContext().getPackageName();//"노티피케이션 채널";
String description = "해당 채널에 대한 설명";
int importance = NotificationManager.IMPORTANCE_HIGH;
NotificationChannel notificationChannel = new NotificationChannel(
NOTIFICATION_CHANNEL_ID,
channelName,
importance
);
notificationChannel.setDescription(description);
if (notificationManager != null) {
notificationManager.createNotificationChannel(notificationChannel);
}
}
notificationManager.notify(notificationId, builder.build());
}
// Toast.makeText(getContext(), "FILE DOWNLOADED!", Toast.LENGTH_SHORT).show();
}
BroadcastReceiver onComplete = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "다운로드 완료", Toast.LENGTH_SHORT).show();
// 다운로드 완료 후 BroadCastReceiver 해제
getContext().unregisterReceiver(this);
}
};
/** Todo: WebView 다운로드 리스너 ======================================
* WebView를 파라메터로 받는 이유는 OpenWindow로 새창이 생성될때 새로 생성된 Webview에서도 생성하기 위함임
*/
private void webViewDownloadListener(@NonNull WebView webview) {
webview.setDownloadListener(new DownloadListener() {
@Override
public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
Log.i("[ IWebView(webViewDownloadListener) : " + webview.getId() , url);
// 파일저장권한 체크. 권한이 없을 경우 설정으로 이동여부 다이얼로그 노출
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
AlertDialog.Builder alertDialog = new AlertDialog.Builder(getContext());
alertDialog.setTitle("앱 권한 설정");
alertDialog.setMessage("파일 저장 권한이 없습니다. 설정으로 이동합니다.");
alertDialog.setPositiveButton("확인",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
// 이 부분은 설정으로 이동하는 코드이므로 안드로이드 운영체제 버전에 따라 상이할 수 있다.
CommonUtil.appSettingOpen(getContext());
dialogInterface.cancel();
}
});
alertDialog.setNegativeButton("취소", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
dialogInterface.cancel();
}
});
alertDialog.show();
// ActivityCompat.requestPermissions((Activity) getContext(), new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, WRITE_EXTERNAL_PERMISSION);
return;
}
}
if (url.startsWith("blob")) {
// Todo : Blob 다운로드
downloadFileFromBlob(url, userAgent, contentDisposition, mimetype, contentLength);
return;
} else if (url.startsWith("data")) {
// Todo : data Base64 다운로드
try {
convertBase64StringToFileAndStoreIt(url, mimetype, null);
} catch (IOException e) {
Log.e("[ onDownloadStart Data Download", e.getMessage());
throw new RuntimeException(e);
}
return;
}
else {
// Todo : 웹 링크 다운로드
try {
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
DownloadManager dm = (DownloadManager) getContext().getSystemService(DOWNLOAD_SERVICE);
//확인 할것..
contentDisposition = URLDecoder.decode(contentDisposition, "UTF-8");
/** 다운로드 파일명 추출
* mimeType이 application/octet-stream 일 경우 확장자가 bin 으로 추출됨
* 별도로 url 에서 파일 타입 추출해서 사용 해야됨
*/
String fileExtenstion = MimeTypeMap.getFileExtensionFromUrl(url);
String fileName = URLUtil.guessFileName(url, contentDisposition, fileExtenstion);
Log.d("[ IWebView(onDownloadStart)", fileName + " / " + url);
if (fileName.length() <= 0) {
fileName = contentDisposition.replace("attachment; filename=", "");
if (fileName != null && fileName.length() > 0) {
int idxFileName = fileName.indexOf("filename =");
if (idxFileName > -1) {
fileName = fileName.substring(idxFileName + 9).trim();
}
if (fileName.endsWith(";")) {
fileName = fileName.substring(0, fileName.length() - 1);
}
if (fileName.startsWith("\"") && fileName.startsWith("\"")) {
fileName = fileName.substring(1, fileName.length() - 1);
}
}
}
// 세션 유지를 위해 쿠키 세팅하기
String cookie = CookieManager.getInstance().getCookie(url);
request.addRequestHeader("Cookie", cookie);
request.setMimeType(mimetype);
request.addRequestHeader("User-Agent", userAgent);
request.setDescription("Downloading File");
request.setAllowedOverMetered(true);
request.setAllowedOverRoaming(true);
request.setTitle(fileName);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
request.setRequiresCharging(false);
}
request.allowScanningByMediaScanner();
request.setAllowedOverMetered(true);
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
dm.enqueue(request);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getContext().registerReceiver(onComplete, new IntentFilter(dm.ACTION_DOWNLOAD_COMPLETE), Context.RECEIVER_NOT_EXPORTED);
} else {
getContext().registerReceiver(onComplete, new IntentFilter(dm.ACTION_DOWNLOAD_COMPLETE));
}
} catch (Exception e) {
Log.e("[ onDownloadStart Link Download", e.getMessage());
if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale((Activity) getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
Toast.makeText(getContext(), "파일 다운로드 권한을 허용해주십시오.", Toast.LENGTH_LONG).show();
ActivityCompat.requestPermissions((Activity) getContext(), new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 100);
} else {
Toast.makeText(getContext(), "파일 다운로드 권한을 허용해주십시오.", Toast.LENGTH_LONG).show();
ActivityCompat.requestPermissions((Activity) getContext(), new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 100);
}
}
}
}
}
});
}
}
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">
<!-- 웹뷰 클래스 로딩 -->
<kr.xxxx.xxxxxxxxx.IWebview.IWebview
android:id="@+id/iWebview_layer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbarAlwaysDrawVerticalTrack="true" <!-- 웹뷰 텍스트 에디터 선택시 키보드만큼 스크롤 올림 -->
>
</kr.xxxx.xxxxxxxxx.IWebview.IWebview>
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.java
public class MainActivity extends AppCompatActivity {
private IWebview iWebview;
// WebViewBridge Callbac Adapter
private IWebViewAdapter mIWebViewAdapter;
//onShowFileChooser 파일첨부시 사용
public ActivityResultLauncher<Intent> mFileForResult;
public ValueCallback<Uri[]> mFilePathCallback; // 파일 탐색기
//Blob 파일 다운로드시 사용
public ActivityResultLauncher<Intent> mBlobDownloadResult;
public String blobBase64String = "";
/**[매우 중요]
* 여러 Activity에서 MainActivity의 WebViewBridge로 연결하는 Adapter
* xxxxActivity -> WebViewBridge 결과 데이터 전송
*/
public void setiWebViewAdapter(IWebViewAdapter adapter) {
this.mIWebViewAdapter = adapter;
}
public IWebViewAdapter getiWebViewAdapter() {
return mIWebViewAdapter;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_webview);
iWebview = findViewById(R.id.iWebview_layer);
iWebview.setLoadUrl("https://wkwebview-download.glitch.me/"); // Blob, data 파일 다운로드
/** Todo: 파일브라우저 Intent Result
* 웹뷰 안에 파일이 첨부
* 참고자료 : https://42kchoi.tistory.com/384
*/
mFileForResult = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
result -> {
if(result.getResultCode() == RESULT_OK) {
if (mFilePathCallback == null) {
return;
}
if (result.getData().getClipData() != null) {
int count = result.getData().getClipData().getItemCount();
Intent intentData = result.getData();
Uri[] uris = new Uri[count];
for (int i = 0; i < count; i++) {
uris[i] = intentData.getClipData().getItemAt(i).getUri();
}
mFilePathCallback.onReceiveValue(uris);
} else if(result.getData() != null) {
Intent intentData = result.getData();
mFilePathCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(result.getResultCode(), intentData));
}
mFilePathCallback = null;
} else {
mFilePathCallback.onReceiveValue(null);
mFilePathCallback = null;
Log.e("WebviewActivity", "Not RESULT_OK!!!");
}
}
);
/** Todo: Blob 다운로드 Intent Result
* 자바스크립트 blob 연동. 파일앱 실행 및 경로 전달 -> 파일 다운로드
*/
mBlobDownloadResultLanucher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
new ActivityResultCallback<ActivityResult>() {
@Override
public void onActivityResult(ActivityResult result) {
//result.getResultCode()를 통하여 결과값 확인
if (result.getResultCode() == RESULT_OK) {
Intent intentData = result.getData();
Uri uri = intentData.getData();
mIWebViewAdapter.onBlobDownloadCallback(uri);
}
if (result.getResultCode() == RESULT_CANCELED) {
Toast.makeText(MainActivity.this, "다운로드가 취소되었습니다.", Toast.LENGTH_LONG).show();
}
}
}
);
/* 다이렉트 파일 저정 (사용안함)
mBlobDownloadResult = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
new ActivityResultCallback<ActivityResult>() {
@Override
public void onActivityResult(ActivityResult result) {
//result.getResultCode()를 통하여 결과값 확인
if (result.getResultCode() == RESULT_OK) {
//ToDo
Intent intentData = result.getData();
Uri uri = intentData.getData();
Log.d("blobDownloadResult", uri.toString());
String tmp = blobBase64String.split("\\,")[1];
byte[] pdfAsBytes = Base64.decode(tmp, 0);
OutputStream os = null;
try {
os = getContentResolver().openOutputStream(uri);
os.write(pdfAsBytes);
os.flush();
} catch (FileNotFoundException e) {
Log.e("Blob Download IO Exception", e.getMessage());
throw new RuntimeException(e);
} catch (IOException e) {
Log.e("Blob Download IO Exception", e.getMessage());
throw new RuntimeException(e);
}
}
if (result.getResultCode() == RESULT_CANCELED) {
//ToDo
}
}
}
);*/
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState, @NonNull PersistableBundle outPersistentState) {
super.onSaveInstanceState(outState, outPersistentState);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if(keyCode == KeyEvent.KEYCODE_BACK) {
int pages = iWebview.getWebviewListIndex();
WebView webView = iWebview.getWebview(pages);
if(webView.canGoBack()) {
webView.goBack();
return true;
} else if (pages > 0) {
// WindowOpen Deeps 존재할 경우 삭제
iWebview.removeWebview(pages);
return true;
} else {
// App(Activity) 종료 차단
return false;
}
}
return super.onKeyDown(keyCode, event);
}
@Override
protected void onResume() {
super.onResume();
iWebview.cookieSyncManagerStartSync();
}
@Override
protected void onPause() {
super.onPause();
iWebview.cookieSyncManagerStopSync();
}
}'Android(Java)' 카테고리의 다른 글
| [Java] Parcelable을 이용한 객체 Intent 전달 (0) | 2024.02.27 |
|---|---|
| [Java] Intent 데이터 전달 방식 (0) | 2024.02.27 |
| [Android]안드로이드 xml에서 투명도 적용 (0) | 2024.02.22 |
| [Java] ActivityResultLauncher 사용 (0) | 2024.02.21 |
| [Java-중요] Permission 여러개 한번에 설정 요청 하기 (1) | 2024.02.19 |