Android

Android(Java)에서 받은 encodedUri를 webView(javascript)에서 파일로 변환하기

evan.k 2023. 2. 2. 15:54

최근 서비스에서 사용하던 이미지 업로드 기능을 커스텀하게 바꾸는 작업을 진행했다.

개발 중인 서비스는 네이티브 앱이 웹뷰를 감싸고 있는 구조라 약간의 설계가 필요했다.

 

기존의 이미지 업로드 방식

WebView (Vue.js)
웹뷰에서 file 타입의 인풋으로 Native Android의 onShowFileChooser 호출 

<input type="file" accept="image/*" ref="imageFile" @chance="onChangeFunc" />

Native Android (JAVA)

권한 비교 후 image picker intent 실행

@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
    mFilePathCallback = filePathCallback;
    String[] deniedPermissions = PermissionUtil.checkDeniedPermissions(getApplicationContext(), needPermissions);
    if (deniedPermissions.length != 0) { //필요한 권한 확인
        ActivityCompat.requestPermissions(MainActivity.this, deniedPermissions, PERMISSION_RESULT);
        return false;
    } else {
        imageChooser();
    }
    return true;
}

private void imageChooser() {
    Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    if (cameraIntent.resolveActivity(getPackageManager()) != null) {
        File photoFile = null;
        try {
            photoFile = createImageFile();
            cameraIntent.putExtra("cameraPhotoPath", mCameraPhotoPath);
        } catch (IOException ex) {
            // SKIP
        }

        photoFile.delete();

        if (photoFile != null) {
            mCameraPhotoPath = "file:" + photoFile.getAbsolutePath();
            takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT,
                    Uri.fromFile(photoFile));
        } else {
            takePictureIntent = null;
        }
    }

    Intent contentSelectionIntent = new Intent(Intent.ACTION_GET_CONTENT);
    contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE);
    contentSelectionIntent.setType(TYPE_IMAGE);

    Intent[] intentArray;
    if (takePictureIntent != null) {
        intentArray = new Intent[]{takePictureIntent};
    } else {
        intentArray = new Intent[0];
    }

    Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER);
    chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent);
    chooserIntent.putExtra(Intent.EXTRA_TITLE, "Image Chooser");
    chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray);

    startActivityForResult(chooserIntent, INPUT_FILE_REQUEST_CODE);
}

picker에서 선택한 방식(사진 촬영, 파일 업로드)으로 사진 선택 후 파일 Uri를 onActivityResult에서 처리해 주는 방식이다.

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    if (requestCode == INPUT_FILE_REQUEST_CODE && resultCode == RESULT_OK) {
        if(mFilePathCallback == null) return;

        Uri[] results = new Uri[]{getResultUri(data)};

        mFilePathCallback.onReceiveValue(results);
        mFilePathCallback = null;
    } else {
    	if (mFilePathCallback != null) {
            mFilePathCallback.onReceiveValue(null);
        }

        if (mUploadMessage != null) {
            mUploadMessage.onReceiveValue(null);
        }

        mFilePathCallback = null;
        mUploadMessage = null;
    }
}

 

수정하게 되는 이미지 업로드 방식

우리는 사진 촬영, 이미지 선택 후 업로드 하는 과정에서 새로운 기능을 추가하기로 했다.

새로운 기능은 face detection이며 face가 인식이 되었을 경우에만 사진이 업로드 가능하게 수정하고자 했다.

해당 기능을 추가해 custom camera와 이미지 선택 업로드 기능을 수정하였으며 그 과정은 여기서 설명하기엔 너무 길어서 다음에 정리하여 업로드하고자 한다.

 

본론으로 돌아와 모든 커스텀 작업을 마치고 선택된 이미지를 webView로 전송 후 파일로 변환할 때 문제가 발생했다.

 

webview와의 통신을 위해 file uri를 base64 string으로 인코딩하고 URLEncoder로 다시 인코딩한 후 webView로 전송했다.

public static String fileUriToBase64(Uri uri, ContentResolver resolver) {
    String encodedBase64 = "";
    try {
        byte[] bytes = readBytes(uri, resolver);
        encodedBase64 = Base64.encodeToString(bytes, 0);
    } catch (IOException e1) {
        e1.printStackTrace();
    }
    return encodedBase64;
}

String encodedUri = URLEncoder.encode(fileUriToBase64(fileUri, getContentResolver()));

webView.evaluateJavascript("javascript: " +"getImageUrl(\"" + "data:image/png;base64," + encodedUri + "\")", null);

webView에서 전송된 string을 받은 후 image source에 넣어 정상적으로 렌더링 하는 데는 성공했지만 파일로 변환하는 곳에서 계속 에러가 발생했다.

convertBase64IntoFile(image, fileName) {
  function b64toBlob(b64Data, contentType = "", sliceSize = 512) {
    if (b64Data === "" || b64Data === undefined) return;

    const byteCharacters = window.atob(b64Data);
    const byteArrays = [];

    for (
      let offset = 0;
      offset < byteCharacters.length;
      offset += sliceSize
    ) {
      const slice = byteCharacters.slice(offset, offset + sliceSize);
      const byteNumbers = new Array(slice.length);
      for (let i = 0; i < slice.length; i++) {
        byteNumbers[i] = slice.charCodeAt(i);
      }
      const byteArray = new Uint8Array(byteNumbers);
      byteArrays.push(byteArray);
    }

    const blob = new Blob(byteArrays, { type: contentType });
    return blob;
  }

  const mimeType = "image/png";

  const blob = b64toBlob(image, mimeType);
  const raw = new File([blob], fileName, { type: mimeType });

  const file = { name: raw.name, size: raw.size, uid: 1, raw };

  return file;
},

const convertedFile = this.convertBase64IntoFile(
  replacedUrl,
  "image.png",
);

error 내용: InvalidCharacterError: Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encodded.

 

원인은 base64 data를 디코딩할 때 발생하였다. 

 

atob 메서드 설명에 보면 '데이터를 btoa()로 인코딩해 전송하고, 받는 쪽에서는 atob()로 디코딩하면 문제없이 원래 데이터를 가져올 수 있습니다.'라 되어 있었고 안드로이드에서 파일을 올려줄 때 인코딩 했기 때문에 디코딩을 해주었는데 여기서 에러가 발생했다.

위 에러에 대해 검색하던 중 JAVA와 Javascript의 uri 인코딩 방식이 다르다는 글을 읽었고 Android에서 인코딩할 때 javascript 규격에 맞춰주고 atob 메서드가 아닌 decodeUriComponent 메서드로 디코딩을 진행 후 파일로 추출하였다.

Android Java

String encodedUri = URLEncoder.encode(fileUriToBase64(fileUri, getContentResolver()));

->

String encodedUri = URLEncoder.encode(fileUriToBase64(fileUri, getContentResolver()))
    .replaceAll("\\+", "%20")
    .replaceAll("\\%21", "!")
    .replaceAll("\\%27", "'")
    .replaceAll("\\%28", "(")
    .replaceAll("\\%29", ")")
    .replaceAll("\\%7E", "~");
webView Javascript

const convertedFile = this.convertBase64IntoFile(
  replacedUrl,
  "image.png",
);

-> 

const convertedFile = this.convertBase64IntoFile(
    decodeURI(replacedUrl),
    "image.png",
);

function b64toBlob(b64Data, contentType = "", sliceSize = 512) {
    if (b64Data === "" || b64Data === undefined) return;

    const byteArrays = [];

    for (
      let offset = 0;
      offset < b64Data.length;
      offset += sliceSize
    ) {
      const slice = b64Data.slice(offset, offset + sliceSize);
      const byteNumbers = new Array(slice.length);
      for (let i = 0; i < slice.length; i++) {
        byteNumbers[i] = slice.charCodeAt(i);
      }
      const byteArray = new Uint8Array(byteNumbers);
      byteArrays.push(byteArray);
    }

    const blob = new Blob(byteArrays, { type: contentType });
    return blob;
}

file을 출력해보니 정상적으로 추출되었다! :D 

 

이제 안드로이드 작업이 모두 마무리되었으니 iOS 작업을 하러 떠나야겠다,,,