본문 바로가기
작업실/도구

Android SAF External File Control: Gamemaker Studio 2 Extension 한국어 문서

by Corin Choi 2021. 9. 6.

Android SAF External File Control Extension
안드로이드의 SAF를 이용한 폴더 및 파일 접근


 

Android External File Control by Corin Choi | GameMaker: Marketplace

This item is now in your basket. Continue Shopping Checkout

marketplace.yoyogames.com

(2022. 06. 22. 업데이트) 익스텐션 v1.1.4 한국어 문서입니다.


목차

  1. 소개
  2. 주의사항
  3. 요구사항
  4. 사용 가능한 함수
  5. 참고사항

소개

기존에 파일 쓰기 및 읽기 권한만을 가지고 있다면 외부 저장소의 파일 접근이 쉽게 됐던 것과 달리, 안드로이드 API 30 (Android 11) 이후부터는 SAF(Storage Access Framework)를 이용해 접근 범위를 사용자가 지정해야 파일 접근을 할 수 있게 되었습니다. 때문에 권한이 있더라도 외부 저장소에서 게임메이커 내장 함수를 사용하다 보면 대부분의 함수가 작동하지 않습니다. 해당 익스텐션은 SAF를 이용하여 사용자가 원하는 경로를 선택하면, 콘텐츠 기반으로 해당 경로 및 하위 경로의 파일들에 접근할 수 있도록 도와줍니다.


주의사항

URI(Uniform Resource Identifiers)는 각 콘텐츠(파일)에 대한 고유한 식별자입니다. 하위 개념으로 익히 들어봤을 법한 URL, 그렇지 않을 URN이 있습니다. 여기서 중요한 것은 URL입니다. URL은 위치를 기반으로 파일을 식별합니다. 즉, 파일의 경로가 식별자가 됩니다. URL 앞부분의 https://, file://, content:// 등과 같은 스키마가 이 파일이 어떤 방식으로 사용될지를 구분합니다. SAF를 이용한 파일 접근은 기존에 사용하던 file://filePath URL 대신, content://contentID URL을 사용합니다.
그러나 이 익스텐션에서는 익숙한 기존의 파일 URL 방식을 FILE(file://filePath URL) 유형으로, 콘텐츠 URL 방식을 SAF(content://contentID) 유형으로 정의했으며, 문서에서도 이를 기준으로 설명합니다. 그 편이 함수 이름 길이를 줄이는데 도움이 되고, 안드로이드에 익숙하지 않은 개발자도 접근하기 쉬울 것이기 때문입니다. 때문에, SAF 공간에 접근해야 하는 함수에 대해 saf_ 접두사가 붙습니다.


요구사항

게임메이커 스튜디오 2 2.3.0 (아마도 하위 버전을 지원하지 않을 겁니다.)
안드로이드 API 24 (Nougat) 이상의 기기만을 지원합니다.

  • SAF (API 19, Kitkat)
  • RealPathUtil (API 23, Mashmallow)
  • DocumentsContract (API 24, Nougat)

사용 가능한 함수

 


Intent 함수


String intent_saf_request(double request_code)

사용자가 원하는 경로를 설정할 수 있도록 SAF 선택 화면을 띄우는 함수입니다. request_code는 다음의 두 가지가 있습니다.

  • SAF_REQUEST_SEARCH
  • SAF_REQUEST_LOAD

SAF_REQUEST_SEARCH: 이 요청 코드를 통해 앱이 사용할 최상위 경로(root)를 사용자가 선택할 수 있게 합니다. 설정한 경로는 내부 설정에 저장됩니다. 함수가 빈 문자열("")을 반환하지만, Async - Social 이벤트가 발생합니다. async_load 가 갖는 키와 내용은 다음과 같습니다.
"type": "saf_request_accepted", "saf_request_canceled"
"path": Absolute Path
type - saf_request_accepted는 사용자가 성공적으로 경로를 지정했음을 알립니다.
type - saf_request_canceled는 사용자가 SAF 인텐트를 종료했음을 알립니다. 이 경우 path는 undefined입니다.

SAF_REQUEST_LOAD: 이 요청 코드를 통해 내부 설정에 저장된 최상위 경로를 반환합니다. 저장된 경로가 없다면 빈 문자열("")을 반환합니다. 저장된 경로가 있다면 저장된 문자열을 반환하며 Async - Social 이벤트가 발생합니다. async_load 가 갖는 키와 내용은 다음과 같습니다.
"type": "saf_request_loaded"
"path": Absolute Path
type - saf_request_loaded는 저장한 경로가 존재함을 알립니다.


void intent_open_setting(String message)

앱 설정 화면을 엽니다. 메시지 매개변수가 빈 문자열("")이 아니라면 토스트 메시지를 띄우면서 실행됩니다. 함수가 아무것도 반환하지 않지만 Async - Social 이벤트가 발생합니다. async_load 가 갖는 키와 내용은 다음과 같습니다.
"type": "permission_check"
이를 이용하여 앱 설정 이후 권한이 제대로 설정되었는지 체크해 볼 수 있습니다.


FILE 유형 함수


String directory_get_external_files() String directory_get_external_cache()

/storage/emulated/0/Android/AppPackage (경로는 기기마다 다를 수 있음)에 접근하는 건 권한이 필요 없으며 경로만 얻을 수 있다면 게임메이커 내의 함수로도 파일을 관리할 수 있습니다. 해당 경로를 얻기 위해서는 위의 두 함수를 사용할 수 있습니다.
각각 /storage/emulated/0/Android/AppPackage/ 내의 files, cache 폴더의 경로를 반환하며, 두 경로는 특수한 권한이 없이 파일 읽고 쓰기가 가능하며, 게임메이커 내의 파일 함수를 이용하여 파일 관리도 가능합니다. 그러나 패키지 폴더 내의 파일은 앱 제거 시 같이 제거되며, 이는 게임 내의 중요한 파일(저장 데이터 또는 사용자 배포 파일)의 손실로 이어질 수 있습니다.

저장소에 대한 자세한 내용은 다음을 참조하세요.
https://developer.android.com/training/data-storage?hl=ko


String directory_get_saf_root()

intent_saf_request를 이용하여 설정한 SAF 최상위 경로를 FILE 유형 절대 경로로 반환합니다. SAF 최상위 경로가 설정되지 않았다면 빈 문자열("")을 반환합니다.


String directory_get_contents(String path)

디렉터리에 어떤 파일이 존재하는지 확인할 수 있습니다. json 형식의 텍스트가 반환되며, json_decode 함수를 이용하여 ds_map으로 변환할 수 있습니다. 키 값은 다음과 같습니다.
"directory_length": 디렉터리 개수
"file_length": 파일 개수
"directory_N": N번째 디렉터리의 이름 (N = 0 ~ "directory_length" - 1)
"file_N": N번째 파일의 이름 (N = 0 ~ "file_length" - 1)


double file_get_size(String path, String name)

파일의 크기를 확인합니다. SAF 유형 파일이더라도 권한과 절대 경로가 있으면 사용할 수 있습니다.


String file_apply_name(String path, String name)

파일의 이름이 유효한지 확인합니다. SAF 유형 파일이더라도 권한과 절대 경로가 있으면 사용할 수 있습니다.파일의 이름에 유효하지 않은 기호( | \ ? * < " : > / )가 있다면 언더바( _ )로 바뀝니다.
또한 해당 경로에 이름이 동일한 파일이 있다면 옆에 추가적으로 인덱스 번호가 붙습니다.


SAF 유형 함수

SAF 유형 함수를 사용하려면 사용자가 앱이 파일에 접근하는 것을 동의해야 합니다.
이 익스텐션에서는 구현하지 않으며, 개발자가 직접 os_check_permission, os_request_permission 두 함수를 이용해 권한을 획득해야 합니다.
SAF 유형에서 경로(path)는 사용자가 설정한 경로를 최상위 경로(루트, "/")로 합니다. 항상 슬래시( / )로 구분해야 하며 역슬래시, 포함할 수 없는 기호 등을 입력하면 대신 언더 바가 삽입되거나 하는 등, 예상한 대로 작동하지 않게 됩니다. 이름(name) 역시 마찬가지로 포함할 수 없는 기호는 언더바( _ )로 변경됩니다. 중복되는 파일이 있다면 자동으로 이름에 번호를 붙입니다. 또한 디렉터리 경로는 하나로 합쳐 써야 하며, 파일 경로는 디렉터리 경로와 파일 경로를 분리해서 써야 합니다.
항상 SAF 범위 내에서만 사용될 수 있습니다.


double saf_directory_create(String path, String name)

입력한 경로에서 이름에 해당하는 디렉터리를 생성합니다. 디렉터리가 존재하지 않거나 이미 같은 이름의 디렉터리가 있다면 실패합니다. 성공하면 1, 실패하면 0을 반환합니다.


void saf_directory_creates(String path)

경로까지의 디렉터리를 생성합니다. 디렉터리가 없다면 디렉터리를 만들어냅니다. (mkdir와 같습니다.)


double saf_directory_exists(String path)

디렉터리가 존재하는지 확인합니다. 성공하면 1, 실패하면 0을 반환합니다.


double saf_directory_rename(String path, String rename)

디렉터리의 이름을 변경합니다. 성공하면 1, 실패하면 0을 반환합니다.


double saf_directory_remove(String path)

디렉터리를 제거합니다. 성공하면 1, 실패하면 0을 반환합니다.


String saf_directory_get_contents(String path)

디렉터리에 어떤 파일이 존재하는지 확인할 수 있습니다. json 형식의 텍스트가 반환되며, json_decode 함수를 이용하여 ds_map으로 변환할 수 있습니다. 키 값은 다음과 같습니다.
"directory_length": 디렉터리 개수
"file_length": 파일 개수
"directory_N": N번째 디렉터리의 이름 (N = 0 ~ "directory_length" - 1)
"file_N": N번째 파일의 이름 (N = 0 ~ "file_length" - 1)


double saf_file_create_text(String path, String name)

경로에 텍스트 파일(MIME Type: "text/plain")을 생성합니다. 성공하면 1, 실패하면 0을 반환합니다.


double saf_file_create_bin(String path, String name)

경로에 바이너리 파일(MIME Type: "application/octet-stream")을 생성합니다. 성공하면 1, 실패하면 0을 반환합니다.


double saf_file_create(String path, String name, String mime_type)

경로에 MIME Type에 해당하는 파일을 생성합니다. 성공하면 1, 실패하면 0을 반환합니다.

MIME Type에 대해서는 다음을 참고하세요.
https://www.iana.org/assignments/media-types/media-types.xhtml


double saf_file_exists(String path, String name)

파일이 존재하는지 확인합니다. 성공하면 1, 실패하면 0을 반환합니다.


double saf_file_rename(String path, String name, String rename)

파일의 이름을 변경합니다. 성공하면 1, 실패하면 0을 반환합니다.


double saf_file_remove(String path, String name)

파일을 제거합니다. 성공하면 1, 실패하면 0을 반환합니다.


double saf_file_move(String src_path, String src_name, String dst_path, String dst_name)

파일을 옮깁니다. 성공하면 1, 실패하면 0을 반환합니다. (SAF to SAF)
동일한 이름을 가진 파일이 있다면 이름이 임의로 변경되므로 move 대신 copy after delete 방식으로 파일을 이동하는 걸 추천합니다.


void saf_file_copy(String path_src, String name_src, String path_dst, name_dst)

src 파일을 dst 파일로 복사합니다. 복사 될 위치에 경로가 없다면 경로를 생성하고 동일한 이름을 가진 파일이 있다면 덮어 씌워집니다. (SAF to SAF)


void saf_file_copy_from_file(String path_src, String name_src, String path_dst, name_dst)

FILE 유형 src 파일을 SAF 유형 dst 파일로 복사합니다. 복사 될 위치에 경로가 없다면 경로를 생성하고 동일한 이름을 가진 파일이 있다면 덮어 씌워집니다. (FILE to SAF)


void saf_file_copy_to_file(String path_src, String name_src, String path_dst, name_dst)

SAF 유형 src 파일을 FILE 유형 dst 파일로 복사합니다. 복사 될 위치에 경로가 없다면 경로를 생성하고 동일한 이름을 가진 파일이 있다면 덮어 씌워집니다. (SAF to FILE)


SAF IOStream 함수

게임메이커 스튜디오 2의 파일 스트림을 요구하는 함수 (file_text_open_read, file_bin_open, ini_open 등) 들은, SAF 공간에서 사용할 수 없습니다. 때문에 익스텐션에서 따로 구현되었습니다. (기존 함수와 기능 차이가 있습니다.)
동일한 파일을 여러 모드로 열 수 없도록 막혀있습니다. 또한 한 파일을 텍스트 파일과 바이너리 파일로 동시에 열어서는 안 됩니다.


double saf_file_text_open_read(String path, String name)

텍스트 파일을 읽기 모드로 엽니다. 파일의 인덱스를 반환하며, 여는데 실패했다면 -1을 반환합니다.


double saf_file_text_open_write(String path, String name)

텍스트 파일을 쓰기 모드로 엽니다. 파일이 존재하지 않는다면 먼저 파일을 생성합니다. 파일의 인덱스를 반환하며, 여는데 실패했다면 -1을 반환합니다.


double saf_file_text_open_append(String path, String name)

텍스트 파일을 이어 쓰기 모드로 엽니다. 파일이 존재하지 않는다면 먼저 파일을 생성합니다. 파일의 인덱스를 반환하며, 여는데 실패했다면 -1을 반환합니다.


double saf_file_text_read_real(double file)

텍스트 파일에서 실수 하나를 읽습니다. 읽기 모드일 때만 사용할 수 있습니다.


String saf_file_text_read_string(double file)

텍스트 파일에서 문자열 하나를 읽습니다. 읽기 모드일 때만 사용할 수 있습니다.


String saf_file_text_readln(double file)

텍스트 파일에서 한 줄을 읽습니다. 읽기 모드일 때만 사용할 수 있습니다.


void saf_file_text_write_real(double file, double value)

텍스트 파일에 실수를 씁니다. 쓰기 또는 이어 쓰기 모드일 때만 사용할 수 있습니다.


void saf_file_text_write_string(double file, String value)

텍스트 파일에 문자열을 씁니다. 쓰기 또는 이어 쓰기 모드일 때만 사용할 수 있습니다.


void saf_file_text_writeln(double file, String value)

텍스트 파일에 문자열을 쓴 후에 줄을 바꿉니다. 쓰기 또는 이어 쓰기 모드일 때만 사용할 수 있습니다.


double saf_file_text_eoln(double file)

텍스트 파일에 다음 줄이 있는지 확인합니다. 존재하면 1, 그렇지 않다면 0을 반환합니다. 읽기 모드일 때만 사용할 수 있습니다.


double saf_file_text_eof(double file)

텍스트 파일이 끝났는지 확인합니다. 끝이라면 1, 아니라면 0을 반환합니다. 읽기 모드일 때만 사용할 수 있습니다.


void saf_file_text_close(double file)

텍스트 파일을 닫습니다.


double saf_file_bin_open_read(String path, String name)

바이너리 파일을 읽기 모드로 엽니다. 파일의 인덱스를 반환하며, 여는데 실패했다면 -1을 반환합니다.


double saf_file_bin_open_write(String path, String name)

바이너리 파일을 쓰기 모드로 엽니다. 파일이 존재하지 않는다면 먼저 파일을 생성합니다. 파일의 인덱스를 반환하며, 여는데 실패했다면 -1을 반환합니다.


double saf_file_bin_open_append(String path, String name)

바이너리 파일을 이어 쓰기 모드로 엽니다. 파일이 존재하지 않는다면 먼저 파일을 생성합니다. 파일의 인덱스를 반환하며, 여는데 실패했다면 -1을 반환합니다.


void saf_file_bin_rewrite(double file)

바이너리 파일을 비웁니다. 쓰기 또는 이어 쓰기 모드일 때만 사용할 수 있습니다.


void saf_file_bin_write_byte(double file, double value)

바이너리 파일에 바이트 하나를 씁니다. 쓰기 또는 이어 쓰기 모드일 때만 사용할 수 있습니다.


double saf_file_bin_read_byte(double file)

바이너리 파일에서 바이트 하나를 읽습니다. 파일의 끝이라면 -1을 반환합니다. 읽기 모드일 때만 사용할 수 있습니다.


void saf_file_bin_close(double file)

바이너리 파일을 닫습니다.


디버그 함수

디버그 용도로 사용할 수 있는 함수입니다. ExternalFile.java 내부에 작성되어 있습니다.


double send_double(double value)

입력한 실수를 반환합니다. 이 함수(ExternalFile.java: 671)를 수정하여 테스트 용도로 사용할 수 있습니다. 인자 갯수, 타입 및 반환 타입을 수정하려면 게임메이커 내의 익스텐션 함수 연결 부분도 같이 수정해야 합니다.


void send_social_log(String text)

함수가 아무것도 반환하지 않지만 Async - Social 이벤트가 발생합니다. async_load 가 갖는 키와 내용은 다음과 같습니다.
"type": "log"
"log": text


통합 함수

SAF 공간에서 게임메이커 일부 내장 함수를 사용할 수 있으므로 의도가 동일하지만 함수 형태가 다른 함수를 통합하기 위해 통합 함수를 만들었습니다. 통합 함수는 ExternalFile.gml 내부에 작성되어 있으며 integrated_ 접두사가 붙습니다.


String integrated_file_path(path, name, type)

FILE 유형의 경로(절대 경로)와 SAF 유형의 경로(상대 경로)에서 절대 경로를 반환합니다. type 은 두 가지가 있습니다.

  • IS_FILE
  • IS_SAF

IS_FILE 일 때는 path + "/" + name 인 절대 경로를 반환합니다.
IS_SAF 일 때는 설정된 SAF 루트의 절대 경로를 불러와 path + "/" + name에 더한 경로를 반환합니다. SAF 공간이 설정되어 있지 않다면 의도한 대로 작동하지 않을 것입니다.


void integrated_file_copy(path_src, name_src, path_dst, name_dst, type_src, type_dst)

src 파일의 유형과 dst 파일의 유형에 따라 각각 다른 함수를 사용하여 파일을 복사합니다. type 은 두 가지가 있습니다.

  • IS_FILE
  • IS_SAF

참고사항

게임메이커 내장 함수는 앱 별 내부 저장소(/data/data/AppPackage) 및 외부 저장소에서(/storage/emulated/0/Android/AppPackage)도 사용할 수 있습니다. 그 중 일부 함수는 SAF 공간 내부에서도 작동합니다. 이 경우, SAF 유형의 경로(상대 경로) 대신 FILE 유형의 경로(절대 경로)가 필요합니다.

  • file_copy (SAF 공간 내부의 파일을 앱 별 저장소로 복사할 수 있습니다. 이 경우 확장자 관련 오류가 있으니 saf_file_copy_to_file을 이용하시는 것을 권장합니다.)
  • font_add
  • audio_create_stream
  • sprite_add

파일을 메모리에 올려두는 함수에 한에서만 작동하는 것을 알 수 있습니다. (이마저도 읽기는 성공할 수 있으나 쓰기는 권한이 있더라도 항상 실패합니다.)


익스텐션에 포함된 RealPathUtil.java는 아래 사이트의 코드를 가져와 하위 호환성을 제거했습니다.
https://gist.github.com/tatocaster/32aad15f6e0c50311626


 

댓글