본문 바로가기

Programming/Hybrid App

[Android, Hybrid]openFileChooser 킷캣에서 동작하지 않는 문제( openFileChooser Kitkat bug )

[2014.10.31]


openFileChooser가 문제군요...ㅎ


코멘트 잘 읽어 보았습니다.


킷캣에서 파일업로드 버그는...웹적인 요소로 해결할 수 있는 방안은 없습니다.


그래서 이런방법도 있다는 것을 알려드리기 위해서 글을 쓴건데 더욱 혼란스럽게 해드린 것 같네요.


제가 아래 글에 빨간색으로 표시를 해뒀는데...이 방법은 파일에 대한 정보를 네이티브 단에 저장해 놓고


전송에 대한 이벤트를 받으면 NATIVE 단에서 서버로 전송하게 됩니다.


서버로 전송하는 부분은 각자 프로젝트에 맞게...직접 구현하셔야되구요.


멀티파츠로 전송하신다면 제 블로그 찾아보시면 네이티브에서 멀티파츠로 전송하는 방법이라고 있습니다.


그부분 참고하시면 됩니다.


아래 발췌된 부분은 참고용이지 복사 붙여넣기를 한다고해서 동작하는 코드는 아닙니다.


그래서 간단한 샘플을 하나 만들었습니다.


아래 코드가 잘 이해안되시는 분들은 다운받아서 확인해보시면 되겠습니다.



imageupload_sample.zip



================================================================


openFileChooser 안드로이드 4.4 킷캣(kitkat) 이하에서는 openFileChooser를 


WebChromeClient class 에서 구현을 하게되면 정상적으로 callback을 받았었다.


그렇기 때문에 네이티브에서 별다른 작업(물론...openFileChooser에 대한 작업은 해줘야한다) 없이도


웹 기능만으로도 파일을 서버로 전송(multiparts 같은...)할 수 있었다. 


하지만 4.4 킷캣 버전부터 해당 메쏘드를 호출하지 않기 때문에, 


하이브리드 앱의 경우 기존의 방법으로는  카메라 및 갤러리 호출, 그리고 전송을 할 수 없다.


해당문제를 해결하기 위해서 페이지가 웹뷰를 통해 보여지고 있고, 킷캣일 경우에는 


JavascriptInterface를 통해 카메라 및 갤러리를 호출 할  수 있도록 대응하였다.


sequence는 다음과 같다.


웹페이지에서 input tag 클릭 -> 클릭이벤트를 통해 window.Android.open 호출(JavascriptInterface) 

-> WebViewImageUploadHelper 를 통해 갤러리 또는 카메라 호출 -> 선택된 파일의 썸네일을 화면에 보여주고, 파일 정보저장

-> 전송 버튼 클릭시 Native 단에서 데이터 전송


* 썸네일 보여주는 방법을 모르시면 (Click!)

* JavascriptInterface 에 대해 모르시면 링크 참조(Click!)


JavascriptInterface 를 만든다. (필자는 open 과 send라는 메쏘드를 만들었다)

만든 후에는 웹뷰에 JavascriptInterface를 연결한다.

public class WebViewInterface {

	private WebView mAppView;
	private Activity mContext;

	/**
	 * 생성자.
	 * @param activity : context
	 * @param view : 적용될 웹뷰
	 */
	public WebViewInterface(Activity activity, WebView view) {
		mAppView = view;
		mContext = activity;
	}
	/**
	 * 갤러리 및 카메라 열기.
	 * @param key : 어떤 input box를 클릭했는지 구분을 위한 Unique한 key.
	 * @param thumbnailId : 선택된 이미지의 Thumbnail을 보여줄 division id.
	 */
	@JavascriptInterface
	public Uri open(String key, String thumbnailId ) {
		WebViewImageUploadHelper.getInstance(mContext, mAppView).open(key, thumbnailId);
		return null;
	}
	/**
	 * 파일 전송.
	 */
	@JavascriptInterface
	public File send(String key) {
		WebViewImageUploadHelper.getInstance(mContext, mAppView).send(type);
		return null;
	}
}


이미지 업로드를 담당할 WebViewImageUploadHelper 클래스를 만든다.


해당 메쏘드에서 카메라, 갤러리 호출 및 파일 전송을 담당하게 된다.


Singleton으로 제작.

public class WebViewImageUploadHelper {

	public final static int INTENT_CALL_GALLERY = 3001; //갤러리 requestCode
	public final static int INTENT_CALL_CAMERA = 4001; //카메라 requestCode

	private static WebViewImageUploadHelper mHelper;
	private Context mContext;

	private File mContents; //파일객체.

	/**
	 * 생성자. 외부에서 불릴일은 없다.
	 * @param context : context
	 */
	private WebViewImageUploadHelper(Context context) { 
		mContext = context;
		mDialog = new CommonDialogs(context);
		mDialogCallbackListener = new DialogInterface.OnClickListener() {

			@Override
			public void onClick(DialogInterface dialog, int which) {

				if (which == 0) {
					callCamera(INTENT_CALL_CAMERA);//카메라 호출.
				} else if (which == 1) {
					callGallery(INTENT_CALL_GALLERY); //갤러리 호출.
				}
			}
		};
	}
	/**
	 * 생성자. WebViewImageUploadHelper에 접근을 위해서는 이 메쏘드를 통해야 한다.
	 * @param context
	 *            : activity context
	 * @return
	 */
	public static final WebViewImageUploadHelper getInstance(Context context, WebView webView) {

		if (mHelper == null) {
			mHelper = new WebViewImageUploadHelper(context);
			mHelper.mWebView = webView;
		}
		return mHelper;

	}
	/**
	 * 갤러리를 호출한다.
	 */
	public void callGallery(int requestCode) {
		Intent intent = new Intent();
		intent.setAction(Intent.ACTION_PICK);
		intent.setType("image/*");
		((MainActivity) mContext).startActivityForResult(Intent.createChooser(intent, "File Chooser"), requestCode);
	}

	/**
	 * 카메라를 호출한다.
	 */
	public void callCamera(int requestCode) {
		Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); //이미지 캡처를 위한 인텐트

		File directory = new File(Constant.DIRECTORY_PHOTO_PATH); //파일이 저장된 디렉토리.

		if (!directory.exists()) {
			directory.mkdir(); //디렉토리가 없으면 만들고.
		}
		mTempFile = new File(directory, "photo_" + new Date().getTime() + ".jpg"); //저장될 파일을 선언.
		intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mTempFile)); //파일정보를 인텐트에 함께 넣어준다.
		// intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
		((MainActivity) mContext).startActivityForResult(intent, requestCode);
	}
	/**
	 * file 정보를 base64로 인코딩한다.
	 * @param file : target file
	 * @return
	 */
	public String fileToString(File file) {
		
		String fileString = new String();
		FileInputStream inputStream = null;
		ByteArrayOutputStream byteOutStream = null;

		try {
			inputStream = new FileInputStream(file);
			byteOutStream = new ByteArrayOutputStream();

			int len = 0;
			byte[] buf = new byte[1024];
			while ((len = inputStream.read(buf)) != -1) {
				byteOutStream.write(buf, 0, len);
			}

			byte[] fileArray = byteOutStream.toByteArray();
			fileString = new String(Base64.encodeBase64(fileArray));

		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				inputStream.close();
				byteOutStream.close();
			} catch (Exception e) {
				e.printStackTrace();
				return null;
			}
		}
		return fileString;
	}
	/**
	 * 화면 세팅을 위해. mimetype을 알아낸다.
	 * @param uri
	 * @return
	 */
	public String getMimeType(Uri uri) {
		ContentResolver cR = mContext.getContentResolver();
		MimeTypeMap mime = MimeTypeMap.getSingleton();
		String type = cR.getType(uri);
//		String type = mime.getExtensionFromMimeType(cR.getType(uri));
	    return type;
	}
	/**
	 * 갤러리로부터 받은 파일정보를 통해 웹뷰 화면을 업데이트 한다.
	 * @param uri : file path
	 */
	public final void updateContent(Uri uri) {
		if (uri == null) {
			return;
		}
		
		File file = uriToFile(uri);
		
		String type = getMimeType(uri);
		
		// 파일 path 저장
		if (mKey.equals("contents")) {
			mContents = file;
		} else if (mKey.equals("contents2")) {
			mContents2 = file;
		} else if (mKey.equals("contents3")) {
			mContents3 = file;
		} else if (mKey.equals("contents4")) {
			mContents4 = file;
		}

		// 웹뷰로 썸네일 보냄.
		updateImage(file, type);

	}
	/**
	 * 카메라로부터 받은 파일정보를 통해 웹뷰 화면을 업데이트 한다.
	 * @param uri : file path
	 */
	public final void updateContent() {
		Uri uri = Uri.fromFile(mTempFile);
		
		// 미디어 스캐닝 실행.
		((MainActivity) mContext).sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));
		
		File file = mTempFile;
		
		String type = getMimeType(uri);
		
		// 파일 path 저장
		if (mKey.equals("contents")) {
			mContents = file;
		} else if (mKey.equals("contents2")) {
			mContents2 = file;
		} else if (mKey.equals("contents3")) {
			mContents3 = file;
		} else if (mKey.equals("contents4")) {
			mContents4 = file;
		}
		
		// 웹뷰로 썸네일 보냄.
		updateImage(file, type);
		
		mTempFile = null;
		
	}

	/**
	 * 썸네일을 보여준다.
*/ private void updateImage(final File file,final String type) { //Task를 실행시킨다. Thread를 돌리지 않으면, ANR로 앱이 강제종료될 수 있기 때문. new AsyncTask() { @Override protected String doInBackground(Void... params) { String mimeType = type; String base64EncodedImage = fileToString(file); return "javascript:$(" + "\"#" + mThumbnailId + "\"" + ").attr(\"src\", " + "\"data:" + mimeType + ";base64," + base64EncodedImage + "\");"; } @Override protected void onPostExecute(String result) { super.onPostExecute(result); mWebView.loadUrl(result); } }.execute(); } }


여기서 상당히 중요한데, 카메라와 갤러리를 호출하였기 때문에


선택되었을 경우 onActivityResult를 통해 uri가 리턴된다.(카메라 : callCamera 메쏘드에서 선언한 file, 갤러리 : uri )


그리고 킷캣부터 Document(?)라는 앱이 추가되어서, 기존의 데이터 Scheme와는 전혀 다르게 리턴해주기 때문에


거기에 따른 처리도 함께 들어가야한다.

public class MainActivity {
	...... 생략 ........
	@Override
	protected void onActivityResult(int requestCode, int resultCode, Intent data) {

		super.onActivityResult(requestCode, resultCode, data);

		if (resultCode == Activity.RESULT_OK) {
			if ( requestCode == WebViewImageUploadHelper.INTENT_CALL_GALLERY ) {

				Uri result = data == null || resultCode != RESULT_OK ? null : data.getData();
				WebViewImageUploadHelper.getInstance(this, mWebView).updateContent(result);

				return;
			} else if ( requestCode == WebViewImageUploadHelper.INTENT_CALL_CAMERA) {
				WebViewImageUploadHelper.getInstance(this, mWebView).updateContent();
			}
		}
	}
}


그리고 웹페이지의 input tag에 클릭이벤트를 추가한다.

$("input").delegate("#inputFileUpload", 'click', function (e) {

	window.Android.open( "contents", "imgThumbnail"); //"contents" : key , "imgThumbnail" : Thumbnail 이 표시될 div
	
});



클래스 생성 및 이벤트 추가 완료 후에, input tag를 클릭하게 되면, 해당 네이티브 로직을 통해 파일을 읽어 들이게 되고,


이미지의 경우 img tag에 보여주게 된다.


궁금한 사항이나...이상한 부분이 있으면 댓글 달아주세요.


파일 전송을 위해서는 HttpConnection을 이용하여 전송하면 되고(파일에 대한 정보는 다 있으니...조금만 응용하면...)


안드로이드에서의 multiparts 전송은 다음에 포스팅하는 걸로...


아유카와 님께서... uriToFile이 없다고하셔서 추가합니다~

/**
 * 카메라 또는 갤러리로부터 받은 url정보를 file 정보로 변환한다.
 * @param uri 
 * @return
 */
@TargetApi(19)
private File uriToFile ( Uri uri ) {
	
	String filePath = "";
	
	if ( uri.getPath().contains(":") ) {
		//:이 존재하는 경우		

		String wholeID = DocumentsContract.getDocumentId(uri);

		// Split at colon, use second item in the array
		String id = wholeID.split(":")[1];

		String[] column = { MediaStore.Images.Media.DATA };     

		// where id is equal to             
		String sel = MediaStore.Images.Media._ID + "=?";

		Cursor cursor = mContext.getContentResolver().
		                          query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 
		                          column, sel, new String[]{ id }, null);


		int columnIndex = cursor.getColumnIndex(column[0]);

		if (cursor.moveToFirst()) {
		    filePath = cursor.getString(columnIndex);
		}   

		cursor.close();
		
	} else {
		//:이 존재하지 않을경우
		String id = uri.getLastPathSegment(); 
	    final String[] imageColumns = {MediaStore.Images.Media.DATA };
	    final String imageOrderBy = null;

	    String selectedImagePath = "path";
	    String scheme = uri.getScheme();
	    if ( scheme.equalsIgnoreCase("content") ) {
	    	 Cursor imageCursor = mContext.getContentResolver().query(uri, imageColumns, null, null, null);

			    if (imageCursor.moveToFirst()) {
			    	filePath = imageCursor.getString(imageCursor.getColumnIndex(MediaStore.Images.Media.DATA));
			    }
	    } else {
	    	filePath = uri.getPath();
	    }
	}
    
    File file = new File( filePath );
    
    return file;
}


멀티파츠 전송 샘플