WebView를 이용한 어플을 만들 때 <input type: "file" accept="image/*" /> 태그를 이용한 이미지 업로드를 구현하면 APP내에서 실행이 안된다.
모바일로 접근할때에는 잘 작동하지만 APP으로 만들게 되면 작동하지가 않는다. 따라서 Android Studio로 설정을 해주어야 한다.
기본적인 흐름을 내가 이해한 정도로 설명하겠다. 혹시 틀린 부분이 있다면 즉각 수정하겠다.
퍼미션들을 위한 라이브러리를 AndroidMainfest.xml에 등록하고 사진을 확인하기 위한 로컬 저장소 경로를 등록해준다. 로컬 저장소에 대한 정보는 res/xml에 file_paths.xml, network_security_config.xml에 작성을 해 준다. res/layout/activity_main.xml에 WebView를 넣어 나타나게 해준다. MainActivity에 WebView에 대한 기본 설정, 퍼미션(권한) 허가, 카메라 접근, 갤러리 접근, 뒤로 가기 버튼 활성화 등등 다양한 작업을 해주면 된다. 아래에 코드와 함께 자세하게 설명하겠다. 이중 한줄이라도 놓칠 경우 오류가 발생하기 때문에 잘 따라오기 바란다. 내가 한줄을 안 써서 삽질을 엄청했다,,,,
1. AndroidMainfest.xml
- 퍼미션(권한)을 위한 라이브러리 등록
- 로컬 저장소 등록
2. res/xml에 file_paths.xml, network_security_config.xml 작성
- res/xml 파일생성
- res/xml아래에 file_paths.xml, network_security_config.xml 생성
3. res/layout/activity_main.xml에 WebView 작성
- WebView 작성
4. MainActicity
- WebView 기본 설정
- 퍼미션(권한) 허가
- 카메라 접근
- 갤러리 접근
- 뒤로 가기 버튼 활성화
- 등등
!!!! 설정이 필요한 파일은 아래의 사진에서 표시하였다. 만약 없는 폴더나 파일이 있다면 생성해 줘야 한다.!!!!
나는 Splash와 Icon을 설정하였기 때문에 코드의 구성이 조금 다를 수 있다. 이는 신경 쓰지 말고 설명만 따라오면 된다. 혹시 Splash와 Icon을 설정하는 법이 궁금하다면 아래의 포스팅을 참고하기 바란다.
2021.10.06 - [APP] - [Android Studio] Icon및 Splash(로딩화면) 변경
1. AndroidMainfest.xml
- 퍼미션(권한)을 위한 라이브러리 등록
- 로컬 저장소 등록
- 상단에 주석이 달린 부분을 모두 추가해 주어야 한다. 퍼미션을 불러오는 부분이다. 또한 application 안쪽 주석의 아랫부분을 추가해 주어야 한다. 추가해야 하는 부분은 문단을 나누어 놨으니 추가해주면 된다. 나는 이 과정을 하나 빼먹어 같은 작업을 계속했다. 혹시 AndroidMainfest.xml의 내용이 조금 다르더라고 아래의 형식을 맞추어 작성해주면 문제없이 프로그램이 돌아갈 것이다.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.taehui.facing_criminal">
<uses-permission android:name="android.permission.INTERNET" />
<!--네트워크 상태 퍼미션-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- 카메라 퍼미션 -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.CAMERA2" />
<uses-feature
android:name="android.hardware.camera"
android:required="true" />
<!-- 5.0 버전 파일업로드 퍼미션 -->
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="18" />
<!-- 외부 저장소 사용 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher_criminal"
android:label="범죄성향 테스트"
<!-- 아래의 network_security_config를 추가해주는 코드 작성 -->
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_criminal_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.NoActionBar">
<!--provider 안쪽 전부 추가-->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<activity
android:name="com.taehui.facing_criminal.SplashActivity"
android:screenOrientation="portrait"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="com.taehui.facing_criminal.MainActivity">
</activity>
</application>
</manifest>
2. res/xml에 file_paths.xml, network_security_config.xml 작성
- res/xml 파일 생성
- res/xml 아래에 file_paths.xml, network_security_config.xml 생성
file_paths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path
name="sdcard"
path="." />
<external-files-path
name="external_files"
path="." />
<cache-path
name="cache"
path="." />
<external-cache-path
name="external_cache"
path="." />
<files-path
name="files"
path="." />
</paths>
network_security_config.xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true"/>
</network-security-config>
3. res/layout/activity_main.xml에 WebView 작성
- WebView 작성
res/layout/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">
<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
4. MainActicity
- WebView 기본 설정
- 퍼미션(권한) 허가
- 카메라 접근
- 갤러리 접근
- 뒤로 가기 버튼 활성화
- 등등
MainActivity
package com.taehui.facing_criminal;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import android.Manifest;
import android.annotation.TargetApi;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Parcelable;
import android.provider.MediaStore;
import android.provider.Settings;
import android.util.Log;
import android.view.KeyEvent;
import android.webkit.JsResult;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;
import java.io.File;
public class MainActivity extends AppCompatActivity {
private WebView mWebView; // 웹뷰 선언
private WebSettings mWebSettings; //웹뷰세팅
private long time = 0;
public ValueCallback<Uri> filePathCallbackNormal;
public ValueCallback<Uri[]> filePathCallbackLollipop;
public final static int FILECHOOSER_NORMAL_REQ_CODE = 2001;
public final static int FILECHOOSER_LOLLIPOP_REQ_CODE = 2002;
private Uri cameraImageUri = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mWebView = (WebView) findViewById(R.id.webView);
checkVerify();
mWebView.setWebContentsDebuggingEnabled(false);
mWebView.setWebViewClient(new WebViewClient()); // 클릭시 새창 안뜨게
mWebSettings = mWebView.getSettings(); //세부 세팅 등록
mWebSettings.setJavaScriptEnabled(true); // 웹페이지 자바스크립트 허용 여부
mWebSettings.setSupportMultipleWindows(false); // 새창 띄우기 허용 여부
mWebSettings.setJavaScriptCanOpenWindowsAutomatically(false); // 자바스크립트 새창 띄우기(멀티뷰) 허용 여부
mWebSettings.setLoadWithOverviewMode(true); // 메타태그 허용 여부
mWebSettings.setUseWideViewPort(true); // 화면 사이즈 맞추기 허용 여부
mWebSettings.setSupportZoom(false); // 화면 줌 허용 여부
mWebSettings.setBuiltInZoomControls(false); // 화면 확대 축소 허용 여부
mWebSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN); // 컨텐츠 사이즈 맞추기
mWebSettings.setCacheMode(WebSettings.LOAD_NO_CACHE); // 브라우저 캐시 허용 여부
mWebSettings.setDomStorageEnabled(true); // 로컬저장소 허용 여부
//mWebSettings.setSaveFormData(false);
mWebView.loadUrl("https://criminal.netlify.app/"); // 웹뷰에 표시할 웹사이트 주소, 웹뷰 시작
mWebView.setWebChromeClient(new WebChromeClientClass()); //웹뷰에 크롬 사용 허용. 이 부분이 없으면 크롬에서 alert가 뜨지 않음
mWebView.setWebViewClient(new WebViewClientClass());
}
private long backBtnTime = 0;
@Override
public void onBackPressed() {
long curTime = System.currentTimeMillis();
long gapTime = curTime - backBtnTime;
if (mWebView.canGoBack()) {
mWebView.goBack();
} else if (0 <= gapTime && 2000 >= gapTime) {
super.onBackPressed();
} else {
backBtnTime = curTime;
Toast.makeText(this, "한번 더 누르면 종료됩니다.", Toast.LENGTH_SHORT).show();
}
}
@TargetApi(Build.VERSION_CODES.M)
public void checkVerify() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(this, Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_NETWORK_STATE) != PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
// Log.d("checkVerify() : ","if문 들어옴");
//카메라 또는 저장공간 권한 획득 여부 확인
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) || ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
Toast.makeText(getApplicationContext(), "권한 관련 요청을 허용해 주셔야 카메라 캡처이미지 사용등의 서비스를 이용가능합니다.", Toast.LENGTH_SHORT).show();
} else {
// Log.d("checkVerify() : ","카메라 및 저장공간 권한 요청");
// 카메라 및 저장공간 권한 요청
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.INTERNET, Manifest.permission.CAMERA,
Manifest.permission.ACCESS_NETWORK_STATE,
Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
}
}
}
//권한 획득 여부에 따른 결과 반환
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
//Log.d("onRequestPermissionsResult() : ","들어옴");
if (requestCode == 1) {
if (grantResults.length > 0) {
for (int i = 0; i < grantResults.length; ++i) {
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
// 카메라, 저장소 중 하나라도 거부한다면 앱실행 불가 메세지 띄움
new AlertDialog.Builder(this).setTitle("알림").setMessage("권한을 허용해주셔야 앱을 이용할 수 있습니다.")
.setPositiveButton("종료", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
finish();
}
}).setNegativeButton("권한 설정", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.parse("package:" + getApplicationContext().getPackageName()));
getApplicationContext().startActivity(intent);
}
}).setCancelable(false).show();
return;
}
}
// Toast.makeText(this, "Succeed Read/Write external storage !", Toast.LENGTH_SHORT).show();
}
}
}
//액티비티가 종료될 때 결과를 받고 파일을 전송할 때 사용
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
Log.d("onActivityResult() ", "resultCode = " + Integer.toString(requestCode));
switch (requestCode) {
case FILECHOOSER_NORMAL_REQ_CODE:
if (resultCode == RESULT_OK) {
if (filePathCallbackNormal == null) return;
Uri result = (data == null || resultCode != RESULT_OK) ? null : data.getData();
// onReceiveValue 로 파일을 전송한다.
filePathCallbackNormal.onReceiveValue(result);
filePathCallbackNormal = null;
}
break;
case FILECHOOSER_LOLLIPOP_REQ_CODE:
Log.d("onActivityResult() ", "FILECHOOSER_LOLLIPOP_REQ_CODE = " + Integer.toString(FILECHOOSER_LOLLIPOP_REQ_CODE));
if (resultCode == RESULT_OK) {
Log.d("onActivityResult() ", "FILECHOOSER_LOLLIPOP_REQ_CODE 의 if문 RESULT_OK 안에 들어옴");
if (filePathCallbackLollipop == null) return;
if (data == null)
data = new Intent();
if (data.getData() == null)
data.setData(cameraImageUri);
filePathCallbackLollipop.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data));
filePathCallbackLollipop = null;
} else {
Log.d("onActivityResult() ", "FILECHOOSER_LOLLIPOP_REQ_CODE 의 if문의 else문 안으로~");
if (filePathCallbackLollipop != null) { // resultCode에 RESULT_OK가 들어오지 않으면 null 처리하지 한다.(이렇게 하지 않으면 다음부터 input 태그를 클릭해도 반응하지 않음)
Log.d("onActivityResult() ", "FILECHOOSER_LOLLIPOP_REQ_CODE 의 if문의 filePathCallbackLollipop이 null이 아니면");
filePathCallbackLollipop.onReceiveValue(null);
filePathCallbackLollipop = null;
}
if (filePathCallbackNormal != null) {
filePathCallbackNormal.onReceiveValue(null);
filePathCallbackNormal = null;
}
}
break;
default:
break;
}
super.onActivityResult(requestCode, resultCode, data);
}
// 카메라 기능 구현
private void runCamera(boolean _isCapture) {
Intent intentCamera = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
//intentCamera.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
File path = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "AndroidExampleFolder"); //저장 경로
if (!path.exists()) {
// Create AndroidExampleFolder at sdcard
path.mkdirs();
}
File file = new File(path+ File.separator + "IMG_" + String.valueOf(System.currentTimeMillis()) + ".jpg"); //저장 이름
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
String strpa = getApplicationContext().getPackageName();
cameraImageUri = FileProvider.getUriForFile(this, strpa + ".fileprovider", file);
} else {
cameraImageUri = Uri.fromFile(file);
}
intentCamera.putExtra(MediaStore.EXTRA_OUTPUT, cameraImageUri);
if (!_isCapture) { // 선택팝업 카메라, 갤러리 둘다 띄우고 싶을 때
Intent pickIntent = new Intent(Intent.ACTION_PICK);
pickIntent.setType(MediaStore.Images.Media.CONTENT_TYPE);
pickIntent.setData(MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
String pickTitle = "사진 가져올 방법을 선택하세요.";
Intent chooserIntent = Intent.createChooser(pickIntent, pickTitle);
// 카메라 intent 포함시키기..
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Parcelable[]{intentCamera});
startActivityForResult(chooserIntent, FILECHOOSER_LOLLIPOP_REQ_CODE);
} else {// 바로 카메라 실행..
startActivityForResult(intentCamera, FILECHOOSER_LOLLIPOP_REQ_CODE);
}
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if ((keyCode == KeyEvent.KEYCODE_BACK) && mWebView.canGoBack()) {
mWebView.goBack();
return true;
}
return super.onKeyDown(keyCode, event);
}
private class WebChromeClientClass extends WebChromeClient {
// 자바스크립트의 alert창
@Override
public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
new AlertDialog.Builder(view.getContext())
.setTitle("Alert")
.setMessage(message)
.setPositiveButton(android.R.string.ok,
new AlertDialog.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
result.confirm();
}
})
.setCancelable(false)
.create()
.show();
return true;
}
// 자바스크립트의 confirm창
@Override
public boolean onJsConfirm(WebView view, String url, String message,
final JsResult result) {
new AlertDialog.Builder(view.getContext())
.setTitle("Confirm")
.setMessage(message)
.setPositiveButton("Yes",
new AlertDialog.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
result.confirm();
}
})
.setNegativeButton("No",
new AlertDialog.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
result.cancel();
}
})
.setCancelable(false)
.create()
.show();
return true;
}
// For Android 5.0+ 카메라 - input type="file" 태그를 선택했을 때 반응
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public boolean onShowFileChooser(
WebView webView, ValueCallback<Uri[]> filePathCallback,
FileChooserParams fileChooserParams) {
Log.d("MainActivity - onShowFileChooser", "5.0+");
// Callback 초기화 (중요!)
if (filePathCallbackLollipop != null) {
filePathCallbackLollipop.onReceiveValue(null);
filePathCallbackLollipop = null;
}
filePathCallbackLollipop = filePathCallback;
boolean isCapture = fileChooserParams.isCaptureEnabled();
Log.d("onShowFileChooser : ", String.valueOf(isCapture));
runCamera(isCapture);
return true;
}
}
private class WebViewClientClass extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
//Log.d("WebViewClient URL : " , request.getUrl().toString());
view.loadUrl(request.getUrl().toString());
return true;
//return super.shouldOverrideUrlLoading(view, request);
}
}
@Override
protected void onDestroy() {
if (mWebView != null)
mWebView.destroy();
super.onDestroy();
}
}
이렇게 하면 퍼미션(권한) 요청이 뜨고 원하는 업로드 방식이 뜬다음 파일이 업로드된다.
범죄성향 테스트가 궁금하다면 링크를 들어가라 criminal.netlify.app/
2021.04.29 업데이트
카메라 촬영으로 업로드 시 안되는 문제 해결
MainActivity 변경 위에 코드에 적용 완료 참고용으로 아래에 어디부분을 바꿨는지 표시함
변경 전
File path = Environment.getExternalStorageDirectory();
File file = new File(path, "sample.png"); // sample.png 는 카메라로 찍었을 때 저장될 파일명이므로 사용자 마음대로
// File 객체의 URI 를 얻는다.
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
변경 후
File path = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "AndroidExampleFolder");
if (!path.exists()) {
// Create AndroidExampleFolder at sdcard
path.mkdirs();
}
File file = new File(path+ File.separator + "IMG_" + String.valueOf(System.currentTimeMillis()) + ".jpg");
'APP' 카테고리의 다른 글
[범죄성향 테스트] APP 출시 (0) | 2021.12.01 |
---|---|
[관상테스트] 관상으로 보는 범죄성향 테스트 (0) | 2021.11.23 |
[Android Studio] Icon및 Splash(로딩화면) 변경 (0) | 2021.10.06 |
[Android Studio] APP 이름 바꾸기 (0) | 2021.10.03 |
[Android Studio] (Goolgle Store App 등록 Erorr) 이미 버전 코드가 1인 APK 또는 Android App Bundle이 있으므로 다른 버전 코드를 사용해야 합니다. (0) | 2021.10.02 |